Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Clone in Desktop Download ZIP
Tree: 5856a223c1
Fetching contributors…

Cannot retrieve contributors at this time

237 lines (205 sloc) 9.8 KB
(ns jepsen.checker
"Validates that a history is correct with respect to some model."
(:refer-clojure :exclude [set])
(:require [clojure.stacktrace :as trace]
[clojure.core :as core]
[clojure.core.reducers :as r]
[clojure.set :as set]
[jepsen.util :as util]
[multiset.core :as multiset]
[knossos.core :as knossos]
[knossos.history :as history]))
(defprotocol Checker
(check [checker test model history]
"Verify the history is correct. Returns a map like
{:valid? true}
or
{:valid? false
:failed-at [details of specific operations]}
and maybe there can be some stats about what fraction of requests
were corrupt, etc."))
(defn check-safe
"Like check, but wraps exceptions up and returns them as a map like
{:valid? nil :error \"...\"}"
[checker test model history]
(try (check checker test model history)
(catch Throwable t
{:valid? false
:error (with-out-str (trace/print-cause-trace t))})))
(def unbridled-optimism
"Everything is awesoooommmmme!"
(reify Checker
(check [this test model history] {:valid? true})))
(def linearizable
"Validates linearizability with Knossos."
(reify Checker
(check [this test model history]
(knossos/analysis model history))))
(def queue
"Every dequeue must come from somewhere. Validates queue operations by
assuming every non-failing enqueue succeeded, and only OK dequeues succeeded,
then reducing the model with that history. Every subhistory of every queue
should obey this property. Should probably be used with an unordered queue
model, because we don't look for alternate orderings. O(n)."
(reify Checker
(check [this test model history]
(let [final (->> history
(r/filter (fn select [op]
(condp = (:f op)
:enqueue (knossos/invoke? op)
:dequeue (knossos/ok? op)
false)))
(reduce knossos/step model))]
(if (knossos/inconsistent? final)
{:valid? false
:error (:msg final)}
{:valid? true
:final-queue final})))))
(def set
"Given a set of :add operations followed by a final :read, verifies that
every successfully added element is present in the read, and that the read
contains only elements for which an add was attempted."
(reify Checker
(check [this test model history]
(let [attempts (->> history
(r/filter knossos/invoke?)
(r/filter #(= :add (:f %)))
(r/map :value)
(into #{}))
adds (->> history
(r/filter knossos/ok?)
(r/filter #(= :add (:f %)))
(r/map :value)
(into #{}))
final-read (->> history
(r/filter knossos/ok?)
(r/filter #(= :read (:f %)))
(r/map :value)
(reduce (fn [_ x] x) nil))]
(if-not final-read
{:valid? false
:error "Set was never read"})
(let [; The OK set is every read value which we tried to add
ok (set/intersection final-read attempts)
; Unexpected records are those we *never* attempted.
unexpected (set/difference final-read attempts)
; Lost records are those we definitely added but weren't read
lost (set/difference adds final-read)
; Recovered records are those where we didn't know if the add
; succeeded or not, but we found them in the final set.
recovered (set/difference ok adds)]
{:valid? (and (empty? lost) (empty? unexpected))
:ok (util/integer-interval-set-str ok)
:lost (util/integer-interval-set-str lost)
:unexpected (util/integer-interval-set-str unexpected)
:recovered (util/integer-interval-set-str recovered)
:ok-frac (util/fraction (count ok) (count attempts))
:unexpected-frac (util/fraction (count unexpected) (count attempts))
:lost-frac (util/fraction (count lost) (count attempts))
:recovered-frac (util/fraction (count recovered) (count attempts))})))))
(defn fraction
"a/b, but if b is zero, returns unity."
[a b]
(if (zero? b)
1
(/ a b)))
(def total-queue
"What goes in *must* come out. Verifies that every successful enqueue has a
successful dequeue. Queues only obey this property if the history includes
draining them completely. O(n)."
(reify Checker
(check [this test model history]
(let [attempts (->> history
(r/filter knossos/invoke?)
(r/filter #(= :enqueue (:f %)))
(r/map :value)
(into (multiset/multiset)))
enqueues (->> history
(r/filter knossos/ok?)
(r/filter #(= :enqueue (:f %)))
(r/map :value)
(into (multiset/multiset)))
dequeues (->> history
(r/filter knossos/ok?)
(r/filter #(= :dequeue (:f %)))
(r/map :value)
(into (multiset/multiset)))
; The OK set is every dequeue which we attempted.
ok (multiset/intersect dequeues attempts)
; Unexpected records are those we *never* attempted. Maybe
; duplicates, maybe leftovers from some earlier state. Definitely
; don't want your queue emitting records from nowhere!
unexpected (multiset/minus dequeues attempts)
; lost records are ones which we definitely enqueued but never
; came out.
lost (multiset/minus enqueues dequeues)
; Recovered records are dequeues where we didn't know if the enqueue
; suceeded or not, but an attempt took place.
recovered (multiset/minus ok enqueues)]
{:valid? (and (empty? lost) (empty? unexpected))
:lost lost
:unexpected unexpected
:recovered recovered
:ok-frac (util/fraction (count ok) (count attempts))
:unexpected-frac (util/fraction (count unexpected) (count attempts))
:lost-frac (util/fraction (count lost) (count attempts))
:recovered-frac (util/fraction (count recovered) (count attempts))}))))
(def counter
"A counter starts at zero; add operations should increment it by that much,
and reads should return the present value. This checker validates that at
each read, the value is at greater than the sum of all :ok increments, and
lower than the sum of all attempted increments.
Note that this counter verifier assumes the value monotonically increases. If
you want to increment by negative amounts, you'll have to recalculate and
possibly widen the intervals for all pending reads with each invoke/ok write.
Returns a map:
{:valid? Whether the counter remained within bounds
:reads [[lower-bound read-value upper-bound] ...]
:errors [[lower-bound read-value upper-bound] ...]
:max-absolute-error The [lower read upper] where read falls furthest outside
:max-relative-error Same, but with error computed as a fraction of the mean}
"
(reify Checker
(check [this test model history]
(loop [history (seq (history/complete history))
lower 0 ; Current lower bound on counter
upper 0 ; Upper bound on counter value
pending-reads {} ; Process ID -> [lower read-val]
reads []] ; Completed [lower val upper]s
(if (nil? history)
; We're done here
(let [errors (remove (partial apply <=) reads)]
{:valid? (empty? errors)
:reads reads
:errors errors})
; But wait, there's more
(let [op (first history)
history (next history)]
(case [(:type op) (:f op)]
[:invoke :read]
(recur history lower upper
(assoc pending-reads (:process op) [lower (:value op)])
reads)
[:ok :read]
(let [r (get pending-reads (:process op))]
(recur history lower upper
(dissoc pending-reads (:process op))
(conj reads (conj r upper))))
[:invoke :add]
(recur history lower (+ upper (:value op)) pending-reads reads)
[:ok :add]
(recur history (+ lower (:value op)) upper pending-reads reads)
(recur history lower upper pending-reads reads))))))))
(defn compose
"Takes a map of names to checkers, and returns a checker which runs each
check (possibly in parallel) and returns a map of names to results; plus a
top-level :valid? key which is true iff every checker considered the history
valid."
[checker-map]
(reify Checker
(check [this test model history]
(let [results (->> checker-map
(pmap (fn [[k checker]]
[k (check checker test model history)]))
(into {}))]
(assoc results :valid? (every? :valid? (vals results)))))))
Jump to Line
Something went wrong with that request. Please try again.