Skip to content

Commit dd5861e

Browse files
committed
Group experimental feature flags and expose them via /version
Summary: Move the scattered `experimental_enable_*` config fields on `CommonOptions` into a dedicated `Experimental` struct, generated by an `experimental!` macro from a single list of feature names, and expose their enabled/disabled state through the admin `/version` endpoint. Adding a new flag is now a one-line change: declare the identifier in the `experimental! { ... }` block and the macro emits the config field, `is_<name>_enabled()` / `set_<name>()` accessors, and the `/version` entry. Naming is split between the two surfaces: - the on-disk config / JSON schema keeps the explicit `experimental_enable_<name>` field, so existing configurations and the schema are unchanged; - `/version` reports the bare feature name (no `experimental_enable_` prefix) to keep the API output compact. Example `/version` response: ``` { "version": "1.7.0-dev", "min_admin_api_version": 2, "max_admin_api_version": 4, "ingress_endpoint": "http://127.0.0.1:8080/", "features": { "vqueues": false, "protocol_v7": true, "invoker_yield": false } } ``` Note: the reason this is done this way is because serde does not allow dyanmic field renames
1 parent 7bc1cb3 commit dd5861e

10 files changed

Lines changed: 175 additions & 37 deletions

File tree

crates/admin-rest-model/src/version.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +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 std::ops::RangeInclusive;
11+
use std::{borrow::Cow, collections::HashMap, ops::RangeInclusive};
1212

1313
use serde::{Deserialize, Serialize};
1414

@@ -75,6 +75,12 @@ pub struct VersionInformation {
7575
///
7676
/// Ingress endpoint that the Web UI should use to interact with.
7777
pub ingress_endpoint: Option<AdvertisedAddress<HttpIngressPort>>,
78+
79+
/// # Restate experimental features
80+
///
81+
/// List experimental features with their
82+
/// enabled state.
83+
pub features: HashMap<Cow<'static, str>, bool>,
7884
}
7985

8086
#[cfg(test)]

crates/admin/src/rest_api/version.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,6 @@ pub async fn version() -> Json<VersionInformation> {
3939
.ingress
4040
.advertised_address(tc.address_book())
4141
})),
42+
features: Configuration::pinned().common.experimental.features(),
4243
})
4344
}

crates/ingress-http/src/handler/service_handler.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,12 @@ where
140140
};
141141

