Skip to content

Commit 779eb09

Browse files
tillrohrmannclaude
andcommitted
[Limiter] Persist rule book per partition via UpsertRuleBook log entry
Wires the cluster-global rule book into the partition processor state machine so leader-driven distribution (Step 5) has somewhere to land: * `Command::UpsertRuleBook(UpsertRuleBook { partition_key_range, rule_book: Bytes })` — new wal-protocol command. The payload is bilrost-encoded `RuleBook` carried as opaque `Bytes` (precedent: `Command::VQSchedulerDecisions`) so flexbuffers-based `Envelope` serde does not need to drag full serde derive through every limiter type. * `ReadFsmTable::get_rule_book` / `WriteFsmTable::put_rule_book` (both `*Since v1.7.0*`) backed by a new FSM slot `RULE_BOOK = 9` in the partition store. Each partition writes the same logical rule book; readback on PP boot gives leader transitions the right state without an extra metadata-store round trip. * `RuleBook` gains `StorageEncode`/`StorageDecode` (bilrost) plus `bilrost_encode_to_bytes` / `bilrost_decode` helpers. * `StateMachine` carries `rule_book: RuleBook` in-memory; loaded from FSM table at PP boot. The `Command::UpsertRuleBook` apply path bilrost-decodes the bytes, idempotency-checks the version (skips when not strictly newer), diffs against the previous in-memory book, persists via `put_rule_book` within the same transaction, updates in-memory state, and emits `Action::RulesUpdated(Vec<RuleUpdate>)` when non-empty. * `Action::RulesUpdated` is dispatched in `leader_state` via a new `SchedulerService::on_rules_updated` API that forwards the batch through the existing resource-manager mpsc to `UserLimiter`. Followers don't dispatch actions, matching the "only the leader's UserLimiter is live" design. * `restate-wal-protocol` does NOT depend on `restate-limiter` — the opaque-bytes payload keeps it isolated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1bdbc41 commit 779eb09

20 files changed

Lines changed: 205 additions & 9 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/limiter/src/rule_book.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,20 @@ impl RuleBook {
388388
self.diff(&Self::empty())
389389
}
390390

391+
/// Encode the rule book as bilrost-encoded bytes. Used by callers that
392+
/// want to embed the book in another wire format opaquely (e.g. the
393+
/// `Command::UpsertRuleBook` envelope) without taking a direct
394+
/// dependency on the `bilrost` crate.
395+
pub fn bilrost_encode_to_bytes(&self) -> bytes::Bytes {
396+
bilrost::Message::encode_to_bytes(self)
397+
}
398+
399+
/// Decode a rule book from bilrost-encoded bytes. Inverse of
400+
/// [`Self::bilrost_encode_to_bytes`].
401+
pub fn bilrost_decode<B: bytes::Buf>(buf: B) -> Result<Self, bilrost::DecodeError> {
402+
<Self as bilrost::OwnedMessage>::decode(buf)
403+
}
404+
391405
/// Test/internal helper.
392406
#[cfg(test)]
393407
pub fn from_parts(version: Version, rules: HashMap<RuleId, PersistedRule>) -> Self {
@@ -497,6 +511,37 @@ impl Versioned for RuleBook {
497511
}
498512
}
499513

514+
mod storage {
515+
use bytes::BytesMut;
516+
517+
use restate_types::storage::{
518+
StorageCodecKind, StorageDecode, StorageDecodeError, StorageEncode, StorageEncodeError,
519+
decode, encode,
520+
};
521+
522+
use super::RuleBook;
523+
524+
impl StorageEncode for RuleBook {
525+
fn encode(&self, buf: &mut BytesMut) -> Result<(), StorageEncodeError> {
526+
encode::encode_bilrost(self, buf)
527+
}
528+
529+
fn default_codec(&self) -> StorageCodecKind {
530+
StorageCodecKind::Bilrost
531+
}
532+
}
533+
534+
impl StorageDecode for RuleBook {
535+
fn decode<B: bytes::Buf>(
536+
buf: &mut B,
537+
kind: StorageCodecKind,
538+
) -> Result<Self, StorageDecodeError> {
539+
assert_eq!(kind, StorageCodecKind::Bilrost);
540+
decode::decode_bilrost(buf)
541+
}
542+
}
543+
}
544+
500545
// `RulePattern<ReString>` rides through bilrost via its `Display`/`FromStr`
501546
// round-trip — same trick as `InvocationId`. This avoids spreading bilrost
502547
// glue across `Pattern`/`RestrictedValue` and keeps the wire form

