Skip to content

Commit 940667d

Browse files
Add commitlog knobs to server config (#5074)
# Description of Changes Adds the commitlog knobs `max_segment_size`, `write_buffer_size`, and `preallocate_segments` to the server's `config.toml`. There are other seemingly more advanced knobs that I did not add at this time. This patch also increases the `DEFAULT_WRITE_BUFFER_SIZE` from `8KiB` to `128KiB` to optimize high throughput workloads like the keynote-2 benchmark. # API and ABI breaking changes None # Expected complexity level and risk 1 # Testing Manual
1 parent 8641c17 commit 940667d

9 files changed

Lines changed: 216 additions & 10 deletions

File tree

crates/commitlog/src/lib.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ pub struct Options {
7070
/// If `true`, require that the segment must be synced to disk before an
7171
/// index entry is added.
7272
///
73-
/// Setting this to `false` (the default) will update the index every
73+
/// Setting this to `false` will update the index every
7474
/// `offset_index_interval_bytes`, even if the commitlog wasn't synced.
7575
/// This means that the index could contain non-existent entries in the
7676
/// event of a crash.
@@ -80,7 +80,7 @@ pub struct Options {
8080
/// This means that the index could contain fewer index entries than
8181
/// strictly every `offset_index_interval_bytes`.
8282
///
83-
/// Default: false
83+
/// Default: true
8484
#[cfg_attr(
8585
feature = "serde",
8686
serde(default = "Options::default_offset_index_require_segment_fsync")
@@ -95,7 +95,7 @@ pub struct Options {
9595
/// Size in bytes of the memory buffer holding commit data before flushing
9696
/// to storage.
9797
///
98-
/// Default: 8KiB
98+
/// Default: 128KiB
9999
#[cfg_attr(feature = "serde", serde(default = "Options::default_write_buffer_size"))]
100100
pub write_buffer_size: usize,
101101
}
@@ -111,7 +111,7 @@ impl Options {
111111
pub const DEFAULT_OFFSET_INDEX_INTERVAL_BYTES: NonZeroU64 = NonZeroU64::new(4096).expect("4096 > 0, qed");
112112
pub const DEFAULT_OFFSET_INDEX_REQUIRE_SEGMENT_FSYNC: bool = true;
113113
pub const DEFAULT_PREALLOCATE_SEGMENTS: bool = false;
114-
pub const DEFAULT_WRITE_BUFFER_SIZE: usize = 8 * 1024;
114+
pub const DEFAULT_WRITE_BUFFER_SIZE: usize = 128 * 1024;
115115

116116
pub const DEFAULT: Self = Self {
117117
log_format_version: DEFAULT_LOG_FORMAT_VERSION,

crates/core/src/db/persistence.rs

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
use std::{io, sync::Arc};
1+
use std::{
2+
io,
3+
num::{NonZeroU64, NonZeroUsize},
4+
sync::Arc,
5+
};
26

37
use async_trait::async_trait;
48
use spacetimedb_commitlog::SizeOnDisk;
@@ -13,6 +17,59 @@ use super::{
1317
snapshot::{self, SnapshotDatabaseState, SnapshotWorker},
1418
};
1519

20+
/// Local durability configuration exposed through server config.
21+
#[derive(Clone, Copy, Debug, Default, serde::Deserialize, PartialEq, Eq)]
22+
#[serde(rename_all = "kebab-case")]
23+
pub struct DurabilityConfig {
24+
#[serde(default)]
25+
pub commitlog: CommitlogConfig,
26+
}
27+
28+
/// Commitlog configuration exposed through server config.
29+
#[derive(Clone, Copy, Debug, Default, serde::Deserialize, PartialEq, Eq)]
30+
#[serde(rename_all = "kebab-case")]
31+
pub struct CommitlogConfig {
32+
pub log_format_version: Option<u8>,
33+
pub max_segment_size: Option<NonZeroU64>,
34+
#[serde(alias = "offset-interval-bytes")]
35+
pub offset_index_interval_bytes: Option<NonZeroU64>,
36+
#[serde(alias = "offset-index-require-fsync")]
37+
pub offset_index_require_segment_fsync: Option<bool>,
38+
pub preallocate_segments: Option<bool>,
39+
pub write_buffer_size: Option<NonZeroUsize>,
40+
}
41+
42+
impl DurabilityConfig {
43+
fn into_options(self) -> spacetimedb_durability::local::Options {
44+
let mut opts = spacetimedb_durability::local::Options::default();
45+
self.commitlog.apply_to(&mut opts.commitlog);
46+
opts
47+
}
48+
}
49+
50+
impl CommitlogConfig {
51+
fn apply_to(self, opts: &mut spacetimedb_commitlog::Options) {
52+
if let Some(log_format_version) = self.log_format_version {
53+
opts.log_format_version = log_format_version;
54+
}
55+
if let Some(max_segment_size) = self.max_segment_size {
56+
opts.max_segment_size = max_segment_size.get();
57+
}
58+
if let Some(offset_index_interval_bytes) = self.offset_index_interval_bytes {
59+
opts.offset_index_interval_bytes = offset_index_interval_bytes;
60+
}
61+
if let Some(offset_index_require_segment_fsync) = self.offset_index_require_segment_fsync {
62+
opts.offset_index_require_segment_fsync = offset_index_require_segment_fsync;
63+
}
64+
if let Some(preallocate_segments) = self.preallocate_segments {
65+
opts.preallocate_segments = preallocate_segments;
66+
}
67+
if let Some(write_buffer_size) = self.write_buffer_size {
68+
opts.write_buffer_size = write_buffer_size.get();
69+
}
70+
}
71+
}
72+
1673
/// [spacetimedb_durability::Durability] impls with a [`Txdata`] transaction
1774
/// payload, suitable for use in the [`relational_db::RelationalDB`].
1875
pub type Durability = dyn spacetimedb_durability::Durability<TxData = Txdata>;
@@ -128,14 +185,21 @@ pub trait PersistenceProvider: Send + Sync {
128185
/// [compresses]: relational_db::snapshot_watching_commitlog_compressor
129186
pub struct LocalPersistenceProvider {
130187
data_dir: Arc<ServerDataDir>,
188+
durability: DurabilityConfig,
131189
}
132190

133191
impl LocalPersistenceProvider {
134192
pub fn new(data_dir: impl Into<Arc<ServerDataDir>>) -> Self {
135193
Self {
136194
data_dir: data_dir.into(),
195+
durability: DurabilityConfig::default(),
137196
}
138197
}
198+
199+
pub fn with_durability_config(mut self, durability: DurabilityConfig) -> Self {
200+
self.durability = durability;
201+
self
202+
}
139203
}
140204

141205
#[async_trait]
@@ -149,7 +213,12 @@ impl PersistenceProvider for LocalPersistenceProvider {
149213
asyncify(move || relational_db::open_snapshot_repo(snapshot_dir, database_identity, replica_id))
150214
.await
151215
.map(|repo| SnapshotWorker::new(repo, snapshot::Compression::Enabled))?;
152-
let (durability, disk_size) = relational_db::local_durability(replica_dir, Some(&snapshot_worker)).await?;
216+
let (durability, disk_size) = relational_db::local_durability_with_options(
217+
replica_dir,
218+
Some(&snapshot_worker),
219+
self.durability.into_options(),
220+
)
221+
.await?;
153222

154223
tokio::spawn(relational_db::snapshot_watching_commitlog_compressor(
155224
snapshot_worker.subscribe(),

crates/core/src/db/relational_db.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1670,6 +1670,20 @@ pub type LocalDurability = Arc<durability::Local<ProductValue>>;
16701670
pub async fn local_durability(
16711671
replica_dir: ReplicaDir,
16721672
snapshot_worker: Option<&SnapshotWorker>,
1673+
) -> Result<(LocalDurability, DiskSizeFn), DBError> {
1674+
local_durability_with_options(replica_dir, snapshot_worker, <_>::default()).await
1675+
}
1676+
1677+
/// Initialize local durability with explicit parameters.
1678+
///
1679+
/// Also returned is a [`DiskSizeFn`] as required by [`RelationalDB::open`].
1680+
///
1681+
/// Note that this operation can be expensive, as it needs to traverse a suffix
1682+
/// of the commitlog.
1683+
pub async fn local_durability_with_options(
1684+
replica_dir: ReplicaDir,
1685+
snapshot_worker: Option<&SnapshotWorker>,
1686+
opts: durability::local::Options,
16731687
) -> Result<(LocalDurability, DiskSizeFn), DBError> {
16741688
let rt = tokio::runtime::Handle::current();
16751689
let on_new_segment = snapshot_worker.map(|snapshot_worker| {
@@ -1684,7 +1698,7 @@ pub async fn local_durability(
16841698
durability::Local::open(
16851699
replica_dir.clone(),
16861700
rt,
1687-
<_>::default(),
1701+
opts,
16881702
// Give the durability a handle to request a new snapshot run,
16891703
// which it will send down whenever we rotate commitlog segments.
16901704
on_new_segment,

crates/standalone/config.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,24 @@ directives = [
4444
# Apply a V8 heap limit in MiB. Set to 0 to use V8's default limit.
4545
# heap-limit-mb = 0
4646

47+
[commitlog]
48+
# The maximum supported commitlog format version, also used for writing.
49+
# log-format-version = 1
50+
51+
# Maximum size in bytes for each commitlog segment.
52+
# max-segment-size = 1073741824
53+
54+
# Number of bytes written to the commitlog after which an entry is added to the offset index.
55+
# offset-index-interval-bytes = 4096
56+
57+
# Require that the commitlog segment is synced before adding an offset index entry.
58+
# offset-index-require-segment-fsync = true
59+
60+
# Preallocate disk space for commitlog segments up to max-segment-size.
61+
# Has no effect unless commitlog fallocate support is enabled.
62+
# preallocate-segments = false
63+
64+
# Size in bytes of the memory buffer holding commit data before flushing to storage.
65+
# write-buffer-size = 131072
66+
4767
# vim: set nowritebackup: << otherwise triggers cargo-watch

crates/standalone/src/lib.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use http::StatusCode;
1212
use spacetimedb::client::ClientActorIndex;
1313
use spacetimedb::config::{CertificateAuthority, MetadataFile, V8Config, WasmConfig};
1414
use spacetimedb::db;
15-
use spacetimedb::db::persistence::LocalPersistenceProvider;
15+
use spacetimedb::db::persistence::{DurabilityConfig, LocalPersistenceProvider};
1616
use spacetimedb::energy::{EnergyBalance, EnergyQuanta, NullEnergyMonitor};
1717
use spacetimedb::host::{DiskStorage, HostController, HostRuntimeConfig, MigratePlanResult, UpdateDatabaseResult};
1818
use spacetimedb::identity::{AuthCtx, Identity};
@@ -41,6 +41,7 @@ pub use spacetimedb_client_api::routes::subscribe::{BIN_PROTOCOL, TEXT_PROTOCOL}
4141
#[derive(Clone, Copy)]
4242
pub struct StandaloneOptions {
4343
pub db_config: db::Config,
44+
pub durability: DurabilityConfig,
4445
pub websocket: WebSocketOptions,
4546
pub wasm: WasmConfig,
4647
pub v8: V8Config,
@@ -76,7 +77,8 @@ impl StandaloneEnv {
7677
let energy_monitor = Arc::new(NullEnergyMonitor);
7778
let program_store = Arc::new(DiskStorage::new(data_dir.program_bytes().0).await?);
7879

79-
let persistence_provider = Arc::new(LocalPersistenceProvider::new(data_dir.clone()));
80+
let persistence_provider =
81+
Arc::new(LocalPersistenceProvider::new(data_dir.clone()).with_durability_config(config.durability));
8082
let host_controller = HostController::new(
8183
data_dir,
8284
config.db_config,
@@ -650,6 +652,7 @@ mod tests {
650652
storage: Storage::Memory,
651653
page_pool_max_size: None,
652654
},
655+
durability: DurabilityConfig::default(),
653656
websocket: WebSocketOptions::default(),
654657
wasm: WasmConfig::default(),
655658
v8: V8Config::default(),

crates/standalone/src/subcommands/start.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use axum::extract::DefaultBodyLimit;
1111
use clap::ArgAction::SetTrue;
1212
use clap::{Arg, ArgMatches};
1313
use spacetimedb::config::{parse_config, CertificateAuthority};
14+
use spacetimedb::db::persistence::{CommitlogConfig, DurabilityConfig};
1415
use spacetimedb::db::{self, Storage};
1516
use spacetimedb::startup::{self, TracingOptions};
1617
use spacetimedb::util::jobs::JobCores;
@@ -99,6 +100,8 @@ struct ConfigFile {
99100
#[serde(flatten)]
100101
common: spacetimedb::config::ConfigFile,
101102
#[serde(default)]
103+
commitlog: CommitlogConfig,
104+
#[serde(default)]
102105
websocket: WebSocketOptions,
103106
}
104107

@@ -181,6 +184,9 @@ pub async fn exec(args: &ArgMatches, db_cores: JobCores) -> anyhow::Result<()> {
181184
let ctx = StandaloneEnv::init(
182185
StandaloneOptions {
183186
db_config,
187+
durability: DurabilityConfig {
188+
commitlog: config.commitlog,
189+
},
184190
websocket: config.websocket,
185191
wasm: config.common.wasm,
186192
v8: config.common.v8,
@@ -525,6 +531,14 @@ mod tests {
525531
heap-gc-trigger-fraction = 0.6
526532
heap-retire-fraction = 0.8
527533
heap-limit-mb = 128
534+
535+
[commitlog]
536+
log-format-version = 1
537+
max-segment-size = 1048576
538+
offset-index-interval-bytes = 8192
539+
offset-index-require-segment-fsync = false
540+
preallocate-segments = true
541+
write-buffer-size = 131072
528542
"#;
529543

530544
let config: ConfigFile = toml::from_str(toml).unwrap();
@@ -543,6 +557,21 @@ mod tests {
543557
assert_eq!(config.common.v8.heap_policy.heap_gc_trigger_fraction, 0.6);
544558
assert_eq!(config.common.v8.heap_policy.heap_retire_fraction, 0.8);
545559
assert_eq!(config.common.v8.heap_policy.heap_limit_bytes, 128 * 1024 * 1024);
560+
assert_eq!(config.commitlog.log_format_version, Some(1));
561+
assert_eq!(
562+
config.commitlog.max_segment_size.map(|val| val.get()),
563+
Some(1024 * 1024)
564+
);
565+
assert_eq!(
566+
config.commitlog.offset_index_interval_bytes.map(|val| val.get()),
567+
Some(8192)
568+
);
569+
assert_eq!(config.commitlog.offset_index_require_segment_fsync, Some(false));
570+
assert_eq!(config.commitlog.preallocate_segments, Some(true));
571+
assert_eq!(
572+
config.commitlog.write_buffer_size.map(|val| val.get()),
573+
Some(128 * 1024)
574+
);
546575

547576
assert_eq!(
548577
config.websocket,
@@ -553,4 +582,20 @@ mod tests {
553582
}
554583
);
555584
}
585+
586+
#[test]
587+
fn commitlog_options_accept_aliases() {
588+
let toml = r#"
589+
[commitlog]
590+
offset-interval-bytes = 16384
591+
offset-index-require-fsync = true
592+
"#;
593+
594+
let config: ConfigFile = toml::from_str(toml).unwrap();
595+
assert_eq!(
596+
config.commitlog.offset_index_interval_bytes.map(|val| val.get()),
597+
Some(16 * 1024)
598+
);
599+
assert_eq!(config.commitlog.offset_index_require_segment_fsync, Some(true));
600+
}
556601
}

crates/testing/src/modules.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ impl CompiledModule {
245245
let env = spacetimedb_standalone::StandaloneEnv::init(
246246
spacetimedb_standalone::StandaloneOptions {
247247
db_config: config,
248+
durability: Default::default(),
248249
websocket: WebSocketOptions::default(),
249250
wasm: Default::default(),
250251
v8: Default::default(),

docs/docs/00300-resources/00200-reference/00100-cli-reference/00200-standalone-config.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ On Linux and macOS, this directory is by default `~/.local/share/spacetime/data`
1818

1919
- [`logs`](#logs)
2020

21+
- [`commitlog`](#commitlog)
22+
23+
- [`websocket`](#websocket)
24+
2125
### `certificate-authority`
2226

2327
```toml
@@ -47,6 +51,56 @@ Can be one of `"error"`, `"warn"`, `"info"`, `"debug"`, `"trace"`, or `"off"`, c
4751

4852
A list of filtering directives controlling what messages get logged, which overwrite the global [`logs.level`](#logslevel). See [`tracing documentation`](https://docs.rs/tracing-subscriber/0.3/tracing_subscriber/filter/struct.EnvFilter.html#directives) for syntax. Note that this is primarily intended as a debugging tool, and log message fields and targets are not considered stable.
4953

54+
### `commitlog`
55+
56+
```toml
57+
[commitlog]
58+
log-format-version = 1
59+
max-segment-size = 1073741824 # 1GiB
60+
offset-index-interval-bytes = 4096
61+
offset-index-require-segment-fsync = true
62+
preallocate-segments = false
63+
write-buffer-size = 131072 # 128KiB
64+
```
65+
66+
The `commitlog` table configures local durability. These settings are advanced and may affect recovery behavior, disk usage, memory usage, and write throughput. Omitted fields use the server's built-in defaults.
67+
68+
#### `commitlog.log-format-version`
69+
70+
The maximum supported commitlog format version, also used for writing.
71+
72+
::::caution
73+
This setting should not normally be changed from the commitlog crate's default. A reason to change it could be to make the server accept an older, incompatible commitlog.
74+
::::
75+
76+
#### `commitlog.max-segment-size`
77+
78+
The maximum size in bytes to which commitlog segments should be allowed to grow.
79+
80+
#### `commitlog.offset-index-interval-bytes`
81+
82+
Number of bytes written to the commitlog after which an entry is added to the offset index.
83+
84+
#### `commitlog.offset-index-require-segment-fsync`
85+
86+
If `true`, require that the segment must be synced to disk before an index entry is added.
87+
88+
Setting this to `false` will update the index every `offset-index-interval-bytes`, even if the commitlog was not synced. This means that the index could contain non-existent entries in the event of a crash.
89+
90+
Setting this to `true` will update the index when the commitlog is synced, and `offset-index-interval-bytes` have been written. This means that the index could contain fewer index entries than strictly every `offset-index-interval-bytes`.
91+
92+
::::note
93+
The commitlog operates correctly under both settings, but the choice can have performance implications.
94+
::::
95+
96+
#### `commitlog.preallocate-segments`
97+
98+
If `true`, preallocate disk space for commitlog segments up to `commitlog.max-segment-size`. This has no effect unless commitlog fallocate support is enabled.
99+
100+
#### `commitlog.write-buffer-size`
101+
102+
Size in bytes of the memory buffer holding commit data before flushing to storage.
103+
50104
### `websocket`
51105

52106
```toml

0 commit comments

Comments
 (0)