Skip to content

Commit 9831a3a

Browse files
committed
Check transaction histories in Jepsen
1 parent 2289d4a commit 9831a3a

2 files changed

Lines changed: 134 additions & 14 deletions

File tree

jepsen/src/moreconsensus/epaxos_test.clj

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -773,15 +773,58 @@
773773
(seq shape-errors) (assoc :bad-shape shape-errors)
774774
(seq bad-groups) (assoc :bad-groups bad-groups)))))
775775

776+
(defn txn-read-value [groups group]
777+
(let [values (get groups group)]
778+
(when (and (vector? values) (apply = values))
779+
(first values))))
780+
781+
(defn txn-op-for-group [group op]
782+
(case (:f op)
783+
:txn-write (when (= group (get-in op [:value :group]))
784+
(assoc op :f :write :value (get-in op [:value :value])))
785+
:txn-delete (when (= group (get-in op [:value :group]))
786+
(assoc op :f :write :value nil))
787+
:txn-read (case (:type op)
788+
:invoke (assoc op :f :read :value nil)
789+
:ok (when (and (empty? (txn-group-shape-errors (:value op)))
790+
(empty? (inconsistent-txn-groups (:value op))))
791+
(assoc op :f :read :value (txn-read-value (:value op) group)))
792+
nil)
793+
nil))
794+
795+
(defn txn-group-history [group history]
796+
(mapv identity (keep #(txn-op-for-group group %) history)))
797+
798+
(defn txn-linearizable-results [test history opts]
799+
(into {} (map (fn [group]
800+
(let [group-test (assoc test
801+
:name (str (or (:name test) "txn-group")
802+
"-" (name group)))]
803+
[group (checker/check (checker/linearizable {:model (model/register)})
804+
group-test
805+
(txn-group-history group history)
806+
opts)]))
807+
txn-group-ids)))
808+
809+
(defn txn-linearizable-errors [results]
810+
(into {} (keep (fn [[group result]]
811+
(when-not (:valid? result)
812+
[group result]))
813+
results)))
814+
776815
(defn txn-atomic-checker []
777816
(reify checker/Checker
778-
(check [_ _ history _]
817+
(check [_ test history opts]
779818
(let [reads (filter #(and (= :ok (:type %)) (= :txn-read (:f %))) history)
780-
bad (vec (keep bad-txn-op reads))]
781-
{:valid? (empty? bad)
819+
bad (vec (keep bad-txn-op reads))
820+
linear-results (txn-linearizable-results test history opts)
821+
linear-errors (txn-linearizable-errors linear-results)]
822+
{:valid? (and (empty? bad) (empty? linear-errors))
782823
:checked (count reads)
783824
:bad-count (count bad)
784-
:bad (take 5 bad)}))))
825+
:bad (take 5 bad)
826+
:linearizable linear-results
827+
:linearizable-errors linear-errors}))))
785828

786829
(defn ordered-ascending? [xs]
787830
(every? (fn [[a b]] (not (pos? (compare a b)))) (partition 2 1 xs)))

jepsen/test/moreconsensus/epaxos_test_test.clj

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -924,20 +924,34 @@
924924
{:type :invoke :f :read :value :stale})
925925
[:type :f :value]))))))
926926

927+
(defn index-history [history]
928+
(mapv (fn [index op] (cond-> op (nil? (:index op)) (assoc :index index))) (range) history))
929+
927930
(defn check-txn-history [history]
928-
(checker/check (epaxos/txn-atomic-checker) nil history nil))
931+
(checker/check (epaxos/txn-atomic-checker)
932+
{:name "txn-atomic-checker-test" :start-time 0}
933+
(index-history history)
934+
nil))
935+
936+
(defn txn-read-state [values-by-group]
937+
(into {} (map (fn [{:keys [group keys]}]
938+
[group (vec (repeat (count keys) (get values-by-group group)))])
939+
epaxos/txn-key-groups)))
929940

