-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpatterns.html
344 lines (297 loc) · 11 KB
/
patterns.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
---
layout: default
title: Patterns
---
# Patterns and Practices
In some ways, STM may seem instantly familiar to people who are used
to the properties of transactions from database worlds: the *atomic*,
*consistent* and *isolated* properties are exactly the same, and the
only difference is the lack of the *durable* property as the AtomizeJS
server does not write anything to disk (though you could easily
imagine a future version which does).
In other ways, STM is quite different from normal database
transactions: database transactions have nothing like the [`retry`][]
or [`orElse`][] operations. Instead, well featured relational
databases tend to add `trigger` features so that upon changes to rows
or tables, user functions can be automatically invoked. Both
approaches allow an *observer-pattern* or *event-driven callbacks* to
be created, though the STM approach is simpler and more intuitive.
## Eventing
By using [`retry`][], you can build neat *proxy* objects. This also
gets around the current limitation of [`lift`][] whereby lifting an
object with a custom constructor and prototype will lose the prototype
when the object arrives at other clients. For example, consider a
`Player` which has an `x` and `y` coordinate and a `name`:
{% highlight javascript %}
function Player(name, x, y) {
this.raw = atomize.lift({name: name});
this.setPos(x, y);
this.watch();
}
Player.prototype = {
setPos: function (x, y) {
var self = this;
atomize.atomically(function () {
self.raw.x = x;
self.raw.y = y;
});
},
watch: function () {
var self, watcher;
self = this;
watcher = function (pos) {
atomize.atomically(function () {
if (pos.x === self.raw.x && pos.y === self.raw.y) {
atomize.retry();
} else {
return {x: self.raw.x, y: self.raw.y}
}
}, function (pos) {
self.x = pos.x;
self.y = pos.y;
watcher(pos);
}
};
watcher({x: this.x, y: this.y});
}
};
{% endhighlight %}
There is a fair amount of boiler-plate here, and it should be clear
how this can be abstracted out to a general proxy mechanism, but as a
simple example it demonstrates the following:
* We rely on the the *watcher* to observe any change in the underlying
`raw` object that is managed by AtomizeJS.
* When we want to set the position, we just perform a transaction that
modifies the `raw` object and allow the watcher to be notified and
propagate the change back to the proxy object.
* It's always safe to read the `x` and `y` fields of the proxy object:
as they're only updated by the *watcher* they will only ever see
values that have been fully committed by other transactions.
* The watcher never finishes.
Possibly one surprising aspect of this design though is that `setPos`
is non-blocking. This can mean that if you perform:
{% highlight javascript %}
var player = new Player("Fred", 45, 12);
player.setPos(46, 12);
player.setPos(46, 13);
{% endhighlight %}
then the two transactions you've issued can commit in *either* order!
To avoid that there are two choices: either you just ignore any calls
to `setPos` whilst a transaction is in-flight:
{% highlight javascript %}
setPos: function (x, y) {
if (this.blocked) {
return;
}
this.blocked = true;
var self = this;
atomize.atomically(function () {
self.raw.x = x;
self.raw.y = y;
}, function () {
delete self.blocked;
});
},
{% endhighlight %}
Or, you convert the whole mechanism to a continuation-passing style:
{% highlight javascript %}
setPos: function (x, y, cont) {
var self = this;
atomize.atomically(function () {
self.raw.x = x;
self.raw.y = y;
}, cont);
},
{% endhighlight %}
but then you'll have to do things like:
{% highlight javascript %}
var player = new Player("Fred", 45, 12);
player.setPos(46, 12,
function () {
player.setPos(46, 13, function () {});
});
{% endhighlight %}
and in fact, you'll note the `Player` constructor calls `setPos`, so
that will likely want a continuation too.
The former approach is much simpler, but will work only when you can
afford to lose updates: for example when you have a regular *tick*
coming from a timer that will reissue the same or similar `setPos`
call a little later on, hopefully after the transaction commits.
## Sharing deep data-structures
As explained in the [background][] page, the statement `a.b.c.d =
e.f;` creates a read-set of `a`, `a.b` and `e`, and a write set of
`c`. If you created a transaction which inspected `a` and then chose
to [`retry`][], then it would not be restarted by this assignment
because `a` has not been changed.
So if you consider a deep data-structure such as a binary tree, then
it's clear that just [`retry`][]-ing after inspecting the root of the
binary tree will not allow you to be informed of all changes to the
tree. There are two choices: either inspect *every* node of the tree,
and retry only after reading every single node, which would be very
expensive if the tree is deep, or create some other simpler mechanism
which ensures that every change does cause a retried transaction to
restart.
One simple way of doing this is to have an *eventCount*. Assume that
we have the functions `tree_new`, `tree_search`, `tree_insert`,
`tree_remove` and `tree_deep_clone`. We could then wrap them:
{% highlight javascript %}
function Tree (raw) {
if (raw === undefined) {
this.raw = atomize.lift({eventCount: 1, tree: tree_new()});
} else {
this.raw = raw;
}
this.copy = tree_new();
this.watch();
}
Tree.prototype = {
insert: function (key, val) {
var self = this;
atomize.atomically(function () {
self.raw.eventCount += 1;
self.raw.tree = tree_insert(self.raw.tree, key, val);
});
},
remove: function (key) {
var self = this;
atomize.atomically(function () {
self.raw.eventCount += 1;
self.raw.tree = tree_remove(self.raw.tree, key);
});
},
search: function (key) {
return tree_search(this.copy, key);
},
watch: function () {
var self, watcher;
self = this;
watcher = function (eventCount) {
atomize.atomically(function () {
if (eventCount === self.raw.eventCount) {
atomize.retry();
} else {
return {eventCount: self.raw.eventCount,
tree: tree_deep_clone(self.raw.tree)};
}
}, function (copy) {
self.copy = copy.tree;
watcher(copy.eventCount);
});
};
watcher(undefined);
}
};
{% endhighlight %}
Again, we build a proxy type object which is constantly being update
from the raw tree object in AtomizeJS. But by ensuring that every
mutating operation also changes the *eventCount* field, it becomes
trivial to ensure that we get notified whenever the tree is changed.
## Nested transactions
AtomizeJS supports nested transactions. When an *inner* transaction
commits, all that happens is that its *read* and *write* sets get
copied into its parent's transaction log. For example:
{% highlight javascript %}
function foo () {
atomize.atomically(function () {
atomize.root.pos = atomize.lift({x: 5, y: 6});
atomize.root.text = "hello";
bar();
atomize.root.text = "goodbye";
return atomize.root.text;
}, function (text) {
console.log("foo continuation:" + text);
});
}
function bar () {
atomize.atomically(function () {
if (atomize.root.pos.y % 2 === 0) {
atomize.root.pos.y + 1;
}
atomize.root.pos.x + 1;
return atomize.root.text;
}, function (text) {
console.log("bar continuation: " + text);
});
}
{% endhighlight %}
As usual, the `bar` continuation gets run after the `bar` transaction
commits. But when the `bar` transaction commits, you're still in the
`foo` transaction. Thus the `bar` continuation gets run *inside* the
`foo` transaction. In this case, at the point of the `bar`
continuation running, the value of `text` will be `"hello"`. If the
`foo` transaction has to be restarted then the `bar` transaction will
also be restarted, which can lead to the `bar` continuation running
multiple times.
To detect this scenario, there is the [`inTransaction`][] API call
which is always safe to call, and will let you detect, for example
when in a continuation, whether or not you are still in a transaction.
One way to ensure that a provided continuation is only ever run once
you're outside a transaction would be:
{% highlight javascript %}
function foo (cont) {
atomize.atomically(function () {
atomize.root.pos = atomize.lift({x: 5, y: 6});
atomize.root.text = "hello";
bar(cont);
atomize.root.text = "goodbye";
return atomize.root.text;
}, maybeCont(cont));
}
function bar (cont) {
atomize.atomically(function () {
if (atomize.root.pos.y % 2 === 0) {
atomize.root.pos.y + 1;
}
atomize.root.pos.x + 1;
return atomize.root.text;
}, maybeCont(cont));
}
function maybeCont(cont) {
return function (result) {
if (atomize.inTransaction()) {
return cont(result);
} else {
return result;
}
};
}
{% endhighlight %}
Thus now, regardless of how `bar` is invoked (i.e. whether directly or
whether from `foo`), the continuation will only be invoked once
outside of all transactions.
It's also worth watching out for nested transactions where the inner
transaction calls [`retry`][]: this will block the parent transaction
too. For example:
{% highlight javascript %}
function foo () {
atomize.atomically(function () {
atomize.root.pos = atomize.lift({x: 5, y: 6});
atomize.root.text = "hello";
bar();
atomize.root.text = "goodbye";
return atomize.root.text;
}, function (text) {
console.log("foo continuation:" + text);
});
}
function bar () {
atomize.atomically(function () {
if (atomize.root.pos.y % 2 === 0) {
atomize.retry();
}
atomize.root.pos.x + 1;
return atomize.root.text;
}, function (text) {
console.log("bar continuation: " + text);
});
}
{% endhighlight %}
Here, if `pos.y` is even, then the whole transaction suspends, waiting
for someone else to modify `pos.y`, *before* either continuation is
run, or `text` gets set to `"goodbye"`. Given our previous *proxy*
objects which started up the *watcher* from within the constructor, if
such an object were to be created from within another transaction,
you'd find the parent transaction blocks as the child transaction in
the new object has hit the [`retry`][].
In the future, we may be able to add other mechanisms which help to
manage these scenarios.