Configuring

Modules

A XTDB node consists of a number of modules, which can be independently configured and augmented.

Once you have an in-memory XTDB node set up, you can then start to configure the various modules. Configure modules using a JSON config file, EDN config file, or programmatically:

  • Command Line

  • Java

  • Kotlin

  • Clojure

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 xtdb.api.IXtdb;

IXtdb xtdbNode = IXtdb.startNode(new File("resources/config.json"));

IXtdb xtdbNode = IXtdb.startNode(MyApp.class.getResource("config.json"));

IXtdb xtdbNode = IXtdb.startNode(n -> {
    // ...
});

For a Kotlin in-process node, the modules are configured using the supplied Configurator, a file, or a classpath resource:

import xtdb.api.IXtdb


val xtdbNode: IXtdb = IXtdb.startNode(File("config.json"))

val xtdbNode = IXtdb.startNode(MyApp::class.java.getResource("config.json"))

val xtdbNode = IXtdb.startNode { n ->
    // ...
}

For a Clojure in-process node, the start-node function accepts a module tree, a file, or a resource.

(require '[xtdb.api :as xt]
         '[clojure.java.io :as io])

(xt/start-node (io/file "resources/config.json"))

(xt/start-node (io/resource "config.json"))

(xt/start-node {
                ;; Configuration Map
                })

Without any explicit configuration, XTDB will start an in-memory node.

At this point, you can start submitting transactions and running queries!

Storage

XTDB has three main pluggable persistence stores: 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

AWS S3

Azure Blobs

DLT - Corda [1]

Google Cloud Storage

Kafka

JDBC

In-memory KV

LMDB (KV)

RocksDB (KV)

Xodus (KV) [2]

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:

  • Java

  • Kotlin

  • JSON

  • Clojure

  • EDN

IXtdb xtdbNode = IXtdb.startNode(n -> {
    n.with("xtdb.http-server/server", http -> {
        http.set("port", 3000);
    });
});
val xtdbNode = IXtdb.startNode { n ->
    n.with("xtdb.http-server/server") { http ->
        http["port"] = 3000
    }
}
{
  "xtdb.http-server/server": {
    "port": 3000
  }
}
(xt/start-node {:xtdb.http-server/server {:port 3000}})
{:xtdb.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 xtdb.s3/->document-store factory:

  • Java

  • Kotlin

  • JSON

  • Clojure

  • EDN

IXtdb xtdbNode = IXtdb.startNode(n -> {
    n.with("xtdb/document-store", docStore -> {
        docStore.module("xtdb.s3/->document-store");
        docStore.set("bucket", "my-bucket");
        docStore.set("prefix", "my-prefix");
    });
});
val xtdbNode = IXtdb.startNode { n ->
    n.with("xtdb/document-store") { docStore ->
        docStore.module("xtdb.s3/->document-store")
        docStore["bucket"] = "my-bucket"
        docStore["prefix"] = "my-prefix"
    }
}
{
  "xtdb/document-store": {
    "xtdb/module": "xtdb.s3/->document-store",
    "bucket": "my-bucket",
    "prefix": "my-prefix"
  }
}
(xt/start-node {:xtdb/document-store {:xtdb/module 'xtdb.s3/->document-store
                                      :bucket "my-bucket"
                                      :prefix "my-prefix"}})
{:xtdb/document-store {:xtdb/module xtdb.s3/->document-store
                       :bucket "my-bucket"
                       :prefix "my-prefix"}}

Nested modules

Modules in XTDB form an arbitrarily nested tree - parent modules depend on child modules. For example, the default implementations of the three main XTDB 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:

  • Java

  • Kotlin

  • JSON

  • Clojure

  • EDN

IXtdb xtdbNode = IXtdb.startNode(n -> {
    n.with("xtdb/tx-log", txLog -> {
        txLog.with("kv-store", kv -> {
            kv.module("xtdb.rocksdb/->kv-store");
            kv.set("db-dir", new File("/tmp/rocksdb"));
        });
    });
    n.with("xtdb/document-store", docStore -> { ... });
    n.with("xtdb/index-store", indexStore -> { ... });
});
val xtdbNode = IXtdb.startNode{ n ->
    n.with("xtdb/tx-log") { txLog ->
        txLog.with("kv-store") { kv ->
            kv.module("xtdb.rocksdb/->kv-store")
            kv["db-dir"] = File("/tmp/rocksdb")
        }
    }
    n.with("xtdb/document-store") { docStore -> ... }
    n.with("xtdb/index-store") { indexStore -> ... }
}
{
  "xtdb/tx-log": {
    "kv-store": {
      "xtdb/module": "xtdb.rocksdb/->kv-store",
      "db-dir": "/tmp/txs"
    }
  },
  "xtdb/document-store": { ... },
  "xtdb/index-store": { ... }
}
(xt/start-node {:xtdb/tx-log {:kv-store {:xtdb/module 'xtdb.rocksdb/->kv-store
                                         :db-dir (io/file "/tmp/txs")}}
                  :xtdb/document-store { }
                  :xtdb/index-store { }
})
{:xtdb/tx-log {:kv-store {:xtdb/module xtdb.rocksdb/->kv-store
                          :db-dir "/tmp/txs"}}
 :xtdb/document-store {...}
 :xtdb/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, XTDB will work fine until you restart the node, at which point some will evaporate, but others will remain. XTDB 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:

{
  "xtdb/tx-log": {
    "kv-store": {
      "xtdb/module": "xtdb.rocksdb/->kv-store",
      "db-dir": "/tmp/txs"
    }
  },
  "xtdb/document-store": {
    "kv-store": {
      "xtdb/module": "xtdb.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:

  • Java

  • Kotlin

  • JSON

  • Clojure

  • EDN

IXtdb xtdbNode = IXtdb.startNode(n -> {
    n.with("my-rocksdb", rocks -> {
        rocks.module("xtdb.rocksdb/->kv-store");
        rocks.set("db-dir", new File("/tmp/rocksdb"));
    });
    n.with("xtdb/document-store", docStore -> {
        docStore.with("kv-store", "my-rocksdb");
    });
    n.with("xtdb/tx-log", txLog -> {
        txLog.with("kv-store", "my-rocksdb");
    });
});
val xtdbNode = IXtdb.startNode { n ->
    n.with("my-rocksdb") { rocks ->
        rocks.module("xtdb.rocksdb/->kv-store")
        rocks["db-dir"] = File("/tmp/rocksdb")
    }
    n.with("xtdb/document-store") { docStore ->
        docStore["kv-store"] = "my-rocksdb"
    }
    n.with("xtdb/tx-log") { txLog ->
        txLog["kv-store"] = "my-rocksdb"
    }
}
{
  "my-rocksdb": {
    "xtdb/module": "xtdb.rocksdb/->kv-store",
    "db-dir": "/tmp/txs"
  },
  "xtdb/tx-log": {
    "kv-store": "my-rocksdb"
  },
  "xtdb/document-store": {
    "kv-store": "my-rocksdb"
  }
}
(xt/start-node {:my-rocksdb {:xtdb/module 'xtdb.rocksdb/->kv-store
                             :db-dir (io/file "/tmp/rocksdb")}
                :xtdb/tx-log {:kv-store :my-rocksdb}
                :xtdb/document-store {:kv-store :my-rocksdb}})
{:my-rocksdb {:xtdb/module xtdb.rocksdb/->kv-store
              :db-dir "/tmp/rocksdb"}
 :xtdb/tx-log {:kv-store :my-rocksdb}
 :xtdb/document-store {:kv-store :my-rocksdb}}

Query Engine

There are a number of different defaults that can be overridden within the XTDB query engine.

  • JSON

  • Clojure

  • EDN

{
"xtdb/query-engine": {
"fn-allow-list": ["foo/bar"]
}
}
{:xtdb/query-engine {:fn-allow-list '[foo/bar]}}
{:xtdb/query-engine {:fn-allow-list [foo/bar]}

Parameters

  • entity-cache-size (int, default 32768): query entity cache size (number of entries, not bytes).

  • query-timeout (int, default 30000): query timeout in milliseconds.

  • batch-size (int, default 100): batch size of results.

  • fn-allow-list (Predicate Allowlist, default nil): list of allowed namespaces/functions in predicate functions.

Predicate Allowlist

By default, users can invoke any predicate function in their queries. To restrict what functions a user can invoke, you can specify a :fn-allow-list of safe functions/namespaces. We include a set of pure functions from clojure.core, for convenience.

Allowlists can be a list of the following:

  • Unqualified symbols/strings, for example, clojure.set. These are assumed to be namespaces, and all functions within that namespace are allowed.

  • Fully qualified symbols/strings, for example, clojure.string/capitalize. These are assumed to be a permitted function.

Upgrading

Break Versioning

XTDB follows a Break Versioning scheme.

This versioning emphasizes the maximum amount of impact an XTDB version update could have on users, and recognises that there are only two types of version bumps: those that are safe, and those that require you to carefully read the changelog.

It is intented to be comfortable to follow strictly and be reliable in practice.

<major>.<minor>.<non-breaking>[-<optional-qualifier>]:
------------------------------------------------------
<major>              - Major breaking changes [or discretionary "major non-breaking changes"]
<minor>              - Minor breaking changes [or discretionary "minor non-breaking changes"]
<non-breaking>       - Strictly no breaking changes
<optional-qualifier> - Tag-style qualifiers: -alpha1, -RC2, etc.

Index Rebuilding

Upgrading a node from XTDB version 1.X.n to 1.Y.n (a minor bump) often requires rebuilding the node’s local KV index-store from the golden stores, i.e. the transaction log and the document store. However, it isn’t always the case that minor bumps require an index rebuild and this distinction is made clear in the release notes for a given version.

You can perform the rebuild process by simply shutting down the node and removing the index-store’s db-dir (and similarly for Lucene’s db-dir indexes where xtdb-lucene is enabled). XTDB will then rebuild the indices when the node is restarted.

During development and other manual interactions, it is strongly suggested to rename or move the index directories rather than delete them, in case you need to rollback.

Writing Custom Modules

XTDB 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 XTDB 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 :xtdb.system/args metadata key - this example declares a required :port option, checked against the given spec, defaulting to 3000:

(require '[xtdb.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 :xtdb.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 :xtdb/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 {:xtdb/module `->other-module
:param "value"}
...}}
[{:keys [other-module]}]
;; start your server
)

You can also use refs - for example, to depend on the XTDB node:

(defn ->server {::sys/deps {:xtdb-node :xtdb/node}
::sys/args {:spec ::sys/int
:doc "Port to start the server on"
:required? true
:default 3000}}
[{:keys [xtdb-node] :as options}]
;; start your server
)

Environment Variables

Environment variable JVM property Description

XTDB_ENABLE_JAVA_TIME_PRINT_METHODS

xtdb.enable-java-time-print-methods

Adds print-method/print-dup support for common java.time classes

XTDB_ENABLE_BASE64_PRINT_METHOD

xtdb.enable-base64-print-method

Adds print-method/print-dup support for Java byte arrays

MALLOC_ARENA_MAX

If you’re using RocksDB and seeing out-of-memory issues, we recommend setting the environment variable MALLOC_ARENA_MAX=2 - see this issue for more details.

XTDB_ENABLE_BYTEUTILS_SHA1

Set to use XT’s Java SHA1 implementation rather than system libraries.

XTDB_DISABLE_LIBCRYPTO

Set to disable use of libcrypto (OpenSSL) for SHA1 hashing

XTDB_DISABLE_LIBGCRYPT

Set to disable use of libgcrypt for SHA1 hashing.


1. configured via its own entry point - see the module docs for more information
2. via third-party crux-xodus module