linkrs_storage/edge/edge_table/config.rs
1use linkrs_core::types::Timestamp;
2use linkrs_core::Value;
3
4use crate::edge::RecordFormPreference;
5
6/// Thresholds deciding when a bundled-eligible table is narrow and
7/// read-heavy enough for the inline form.
8///
9/// Single source of truth for the width and access gates: shape admission
10/// still vetoes first, then the observed profile must pass all three gates.
11/// Recommended ranges are documented on each field; values never adapt to
12/// load at runtime.
13#[derive(Debug, Clone, Copy)]
14pub struct RecordFormProfileThresholds {
15 /// Maximum average property-byte width for the inline form.
16 /// Recommended range 8..=16. The bundled slot holds one 8-byte scalar
17 /// word plus a validity bit, so wider averages stay columnar.
18 pub max_inline_avg_bytes: u64,
19 /// Maximum write share `writes / (reads + writes)` for the inline form.
20 /// Recommended range 0.2..=0.4. Above this the table is write-hot and
21 /// stays columnar.
22 pub max_write_share: f64,
23 /// Minimum read-to-write ratio for the inline form. Recommended range
24 /// 2.0..=4.0. Only read-heavy tables migrate inline.
25 pub min_read_to_write_ratio: f64,
26}
27
28impl Default for RecordFormProfileThresholds {
29 fn default() -> Self {
30 Self {
31 max_inline_avg_bytes: 8,
32 max_write_share: 0.3,
33 min_read_to_write_ratio: 3.0,
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
39pub struct EdgeTableConfig {
40 pub initial_vertex_capacity: usize,
41 /// Fixed number of edges allocated per high-degree overflow chunk.
42 pub overflow_chunk_edges: usize,
43 /// Address bits per topology node group: one group covers
44 /// `1 << node_group_bits` bound-endpoint rows. Out groups partition by
45 /// source, in groups by destination.
46 ///
47 /// Locked at table creation: there is no online width-change branch.
48 /// The only adjustment outlet is the offline `EdgeStore::reshard` tool,
49 /// which rebuilds the table group by group and switches the manifest on
50 /// the next checkpoint. Endpoints are expected dense; sparse large
51 /// endpoints must be densified offline via vertex remapping first,
52 /// otherwise only existing groups consume memory and files while the
53 /// span stays wide.
54 pub node_group_bits: u32,
55 /// Write backpressure: max size of the node-group CSR (in bytes)
56 /// before background compaction is requested. Set to 0 to disable.
57 pub max_mutable_csr_bytes: usize,
58 /// Bound for one group append sidecar: disk plus memory op count at or
59 /// above this limit forces a base rewrite on the next checkpoint. Set to
60 /// 0 to disable, leaving sidecar growth unbounded.
61 pub max_append_ops_per_group: usize,
62 /// Region density at or above which a dirty region merges as a whole.
63 /// Below it only rows holding reclaimable entries are visited. Tune
64 /// against checkpoint flushed bytes per live edge; raising it narrows
65 /// merges and never changes the live set.
66 pub region_merge_min_density: f32,
67 /// Group density at or above which a multi-region dirty span merges at
68 /// group scope. Below it merges stay region-scoped. Same tuning source
69 /// and safety as the region threshold.
70 pub group_merge_min_density: f32,
71 /// Automatic maintenance: property compaction on the
72 /// write path when the configured thresholds are exceeded.
73 pub auto_maintenance: AutoMaintenanceConfig,
74 /// User-facing preference for record form selection at table creation.
75 /// `Auto` derives the safe default (pure for empty schemas, otherwise
76 /// columnar); `Columnar` forces the standard multi/single/none strategy
77 /// path; `Bundled` explicitly opts into the single-scalar inline form.
78 pub record_form: RecordFormPreference,
79 /// Opt-in background record-form migration. When enabled, background
80 /// maintenance switches a table to its recommended form once (narrow
81 /// single-scalar columnar tables move inline); the switch fences
82 /// pre-switch WAL redo and requires the next checkpoint, so it stays off
83 /// by default and manual migration remains the primary path.
84 pub auto_migrate_record_form: bool,
85 /// Adapt property column encodings during checkpoints from dirty-column
86 /// values. Enabled by default; disable only to pin encodings manually.
87 pub auto_encode_on_checkpoint: bool,
88 /// Minimum rows for checkpoint-time encoding inference. Smaller columns
89 /// skip inference to avoid fitting elaborate encodings to noise.
90 pub auto_encode_min_rows: usize,
91 /// Declared memory intent for read-heavy paths. `HeapDefault` keeps the
92 /// historical behavior; `ReadSnapshot` marks the read-only snapshot path
93 /// and `BulkLoad` the batch-ingest path so mapping hints follow intent.
94 pub memory_intent: MemoryIntent,
95 /// Width and access gates for automatic bundled migration. Shape
96 /// admission still vetoes first; the observed profile must then pass all
97 /// gates before the background path recommends the inline form.
98 pub record_form_profile: RecordFormProfileThresholds,
99}
100
101/// Declared memory intent for read-heavy paths.
102///
103/// Explicit configuration rather than a best-effort hint: the read-only
104/// snapshot open and the bulk-load path declare which behavior they want, so
105/// huge-page and prefetch choices stay reviewable instead of implicit.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
107pub enum MemoryIntent {
108 /// Default heap behavior: no huge-page hint, mappings stay on base pages.
109 #[default]
110 HeapDefault,
111 /// Read-only snapshot scans: explicitly request transparent huge pages on
112 /// Linux (still best-effort, rejection falls back without failing).
113 ReadSnapshot,
114 /// Batch ingest: skip the huge-page hint to avoid THP pressure during
115 /// sequential bulk writes; mappings stay on base pages.
116 BulkLoad,
117}
118
119/// Thresholds that trigger automatic maintenance on the write path.
120#[derive(Debug, Clone, Copy)]
121pub struct AutoMaintenanceConfig {
122 /// Run property compaction when deleted-but-not-reclaimed property rows
123 /// exceed this ratio of total rows. Recommended range 0.1..=0.25.
124 /// Set to 0.0 to disable.
125 pub property_compact_ratio: f32,
126 /// Minimum tracked tombstones arming the write-path reclaim pass when the
127 /// watermark did not advance. Recommended range 2..=8. Below this count
128 /// with an unchanged watermark the commit skips the group scan entirely.
129 pub reclaim_tombstone_threshold: usize,
130 /// Rebuild the secondary property index when lag since the last rebuild
131 /// reaches this failure count. Set to 0 to disable the count trigger.
132 pub index_rebuild_failure_threshold: u64,
133 /// Rebuild the secondary property index when any lag persists longer
134 /// than this many seconds. Set to 0 to disable the age trigger.
135 pub index_max_stale_secs: u64,
136}
137
138impl Default for AutoMaintenanceConfig {
139 fn default() -> Self {
140 Self {
141 property_compact_ratio: 0.15,
142 reclaim_tombstone_threshold: 4,
143 index_rebuild_failure_threshold: 64,
144 index_max_stale_secs: 300,
145 }
146 }
147}
148
149impl Default for EdgeTableConfig {
150 fn default() -> Self {
151 Self {
152 initial_vertex_capacity: 4096,
153 overflow_chunk_edges: 4096,
154 node_group_bits: crate::edge::node_group::DEFAULT_NODE_GROUP_BITS,
155 max_mutable_csr_bytes: 100 * 1024 * 1024,
156 max_append_ops_per_group: 4096,
157 region_merge_min_density: crate::edge::node_group::REGION_MERGE_MIN_DENSITY,
158 group_merge_min_density: crate::edge::node_group::GROUP_MERGE_MIN_DENSITY,
159 auto_maintenance: AutoMaintenanceConfig::default(),
160 record_form: RecordFormPreference::default(),
161 auto_migrate_record_form: false,
162 auto_encode_on_checkpoint: true,
163 auto_encode_min_rows: 128,
164 memory_intent: MemoryIntent::default(),
165 record_form_profile: RecordFormProfileThresholds::default(),
166 }
167 }
168}
169
170/// Parameters for keyed edge property update
171pub struct UpdateEdgePropertyByKeyParams {
172 pub src: u32,
173 pub dst: u32,
174 pub rank: i64,
175 pub prop_id: u16,
176 pub value: Value,
177 pub ts: Timestamp,
178}