142142
// Scoped invocations require vqueues to be enabled
143-
if scope.is_some() && !Configuration::pinned().common.experimental_enable_vqueues {
143+
if scope.is_some()
144+
&& !Configuration::pinned()
145+
.common
146+
.experimental
147+
.is_vqueues_enabled()
148+
{
144149
return Err(HandlerError::ScopeRequiresVQueues);
145150
}
146151

crates/invoker-impl/src/lib.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,8 @@ impl<StorageReader, TEntryEnricher, Schemas> Service<StorageReader, TEntryEnrich
257257
action_token_bucket,
258258
allow_protocol_v7: Configuration::pinned()
259259
.common
260-
.experimental_allow_protocol_v7,
260+
.experimental
261+
.is_protocol_v7_enabled(),
261262
},
262263
schemas,
263264
invocation_tasks: Default::default(),
@@ -1291,7 +1292,8 @@ where
12911292
// their memory.
12921293
if Configuration::pinned()
12931294
.common
1294-
.experimental_enable_invoker_yield
1295+
.experimental
1296+
.is_invoker_yield_enabled()
12951297
{
12961298
debug!(
12971299
restate.invocation.target = %ism.invocation_target,
@@ -3041,7 +3043,7 @@ mod tests {
30413043
async fn yield_flag_enabled_sends_yield_effect() {
30423044
// Enable the experimental yield flag
30433045
let mut config = Configuration::default();
3044-
config.common.experimental_enable_invoker_yield = true;
3046+
config.common.experimental.set_invoker_yield(true);
30453047
restate_types::config::set_current_config(config);
30463048

30473049
let invoker_options = InvokerOptionsBuilder::default()

crates/types/src/config/common.rs

Lines changed: 100 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use std::sync::LazyLock;
1515
use std::time::Duration;
1616

1717
use enumset::EnumSet;
18+
use paste::paste;
1819
use serde::{Deserialize, Serialize};
1920
use serde_with::serde_as;
2021

@@ -463,20 +464,6 @@ pub struct CommonOptions {
463464
#[serde(flatten)]
464465
pub gossip: GossipOptions,
465466

466-
/// Current in heavy development, do not enable this feature unless you are a contributor
467-
#[cfg_attr(feature = "schemars", schemars(skip))]
468-
#[serde(skip_serializing_if = "std::ops::Not::not", default)]
469-
pub experimental_enable_vqueues: bool,
470-
471-
/// When enabled, invocations that exhaust their memory budget will yield back to
472-
/// the scheduler instead of consuming retry attempts. Requires all nodes in the
473-
/// cluster to be running v1.7.0 or later because it introduces a new WAL variant.
474-
///
475-
/// Since v1.6.3
476-
#[cfg_attr(feature = "schemars", schemars(skip))]
477-
#[serde(skip_serializing_if = "std::ops::Not::not", default)]
478-
pub experimental_enable_invoker_yield: bool,
479-
480467
/// # HLC maximum drift
481468
///
482469
/// Restate uses an internal hybrid-logical-clock (HLC) to track causality between
@@ -494,6 +481,103 @@ pub struct CommonOptions {
494481
#[serde(default)]
495482
hlc_max_drift: FriendlyDuration,
496483

484+
#[serde(flatten)]
485+
pub experimental: Experimental,
486+
}
487+
488+
/// Declares the [`Experimental`] feature-flag struct from a list of feature names.
489+
///
490+
/// Each entry is a bare identifier (optionally preceded by doc comments / attributes) inside
491+
/// `experimental! { ... }`. For a feature `foo` the macro generates:
492+
/// - a `experimental_enable_foo: bool` field on [`Experimental`] — this is the on-disk /
493+
/// JSON-schema name, so the configuration schema always exposes flags as
494+
/// `experimental_enable_<feature>`;
495+
/// - `Experimental::is_foo_enabled()` and `Experimental::set_foo(enable)` accessors;
496+
/// - an entry in [`Experimental::features`] keyed on the bare name `"foo"` (without the
497+
/// `experimental_enable_` prefix), which is what is surfaced through the admin `/version` API.
498+
///
499+
/// Adding a new experimental flag is therefore a one-line change at the invocation site below:
500+
/// no other code needs to be touched for the flag to show up in `/version`.
501+
macro_rules! experimental {
502+
(@gen_struct [] -> [$($body:tt)*]) => {
503+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
504+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
505+
#[cfg_attr(feature = "schemars", schemars(default))]
506+
#[serde(rename_all = "kebab-case")]
507+
pub struct Experimental {
508+
$($body)*
509+
}
510+
};
511+
(@gen_struct [$(#[$($attrss:meta)*])* $feat:ident $(, $($tail:tt)*)?] -> [$($body:tt)*]) => {
512+
paste!{
513+
experimental!(@gen_struct [$($($tail)*)?] -> [
514+
$($body)*
515+
516+
$(#[$($attrss)*])*
517+
#[cfg_attr(feature = "schemars", schemars(skip))]
518+
#[serde(skip_serializing_if = "std::ops::Not::not", default)]
519+
[<experimental_enable_ $feat>]: bool,
520+
]);
521+
}
522+
};
523+
(@gen_features [] -> [$($field:ident)*]) => {
524+
impl Experimental {
525+
pub fn features(&self) -> std::collections::HashMap<std::borrow::Cow<'static, str>, bool> {
526+
let mut map = std::collections::HashMap::default();
527+
$(
528+
paste!{
529+
map.insert(std::borrow::Cow::Borrowed(stringify!($field)), self.[<experimental_enable_ $field>]);
530+
}
531+
)*
532+
map
533+
}
534+
}
535+
};
536+
(@gen_features [$(#[$($attrss:meta)*])* $feat:ident $(, $($tail:tt)*)?] -> [$($acc:ident)*]) => {
537+
experimental!(@gen_features [$($($tail)*)?] -> [$($acc)* $feat]);
538+
};
539+
(@gen_getters [] -> [$($field:ident)*]) => {
540+
impl Experimental {
541+
$(
542+
paste!{
543+
pub fn [<is_ $field _enabled>](&self) -> bool {
544+
self.[<experimental_enable_ $field>]
545+
}
546+
547+
pub fn [<set_ $field>](&mut self, enable: bool) {
548+
self.[<experimental_enable_ $field>] = enable;
549+
}
550+
}
551+
)*
552+
}
553+
};
554+
(@gen_getters [$(#[$($attrss:meta)*])* $feat:ident $(, $($tail:tt)*)?] -> [$($acc:ident)*]) => {
555+
experimental!(@gen_getters [$($($tail)*)?] -> [$($acc)* $feat]);
556+
};
557+
558+
559+
{$($tokens:tt)*} => {
560+
experimental!(@gen_struct [$($tokens)*] -> []);
561+
experimental!(@gen_features [$($tokens)*] -> []);
562+
experimental!(@gen_getters [$($tokens)*] -> []);
563+
};
564+
}
565+
566+
// List of experimental features. Add a new identifier below to introduce a flag; the
567+
// `experimental!` macro will generate the `experimental_enable_<name>` config field, the
568+
// `is_<name>_enabled()` / `set_<name>()` accessors, and the entry exposed (under the bare
569+
// name, without the `experimental_enable_` prefix) by the admin `/version` API.
570+
experimental! {
571+
/// Current in heavy development, do not enable this feature unless you are a contributor
572+
vqueues,
573+
574+
/// When enabled, invocations that exhaust their memory budget will yield back to
575+
/// the scheduler instead of consuming retry attempts. Requires all nodes in the
576+
/// cluster to be running v1.7.0 or later because it introduces a new WAL variant.
577+
///
578+
/// Since v1.6.3
579+
invoker_yield,
580+
497581
/// # Enables service protocol v7
498582
///
499583
/// Introduced in Restate v1.7
@@ -502,9 +586,7 @@ pub struct CommonOptions {
502586
///
503587
/// Once enabled, you **cannot** rollback back to previous versions
504588
/// where v7 is not supported < v1.7
505-
#[cfg_attr(feature = "schemars", schemars(skip))]
506-
#[serde(skip_serializing_if = "std::ops::Not::not", default)]
507-
pub experimental_allow_protocol_v7: bool,
589+
protocol_v7,
508590
}
509591

510592
serde_with::with_prefix!(pub prefix_tokio_console "tokio_console_");
@@ -740,10 +822,8 @@ impl Default for CommonOptions {
740822
initialization_timeout: NonZeroFriendlyDuration::from_secs_unchecked(5 * 60),
741823
disable_telemetry: false,
742824
gossip: GossipOptions::default(),
743-
experimental_enable_vqueues: false,
744-
experimental_enable_invoker_yield: false,
745825
hlc_max_drift: FriendlyDuration::from_millis(5000),
746-
experimental_allow_protocol_v7: false,
826+
experimental: Experimental::default(),
747827
}
748828
}
749829
}

crates/worker/src/partition/leadership/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ where
456456
})?
457457
.into_guard();
458458

459-
let scheduler_service = if config.common.experimental_enable_vqueues {
459+
let scheduler_service = if config.common.experimental.is_vqueues_enabled() {
460460
SchedulerService::create(
461461
ResourceManager::create(
462462
partition_store.partition_db().clone(),

crates/worker/src/partition/state_machine/lifecycle/paused.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@ where
6565
// Invoker paused the invocation, let's record the event, then set the status to paused
6666
debug_if_leader!(ctx.is_leader, "Paused the invocation");
6767

68-
if Configuration::pinned().common.experimental_enable_vqueues {
68+
if Configuration::pinned()
69+
.common
70+
.experimental
71+
.is_vqueues_enabled()
72+
{
6973
// todo: use the new status
7074
let entry_id = EntryId::from(&invocation_id);
7175
let Some(header) = ctx

crates/worker/src/partition/state_machine/lifecycle/resume.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ where
4141

4242
metadata.timestamps.update(ctx.record_created_at);
4343

44-
if Configuration::pinned().common.experimental_enable_vqueues {
44+
if Configuration::pinned()
45+
.common
46+
.experimental
47+
.is_vqueues_enabled()
48+
{
4549
ctx.vqueue_move_invocation_to_inbox_stage(&self.invocation_id)
4650
.await?;
4751
} else {

crates/worker/src/partition/state_machine/lifecycle/suspend.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,11 @@ where
109109
.timestamps
110110
.update(ctx.record_created_at);
111111

112-
if Configuration::pinned().common.experimental_enable_vqueues {
112+
if Configuration::pinned()
113+
.common
114+
.experimental
115+
.is_vqueues_enabled()
116+
{
113117
let now = UniqueTimestamp::from_unix_millis_unchecked(ctx.record_created_at);
114118
let entry_id = EntryId::from(&self.invocation_id);
115119
let Some(header) = ctx

crates/worker/src/partition/state_machine/mod.rs

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -849,7 +849,11 @@ impl<S> StateMachineApplyContext<'_, S> {
849849
});
850850
}
851851

852-
if Configuration::pinned().common.experimental_enable_vqueues {
852+
if Configuration::pinned()
853+
.common
854+
.experimental
855+
.is_vqueues_enabled()
856+
{
853857
// skips the rest of this logic and jumps straight to vqueues' implementation
854858
return self
855859
.vqueue_enqueue(
@@ -1440,7 +1444,11 @@ impl<S> StateMachineApplyContext<'_, S> {
14401444
+ WriteLockTable
14411445
+ ReadVQueueTable,
14421446
{
1443-
if Configuration::pinned().common.experimental_enable_vqueues {
1447+
if Configuration::pinned()
1448+
.common
1449+
.experimental
1450+
.is_vqueues_enabled()
1451+
{
14441452
self.vqueue_enqueue_state_mutation(mutation).await?;
14451453
} else {
14461454
let service_status = self
@@ -1779,7 +1787,11 @@ impl<S> StateMachineApplyContext<'_, S> {
17791787
Some(&invocation_target),
17801788
)?;
17811789

1782-
if Configuration::pinned().common.experimental_enable_vqueues {
1790+
if Configuration::pinned()
1791+
.common
1792+
.experimental
1793+
.is_vqueues_enabled()
1794+
{
17831795
let record_unique_ts =
17841796
UniqueTimestamp::from_unix_millis_unchecked(self.record_created_at);
17851797
let new_status = match termination_flavor {
@@ -1892,7 +1904,11 @@ impl<S> StateMachineApplyContext<'_, S> {
18921904
Some(&invocation_target),
18931905
)?;
18941906

1895-
if Configuration::pinned().common.experimental_enable_vqueues {
1907+
if Configuration::pinned()
1908+
.common
1909+
.experimental
1910+
.is_vqueues_enabled()
1911+
{
18961912
let record_unique_ts =
18971913
UniqueTimestamp::from_unix_millis_unchecked(self.record_created_at);
18981914
let new_status = match termination_flavor {
@@ -2758,7 +2774,11 @@ impl<S> StateMachineApplyContext<'_, S> {
27582774
.await?;
27592775
}
27602776

2761-
if Configuration::pinned().common.experimental_enable_vqueues {
2777+
if Configuration::pinned()
2778+
.common
2779+
.experimental
2780+
.is_vqueues_enabled()
2781+
{
27622782
if invocation_target.invocation_target_ty()
27632783
== InvocationTargetType::VirtualObject(VirtualObjectHandlerType::Exclusive)
27642784
{
@@ -3109,7 +3129,11 @@ impl<S> StateMachineApplyContext<'_, S> {
31093129
+ WriteJournalTable
31103130
+ journal_table_v2::WriteJournalTable,
31113131
{
3112-
if Configuration::pinned().common.experimental_enable_vqueues {
3132+
if Configuration::pinned()
3133+
.common
3134+
.experimental
3135+
.is_vqueues_enabled()
3136+
{
31133137
return Ok(());
31143138
}
31153139

@@ -4488,7 +4512,11 @@ impl<S> StateMachineApplyContext<'_, S> {
44884512
.put_invocation_status(&invocation_id, &InvocationStatus::Invoked(metadata))
44894513
.map_err(Error::Storage)?;
44904514

4491-
if Configuration::pinned().common.experimental_enable_vqueues {
4515+
if Configuration::pinned()
4516+
.common
4517+
.experimental
4518+
.is_vqueues_enabled()
4519+
{
44924520
self.vqueue_move_invocation_to_inbox_stage(&invocation_id)
44934521
.await?;
44944522
} else {
@@ -4529,7 +4557,11 @@ impl<S> StateMachineApplyContext<'_, S> {
45294557

45304558
metadata.timestamps.update(self.record_created_at);
45314559

4532-
if Configuration::pinned().common.experimental_enable_vqueues {
4560+
if Configuration::pinned()
4561+
.common
4562+
.experimental
4563+
.is_vqueues_enabled()
4564+
{
45334565
let now = UniqueTimestamp::from_unix_millis_unchecked(self.record_created_at);
45344566
let entry_id = EntryId::from(&invocation_id);
45354567
let Some(header) = self

0 commit comments

Comments
 (0)