Compare and Swap with Crux – Saturn Assignment

Introduction

This is the cas (compare and swap) installment of the Crux tutorial.

Setup

You need to get Crux running before you can use it.

{:deps
 {org.clojure/clojure {:mvn/version "1.10.0"}
  org.clojure/tools.deps.alpha
  {:git/url "https://github.com/clojure/tools.deps.alpha.git"
   :sha "f6c080bd0049211021ea59e516d1785b08302515"}
  juxt/crux-core {:mvn/version "RELEASE"}}}
deps.edn
Extensible Data Notation
(require '[crux.api :as crux])
11.2s
Clojure

Arrival on Saturn

As you pass through the innermost ring of Saturn, A warning light appears on your communications panel. You open the message. It’s from Space Customs and reads:

"We extend you the warmest welcome.
We must check papers before we can give you permission to land."
 Cronus Peaceful Nations 
Clojure

They are asking to see your flight manifest.

Choose your path:

You have your manifest : You have permission to land, continue to the space port.

You do not have your manifest : You do not have permission to land. You can either return to Neptune or continue at your own risk.

Space Port

As you prepare to land you open your Crux manual to the page on Compare and Swap (cas)

"Currently there are only four transaction operations in Crux: put, delete, cas and evict.
		Transaction 	(Description)
    put    		(Writes a version of a document)
    delete    (Deletes a version of a document)
    cas       (Writes new version of a document if the old version is as expected)
    evict    	(Removes an document entirely)
CAS:
The CAS operation (compare and swap) swaps an existing document version with a newer one, if the existing document is as expected.
Along with the old doc and the new doc cas, like put, gives you the option of providing a start and an end valid-time.
A complete cas transaction has the form:
[:crux.tx/cas old-doc new-doc valid-time-start valid-time-end]
If the old-doc does not match the one stored in the system at the given valid time then the operation will not be performed. If there are multiple operations in the same transaction then none of these will happen either.
Note that if there is no old-doc in the system, you can provide `nil` in its place."
 Crux Manual
Crux Manual
Clojure

Read More

You are happy with what you have read, and in anticipation of the assignment you define the standalone node.

(def crux
  (crux/start-node
               {:crux.node/topology :crux.standalone/topology
                :crux.node/kv-store "crux.kv.memdb/kv"
                :crux.standalone/event-log-dir "data/eventlog-1"
                :crux.kv/db-dir "data/db-dir-1"
                :crux.standalone/event-log-kv-store "crux.kv.memdb/kv"}))
5.8s
Clojure
user/crux

Assignment

As you land on the surface of Saturn the job ticket for this assignment is unlocked.

Ticket

Task 			"Secure trading system"
Company		"Cronus Market Technologies"
Contact 			"Ubuku Eppimami"
Submitted "2115-02-23T13:38:20"
Additional information:
"We need to be shown how to ensure no trades are done without the buyer and seller having the necessary funds or stock respectively"
Job Ticket
Clojure
example_data.txt

The next shuttle to the CMT office leaves in 5 Earth minutes. While you wait you use your easy ingest function you created on Pluto to put the example data into your system.

(defn easy-ingest
  "Uses Crux put transaction to add a vector of 
  documents to a specified node"
  [node docs]
  (crux/submit-tx node
                  (vec (for [doc docs]
                         [:crux.tx/put doc]))))
(def data
  [{:crux.db/id :gold-harmony
    :company-name "Gold Harmony"
    :seller? true
    :buyer? false
    :units/Au 10211
    :credits 51}
   {:crux.db/id :tombaugh-resources
    :company-name "Tombaugh Resources Ltd."
    :seller? true
    :buyer? false
    :units/Pu 50
    :units/N 3
    :units/CH4 92
    :credits 51}
   {:crux.db/id :encompass-trade
    :company-name "Encompass Trade"
    :seller? true
    :buyer? true
    :units/Au 10
    :units/Pu 5
    :units/CH4 211
    :credits 1002}
   {:crux.db/id :blue-energy
    :seller? false
    :buyer? true
    :company-name "Blue Energy"
    :credits 1000}])
(easy-ingest crux data)
0.4s
Clojure
Map {:crux.tx/tx-id: 1609118372077569, :crux.tx/tx-time: #inst "2019-10-18T13:17:40.232Z"}

You also decide to make some Clojure functions so you can easily show Ubuku the stock and fund levels after the trades.

(defn stock-check
  [company-id item]
  {:result (crux/q (crux/db crux)
                   {:find '[name funds stock]
                    :where ['[e :company-name name]
                            '[e :credits funds]
                            ['e item 'stock]]
                    :args [{'e company-id}]})
   :item item})
(defn format-stock-check
  [{:keys [result item] :as stock-check}]
  (for [[name funds commod] result]
    (str "Name: " name ", Funds: " funds ", " item " " commod)))
0.2s
Clojure
user/format-stock-check

Just as you are finishing off your shuttle arrives.

After a short journey through the icy lower clouds of Saturn you are met by a friendly faced Ubuku.

"Hello friend.
We have been using Crux for a short time now and think it is great. The problem is a human one. Occasionally we process trades without checking that are enough funds in the buyers account.
I know there is a way that we can stop this happening in Crux.
I sent you some example data in the job ticket for you to use, I trust you found it.
Do you think you can help us?"
 Ubuku Eppimami
CMT
Clojure

Choose your path:

"Yes, I'll give it a go." : Continue to complete the assignment.

"I'm not even sure how to begin" : Take some time to read through the Crux manual again. If you're still unsure then you can follow along anyway and see if things become clear.

Assignment

You explain to Ubuku that all they need to do to solve their problem is to use the cas operation instead of put when they are processing their trades.

You show Ubuku the cas operation for a valid transaction. You move 10 units of Methane (:units/CH4) each at the cost of 100 credits to Blue Energy:

(crux/submit-tx
 crux
 [[:crux.tx/cas
   ;; Old doc
   {:crux.db/id :blue-energy
    :seller? false
    :buyer? true
    :company-name "Blue Energy"
    :credits 1000}
   ;; New doc
   {:crux.db/id :blue-energy
    :seller? false
    :buyer? true
    :company-name "Blue Energy"
    :credits 900
    :units/CH4 10}]
  [:crux.tx/cas
   ;; Old doc
   {:crux.db/id :tombaugh-resources
    :company-name "Tombaugh Resources Ltd."
    :seller? true
    :buyer? false
    :units/Pu 50
    :units/N 3
    :units/CH4 92
    :credits 51}
   ;; New doc
   {:crux.db/id :tombaugh-resources
    :company-name "Tombaugh Resources Ltd."
    :seller? true
    :buyer? false
    :units/Pu 50
    :units/N 3
    :units/CH4 82
    :credits 151}]])
0.2s
Clojure
Map {:crux.tx/tx-id: 1609118372556801, :crux.tx/tx-time: #inst "2019-10-18T13:17:40.700Z"}

You explain that because the old doc is as expected for both the buyer and the seller that the transaction goes through.

You show Ubuku the result of the trade using the function you created earlier:

(format-stock-check (stock-check :tombaugh-resources :units/CH4))
0.4s
Clojure
List(1) ("Name: Tombaugh Resources Ltd., Funds: 51, :units/CH4 92")
(format-stock-check (stock-check :blue-energy :units/CH4))
0.1s
Clojure
List(1) ("Name: Blue Energy, Funds: 900, :units/CH4 10")

They are happy that this works as he sees the 1000 credits move from Blue energy to Tombaugh Resources Ltd. and 10 units of Methane the other way.

Ubuku asks if you can show them what would happen if there was not enough funds in the account of a buyer. You show him a trade where the old doc is not as expected for Encompass trade, to buy 10,000 units of Gold from Gold Harmony.

(crux/submit-tx
 crux
 [[:crux.tx/cas
   ;; Old doc
   {:crux.db/id :gold-harmony
    :company-name "Gold Harmony"
    :seller? true
    :buyer? false
    :units/Au 10211
    :credits 51}
   ;; New doc
   {:crux.db/id :gold-harmony
    :company-name "Gold Harmony"
    :seller? true
    :buyer? false
    :units/Au 211
    :credits 51}]
  [:crux.tx/cas
   ;; Old doc
   {:crux.db/id :encompass-trade
    :company-name "Encompass Trade"
    :seller? true
    :buyer? true
    :units/Au 10
    :units/Pu 5
    :units/CH4 211
    :credits 100002}
   ;; New doc
   {:crux.db/id :encompass-trade
    :company-name "Encompass Trade"
    :seller? true
    :buyer? true
    :units/Au 10010
    :units/Pu 5
    :units/CH4 211
    :credits 1002}]])
0.2s
Clojure
Map {:crux.tx/tx-id: 1609118373218305, :crux.tx/tx-time: #inst "2019-10-18T13:17:41.346Z"}
(format-stock-check (stock-check :gold-harmony :units/Au))
0.1s
Clojure
List(1) ("Name: Gold Harmony, Funds: 51, :units/Au 10211")
(format-stock-check (stock-check :encompass-trade :units/Au))
0.1s
Clojure
List(1) ("Name: Encompass Trade, Funds: 1002, :units/Au 10")

You explain to Ubuku that this time, because you have both cas operations in the same transaction, the trade does not go through. The accounts remain the same, even though the failing cas was the second operation.

Ubuku thanks you. This is just what they are looking for. You head back to the space station to see if there is another assignment waiting for you.

Space Port

Back at the spaceship there is a light waiting for you on your communications panel.

"Well done, you’ve had a productive week. We have one final task for you to do before you finish for the week
You need to go to Jupiter and meet Kaarlang, it’s his last day working for us and he needs to delete his trade clients from his personal Crux node for data protection."
 Helios Banking Inc. 
Communications Panel
Clojure

You update your manifest with your most recent badge.

(crux/submit-tx
 crux
 [[:crux.tx/put 
   {:crux.db/id :manifest
    :pilot-name "Johanna"
    :id/rocket "SB002-sol"
    :id/employee "22910x2"
    :badges ["SETUP" "PUT" "DATALOG-QUERIES" "CAS"]
    :cargo ["stereo" "gold fish" "slippers" "secret note"]}]])
0.1s
Clojure
Map {:crux.tx/tx-id: 1609118373431297, :crux.tx/tx-time: #inst "2019-10-18T13:17:41.554Z"}

As you do so, you check to see if you still have the note that the porter gave you for Kaarlang back on Earth.

(crux/q (crux/db crux)
          {:find '[belongings]
           :where '[[e :cargo belongings]]
           :args [{'belongings "secret note"}]})
0.1s
Clojure
Set(1) #{Vector(1)}

Feeling a bit apprehensive, you enter countdown for lift off to Jupiter. See you soon.

Runtimes (1)