Skip to content

Commit bccbc1e

Browse files
committed
list-append: Infer ww and rw dependencies for unobserved appends
When we observe a longest value for some key x like [1 2], we can now prove that appends of 3, 4, and 5 must all fall after the append of 2, and any read of [1 2].
1 parent 7eac254 commit bccbc1e

File tree

2 files changed

+134
-14
lines changed

2 files changed

+134
-14
lines changed

src/elle/list_append.clj

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -465,15 +465,23 @@
465465

466466
(defn previously-appended-element
467467
"Given an append mop, finds the element that was appended immediately prior
468-
to this append. Returns ::init if this was the first append."
468+
to this append. If we don't know precisely which element was appended
469+
immediately prior (e.g. we never read this append), we still know it came
470+
after the highest observed element, so we return that instead.
471+
472+
Returns ::init if this was, or can only be shown to come after, the first
473+
append."
469474
[append-index write-index op [f k v]]
470-
; We may not know what version this append was--for instance, if we never
471-
; read a state reflecting this append.
472-
(when-let [index (get-in append-index [k :indices v])]
475+
(if-let [index (get-in append-index [k :indices v])]
473476
; What value was appended immediately before us in version order?
474477
(if (pos? index)
475478
(get-in append-index [k :values (dec index)])
476-
::init)))
479+
::init)
480+
; We don't know what version this append was exactly, because we never
481+
; observed it. That still tells us that it came *after* the highest
482+
; observed element. If we never observed any appends, it must have come
483+
; after the initial state.
484+
(-> append-index (get k) :values peek (or ::init))))
477485

478486
(defn ww-mop-dep
479487
"What (other) operation wrote the value just before this write mop?"

test/elle/list_append_test.clj

Lines changed: 121 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,12 @@
197197
; infer the version order for that key, even if we never observe it in a
198198
; read: it has to go from nil -> [x].
199199
(testing "single appends without reads"
200-
(is (= {rx [ax1] ax1 []} (g rx ax1)))
201-
; But with two appends, we can't infer any more
202-
(is (= {} (g rx ax1 ax2))))
200+
(is (= {rx [ax1] ax1 []} (g rx ax1))))
201+
202+
(testing "multiple appends without reads"
203+
; With two appends, we can't infer a precise order, but we still know ax1
204+
; and ax2 both had to come after rx!
205+
(is (= {rx [ax1 ax2] ax1 [] ax2 []} (g rx ax1 ax2))))
203206

204207
(testing "duplicate inserts attempts"
205208
(let [ax1ry {:index 0, :type :invoke, :value [[:append :x 1] [:r :y nil]]}
@@ -414,6 +417,9 @@
414417
[t1 t1'] (pair (op "ax1ax2"))
415418
[t2 t2'] (pair (op "rx1"))
416419
[t1 t1' t2 t2' :as h] (history/index [t1 t1' t2 t2'])]
420+
; This is not only G1b, since it has an intermediate read, but also
421+
; G-single, since rx1 observes ax1 but does not observe ax2!
422+
417423
; G0 checker won't catch this
418424
(is (= {:valid? true} (c {:consistency-models nil, :anomalies [:G0]} h)))
419425
; G1 will
@@ -425,18 +431,43 @@
425431
:mop [:r :x [1]]
426432
:element 1}]}}
427433
(c {:consistency-models nil, :anomalies [:G1]} h)))
428-
; G2 won't: even though the graph covers G1c, we don't do the specific
429-
; G1a/b checks unless asked.
430-
(is (= {:valid? true}
431-
(c {:consistency-models nil, :anomalies [:G2]} h)))
434+
; G2 catches G-single but won't actually report G1b: even though the
435+
; graph covers G1c, we filter out the G1b anomaly since we weren't asked
436+
; for it. Still report that it's not read-committed, which is... maybe
437+
; questionable. Might change this later?
438+
(is (= {:valid? false
439+
:not #{:read-committed}
440+
:anomaly-types [:G-single]}
441+
(-> (c {:consistency-models nil, :anomalies [:G2]} h)
442+
(select-keys [:valid? :not :anomaly-types]))))
432443
; But, say, strict-1SR will
433444
(is (= {:valid? false
434-
:anomaly-types [:G1b]
445+
:anomaly-types [:G-single :G1b]
435446
:not #{:read-committed}
436447
:anomalies {:G1b [{:op t2'
437448
:writer t1'
438449
:mop [:r :x [1]]
439-
:element 1}]}}
450+
:element 1}]
451+
:G-single
452+
[{:cycle
453+
[{:type :ok, :value [[:r :x [1]]], :index 3}
454+
{:type :ok,
455+
:value [[:append :x 1] [:append :x 2]],
456+
:index 1}
457+
{:type :ok, :value [[:r :x [1]]], :index 3}],
458+
:steps
459+
[{:type :rw,
460+
:key :x,
461+
:value 1,
462+
:value' 2,
463+
:a-mop-index 0,
464+
:b-mop-index 1}
465+
{:type :wr,
466+
:key :x,
467+
:value 1,
468+
:a-mop-index 0,
469+
:b-mop-index 0}],
470+
:type :G-single}]}}
440471
(c {:consistency-models [:strict-serializable]} h)))))
441472

