Configuration
A Crux node consists of a number of modules, which can be independently configured and augmented.
Once you have an in-memory Crux node set up, you can then start to configure the various modules - either through a JSON config file, EDN config file, or programmatically:
On the command line, you can supply a JSON/EDN configuration file using -f <file>
For a Java in-process node, the modules are configured using the supplied Configurator, a file, or a classpath resource:
import crux.api.Crux;
import crux.api.ICruxAPI;
ICruxAPI cruxNode = Crux.startNode(new File("resources/config.json"));
ICruxAPI cruxNode = Crux.startNode(MyApp.class.getResource("config.json"));
ICruxAPI cruxNode = Crux.startNode(n -> {
// ...
});
For a Kotlin in-process node, the modules are configured using the supplied Configurator, a file, or a classpath resource:
import crux.api.Crux
val cruxNode: ICruxAPI = Crux.startNode(File("config.json"))
val cruxNode = Crux.startNode(MyApp::class.java.getResource("config.json"))
val cruxNode = Crux.startNode { n ->
// ...
}
For a Clojure in-process node, the start-node
function accepts a module tree, a file, or a resource.
(require '[crux.api :as crux]
'[clojure.java.io :as io])
(crux/start-node (io/file "resources/config.json"))
(crux/start-node (io/resource "config.json"))
(crux/start-node {
;; Configuration Map
})
Without any explicit configuration, Crux will start an in-memory node.
At this point, you can start submitting transactions and running queries!
Modules
Crux has three main pluggable components - the transaction log, the document store, and the query index store. All three are backed by local KV stores by default, but they can be independently configured and overridden - you might choose to host the transaction log in Kafka, the document store in AWS’s S3, and the query indices in RocksDB.
Transaction Log | Document Store | Index Store | |
---|---|---|---|
✓ |
|||
✓ |
|||
✓ |
|||
✓ |
✓ |
||
✓ |
✓ |
||
In-memory KV |
✓ |
✓ |
✓ |
LMDB (KV) |
✓ |
✓ |
✓ |
RocksDB (KV) |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
For specific details and examples of how to configure each of these modules, see their individual sections.
Each module has both an underlying implementation and overridable parameters - for each module, you can choose to keep the implementation and override its parameters, or you can choose to override the implementation entirely.
To add the HTTP server module, and specify its port:
ICruxAPI cruxNode = Crux.startNode(n -> {
n.with("crux.http-server/server", http -> {
http.set("port", 3000);
});
});
val cruxNode = Crux.startNode { n ->
n.with("crux.http-server/server") { http ->
http["port"] = 3000
}
}
{
"crux.http-server/server": {
"port": 3000
}
}
(crux/start-node {:crux.http-server/server {:port 3000}})
{:crux.http-server/server {:port 3000}}
Overriding the module implementation
To override the underlying implementation, specify the factory function of the new implementation.
For example, using S3’s crux.s3/->document-store
factory:
ICruxAPI cruxNode = Crux.startNode(n -> {
n.with("crux/document-store", docStore -> {
docStore.module("crux.s3/->document-store");
docStore.set("bucket", "my-bucket");
docStore.set("prefix", "my-prefix");
});
});
val cruxNode = Crux.startNode { n ->
n.with("crux/document-store") { docStore ->
docStore.module("crux.s3/->document-store")
docStore["bucket"] = "my-bucket"
docStore["prefix"] = "my-prefix"
}
}
{
"crux/document-store": {
"crux/module": "crux.s3/->document-store",
"bucket": "my-bucket",
"prefix": "my-prefix"
}
}
(crux/start-node {:crux/document-store {:crux/module 'crux.s3/->document-store
:bucket "my-bucket"
:prefix "my-prefix"}})
{:crux/document-store {:crux/module crux.s3/->document-store
:bucket "my-bucket"
:prefix "my-prefix"}}
Nested modules
Modules in Crux form an arbitrarily nested tree - parent modules depend on child modules.
For example, the default implementations of the three main Crux modules are KV store backed implementations - the KV transaction log, the KV document store and the KV index store.
Each of these implementations depends on being given a concrete KV store implementation - by default, an in-memory KV store.
To override the implementation and parameters of this KV store (for example, to replace it with RocksDB), we override its kv-store
dependency, replacing the implementation of the nested module:
ICruxAPI cruxNode = Crux.startNode(n -> {
n.with("crux/tx-log", txLog -> {
txLog.with("kv-store", kv -> {
kv.module("crux.rocksdb/->kv-store");
kv.set("db-dir", new File("/tmp/rocksdb"));
});
});
n.with("crux/document-store", docStore -> { ... });
n.with("crux/index-store", indexStore -> { ... });
});
val cruxNode = Crux.startNode{ n ->
n.with("crux/tx-log") { txLog ->
txLog.with("kv-store") { kv ->
kv.module("crux.rocksdb/->kv-store")
kv["db-dir"] = File("/tmp/rocksdb")
}
}
n.with("crux/document-store") { docStore -> ... }
n.with("crux/index-store") { indexStore -> ... }
}
{
"crux/tx-log": {
"kv-store": {
"crux/module": "crux.rocksdb/->kv-store",
"db-dir": "/tmp/txs"
}
},
"crux/document-store": { ... },
"crux/index-store": { ... }
}
(crux/start-node {:crux/tx-log {:kv-store {:crux/module 'crux.rocksdb/->kv-store
:db-dir (io/file "/tmp/txs")}}
:crux/document-store { }
:crux/index-store { }
})
{:crux/tx-log {:kv-store {:crux/module crux.rocksdb/->kv-store
:db-dir "/tmp/txs"}}
:crux/document-store {...}
:crux/index-store {...}}
The tx-log and document-store are considered 'golden stores'. The query indices can, should you wish to, be thrown away and rebuilt from these golden stores. Ensure that you either persist both or neither of these golden stores. If not, Crux will work fine until you restart the node, at which point some will evaporate, but others will remain. Crux tends to get rather confused in this situation! Likewise, if you persist the query indices, you’ll need to persist both the golden stores. |
Sharing modules - references
When two modules depend on a similar type of module, by default, they get an instance each. For example, if we were to write the following, the transaction log and the document store would get their own RocksDB instance:
{
"crux/tx-log": {
"kv-store": {
"crux/module": "crux.rocksdb/->kv-store",
"db-dir": "/tmp/txs"
}
},
"crux/document-store": {
"kv-store": {
"crux/module": "crux.rocksdb/->kv-store",
"db-dir": "/tmp/docs"
}
}
}
We can store both the transaction log and the document store in the same KV store, to save ourselves some hassle. We specify a new top-level module, and then refer to it by name where required:
ICruxAPI cruxNode = Crux.startNode(n -> {
n.with("my-rocksdb", rocks -> {
rocks.module("crux.rocksdb/->kv-store");
rocks.set("db-dir", new File("/tmp/rocksdb"));
});
n.with("crux/document-store", docStore -> {
docStore.with("kv-store", "my-rocksdb");
});
n.with("crux/tx-log", txLog -> {
txLog.with("kv-store", "my-rocksdb");
});
});
val cruxNode = Crux.startNode { n ->
n.with("my-rocksdb") { rocks ->
rocks.module("crux.rocksdb/->kv-store")
rocks["db-dir"] = File("/tmp/rocksdb")
}
n.with("crux/document-store") { docStore ->
docStore["kv-store"] = "my-rocksdb"
}
n.with("crux/tx-log") { txLog ->
txLog["kv-store"] = "my-rocksdb"
}
}
{
"my-rocksdb": {
"crux/module": "crux.rocksdb/->kv-store",
"db-dir": "/tmp/txs"
},
"crux/tx-log": {
"kv-store": "my-rocksdb"
},
"crux/document-store": {
"kv-store": "my-rocksdb"
}
}
(crux/start-node {:my-rocksdb {:crux/module 'crux.rocksdb/->kv-store
:db-dir (io/file "/tmp/rocksdb")}
:crux/tx-log {:kv-store :my-rocksdb}
:crux/document-store {:kv-store :my-rocksdb}})
{:my-rocksdb {:crux/module crux.rocksdb/->kv-store
:db-dir "/tmp/rocksdb"}
:crux/tx-log {:kv-store :my-rocksdb}
:crux/document-store {:kv-store :my-rocksdb}}
Writing your own module (Clojure)
Crux modules are (currently) vanilla 1-arg Clojure functions with some optional metadata to specify dependencies and arguments.
By convention, these are named ->your-component
, to signify that it’s returning an instance of your component.
If the value returned implements AutoCloseable
/Closeable
, the module will be closed when the Crux node is stopped.
The most basic component would be just a Clojure function, returning the started module:
(defn ->server [opts]
;; start your server
)
You can specify arguments using the :crux.system/args
metadata key - this example declares a required :port
option, checked against the given spec, defaulting to 3000:
(require '[crux.system :as sys])
(defn ->server {::sys/args {:port {:spec ::sys/int
:doc "Port to start the server on"
:required? true
:default 3000}}}
[{:keys [port] :as options}]
;; start your server
)
You can specify dependencies using :crux.system/deps
- a map of the dependency key to its options.
The options takes the same form as the end-user options - you can specify :crux/module
for the default implementation, as well as any parameters.
The started dependencies are passed to you as part of the function’s parameter, with the args
.
Bear in mind that any options you do specify can be overridden by end-users!
(defn ->server {::sys/deps {:other-module {:crux/module `->other-module
:param "value"}
...}}
[{:keys [other-module]}]
;; start your server
)
You can also use refs - for example, to depend on the Crux node:
(defn ->server {::sys/deps {:crux-node :crux/node}
::sys/args {:spec ::sys/int
:doc "Port to start the server on"
:required? true
:default 3000}}
[{:keys [crux-node] :as options}]
;; start your server
)
crux-xodus
module