930941
(deftest txn-atomic-checker-checks-each-group-independently
931942
(testing "values may differ across groups when each group is internally atomic"
932-
(let [result (check-txn-history [{:type :ok
933-
:f :txn-read
934-
:value {:tx-a [1 1]
935-
:tx-b [2 2 2]
936-
:tx-c [nil nil]}}])]
943+
(let [result (check-txn-history [{:type :invoke :process 0 :f :txn-write :value {:group :tx-a :value 1}}
944+
{:type :ok :process 0 :f :txn-write :value {:group :tx-a :value 1}}
945+
{:type :invoke :process 1 :f :txn-write :value {:group :tx-b :value 2}}
946+
{:type :ok :process 1 :f :txn-write :value {:group :tx-b :value 2}}
947+
{:type :invoke :process 2 :f :txn-read :value nil}
948+
{:type :ok :process 2 :f :txn-read :value (txn-read-state {:tx-a 1 :tx-b 2})}])]
937949
(is (= {:valid? true :checked 1 :bad-count 0}
938950
(select-keys result [:valid? :checked :bad-count])))))
939951
(testing "fully deleted transaction groups are atomic reads"
940-
(let [result (check-txn-history [{:type :ok
952+
(let [result (check-txn-history [{:type :invoke :process 0 :f :txn-read :value nil}
953+
{:type :ok
954+
:process 0
941955
:f :txn-read
942956
:value {:tx-a [nil nil]
943957
:tx-b [nil nil nil]
@@ -946,41 +960,104 @@
946960
(select-keys result [:valid? :checked :bad-count])))))
947961
(testing "mixed or partial values inside any one group make the read invalid"
948962
(let [read-op {:type :ok
963+
:process 0
949964
:f :txn-read
950965
:value {:tx-a [1 2]
951966
:tx-b [9 9 9]
952967
:tx-c [nil 3]}}
953-
result (check-txn-history [read-op])
968+
result (check-txn-history [{:type :invoke :process 0 :f :txn-read :value nil}
969+
read-op])
954970
bad-op (first (:bad result))]
955971
(is (= {:valid? false :checked 1 :bad-count 1}
956972
(select-keys result [:valid? :checked :bad-count])))
957973
(is (= {:tx-a [1 2] :tx-c [nil 3]}
958974
(:bad-groups bad-op)))))
959975
(testing "mixed delete and write visibility inside a group is invalid"
960976
(let [read-op {:type :ok
977+
:process 0
961978
:f :txn-read
962979
:value {:tx-a [4 4]
963980
:tx-b [nil 5 nil]
964981
:tx-c [6 6]}}
965-
result (check-txn-history [read-op])
982+
result (check-txn-history [{:type :invoke :process 0 :f :txn-read :value nil}
983+
read-op])
966984
bad-op (first (:bad result))]
967985
(is (= {:valid? false :checked 1 :bad-count 1}
968986
(select-keys result [:valid? :checked :bad-count])))
969987
(is (= {:tx-b [nil 5 nil]}
970988
(:bad-groups bad-op))))))
971989

990+
(deftest txn-atomic-checker-enforces-completed-write-visibility
991+
(testing "a later read observing the completed write remains valid"
992+
(let [history [{:type :invoke :process 0 :f :txn-write :value {:group :tx-a :value :committed}}
993+
{:type :ok :process 0 :f :txn-write :value {:group :tx-a :value :committed}}
994+
{:type :invoke :process 1 :f :txn-read :value nil}
995+
{:type :ok :process 1 :f :txn-read :value (txn-read-state {:tx-a :committed})}]
996+
result (check-txn-history history)]
997+
(is (= true (:valid? result)))))
998+
(testing "a later read of an older value after the completed write is invalid"
999+
(let [history [{:type :invoke :process 0 :f :txn-write :value {:group :tx-a :value :old}}
1000+
{:type :ok :process 0 :f :txn-write :value {:group :tx-a :value :old}}
1001+
{:type :invoke :process 1 :f :txn-write :value {:group :tx-a :value :committed}}
1002+
{:type :ok :process 1 :f :txn-write :value {:group :tx-a :value :committed}}
1003+
{:type :invoke :process 2 :f :txn-read :value nil}
1004+
{:type :ok :process 2 :f :txn-read :value (txn-read-state {:tx-a :old})}]
1005+
result (check-txn-history history)]
1006+
(is (= false (:valid? result))))))
1007+
1008+
(deftest txn-atomic-checker-enforces-completed-delete-visibility
1009+
(testing "a later read observing the completed delete remains valid"
1010+
(let [history [{:type :invoke :process 0 :f :txn-write :value {:group :tx-b :value :old}}
1011+
{:type :ok :process 0 :f :txn-write :value {:group :tx-b :value :old}}
1012+
{:type :invoke :process 1 :f :txn-delete :value {:group :tx-b}}
1013+
{:type :ok :process 1 :f :txn-delete :value {:group :tx-b}}
1014+
{:type :invoke :process 2 :f :txn-read :value nil}
1015+
{:type :ok :process 2 :f :txn-read :value (txn-read-state {})}]
1016+
result (check-txn-history history)]
1017+
(is (= true (:valid? result)))))
1018+
(testing "a later read of the deleted value is invalid"
1019+
(let [history [{:type :invoke :process 0 :f :txn-write :value {:group :tx-b :value :old}}
1020+
{:type :ok :process 0 :f :txn-write :value {:group :tx-b :value :old}}
1021+
{:type :invoke :process 1 :f :txn-delete :value {:group :tx-b}}
1022+
{:type :ok :process 1 :f :txn-delete :value {:group :tx-b}}
1023+
{:type :invoke :process 2 :f :txn-read :value nil}
1024+
{:type :ok :process 2 :f :txn-read :value (txn-read-state {:tx-b :old})}]
1025+
result (check-txn-history history)]
1026+
(is (= false (:valid? result))))))
1027+
1028+
(deftest txn-atomic-checker-rejects-never-written-equal-transaction-value
1029+
(testing "equal values across every key in a group still require a prior write"
1030+
(let [history [{:type :invoke :process 0 :f :txn-read :value nil}
1031+
{:type :ok :process 0 :f :txn-read :value (txn-read-state {:tx-c :phantom})}]
1032+
result (check-txn-history history)]
1033+
(is (= false (:valid? result))))))
1034+
1035+
(deftest txn-atomic-checker-accepts-observed-indeterminate-write
1036+
(testing "an info write may be linearized when a later transaction read observes it"
1037+
(let [history [{:type :invoke :process 0 :f :txn-write :value {:group :tx-a :value :maybe}}
1038+
{:type :info :process 0 :f :txn-write :value {:group :tx-a :value :maybe} :error 503}
1039+
{:type :invoke :process 1 :f :txn-read :value nil}
1040+
{:type :ok :process 1 :f :txn-read :value (txn-read-state {:tx-a :maybe})}]
1041+
result (check-txn-history history)]
1042+
(is (= true (:valid? result))))))
1043+
9721044
(deftest txn-atomic-checker-rejects-missing-and-wrong-sized-groups
9731045
(testing "every transaction read must contain exactly the configured groups with one value per key"
9741046
(let [missing-group {:type :ok
1047+
:process 0
9751048
:f :txn-read
9761049
:value {:tx-a [1 1]
9771050
:tx-c [3 3]}}
9781051
wrong-lengths {:type :ok
1052+
:process 1
9791053
:f :txn-read
9801054
:value {:tx-a [7]
9811055
:tx-b [8 8]
9821056
:tx-c [9 9 9]}}
983-
result (check-txn-history [missing-group wrong-lengths])]
1057+
result (check-txn-history [{:type :invoke :process 0 :f :txn-read :value nil}
1058+
missing-group
1059+
{:type :invoke :process 1 :f :txn-read :value nil}
1060+
wrong-lengths])]
9841061
(is (= {:valid? false :checked 2 :bad-count 2}
9851062
(select-keys result [:valid? :checked :bad-count])))
9861063
(is (= [(:value missing-group) (:value wrong-lengths)]

0 commit comments

Comments
 (0)