rototo 0.1.0-alpha.4

Control plane for runtime configuration of your application.
Documentation
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
# Service Degradation Policy

Incidents rarely recover because of one perfect switch. A team may reduce
concurrency, watch the queue, pause non-critical work, try a fallback provider,
then tighten or relax the posture as the system responds. I want those moves to
be reviewed and reversible, but I do not want to redeploy the service for every
adjustment during recovery.

Rototo fits the policy layer in that loop. The service still owns metrics,
queues, retries, provider health, and enforcement. Rototo selects the reviewed
operating policy from the runtime facts the service supplies.

We will model that as `degradation-config`, with one variable named
`service-degradation-policy`.

## Start With The Recovery Boundary

The runtime question is not "is the service healthy?" Rototo should not decide
that. The service and observability system already know queue pressure,
provider health, error rates, and retry behavior.

The runtime question I want is:

```text
Given the service state we already measured, which reviewed operating policy
should this request use?
```

The first version of that policy can be small: run normally while pressure is
normal, and reduce load when queue pressure is high.

## Create The Workspace

Create the workspace with a variable and a resource template:

```sh
rototo init degradation-config --variable service-degradation-policy
rototo init degradation-config --resource service-degradation-policy
```

Replace `degradation-config/variables/service-degradation-policy.toml`:

```toml
schema_version = 1

description = "Operating policy selected while the service is under pressure"
type = "resource:service-degradation-policy"

[resolve]
default = "normal"
```

Replace `degradation-config/resources/service-degradation-policy.toml`:

```toml
schema_version = 1

description = "Service degradation policy objects"
schema = "../schemas/service-degradation-policy.schema.json"
```

The variable chooses a policy key. The resource validates the policy object
behind that key. During an incident, the app should not have to trust a
half-shaped object while operators are making fast changes.

## Define The Policy Shape

Before adding policies, define the knobs the service is willing to apply.
Replace `degradation-config/schemas/service-degradation-policy.schema.json`:

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": [
    "mode",
    "max_concurrency",
    "background_jobs_enabled",
    "non_critical_fanout",
    "fallback_provider"
  ],
  "properties": {
    "mode": { "type": "string", "enum": ["normal", "degraded", "severe"] },
    "max_concurrency": { "type": "integer", "minimum": 1, "maximum": 200 },
    "background_jobs_enabled": { "type": "boolean" },
    "non_critical_fanout": { "type": "string", "enum": ["send", "defer", "pause"] },
    "fallback_provider": { "type": "string", "enum": ["primary", "secondary"] }
  },
  "additionalProperties": false
}
```

The schema is deliberately operational. It says which fields the service will
honor, which modes are allowed, and how far concurrency can be pushed. If
someone tries to set `max_concurrency = 0` during an incident, lint catches
that before the workspace is released.

## Add The First Policies

Rename the generated object file from
`degradation-config/resources/service-degradation-policy-objects/default.toml`
to `degradation-config/resources/service-degradation-policy-objects/normal.toml`,
then replace its contents:

```toml
mode = "normal"
max_concurrency = 100
background_jobs_enabled = true
non_critical_fanout = "send"
fallback_provider = "primary"
```

Create
`degradation-config/resources/service-degradation-policy-objects/degraded.toml`:

```toml
mode = "degraded"
max_concurrency = 30
background_jobs_enabled = false
non_critical_fanout = "defer"
fallback_provider = "primary"
```

Create
`degradation-config/resources/service-degradation-policy-objects/severe.toml`:

```toml
mode = "severe"
max_concurrency = 10
background_jobs_enabled = false
non_critical_fanout = "pause"
fallback_provider = "secondary"
```

The `severe` policy is not selected yet. I still like defining it early because
it gives reviewers a concrete recovery posture to inspect before the team needs
it under pressure.

## Select Degraded During High Pressure

Now add the condition that moves the service from normal to degraded mode.

Create `degradation-config/qualifiers/high-queue-pressure.toml`:

```toml
schema_version = 1
description = "Service reports high queue pressure"

