1# Journaled Mode
2
3(note: this section is speculative, the code has not yet been written)
4
5Magic-Wormhole supports applications which are written in a "journaled" or
6"checkpointed" style. These apps store their entire state in a well-defined
7checkpoint (perhaps in a database), and react to inbound events or messages
8by carefully moving from one state to another, then releasing any outbound
9messages. As a result, they can be terminated safely at any moment, without
10warning, and ensure that the externally-visible behavior is deterministic and
11independent of this stop/restart timing.
12
13This is the style encouraged by the E event loop, the
14original [Waterken Server](http://waterken.sourceforge.net/), and the more
15modern [Ken Platform](http://web.eecs.umich.edu/~tpkelly/Ken/), all
16influential in the object-capability security community.
17
18## Requirements
19
20Applications written in this style must follow some strict rules:
21
22* all state goes into the checkpoint
23* the only way to affect the state is by processing an input message
24* event processing is deterministic (any non-determinism must be implemented
25  as a message, e.g. from a clock service or a random-number generator)
26* apps must never forget a message for which they've accepted responsibility
27
28The main processing function takes the previous state checkpoint and a single
29input message, and produces a new state checkpoint and a set of output
30messages. For performance, the state might be kept in memory between events,
31but the behavior should be indistinguishable from that of a server which
32terminates completely between events.
33
34In general, applications must tolerate duplicate inbound messages, and should
35re-send outbound messages until the recipient acknowledges them. Any outbound
36responses to an inbound message must be queued until the checkpoint is
37recorded. If outbound messages were delivered before the checkpointing, then
38a crash just after delivery would roll the process back to a state where it
39forgot about the inbound event, causing observably inconsistent behavior that
40depends upon whether the outbound message successfully escaped the dying
41process or not.
42
43As a result, journaled-style applications use a very specific process when
44interacting with the outside world. Their event-processing function looks
45like:
46
47* receive inbound event
48* (load state)
49* create queue for any outbound messages
50* process message (changing state and queuing outbound messages)
51* serialize state, record in checkpoint
52* deliver any queued outbound messages
53
54In addition, the protocols used to exchange messages should include message
55IDs and acks. Part of the state vector will include a set of unacknowledged
56outbound messages. When a connection is established, all outbound messages
57should be re-sent, and messages are removed from the pending set when an
58inbound ack is received. The state must include a set of inbound message ids
59which have been processed already. All inbound messages receive an ack, but
60only new ones are processed. Connection establishment/loss is not strictly
61included in the journaled-app model (in Waterken/Ken, message delivery is
62provided by the platform, and apps do not know about connections), but
63general:
64
65* "I want to have a connection" is stored in the state vector
66* "I am connected" is not
67* when a connection is established, code can run to deliver pending messages,
68  and this does not qualify as an inbound event
69* inbound events can only happen when at least one connection is established
70* immediately after restarting from a checkpoint, no connections are
71  established, but the app might initiate outbound connections, or prepare to
72  accept inbound ones
73
74## Wormhole Support
75
76To support this mode, the Wormhole constructor accepts a `journal=` argument.
77If provided, it must be an object that implements the `wormhole.IJournal`
78interface, which consists of two methods:
79
80* `j.queue_outbound(fn, *args, **kwargs)`: used to delay delivery of outbound
81  messages until the checkpoint has been recorded
82* `j.process()`: a context manager which should be entered before processing
83  inbound messages
84
85`wormhole.Journal` is an implementation of this interface, which is
86constructed with a (synchronous) `save_checkpoint` function. Applications can
87use it, or bring their own.
88
89The Wormhole object, when configured with a journal, will wrap all inbound
90WebSocket message processing with the `j.process()` context manager, and will
91deliver all outbound messages through `j.queue_outbound`. Applications using
92such a Wormhole must also use the same journal for their own (non-wormhole)
93events. It is important to coordinate multiple sources of events: e.g. a UI
94event may cause the application to call `w.send(data)`, and the outbound
95wormhole message should be checkpointed along with the app's state changes
96caused by the UI event. Using a shared journal for both wormhole- and
97non-wormhole- events provides this coordination.
98
99The `save_checkpoint` function should serialize application state along with
100any Wormholes that are active. Wormhole state can be obtained by calling
101`w.serialize()`, which will return a dictionary (that can be
102JSON-serialized). At application startup (or checkpoint resumption),
103Wormholes can be regenerated with `wormhole.from_serialized()`. Note that
104only "delegated-mode" wormholes can be serialized: Deferreds are not amenable
105to usage beyond a single process lifetime.
106
107For a functioning example of a journaled-mode application, see
108misc/demo-journal.py. The following snippet may help illustrate the concepts:
109
110```python
111class App:
112    @classmethod
113    def new(klass):
114        self = klass()
115        self.state = {}
116        self.j = wormhole.Journal(self.save_checkpoint)
117        self.w = wormhole.create(.., delegate=self, journal=self.j)
118
119    @classmethod
120    def from_serialized(klass):
121        self = klass()
122        self.j = wormhole.Journal(self.save_checkpoint)
123        with open("state.json", "r") as f:
124            data = json.load(f)
125        self.state = data["state"]
126        self.w = wormhole.from_serialized(data["wormhole"], reactor,
127                                          delegate=self, journal=self.j)
128
129    def inbound_event(self, event):
130        # non-wormhole events must be performed in the journal context
131        with self.j.process():
132            parse_event(event)
133            change_state()
134            self.j.queue_outbound(self.send, outbound_message)
135
136    def wormhole_received(self, data):
137        # wormhole events are already performed in the journal context
138        change_state()
139        self.j.queue_outbound(self.send, stuff)
140
141    def send(self, outbound_message):
142        actually_send_message(outbound_message)
143
144    def save_checkpoint(self):
145        app_state = {"state": self.state, "wormhole": self.w.serialize()}
146        with open("state.json", "w") as f:
147            json.dump(app_state, f)
148```
149