Skip to content

[Limiter] Add RuleBook persistent data model and writer/diff logic#4677

Open
tillrohrmann wants to merge 2 commits intorestatedev:mainfrom
tillrohrmann:issues/4655
Open

[Limiter] Add RuleBook persistent data model and writer/diff logic#4677
tillrohrmann wants to merge 2 commits intorestatedev:mainfrom
tillrohrmann:issues/4655

Conversation

@tillrohrmann
Copy link
Copy Markdown
Contributor

Introduces the cluster-global rule book that backs the in-memory Rules store of each partition processor's UserLimiter. This commit lands the foundational data layer for issue #4655:

  • RuleBook / PersistedRule / PersistedUserLimits in restate-limiter::rule_book, bilrost-encoded, Versioned. The RuleBook is keyed by RuleId (xxh3-64 of the rule pattern's canonical display form) rendered as rul_… resource ids. A deliberate 64-bit hash is used instead of the 128-bit norm for rendered-id brevity; the doc comment captures the trade-off.

  • Soft-tombstone semantics: PersistedRule.disabled: bool defaults to false so an active rule is bilrost's empty state and gets omitted from the wire.

  • Writer logic: RuleBook::apply_change for Create / Patch / Delete, with the version-bump contract — create/recreate uses the new book version, runtime-relevant patches advance the per-rule version, reason-only edits bump only last_modified and the book version, no-ops bump nothing. Hard cap on total rules (MAX_RULES_PER_BOOK) — configurable knob comes later.

  • RuleBook::diff: presence + per-rule version drives Vec<RuleUpdate> for the runtime, with disabled rules treated as absent. diff_from_empty for bootstrap consumers.

Supporting infra:

  • Adds Rule("rul") to IdResourceType; promotes restate-types::id_util and base62_util to public modules and IdEncoder::{new,push_u64,push_u128} to public so external crates can implement ResourceId.

  • Moves UserLimits and RuleUpdate from restate-worker-api into restate-limiter

  • Adds generic-array to the workspace deps.

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@tillrohrmann tillrohrmann force-pushed the issues/4655 branch 2 times, most recently from 9af151a to 4d8d0ad Compare April 30, 2026 15:12
Comment on lines +34 to +35
/// here as a constant for now — Step 6 makes this configurable via
/// `worker.rule_book.max_rules`.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tbd

@tillrohrmann
Copy link
Copy Markdown
Contributor Author

@codex review

Comment on lines +58 to +59
/// Collisions are detectable at admin write time and surface as a 409 from
/// the create endpoint.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But nothing can be done about them if they occur.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/limiter/Cargo.toml
Comment on lines +17 to +27
# Enables the persistent rule book (RuleBook, PersistedRule, RuleId,
# diff/apply_change machinery). Implies `bilrost`.
rule-book = [
"bilrost",
"dep:bytes",
"dep:generic-array",
"dep:restate-clock",
"dep:restate-encoding",
"dep:serde",
"dep:xxhash-rust",
]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feature is probably an overkill and we should either remove the features or move the rule-book to a different crate. Will become clearer as the user facing API for the rule book materializes.

}
/// Appends a u64 value as a padded base62 encoded string to the underlying buffer
pub(crate) fn push_u64(&mut self, i: u64) {
pub fn push_u64(&mut self, i: u64) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used to implement ResourceIds outside of the restate-types crate.

Comment thread crates/limiter/src/rule_book.rs Outdated
pub disabled: bool,
/// Last time this rule was modified
#[bilrost(tag(6))]
pub last_modified: RoughTimestamp,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reasoning behind choosing RoughTimestamp here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Primarily to safe a few bytes as I thought that a resolution of seconds for when the rule was last modified is fine enough for all practical purposes.

#[bilrost(tag(1))]
version: Version,
#[bilrost(tag(2), encoding(map<general, general>))]
rules: HashMap<RuleId, PersistedRule>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One option is to use the string representation of the rule pattern as the key directly and remove it from the value. We can leave it as as a raw ReString or RulePattern` if it's strictly necessary.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea for introducing the RuleId was to have a handle for the REST API to refer to rules when patching them for example. The stringified version of the pattern would also work but it will result in slightly less nice looking URLs:

curl -X PATCH localhost:9070/rules/rul_XYZ

vs.

curl -X PATCH localhost:9070/rules/*/foobar/*

Alternatively, we can send the pattern as part of the request body if specifying it as part of the path is not so nice.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another reason was that @slinkydeveloper has concerns about exposing the RulePattern to users as the wildcards can be confusing. We had a discussion whether a higher level language would be better to express the rule patterns https://www.notion.so/restatedev/Concurrency-limits-API-31ebfec2629180e3b091cb72e506b86d?source=copy_link#321bfec2629180719a0fc7f099b22f2d. So far we didn't find a good alternative, though.

Copy link
Copy Markdown
Contributor

@slinkydeveloper slinkydeveloper May 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The character * cannot be used in an url, needs to be percent encoded. I suggest the rule goes in the body

Comment thread crates/limiter/src/user_limits.rs
tillrohrmann and others added 2 commits May 5, 2026 16:22
Introduces the cluster-global rule book that backs the in-memory `Rules`
store of each partition processor's `UserLimiter`. This commit lands the
foundational data layer for issue restatedev#4655 (steps 1 and 2 of the plan):

  * `RuleBook` / `PersistedRule` / `PersistedUserLimits` in
    `restate-limiter::rule_book`, bilrost-encoded, `Versioned`. The
    `RuleBook` is keyed by `RuleId` (xxh3-64 of the rule pattern's
    canonical display form) rendered as `rul_…` resource ids. A
    deliberate 64-bit hash is used instead of the 128-bit norm for
    rendered-id brevity; the doc comment captures the trade-off.

  * Soft-tombstone semantics: `PersistedRule.disabled: bool` defaults
    to `false` so an active rule is bilrost's empty state and gets
    omitted from the wire.

  * Writer logic: `RuleBook::apply_change` for `Create` / `Patch` /
    `Delete`, with the version-bump contract — create/recreate uses
    the new book version, runtime-relevant patches advance the
    per-rule version, reason-only edits bump only `last_modified` and
    the book version, no-ops bump nothing. Hard cap on total rules
    (`MAX_RULES_PER_BOOK`) — configurable knob comes later.

  * `RuleBook::diff`: presence + per-rule version drives
    `Vec<RuleUpdate>` for the runtime, with `disabled` rules treated
    as absent. `diff_from_empty` for bootstrap consumers.

Supporting infra:

  * Adds `Rule("rul")` to `IdResourceType`; promotes
    `restate-types::id_util` and `base62_util` to public modules and
    `IdEncoder::{new,push_u64,push_u128}` to public so external crates
    can implement `ResourceId`.

  * Moves `UserLimits` and `RuleUpdate` from `restate-worker-api` into
    `restate-limiter`

  * Adds `generic-array` to the workspace deps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants