Repository navigation
Expand file tree
/
Copy pathconfig.toml
More file actions
1281 lines (1122 loc) · 65 KB
/
Copy pathconfig.toml
File metadata and controls
1281 lines (1122 loc) · 65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# Root directory for server data. Environment override: IGGY_PATH.
path = "local_data"
# Consumer group rebalancing and session reports. [heartbeat] sets the session timeout.
[consumer_group]
# Maximum time a partition can remain in pending revocation before being force-transferred to the target member.
rebalancing_timeout = "30s"
# Server-to-server session reports run even when client heartbeat checks are disabled.
# Must be nonzero. Use the same settings on all nodes.
heartbeat_interval = "5s"
[data_maintenance.messages]
# Enables or disables the segment cleaner. It deletes the oldest sealed segments of
# topics with a finite message_expiry or max_topic_size, per partition, best-effort.
cleaner_enabled = true
# Interval for running the message cleaner.
interval = "1 m"
# HTTP server configuration
[http]
# Determines if the HTTP server is active.
# `true` enables the server, allowing it to handle HTTP requests.
# `false` disables the server, preventing it from handling HTTP requests.
# In cluster mode, followers forward control-plane requests (streams, topics,
# users, ...) to the current primary when a cluster-wide JWT key exists (see
# http.jwt / cluster.auth below).
# TODO: forwarding does not cover the partition-plane APIs yet - message
# produce and consumer-offset writes are never forwarded and must reach the
# partition's primary node directly (message polls read locally on any node).
enabled = true
# Specifies the network address and port for the HTTP server.
# The format is "HOST:PORT". For example, "127.0.0.1:3000" listens on localhost only on port 3000.
# In cluster mode the HOST still picks the bind interface, while the port
# comes from this node's cluster.nodes ports.http entry.
address = "127.0.0.1:3000"
# Maximum size of the request body in bytes. For security reasons, the default limit is 2 MB.
# HTTP produce requests are not framed by the message bus, so this body cap is
# what bounds the widest batch record the HTTP path can persist. Boot refuses a
# value above the frozen 256 MiB recovery ceiling (see
# message_bus.max_message_size), and a body above message_bus.max_message_size
# can still admit a batch that no peer accepts a replication frame for -- keep
# this at or below that cap unless replica_count = 1.
max_request_size = "2 MB"
# Enables the embedded Web UI dashboard at '/ui'.
# When set to `true` and the server is compiled with the 'iggy-web' feature,
# the Svelte dashboard will be served at the '/ui' endpoint, providing a
# browser-based interface for managing streams, topics, and viewing messages.
# If the server is compiled without 'iggy-web' feature and this is set to `true`,
# a warning will be logged at startup but the server will continue to run.
# `true` enables the embedded Web UI (requires server built with 'iggy-web' feature).
# `false` disables the embedded Web UI (default).
web_ui = false
# Configuration for Cross-Origin Resource Sharing (CORS).
[http.cors]
# Controls whether CORS is enabled for the HTTP server.
# `true` allows handling cross-origin requests with specified rules.
# `false` blocks cross-origin requests, enhancing security.
enabled = true
# Specifies which HTTP methods are allowed when CORS is enabled.
# For example, ["GET", "POST"] would allow only GET and POST requests.
allowed_methods = ["GET", "POST", "PUT", "DELETE"]
# Defines which origins are permitted to make cross-origin requests.
# An asterisk "*" as the first entry allows all origins (any entries after it
# are ignored); "*" in any other position fails the config. Specific domains
# can be listed to restrict access.
allowed_origins = ["*"]
# Lists allowed headers that can be used in CORS requests.
# For example, ["content-type"] permits only the content-type header.
allowed_headers = ["content-type", "authorization"]
# Headers that browsers are allowed to access in CORS responses.
# `iggy-view` carries the current VSR view number; exposing it lets browser
# clients read it on cross-origin responses.
exposed_headers = ["iggy-view"]
# Determines if credentials like cookies or HTTP auth can be included in CORS requests.
# `true` allows credentials to be included, useful for authenticated sessions;
# it requires explicit (non-wildcard) allowed_origins, allowed_headers, and
# exposed_headers.
# `false` prevents credentials, enhancing privacy and security.
allow_credentials = false
# Allows or blocks requests from private networks in CORS.
# `true` permits requests from private networks.
# `false` disallows such requests, providing additional security.
allow_private_network = false
# JWT (JSON Web Token) configuration for HTTP.
[http.jwt]
# Specifies the algorithm used for signing JWTs.
# For example, "HS256" indicates HMAC with SHA-256.
algorithm = "HS256"
# The issuer of the JWT, typically a URL or an identifier of the issuing entity.
issuer = "iggy.apache.org"
# Intended audience for the JWT, usually the recipient or system intended to process the token.
audience = "iggy.apache.org"
# Lists valid issuers for JWT validation to ensure tokens are from trusted sources.
valid_issuers = ["iggy.apache.org"]
# Lists valid audiences for JWT validation to confirm tokens are for the intended recipient.
valid_audiences = ["iggy.apache.org"]
# Expiry time for access tokens.
access_token_expiry = "1 h"
# Tolerance for timing discrepancies during token validation.
clock_skew = "5 s"
# Time before which the token should not be considered valid.
not_before = "0 s"
# Secret key for encoding JWTs.
# If only one secret is set, it supplies both keys. With both empty, use the
# cluster PSK when enabled, otherwise a random secret per start. Explicit JWT
# secrets take precedence over the PSK. Identical keys on every node make bearers
# valid cluster-wide and activate follower-to-primary HTTP forwarding. Without
# configured JWT secrets or a cluster PSK, tokens are node-local and forwarding stays disabled.
# HTTP message sends, polls and consumer offset calls that reach a node that is
# not the partition primary then answer a transient 503 (code 58). Partition
# primaries are spread over the nodes, which makes that the common case.
encoding_secret = ""
# Secret key for decoding JWTs.
# When empty, the same fallback rules as encoding_secret apply.
decoding_secret = ""
# Indicates if the secret key is base64 encoded.
# `true` means the secret is base64 encoded.
# `false` means the secret is in plain text.
use_base64_secret = false
# Trusted issuers for A2A (Application-to-Application) authentication. Opt-in:
# with none configured the listener accepts only self-issued HMAC tokens.
# `issuer`, `audience`, `jwks_url` and `username` are required per entry. Every
# token from an issuer acts as the iggy user named `username`, looked up on each
# request. A token is refused while that user is missing, inactive or root.
#
# Operational note: enabling an issuer opens an outbound JWKS fetch that is
# reachable before a token's signature is verified - a token naming this issuer
# with an unknown key id can trigger a fetch to `jwks_url`. The target is fixed
# (not attacker-chosen); concurrent misses coalesce onto one fetch and repeats
# are rate-limited to at most one outbound request per issuer per short window,
# so an unknown-key-id flood cannot amplify. The same window bounds how quickly a
# freshly rotated signing key is picked up.
# [[http.jwt.trusted_issuers]]
# issuer = "test-issuer"
# jwks_url = "http://127.0.0.1:8081/.well-known/jwks.json"
# audience = "iggy.apache.org"
# username = "a2a-service"
# Metrics configuration for HTTP.
[http.metrics]
# Enable or disable the metrics endpoint.
# `true` makes metrics available at the specified endpoint.
# `false` disables metrics collection.
enabled = true
# Specifies the endpoint for accessing metrics, e.g., "/metrics".
# This route requires authentication like every other read: scrapers must
# present a bearer credential (JWT or personal access token). Any
# authenticated user gets the aggregate gauges; the per-topic series are
# exported only to users with the read_servers or manage_servers permission.
endpoint = "/metrics"
# TLS (Transport Layer Security) configuration for HTTP.
[http.tls]
# Controls the use of TLS for encrypted HTTP connections.
# `true` enables TLS, enhancing security.
# `false` disables TLS, which may be appropriate in secure internal networks.
enabled = false
# Path to the TLS certificate file.
cert_file = "core/certs/iggy_cert.pem"
# Path to the TLS key file.
key_file = "core/certs/iggy_key.pem"
# TCP server configuration.
[tcp]
# Determines if the TCP server is active.
# `true` enables the TCP server for handling TCP connections.
# `false` disables it, preventing any TCP communication.
enabled = true
# Defines the network address and port for the TCP server.
# For example, "127.0.0.1:8090" listens on localhost only on port 8090.
address = "127.0.0.1:8090"
# TLS configuration for the TCP server.
[tcp.tls]
# Enables or disables TLS for TCP connections.
# `true` secures TCP connections with TLS.
# `false` leaves TCP connections unencrypted.
enabled = false
# Certificate source for the TCP TLS listener.
# `true` generates an ephemeral loopback certificate, but only while cert_file
# does not exist on disk: an existing PEM pair is loaded instead (the server
# logs which way it went).
# `false` loads cert_file / key_file, which must both exist.
self_signed = true
# Path to the TLS certificate file. Loaded when the file exists or
# self_signed = false.
cert_file = "core/certs/iggy_cert.pem"
# Path to the TLS key file. Read together with cert_file.
key_file = "core/certs/iggy_key.pem"
# QUIC protocol configuration.
[quic]
# Controls whether the QUIC server is enabled.
# `true` enables QUIC for fast, secure connections.
# `false` disables QUIC, possibly for compatibility or simplicity.
enabled = true
# Network address and port for the QUIC server.
# For example, "127.0.0.1:8080" binds to localhost on port 8080.
address = "127.0.0.1:8080"
# How many bidirectional streams a client may keep open at once on a single
# QUIC connection. The SDK opens a fresh stream per command, so this caps the
# commands one connection has in flight. The server serves the streams of a
# connection at once and gives each reply to its stream by request id. 256
# matches `message_bus.client_queue_capacity`, the most requests the server
# runs at once for one connection. 1 makes the commands of a connection wait
# for each other.
max_concurrent_bidi_streams = 256
# Initial path MTU for QUIC connections, before MTU discovery has run.
# 1200 bytes is the QUIC minimum and quinn's own default: safe on every
# path, and MTU discovery probes upward from it automatically. Values
# above the real path MTU cause packet loss until black-hole detection
# resets the connection back to 1200.
initial_mtu = "1200 B"
# Server-side flow-control budget for unacknowledged data the server may have
# in flight on one connection. A ceiling, not a reservation, and rarely the
# operative limit: the receiver's own window binds first, and the Rust SDK's
# QUIC client sets both of its windows to about 100 KB.
send_window = "64 MiB"
# Server-side flow-control budget for data the server will buffer from one
# connection, shared by all of its streams. Symmetric with `send_window`.
receive_window = "64 MiB"
# Per-stream slice of `receive_window`. A value below `receive_window` keeps
# one unread stream from pinning the whole connection window. It is equal,
# because the server reads each stream as soon as the stream opens. Must not
# exceed `receive_window`; equality is allowed.
stream_receive_window = "64 MiB"
# Interval for sending QUIC keep-alive PINGs. One third of
# `max_idle_timeout` so up to two consecutive losses fit before the
# idle timer closes the connection. Set to "0 s" to disable.
keep_alive_interval = "10 s"
# Maximum idle time before a QUIC connection is closed. Set to
# "0 s" to disable (not recommended).
max_idle_timeout = "30 s"
# QUIC certificate configuration.
[quic.certificate]
# Certificate source for the QUIC listener.
# `true` always generates an ephemeral loopback certificate and ignores
# cert_file / key_file even when those files exist on disk; the server logs a
# warning naming the files it skipped.
# `false` loads cert_file / key_file, which must both exist.
self_signed = true
# Path to the QUIC TLS certificate file. Required when self_signed = false,
# left empty here so the shipped self-signed default has nothing to skip.
cert_file = ""
# Path to the QUIC TLS key file. Required when self_signed = false.
key_file = ""
# Personal access token configuration.
[personal_access_token]
# Sets the maximum number of active tokens allowed per user.
max_tokens_per_user = 100
# Personal access token cleaner configuration.
[personal_access_token.cleaner]
# Enables or disables the token cleaner process.
# `true` activates periodic token cleaning.
# `false` disables it, tokens remain active until manually revoked or expired.
enabled = true
# Interval for running the token cleaner.
interval = "1 m"
# Heartbeat configuration
[heartbeat]
# Enables or disables client heartbeat checks. When enabled, a connection
# that sends nothing (no request, no PING) for 1.2 x interval stops renewing
# its session, and the verifier evicts it if it holds a group membership.
# When disabled, an open connection keeps its session alive however long it
# stays silent.
enabled = true
# Interval for expected client heartbeats. 1.2 x interval is the session
# timeout: the metadata primary ends a session that no server reported for
# that long, whether or not heartbeat checks are enabled. That covers a closed
# connection and a lost node. With checks on, it also covers a silent
# connection, which loses its session after about 2.4 x interval. With checks
# off, a client host that dies without closing its socket keeps its session:
# TCP and WebSocket set no keepalive, and only QUIC closes such a connection,
# after quic.max_idle_timeout. An idle HTTP session expires after about
# 2.4 x interval either way.
# The consumer groups of an ended session rebalance, and its client must log
# in again. Each new primary grants a fresh timeout. Incomplete client lists
# defer expiry across the cluster until a full timeout has passed without an
# incomplete report. 1.2 x interval must exceed cluster.heartbeat_timeout when
# clustering is enabled, and three consumer_group.heartbeat_interval periods
# plus two gather (3s) and send (100ms) deadlines, allowing one missed report.
# So interval must exceed (3 x consumer_group.heartbeat_interval + 6.2s) / 1.2:
# about 17.67s at the default 5s, and always more than 5.16s.
# The SDKs ping automatically every 5 s, but only from a connected high-level
# client: the Rust and Java pingers start in connect(), so a session that logs
# in without it never pings. Use the same settings on all nodes.
interval = "30 s"
# OpenTelemetry configuration
[telemetry]
# Enables or disables telemetry.
enabled = false
# Service name for telemetry.
service_name = "iggy"
# OpenTelemetry logs configuration
[telemetry.logs]
# Transport for sending logs. Options: "grpc", "http".
transport = "grpc"
# Endpoint for sending logs.
endpoint = "http://localhost:7281/v1/logs"
# OpenTelemetry traces configuration
[telemetry.traces]
# Transport for sending traces. Options: "grpc", "http".
transport = "grpc"
# Endpoint for sending traces.
endpoint = "http://localhost:7281/v1/traces"
# System configuration.
[runtime]
# Path for storing runtime data.
# Specifies the directory where any runtime data is stored, relative to `path`.
path = "runtime"
# Logging configuration.
[logging]
# Path for storing log files.
path = "logs"
# Log filtering directive using the same syntax as the RUST_LOG environment variable.
# Supports simple levels ("trace", "debug", "info", "warn", "error", "off" or "none")
# as well as complex directives like "warn,server=debug,iggy=trace".
# A bare word must be a level: write "target=level" to filter one target.
# An invalid value fails the boot.
# Note: RUST_LOG environment variable always takes precedence over this setting.
level = "info"
# Whether to write logs to file. When false, logs are only written to stdout.
# When enabled, logs are stored in {path}/{logging.path} (default: local_data/logs).
file_enabled = true
# Maximum size of a single log file before rotation occurs. When a log
# file reaches this size, it will be rotated (closed and a new file
# created). This setting works together with max_total_size to control
# log storage. You can set it to 0 to enable unlimited size of single
# log, but all logs will be written to a single file, thus disabling
# log rotation. Please configure 0 with caution, esp. RUST_LOG > debug
max_file_size = "500 MB"
# Maximum total size of all log files. When this size is reached,
# the oldest log files will be deleted first. Set it to 0 to allow
# an unlimited number of archived logs. This does not disable
# retention or per-log-file size limits.
max_total_size = "4 GB"
# Time interval for checking log rotation status. Must be at least 1 s.
rotation_check_interval = "1 h"
# Time to keep a rotated log file after its last write. Must be at least
# 1 s, or 0 to turn off deletion by age (max_total_size still deletes the
# oldest files). The log file in use is never deleted, so a line can stay
# on disk for up to the time to fill one file plus the retention.
retention = "7 days"
# Interval for printing process and host usage (CPU, memory, disk, clients,
# messages, open file descriptors) to the log from shard 0. "0 s" disables the
# line. Any other value must be at least 1 s. CPU usage covers the latest
# sampler interval (1 s), independently of this print interval.
sysinfo_print_interval = "10 s"
# Encryption configuration
[encryption]
# Encrypt message payloads and user headers. Metadata and structural headers remain unencrypted.
# `true` enables encryption for stored payloads and user headers using AES-256-GCM-SIV.
# `false` means data is stored without encryption.
enabled = false
# The server encryption key (string), 32 bytes as standard base64.
# Every encrypted stream has its own data key, stored wrapped under a key derived from this one.
# Required when encryption is enabled. With encryption disabled a key may still be set:
# streams created with their own key are then encrypted, keyless streams stay plaintext.
# A stream created without encryption stays plaintext for life.
key = ""
# Earlier server keys (list of strings) a key rotation still needs, at most two.
# A node unwraps stream keys with them and wraps every new stream key under them too.
# Rotation runs in two phases:
# 1. Set `key` to the new key and `previous_keys` to the old one on every node. The metadata
# primary adds a wrap under the new key to every encrypted stream and keeps the old wraps,
# so nodes still on the old key keep serving and booting, new streams included.
# 2. Once every node runs the phase 1 config and every encrypted stream reports the new
# key's fingerprint in `encryption_kek_fingerprint`, empty `previous_keys` and put the
# old key's fingerprint in `retired_kek_fingerprints`. The primary drops the wraps under it.
# 3. Once every stream has dropped the old wrap, clear `retired_kek_fingerprints` on every node.
# A stream holds at most three wraps. Retire an old fingerprint before adding a fourth key.
# Messages are never re-encrypted. Losing a key that still wraps a stream makes it unreadable.
# As an environment variable: IGGY_ENCRYPTION_PREVIOUS_KEYS='["<old base64 key>"]', a single key may also be given bare.
previous_keys = []
# Fingerprints of retired server keys (list of strings), 16 lowercase hex characters each, as
# `encryption_kek_fingerprint` reports them. The metadata primary drops stream key wraps under
# these keys and never drops a wrap under a key that is not listed, even one this node lacks.
# Needs `key`, and may not name `key` or a key in `previous_keys`. Not secret.
# As an environment variable: IGGY_ENCRYPTION_RETIRED_KEK_FINGERPRINTS='["<fingerprint>"]'.
retired_kek_fingerprints = []
# Memory pool configuration
[memory_pool]
# Enables or disables the memory pool (boolean).
# `true` enables the memory pool.
# `false` disables the memory pool.
enabled = true
# Size of the memory pool (string).
# Example: "512 MiB" or "1 GiB".
# Buffers are allocated on demand; allocations beyond this budget use memory outside the pool.
# Note: This number has to be multiplication of 4096 (default linux page size).
# Minimum size is 512 MiB due to internal implementation details.
size = "4 GiB"
# Maximum number of buffers in each bucket (u32).
# There are 28 buckets in the memory pool. Each bucket can hold up to this number of buffers
# and holds different buffer sizes, from 4 KiB to 512 MiB.
# Note: This number has to be a power of 2. Minimum value is 128 due to internal implementation details.
bucket_capacity = 8192
# Cluster configuration
[cluster]
# Enables or disables cluster mode (boolean).
# When enabled, this node will participate in the cluster and coordinate with other nodes.
enabled = false
# Unique cluster name (string).
# All nodes in the same cluster must share the same name.
# This prevents accidental cross-cluster communication.
# Permanent on-disk identity: the name is hashed into the cluster id stamped
# into every metadata and partition superblock on first boot. Changing it later
# makes the server refuse to start against existing data, since the recovered id
# no longer matches the configured one. Renaming means starting from an empty
# data directory.
name = "iggy-cluster"
# Consensus runs on a fixed 10ms tick, so every duration below is converted to
# whole ticks: values are rounded down to a multiple of 10ms, and anything under
# 10ms is raised to a single tick rather than firing sooner.
# Backup-side liveness window for a consensus plane's primary (duration).
# A replica that sees no primary traffic for this long starts a view change.
# Raise it on oversubscribed hosts where scheduling stalls fake primary
# death. Must be at least "2s" and at least 4x commit_broadcast_interval: the
# primary signals liveness through its commit broadcast, and the window must
# span several broadcasts so one delayed broadcast never trips an election.
# On Linux it is also the TCP_USER_TIMEOUT of every replica link: sent data
# or keepalive probes that stay unacknowledged this long close the link.
heartbeat_timeout = "5s"
# How often the primary broadcasts its commit point to every backup (duration).
# This is the cluster's liveness signal: each broadcast resets every backup's
# heartbeat_timeout window and carries the latest commit point forward. Must be
# nonzero and, with heartbeat_timeout, satisfy heartbeat_timeout >= 4x this
# value. Drives the consensus CommitMessage timer.
commit_broadcast_interval = "500ms"
# How often the primary retransmits prepares that backups have not yet acked
# (duration). Lower values recover faster from a dropped prepare at the cost of
# more replica traffic; must be nonzero. Drives the consensus Prepare timer.
prepare_retransmit_interval = "250ms"
# How often a replica retransmits its StartViewChange / DoViewChange while a
# view change is in progress (duration). Lower values converge a healthy
# election faster at the cost of more replica traffic; must be nonzero. Drives
# both consensus view-change retransmit timers.
view_change_retransmit_interval = "500ms"
# Backstop for a stalled view change (duration): one that does not conclude
# within this window escalates to a fresh cluster-wide election. Must be nonzero
# and at least 4x view_change_retransmit_interval, so a few dropped view-change
# messages retransmit rather than prematurely escalate.
view_change_status_timeout = "5s"
# How often a recovering or view-change backup re-requests the current view's
# StartView from its primary (duration); must be nonzero. Drives the consensus
# RequestStartView timer.
request_start_view_retransmit_interval = "1s"
# How many consecutive unanswered RequestStartView probes a recovering replica
# tolerates before falling back to an election (integer). A full-cluster restart
# leaves nobody settled to answer, so the replica elects on its recovered log.
# Must be between 1 and 100.
view_probe_attempts_max = 5
# How long a stalled journal-repair stream waits before re-requesting its
# remaining window from the serving peer (duration). Repair frames are
# fire-and-forget over the lossy bus, so a session with no retry wedges forever
# on a single dropped frame. Paces both the metadata and partition repair loops;
# must be nonzero. Paces streams that are already OPEN only: how long a hole
# waits before one is opened for it is repair_gap_debounce_interval below.
# Also paces the RequestStartView probe a partition backup sends while its
# entries from before a newer view wait for a header to prove them against;
# those probes have no attempt cap.
repair_retry_interval = "1s"
# How long a backup must hold committed ops it cannot walk to before the shard's
# tick opens a repair session for it (duration). The level-triggered floor under
# every edge-triggered arming site, which a produce stream can starve; must be
# nonzero. Paces the partition sweep and the metadata detector alike.
#
# Floored at 50 consensus ticks (500ms at the 10ms tick), so values under that
# arm no sooner: one tick of lag is ordinary pipelining, and repair against it
# would fire on healthy traffic.
#
# Recovery latency for one hole is max(this, 500ms) plus up to one tick of sweep
# granularity. Under a correlated fault the partition sweep opens at most 3
# sessions per tick and holds at most 8 at once, so the Nth group waiting on
# this shard adds ceil(N / 3) ticks on top, and more while sessions are already
# in flight. That cap is what keeps a node-wide rejoin from putting every
# group's repair stream on one serving peer at once. There is one metadata group
# per node, so its detector is not rate-capped.
repair_gap_debounce_interval = "1s"
# Prepares a peer serves per repair round before the requester asks for the next
# chunk (integer). Each frame rides the per-peer message-bus queue, so this must
# stay strictly below message_bus.peer_queue_capacity or a full round overruns
# the queue and drops frames. Must be > 0 and <= 1024.
repair_chunk_max = 128
# How long a superblock may stay unwritable before the replica fail-stops
# (duration). Applies to the metadata superblock and to each partition's own. A
# group that cannot persist its view is already fenced quorum-invisible and
# retries with capped backoff; past this window the process exits with a distinct
# status so a supervisor restarts or replaces it instead of an operator finding
# the wedge in logs. It is also the deadline of each running partition file job
# (see [sharding] partition_io_capacity); expiry fences the owner and stops the
# node. "0" disables the fail-stop and leaves the group fenced indefinitely, and
# it also disables the file-job deadline. Nonzero values must be at least 30s so
# a transient disk hiccup cannot kill the process.
superblock_wedged_fatal_timeout = "2m"
# Superblock compatibility: version 2 atomically records adopted view/head/header
# identity separately from WAL body certification. Legacy version 1 records with
# 66/74-byte payloads remain readable. After a version 2 write, an older binary
# refuses boot; rollback needs a consistent pre-upgrade data backup or a deliberate
# clean rejoin. Do not remove individual superblock slots to force a downgrade.
# Replica-to-replica authentication (PSK + BLAKE3 keyed-MAC handshake).
[cluster.auth]
# When true, every replica peer must complete the authenticated handshake or be
# rejected, and shared_secret becomes mandatory. Off by default = legacy
# unauthenticated replica traffic. Enabling it is a coordinated-restart change.
# With http enabled and no http.jwt secrets configured, the PSK also becomes
# the JWT key source, making bearers valid cluster-wide and activating
# follower-to-primary HTTP forwarding.
enabled = false
# Cluster-wide pre-shared key, >= 32 bytes of CSPRNG output, byte-identical on
# every node. Prefer the IGGY_CLUSTER_AUTH_SHARED_SECRET env var (masked in
# logs, never persisted) over storing it on disk. Ignored when enabled = false.
shared_secret = ""
# Retiring pre-shared key, accepted for verification only during a rolling key
# rotation (this node keeps signing with shared_secret). Rotate in three rolls:
# 1) shared_secret = old + previous_shared_secret = new on every node,
# 2) shared_secret = new + previous_shared_secret = old on every node,
# 3) shared_secret = new alone. Leave empty outside a rotation. Same length
# floor and env-var preference as shared_secret
# (IGGY_CLUSTER_AUTH_PREVIOUS_SHARED_SECRET).
previous_shared_secret = ""
# Replica-to-replica TLS for the consensus (tcp_replica) port.
[cluster.tls]
# When true every replica connection is wrapped in TLS 1.3 (ALPN
# "iggy-replica") before the replica handshake runs. Requires
# cluster.auth.enabled: TLS carries no client certificates, so it
# authenticates the acceptor only; the PSK handshake authenticates the
# peer, TLS supplies confidentiality. Off by default = plaintext replica
# traffic. Enabling it is a coordinated-restart change: a TLS dialer
# cannot talk to a plaintext acceptor or vice versa.
enabled = false
# When true the node auto-generates a self-signed certificate at boot and
# the dialer accepts ANY peer certificate. When false (default),
# cert_file / key_file / ca_file are all required.
self_signed = false
# PEM certificate chain presented by this node's acceptor side.
cert_file = ""
# PEM private key matching cert_file.
key_file = ""
# PEM trust anchor(s) the dialer verifies peer certificates against.
# Unused when self_signed = true.
# The dialer verifies each peer against the 'ip' string from that peer's
# cluster.nodes entry, and that string is the TLS server name as well as a
# literal IP, so every peer certificate needs a matching IP SAN. A
# certificate carrying only DNS SANs fails verification.
ca_file = ""
# Shard-0 coordinator placement.
[cluster.coordinator]
# Keep replica links off shard 0 while the shard range of link 0 has another
# shard. Shard 0 already hosts the coordinator, the metadata writer, and both
# listeners; replica links are long-lived steady flows, so they are offloaded
# to peer shards by default.
skip_shard_zero_for_replicas = true
# TCP links to each peer replica, at most the shard count. Link j carries the
# frames of the shards in range j, so more links spread replication over more
# shards. Every node must run the same value: the replica handshake refuses a
# peer that differs. Shard counts may differ between nodes. A range with fewer
# shards than there are peers puts links to two peers on one shard. With
# skip_shard_zero_for_replicas on, range 0 keeps its links off shard 0, so it
# needs one shard more than there are peers.
replica_connections_per_peer = 1
# When the server runs more than one shard, exclude shard 0 from client
# placement. Off by default: client connections are short-lived and benefit
# from shard-0 parallelism more than replicas do.
skip_shard_zero_for_clients = false
# Full roster of cluster members. Byte-identical on every node. The running
# node's identity is resolved at launch from the '--replica-id <N>' CLI
# flag, which selects the entry in this list that describes the current
# node. All other entries are remote peers.
#
# 'ip' is the node's roster address. Replica-to-replica traffic and
# follower-to-primary HTTP forwarding use it. It is not the bind interface for
# tcp/quic/http/websocket, which comes from each transport's own 'address'
# setting above; the roster supplies those transports their port only. A
# cluster spread across hosts therefore needs each transport's 'address' set to
# '0.0.0.0' or the routable NIC; the defaults below listen on loopback only,
# and a bind that cannot serve the advertised 'ip' is warned about at startup.
#
# Each node may also set 'advertised_address': the client-facing address
# handed out in cluster metadata and leader redirects. Set it when 'ip' is
# a private replica-network address unreachable by clients (Docker,
# Kubernetes, NAT). Accepts a literal IPv4/IPv6 address or a DNS hostname
# (RFC 1123: ASCII letters, digits, '-' and '.'; no port, no trailing dot).
# When unset, clients receive 'ip'.
#
# When different client networks need different addresses (a public
# 'advertised_address' would route in-VPC clients out through the public
# side), add per-network 'advertised_addresses' selectors: clients whose
# peer IP falls inside 'client_cidr' are handed 'address' instead of the
# catch-all. 'address' takes the same forms as 'advertised_address'
# (literal IP or RFC 1123 hostname, never a port - ports always come from
# 'ports'). At most 16 selectors per node; boot also rejects duplicate
# 'client_cidr' entries on one node (compared truncated, so '10.0.1.0/16'
# duplicates '10.0.0.0/16') and any two nodes advertising one host:port
# to overlapping client sets - reusing a host:port across nodes is legal
# only when no client would resolve both nodes to it.
#
# The longest matching prefix wins; clients matching no selector fall
# back to 'advertised_address', then 'ip'. Matching is per address
# family: '0.0.0.0/0' matches no IPv6 client and '::/0' matches no IPv4
# client, so covering both families takes one selector per family (or the
# catch-all). IPv4-mapped IPv6 CIDRs ('::ffff:10.0.0.0/104') match like
# their IPv4 form only at prefix length 96 or longer; shorter ones match
# native IPv6 clients only. Matching sees the transport-level peer
# address, so clients behind a proxy or load balancer match the proxy's
# network, not their own.
#
# Every 'address' must be routable from inside its own 'client_cidr':
# leader-aware SDK clients redial whatever address metadata advertises,
# so a selector pointing at a host its own clients cannot reach strands
# them mid-redirect. Prefer literal IPs over hostnames - the SDKs differ
# in how they compare an advertised hostname against the address they
# dialed, and a mismatch costs a reconnect on every fresh connect.
#
# Note for rolling upgrades: older server binaries reject a TOML config
# containing 'advertised_addresses' but silently ignore the equivalent
# 'IGGY_CLUSTER_NODES_*_ADVERTISED_ADDRESSES_*' env vars; either way,
# upgrade every binary first, then add selectors. Mid-upgrade, an env-var
# roster would serve selector addresses from upgraded nodes and the
# catch-all from the rest.
#
# [[cluster.nodes]]
# name = "iggy-node-1"
# ip = "10.0.1.5" # replica plane, literal IP only
# advertised_address = "203.0.113.10" # catch-all for unmatched clients
# replica_id = 0
# ports = { tcp = 8090, http = 3000, tcp_replica = 9090 }
#
# [[cluster.nodes.advertised_addresses]]
# client_cidr = "10.0.0.0/16" # in-VPC clients stay private
# address = "10.0.1.5"
#
# In cluster mode, 'ports' is the single source of listener ports: every
# enabled transport needs an explicit per-node port, otherwise the server
# refuses to start.
[[cluster.nodes]]
name = "iggy-node-1"
ip = "127.0.0.1"
replica_id = 0
ports = { tcp = 8090, quic = 8080, http = 3000, websocket = 8092, tcp_replica = 9090 }
[[cluster.nodes]]
name = "iggy-node-2"
ip = "127.0.0.1"
replica_id = 1
ports = { tcp = 8091, quic = 8081, http = 3001, websocket = 8093, tcp_replica = 9091 }
# Example additional node (commented out). tcp skips 8092-8094: those are the
# websocket ports of the three nodes, which collide once nodes share a host.
# [[cluster.nodes]]
# name = "iggy-node-3"
# ip = "192.168.1.100"
# advertised_address = "iggy-node-3.example.com"
# replica_id = 2
# ports = { tcp = 8095, quic = 8082, http = 3002, websocket = 8094, tcp_replica = 9092 }
# Sharding configuration
[sharding]
# CPU allocation - controls the number of shards and their CPU affinity.
# Possible values:
# - "all": Use all available CPU cores
# - numeric value (e.g. 4): Use 4 shards on the first 4 CPUs in the allowed set
# - range (e.g. "5..8"): Use 3 shards with affinity to cores 5, 6, 7
# - numa settings:
# + "numa:auto": Use all available NUMA nodes and cores (default)
# + "numa:nodes=0,1;cores=4;no_ht=true": Use NUMA node 0 and 1, each nodes use 4 cores, and no hyperthreads
cpu_allocation = "numa:auto"
# Whether shard threads are pinned to dedicated CPU cores (default: true).
# Pinned cores are drawn from the process's allowed CPU set (affinity/cpuset
# mask), so the server cooperates with systemd `AllowedCPUs=` and container
# cpusets. Set to false when the server shares cores with other workloads
# (e.g. a multi-tenant host slicing CPU via cgroup quotas): unpinned shards
# let the kernel scheduler place threads freely instead of piling every
# process onto the same low-numbered cores.
pin_cores = true
# Per-shard inter-shard inbox capacity (the main lane: consensus frames,
# connection setup, reconcile wakes). Bounded by design; consensus-frame
# drops recover via VSR retransmit. Size for the combined consensus traffic of
# all groups hosted by the shard, including replica fan-out and burst headroom.
# Per-group prepare queue depth alone does not determine this capacity.
# Slots are allocated at startup per shard, even before traffic arrives.
inbox_capacity = 65536
# Capacity of the reply lane: a separate bounded channel for cross-shard
# client-reply forwards, whose drops are terminal (no in-protocol
# retransmit - the client never receives the reply). Split from
# inbox_capacity so a consensus burst cannot evict reply forwards; size for
# peak client-reply fan-out per shard.
reply_inbox_capacity = 1024
# Maximum running disk polls plus queued completions per shard. Each disk read
# reserves a slot before I/O and retains it until the owner dequeues or discards
# the result, including after a requester timeout. When all slots are occupied,
# new disk polls are rejected before I/O. Main and reply inbox traffic uses
# separate capacity. This limits operation count, not retained message bytes.
poll_completion_capacity = 1024
# Longest wait of a deferred poll or readiness wait (at most 10 minutes). A
# longer requested wait is shortened to it.
deferred_poll_max_wait = "30 s"
# Deferred polls and readiness waits parked on one shard, and the part of
# them that one session may hold. An HTTP request without a binary session
# counts against its user. A request above either limit is refused with a
# retryable error.
deferred_poll_max_pending = 1024
deferred_poll_max_pending_per_session = 64
# A deferred disk read reserves three times the max_bytes of its poll, and at
# least three times 1 MiB, because one disk read can take a 1 MiB chunk
# whatever its max_bytes. It reserves no more than the first value. The
# deferred reads of one shard reserve no more than the second value together
# (at most 1 GiB). A read that finds no room waits for one, behind the reads
# that found no room before it. A reply carries no more than the first value,
# unless its first message alone is wider.
deferred_poll_max_read_bytes = "16 MiB"
deferred_poll_max_inflight_bytes = "64 MiB"
# Active partition file jobs plus results awaiting owner acceptance per shard (1..=256).
# Exhausting either limit queues partition file work until capacity is released.
# Running jobs use [cluster] superblock_wedged_fatal_timeout, counted in pump ticks.
# Expiry fences the owner and stops the node; 0 disables this deadline.
partition_io_capacity = 16
# Omission selects the largest legal job charge, slightly above 4 GiB per shard.
# This is an admission ceiling, not an eager allocation. A smaller budget fails boot.
# Charges include retained backing allocations, even for small visible slices.
# partition_io_bytes_max = "8 GiB"
# Wall-clock budget for a single shard's bus drain on shutdown. Drives
# the per-shard watchdog and the parallel-join survivor path; sized
# larger than typical TCP RTT times in-flight write-batch so writers
# receive their full last `write_vectored_all` budget before the
# connection registry force-tears the bus. Hosts with slow durable writes may need
# to extend this past the default.
shutdown_drain_timeout = "10 s"
# Poll cadence for the cross-thread shutdown flag and for the
# metadata-handoff loops. Trades off Ctrl-C latency against idle wakeup
# cost; the default keeps shutdown observably prompt without measurable
# scheduler overhead. Must be less than or equal to shutdown_drain_timeout.
shutdown_poll_interval = "50 ms"
# Hard wall-clock deadline for joining shard threads at process exit. A
# shard whose pump or listener wedges past this budget is abandoned with
# an error log instead of blocking exit forever. Must be at least
# shutdown_drain_timeout, or shards would be abandoned mid-drain.
shutdown_join_timeout = "30 s"
# Safety-tick cadence for the partition reconciliation loop. The reconciler
# also wakes on every metadata commit from shard 0, so this only covers
# dropped wake-ups and the initial post-bootstrap convergence window.
reconcile_periodic_interval = "1 s"
# WebSocket listener configuration. The frame-tuning knobs below are the
# live source for the server's WS / WSS plane; they are folded into a
# compio-ws WebSocketConfig once at bus construction. Each size knob is
# optional: commenting it out keeps the compio-ws (tungstenite) default
# noted next to it. A malformed size string fails config load.
[websocket]
enabled = true
address = "127.0.0.1:8092"
# Target minimum size of the frame read buffer. compio-ws default: "128 KiB".
# read_buffer_size = "128 KiB"
# Target buffer size for batched writes before flush. compio-ws
# default: "128 KiB".
# write_buffer_size = "128 KiB"
# Hard ceiling on the write buffer; writes past it error instead of
# buffering, so it must exceed write_buffer_size by at least one message.
# compio-ws default: unlimited.
# max_write_buffer_size = "128 MiB"
# Hard upper bound on a single inbound WebSocket message
# (post-fragment-reassembly). Must not exceed message_bus.max_message_size.
# compio-ws default: "64 MiB".
# max_message_size = "64 MiB"
# Hard upper bound on a single inbound WebSocket frame
# (pre-fragment-reassembly). Must not exceed max_message_size.
# compio-ws default: "16 MiB".
# max_frame_size = "16 MiB"
# Whether to accept unmasked frames from clients in violation of
# RFC 6455 client-to-server framing rules. Strict (false) by default.
accept_unmasked_frames = false
[websocket.tls]
enabled = false
# Certificate source for the WSS listener.
# `true` generates an ephemeral loopback certificate, but only while cert_file
# does not exist on disk: an existing PEM pair is loaded instead (the server
# logs which way it went).
# `false` loads cert_file / key_file, which must both exist.
self_signed = true
# Loaded when the file exists or self_signed = false.
cert_file = "core/certs/iggy_cert.pem"
key_file = "core/certs/iggy_key.pem"
# Metadata consensus plane tunables (shard 0's VSR replica: users,
# streams, topics, sessions). Size these together: a deeper prepare queue
# admits more concurrent in-flight metadata ops (e.g. login storms), and
# the journal must hold enough slots that a forced checkpoint (triggered
# when remaining slots fall to the checkpoint margin, which itself is
# max(64, prepare_queue_depth)) stays rare. Validation enforces
# journal_slots >= 4 * max(64, prepare_queue_depth).
[metadata]
# Depth of the metadata prepare queue: how many uncommitted metadata ops
# may be in flight at once. Submits beyond it are rejected with the
# transient "metadata prepare queue is full" and retried by the SDK.
# Capped at 127 by the view-change wire format: a DoViewChange describes the
# uncommitted suffix with one nack bit and one present bit per entry in a u128 each,
# so a deeper queue produces entries a view change can neither adopt nor prove dead.
prepare_queue_depth = 32
# Size of the metadata WAL's in-memory index, in slots (one committed but
# not-yet-snapshotted op per slot). Larger values buy more headroom
# between forced checkpoints at the cost of memory and bigger WAL
# rewrites per checkpoint. Reducing this for an existing data directory is
# refused at boot if the live WAL suffix collides in the smaller index;
# restore the previous value to recover.
journal_slots = 1024
# Initial capacity of the metadata session registry, shared by binary clients
# and HTTP sessions. Live and unretired sessions occupy slots. A full registry
# refuses new sessions; it never evicts retry protection. The first committed
# operation fixes the capacity, so changing this value does not resize an
# existing registry after restart or transfer. The HTTP cache cap uses half
# the configured value, but cannot raise the committed registry limit.
# Must be between 2 and 65536.
clients_table_max = 8192
# Cap on the partitions of all streams and topics. A request to create a
# topic or partitions that would exceed it fails with PartitionsLimitReached.
# The metadata primary checks it with its own value, so give every node the
# same value. The check counts the committed partitions and those of the
# creates still in flight, so creates never take the count past the cap. A
# create that will fail holds its partitions until it commits. If only the
# creates in flight leave no room, the request gets 58 (TransientNotAccepted)
# instead, and the client retries it until its deadline. 0 means no cap.
partitions_max = 0
# Records the metadata change log keeps for streams, topics, partitions,
# consumer groups, users and personal access tokens. Once the count passes
# it, the oldest records go, and a reader whose position they held must
# rebuild from current state. The log is part of the metadata snapshot, so a
# larger value costs memory and checkpoint size. Node-local: replicas may keep
# different depths. Must be between 1 and 1048576. Each of the two record
# budgets also keeps at most 128 MiB of record bodies.
change_log_resource_capacity = 65536
# Records the metadata change log keeps for sessions and consumer group
# membership. A separate budget, so a login or membership storm cannot evict
# resource history. Must be between 1 and 1048576.
change_log_activity_capacity = 16384
# Per-partition consensus plane tunables. Unlike [metadata] (one shard-0
# plane), a pipeline exists per partition, so raising this multiplies pinned
# request-buffer memory by the partition count. Keep it modest.
[partition]
# Retention is per topic, set at CreateTopic and readable on GetTopic:
# max_topic_size - delete oldest sealed segments past this size
# ("unlimited" when unset)
# message_expiry - delete sealed segments older than this ("none" when unset)
# Both policies can be active at once. The active segment is never touched.
# WAL-backed partitions request a checkpoint before deleting referenced segments;
# retention waits for that checkpoint, even below the usual WAL capacity trigger.
# Call GET /options/topic (or the SDK's describe_options) for the full catalog
# with this server's defaults.
# Topic durability and flush scheduling are set at creation and returned by GetTopic.
# durability controls message completion. consumer_offset_durability controls explicit offset stores and deletes.
# Both independently default to "replicated". Neither inherits the other.
# Every disk-backed partition uses a WAL for recovery and retries.
# WAL references retain segment bodies by hard link until reclamation.
# Full message payloads count against wal_bytes_max under both durability policies.
# Persistence barriers can also persist co-batched messages.
# Persisted prepares are limited to 64 MiB including the consensus header. Oversized operations are rejected permanently.
# Both policies write data to storage. "replicated" waits for VSR commit and application without an additional stable-storage barrier.
# "persisted" also requires recoverable stable-storage copies at the replication quorum before explicit success.
# A singleton quorum is one replica. Its persisted completion follows recoverable local persistence.
# Poll auto-commit remains asynchronous. A successful poll does not confirm that its submitted offset operation committed or became durable.
# HTTP ack=none returns dispatch acceptance with Iggy-Durability: none, even for persisted topics.
# messages_required_to_save defaults to 1024 and size_of_messages_required_to_save defaults to 1 MiB.
# These thresholds schedule ordinary segment writes. Required persistence, capacity pressure and lifecycle operations can flush sooner.
# Segment storage
# Segment size is per topic, set at CreateTopic as `segment_size` (1 GiB when
# unset). It is a soft limit: a segment may close one whole batch past it.