crates/partition-store/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ restate-workspace-hack = { workspace = true }
1717
restate-clock = { workspace = true }
1818
restate-core = { workspace = true }
1919
restate-errors = { workspace = true }
20+
restate-limiter = { workspace = true, features = ["rule-book"] }
2021
restate-memory = { workspace = true }
2122
restate-object-store-util = { workspace = true }
2223
restate-rocksdb = { workspace = true }

crates/partition-store/src/fsm_table/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
// the Business Source License, use of this software will be governed
99
// by the Apache License, Version 2.0.
1010

11+
use restate_limiter::RuleBook;
1112
use restate_storage_api::Result;
1213
use restate_storage_api::fsm_table::{
1314
CachedEpochMetadata, PartitionDurability, ReadFsmTable, SequenceNumber, WriteFsmTable,
@@ -66,6 +67,13 @@ pub(crate) mod fsm_variable {
6667
/// deployments.
6768
/// *Since v1.6.3*
6869
pub(crate) const JC_ORPHAN_CLEANUP_DONE: u64 = 8;
70+
71+
/// Cluster-global rule book persisted per-partition. Each partition writes
72+
/// the same logical rule book (via `Command::UpsertRuleBook` log entries),
73+
/// and reads it back on PP startup so leader transitions inherit the same
74+
/// rule set without an extra metadata-store round trip.
75+
/// *Since v1.7.0*
76+
pub(crate) const RULE_BOOK: u64 = 9;
6977
}
7078

7179
fn get<T: PartitionStoreProtobufValue, S: StorageAccess>(
@@ -196,6 +204,11 @@ impl ReadFsmTable for PartitionStore {
196204
let key = create_key(self.partition_id(), fsm_variable::PARTITION_CONFIG_STATE);
197205
self.get_value_storage_codec(key)
198206
}
207+
208+
async fn get_rule_book(&mut self) -> Result<Option<RuleBook>> {
209+
let key = create_key(self.partition_id(), fsm_variable::RULE_BOOK);
210+
self.get_value_storage_codec(key)
211+
}
199212
}
200213

201214
impl WriteFsmTable for PartitionStoreTransaction<'_> {
@@ -253,4 +266,9 @@ impl WriteFsmTable for PartitionStoreTransaction<'_> {
253266
let key = create_key(self.partition_id(), fsm_variable::PARTITION_CONFIG_STATE);
254267
self.put_kv_storage_codec(key, state)
255268
}
269+
270+
fn put_rule_book(&mut self, rule_book: &RuleBook) -> Result<()> {
271+
let key = create_key(self.partition_id(), fsm_variable::RULE_BOOK);
272+
self.put_kv_storage_codec(key, rule_book)
273+
}
256274
}

crates/storage-api/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ test-util = []
1414
restate-workspace-hack = { workspace = true }
1515

1616
restate-clock = { workspace = true }
17-
restate-limiter = { workspace = true, features = ["bilrost"] }
17+
restate-limiter = { workspace = true, features = ["bilrost", "rule-book"] }
1818
restate-memory = { workspace = true }
1919
restate-sharding = { workspace = true }
2020
restate-types = { workspace = true }

crates/storage-api/src/fsm_table/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use std::future::Future;
1212

1313
use bytes::BytesMut;
1414

15+
use restate_limiter::RuleBook;
1516
use restate_types::identifiers::LeaderEpoch;
1617
use restate_types::logs::Lsn;
1718
use restate_types::message::MessageIndex;
@@ -48,6 +49,12 @@ pub trait ReadFsmTable {
4849
fn get_partition_config_state(
4950
&mut self,
5051
) -> impl Future<Output = Result<Option<CachedEpochMetadata>>> + Send + '_;
52+
53+
/// The rule book persisted for this partition. Returns `None` when the
54+
/// partition has not yet observed any rule-book update, in which case
55+
/// callers should treat it as the empty default.
56+
/// *Since v1.7.0*
57+
fn get_rule_book(&mut self) -> impl Future<Output = Result<Option<RuleBook>>> + Send + '_;
5158
}
5259

5360
pub trait WriteFsmTable {
@@ -64,6 +71,10 @@ pub trait WriteFsmTable {
6471
fn put_schema(&mut self, schema: &Schema) -> Result<()>;
6572

6673
fn put_partition_config_state(&mut self, state: &CachedEpochMetadata) -> Result<()>;
74+
75+
/// Persist the rule book for this partition.
76+
/// *Since v1.7.0*
77+
fn put_rule_book(&mut self, rule_book: &RuleBook) -> Result<()>;
6778
}
6879

6980
#[derive(Debug, Clone, Copy, derive_more::From, derive_more::Into)]

crates/vqueues/src/scheduler.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use std::pin::Pin;
1515
use std::future::poll_fn;
1616
use std::task::Poll;
1717

18+
use restate_limiter::RuleUpdate;
1819
use restate_storage_api::StorageError;
1920
use restate_storage_api::vqueue_table::scheduler::{RunAction, SchedulerAction, YieldAction};
2021
use restate_storage_api::vqueue_table::{EntryKey, ScanVQueueTable, VQueueStore};
@@ -166,6 +167,14 @@ impl<S: VQueueStore> SchedulerService<S> {
166167
}
167168
}
168169