[[predicate]]
attribute = "service.queue_pressure"
op = "eq"
value = "high"
```

Update `degradation-config/variables/service-degradation-policy.toml`:

```toml
schema_version = 1

description = "Operating policy selected while the service is under pressure"
type = "resource:service-degradation-policy"

[resolve]
default = "normal"

[[resolve.rule]]
qualifier = "high-queue-pressure"
value = "degraded"
```

The app still decides when queue pressure is high. Rototo only turns that
runtime fact into the reviewed policy object.

## Generate The First Context Contract

The qualifier introduced `service.queue_pressure`. Generate the context schema
after that path exists:

```sh
rototo init degradation-config --context
```

On this workspace, rototo writes
`degradation-config/schemas/context.schema.json`:

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "additionalProperties": true,
  "properties": {
    "service": {
      "additionalProperties": true,
      "properties": {
        "queue_pressure": { "type": "string" }
      },
      "type": "object"
    }
  },
  "type": "object"
}
```

Lint the workspace:

```sh
rototo lint degradation-config
```

Then resolve both paths.

Normal pressure selects `normal`:

```sh
rototo resolve degradation-config \
  --variable service-degradation-policy \
  --context service.queue_pressure=normal
```

```text
value key: normal
value: {"background_jobs_enabled":true,"fallback_provider":"primary","max_concurrency":100,"mode":"normal","non_critical_fanout":"send"}
```

High pressure selects `degraded`:

```sh
rototo resolve degradation-config \
  --variable service-degradation-policy \
  --context service.queue_pressure=high
```

```text
value key: degraded
value: {"background_jobs_enabled":false,"fallback_provider":"primary","max_concurrency":30,"mode":"degraded","non_critical_fanout":"defer"}
```

This is the first recovery move: reduce work everywhere that reports high
pressure.

## Try A Stronger Policy On A Bucket

Sometimes the first move is not enough. Queue depth keeps climbing, the
primary provider stays slow, or deferred work is still taking too much capacity.
The next move might be `severe`, but applying it to every account at once can
be more disruption than the team needs.

A bucket gives us a stable trial path. The same account stays in or out of the
trial while the salt and range stay the same, so logs and support cases remain
explainable.

Create `degradation-config/qualifiers/degradation-trial-bucket.toml`:

```toml
schema_version = 1
description = "Stable account bucket for trying a stronger recovery policy"

[[predicate]]
attribute = "account.id"
op = "bucket"
salt = "service-degradation-recovery-2026-06"
range = [0, 1000]
```

The bucket range is on a 0 to 10000 scale, so `[0, 1000]` is ten percent.

Now compose the bucket with high pressure.

Create `degradation-config/qualifiers/severe-recovery-trial.toml`:

```toml
schema_version = 1
description = "High-pressure requests in the severe recovery trial bucket"

[[predicate]]
attribute = "qualifier.high-queue-pressure"
op = "eq"
value = true

[[predicate]]
attribute = "qualifier.degradation-trial-bucket"
op = "eq"
value = true
```

Update the variable so the severe trial wins before the broader degraded rule:

```toml
schema_version = 1

description = "Operating policy selected while the service is under pressure"
type = "resource:service-degradation-policy"

[resolve]
default = "normal"

[[resolve.rule]]
qualifier = "severe-recovery-trial"
value = "severe"

[[resolve.rule]]
qualifier = "high-queue-pressure"
value = "degraded"
```

Rule order carries the recovery intent. High-pressure requests in the trial
bucket get `severe`; the rest of the high-pressure traffic stays on
`degraded`.

## Regenerate The Context Contract

The bucket introduced `account.id`. Regenerate the context schema and review
the diff:

```sh
rototo init degradation-config --context --force
```

On this workspace, the regenerated
`degradation-config/schemas/context.schema.json` includes both runtime facts:

```json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "additionalProperties": true,
  "properties": {
    "account": {
      "additionalProperties": true,
      "properties": {
        "id": { "type": ["boolean", "number", "string"] }
      },
      "type": "object"
    },
    "service": {
      "additionalProperties": true,
      "properties": {
        "queue_pressure": { "type": "string" }
      },
      "type": "object"
    }
  },
  "type": "object"
}
```

