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