170+
/// Forward a batch of rule-book updates to the embedded resource
171+
/// manager. No-op when the scheduler is disabled (followers).
172+
pub fn on_rules_updated(&self, updates: Vec<RuleUpdate>) {
173+
if let State::Active(ref drr_scheduler) = self.state {
174+
drr_scheduler.on_rules_updated(updates);
175+
}
176+
}
177+
169178
/// Return reserved resources (concurrency permit + memory lease) for a given
170179
/// item hash if it was assigned by the scheduler.
171180
///

crates/vqueues/src/scheduler/drr.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,11 @@ impl<S: VQueueStore> DRRScheduler<S> {
243243
Some(permit.build(&self.resource_manager))
244244
}
245245

246+
/// Forward a batch of rule-book updates to the embedded resource manager.
247+
pub fn on_rules_updated(&self, updates: Vec<restate_limiter::RuleUpdate>) {
248+
self.resource_manager.on_rules_updated(updates);
249+
}
250+
246251
#[tracing::instrument(skip_all)]
247252
#[track_caller]
248253
pub fn on_inbox_event(&mut self, event: VQueueEvent) {

crates/vqueues/src/scheduler/resource_manager.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use tracing::trace;
2525

2626
use restate_futures_util::concurrency::Concurrency;
2727
use restate_limiter::RuleHandle;
28+
use restate_limiter::RuleUpdate;
2829
use restate_memory::{MemoryPool, NonZeroByteCount};
2930
use restate_storage_api::StorageError;
3031
use restate_storage_api::lock_table::LoadLocks;
@@ -92,6 +93,15 @@ impl ResourceManager {
9293
})
9394
}
9495

96+
/// Forward a batch of rule-book updates to the resource manager's
97+
/// internal channel. Picked up on the next `poll_resources` tick and
98+
/// applied to the `UserLimiter`.
99+
pub fn on_rules_updated(&self, updates: Vec<RuleUpdate>) {
100+
// Sending via `tx` (which is held alongside `rx` for keep-alive)
101+
// never fails: the receiver is owned by `self`.
102+
let _ = self.tx.send(ResourceManagerUpdate::RulesUpdated(updates));
103+
}
104+
95105
/// Removes the vqueue from the resource it's blocked on
96106
pub(super) fn remove_vqueue(&mut self, handle: VQueueHandle, blocked_resource: &ResourceKind) {
97107
match blocked_resource {

crates/wal-protocol/src/control.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
// the Business Source License, use of this software will be governed
99
// by the Apache License, Version 2.0.
1010

11+
use bytes::Bytes;
12+
1113
use restate_storage_api::fsm_table::{CurrentReplicaSetState, NextReplicaSetState};
1214
use restate_types::identifiers::{LeaderEpoch, PartitionId};
1315
use restate_types::logs::{Keys, Lsn, SequenceNumber};
@@ -170,3 +172,24 @@ pub struct UpsertSchema {
170172
pub partition_key_range: Keys,
171173
pub schema: Schema,
172174
}
175+
176+
/// Consistently distribute the cluster-global rule book across partition
177+
/// replicas. Each partition's leader observes a node-level cache of the
178+
/// rule book stored in the metadata store and proposes this command when
179+
/// it sees a higher version than the partition's in-memory state.
180+
/// Followers and the leader (replaying) apply it idempotently — no-op if
181+
/// the carried rule book's version is not greater than the current
182+
/// in-memory version.
183+
///
184+
/// `rule_book` is the bilrost-encoded [`restate_limiter::RuleBook`]. It
185+
/// is carried as opaque bytes (same precedent as
186+
/// [`crate::Command::VQSchedulerDecisions`]) so the wal-protocol crate
187+
/// doesn't need to drag full serde derive through every limiter type.
188+
/// The state machine decodes once on apply.
189+
///
190+
/// Since v1.7.0.
191+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
192+
pub struct UpsertRuleBook {
193+
pub partition_key_range: Keys,
194+
pub rule_book: Bytes,
195+
}

0 commit comments

Comments
 (0)