Lint again:

```sh
rototo lint degradation-config
```

## Resolve The Trial Paths

`acct-0001` is outside the severe trial bucket, so high pressure still selects
`degraded`:

```sh
rototo resolve degradation-config \
  --variable service-degradation-policy \
  --context service.queue_pressure=high \
  --context account.id=acct-0001
```

```text
test: bucket salt=service-degradation-recovery-2026-06 range=[0,1000] bucket=5274
value key: degraded
```

`acct-001` is inside the bucket, so the same high-pressure state selects
`severe`:

```sh
rototo resolve degradation-config \
  --variable service-degradation-policy \
  --context service.queue_pressure=high \
  --context account.id=acct-001
```

```text
test: bucket salt=service-degradation-recovery-2026-06 range=[0,1000] bucket=540
value key: severe
value: {"background_jobs_enabled":false,"fallback_provider":"secondary","max_concurrency":10,"mode":"severe","non_critical_fanout":"pause"}
```

This is the second recovery move: try the stronger policy on a stable slice
while the rest of the pressured traffic stays on the first degraded policy.

## Iterate Through Review

Recovery may need a few variations. Because the policy lives in the workspace,
each variation can be a small reviewed diff.

To widen the severe policy without reshuffling account assignment, keep the
salt and expand the range:

```toml
[[predicate]]
attribute = "account.id"
op = "bucket"
salt = "service-degradation-recovery-2026-06"
range = [0, 3000]
```

To make the severe policy stronger without widening it, change the policy
object:

```toml
mode = "severe"
max_concurrency = 5
background_jobs_enabled = false
non_critical_fanout = "pause"
fallback_provider = "secondary"
```

To roll back the trial, remove the `severe-recovery-trial` rule or move the
bucket range back down. The service can refresh the workspace and apply the new
policy to future resolutions while the last successfully loaded workspace
stays active if a refresh fails.

Rototo does not decide whether the variation worked. The service metrics,
alerts, dashboards, and incident process still answer that. Rototo makes the
policy change reviewed, typed, reproducible, and reversible.

## Use The Policy In The App

The app should resolve the policy where it is about to apply concurrency,
fanout, background work, or provider routing. It should pass facts it already
knows: current service pressure and the account ID used for stable assignment.

```rust
use serde::Deserialize;

use rototo::{ResolveContext, Workspace};

#[derive(Debug, Deserialize)]
struct DegradationPolicy {
    mode: String,
    max_concurrency: u64,
    background_jobs_enabled: bool,
    non_critical_fanout: String,
    fallback_provider: String,
}

async fn degradation_policy(
    workspace: &Workspace,
    queue_pressure: &str,
    account_id: &str,
) -> Result<DegradationPolicy, Box<dyn std::error::Error>> {
    let context = ResolveContext::from_json(serde_json::json!({
        "service": {
            "queue_pressure": queue_pressure
        },
        "account": {
            "id": account_id
        }
    }))?;

    let resolution = workspace
        .resolve_variable("service-degradation-policy", &context)
        .await?;
    let value_key = resolution.value_key.clone();
    let policy: DegradationPolicy = serde_json::from_value(resolution.value)?;

    println!(
        "selected service-degradation-policy `{}` from {:?}",
        value_key,
        workspace.source_fingerprint()
    );

    Ok(policy)
}
```

The selected policy is not the incident state. It is one input to the service's
own backpressure and routing behavior.

## Keep The Control Loop Clear

At this boundary, rototo should own:

- reviewed degradation modes;
- concurrency and fanout policy;
- fallback-provider selection;
- stable buckets for trying a stronger recovery posture;
- reversible changes during recovery.

Keep these in the service, observability system, or incident process:

- queue depth measurement;
- provider health detection;
- retry scheduling;
- per-request execution;
- metrics that show whether recovery is working;
- incident ownership and customer communication.

That is the split that keeps recovery sane. The service keeps running the live
control loop. Rototo gives the team reviewed policy it can change, observe,
widen, tighten, and roll back without changing the application binary.