442473
(testing "G1c"
@@ -708,6 +739,87 @@
708739
; There's a G0 here too, but we don't care.
709740
(c {:consistency-models nil, :anomalies [:internal]} h))))))
710741

742+
(deftest unobserved-write-test
743+
; When we see [:r :x [1 2]], and 1 was written by t1, 2 written by t2, and 3
744+
; written by t3, we can infer a dependency not only from the transaction 1 ->
745+
; 2 but *also* from 2 -> 3: transactions which are not observed in the
746+
; longest read must fall after the transaction which generated that value!
747+
;
748+
; To test this, we perform writes of 1, 2, and 3 to both x and y, but let y
749+
; fail to observe 1.
750+
(let [[w1 w1'] (pair (op "ax1ay1"))
751+
[w2 w2'] (pair (op "ax2ay2"))
752+
[w3 w3'] (pair (op "ax3ay3"))
753+
[rx rx'] (pair (op "rx12"))
754+
[ry ry'] (pair (op "ry2"))
755+
h (history/index [w1 w2 w3 rx ry w1' w2' w3' rx' ry'])]
756+
; w1 -ww-> w2, by rx12
757+
; w2 -ww-> w1, by ry2
758+
; ry -rw-> w1, since y fails to observe w1
759+
; w3 is a red herring; just there to create multiple final edges
760+
(is (= {:valid? false
761+
:anomaly-types [:G-single :G0]
762+
:anomalies
763+
; We know this is G-single because ry -rw-> w1 -ww-> w2 -wr-> ry
764+
{:G-single
765+
[{:cycle
766+
[{:type :ok, :value [[:r :y [2]]], :index 9}
767+
{:type :ok,
768+
:value [[:append :x 1] [:append :y 1]],
769+
:index 5}
770+
{:type :ok,
771+
:value [[:append :x 2] [:append :y 2]],
772+
:index 6}
773+
{:type :ok, :value [[:r :y [2]]], :index 9}],
774+
:steps
775+
[{:type :rw,
776+
:key :y,
777+
:value 2,
778+
:value' 1,
779+
:a-mop-index 0,
780+
:b-mop-index 1}
781+
{:type :ww,
782+
:key :x,
783+
:value 1,
784+
:value' 2,
785+
:a-mop-index 0,
786+
:b-mop-index 0}
787+
{:type :wr,
788+
:key :y,
789+
:value 2,
790+
:a-mop-index 1,
791+
:b-mop-index 0}],
792+
:type :G-single}],
793+
; But worse, it's G0 because w2 -ww-> w1 -ww->w2
794+
:G0
795+
[{:cycle
796+
[{:type :ok,
797+
:value [[:append :x 2] [:append :y 2]],
798+
:index 6}
799+
{:type :ok,
800+
:value [[:append :x 1] [:append :y 1]],
801+
:index 5}
802+
{:type :ok,
803+
:value [[:append :x 2] [:append :y 2]],
804+
:index 6}],
805+
:steps
806+
[{:type :ww,
807+
:key :y,
808+
:value 2,
809+
:value' 1,
810+
:a-mop-index 1,
811+
:b-mop-index 1}
812+
{:type :ww,
813+
:key :x,
814+
:value 1,
815+
:value' 2,
816+
:a-mop-index 0,
817+
:b-mop-index 0}],
818+
:type :G0}]},
819+
:not #{:read-uncommitted}}
820+
(-> (c {:consistency-models [:serializable]} h)
821+
(dissoc :also-not))))))
822+
711823
(deftest repeatable-read-test
712824
; This is a long fork, which is also G2-item, by virtue of its only cycle
713825
; being two anti-dependency edges. We shouldn't be able to detect this with

0 commit comments

Comments
 (0)