Skip to content

Commit f8edd96

Browse files
fix(batch): stripe projection discovery startup cursors (#24716)
## Problem Projection discovery instances begin at the same keyset head after a roll, repeatedly offering the same owners to the advisory claim. Revision, hostname and PID alone may be identical across Cloud Run replicas. ## Fix Select a random 128-bit process seed on first use, mix it with the revision, and cache the resulting SHA-256 stripe modulo 4096 for the process lifetime. A local lock makes first-use initialization and logging occur once even with concurrent scanners. No metadata server or network I/O is used. The larger stripe space reduces collisions at 30–50 instances; restarts may choose a different stripe. Log the chosen stripe once in the process selector, rather than for every scanner. Seed each scanner at its committed_at lower bound between the oldest matching result and startup time. Existing committed_at + document ID ordering remains oldest-first after that bound, then wraps to the head. Every row remains discoverable; no durable state changes. The existing follow-up seed takes precedence. Page sizes, sweep cadence, owner-drain concurrency and owner claims are unchanged. ## Tests 314 passed across 8 files using `BACKEND_UNIT_TEST_FILE_LIST=<file> bash backend/test.sh` through `bin/omi-heavy run --`: projection discovery, batch-stage environment ratchet, flag budget, unit policy, follow-up desync, follow-up reads, sweep owner-chain and owner-drain tests. Injected seeds model separate processes with identical metadata and verify process-stable selection, different stripes and one log per process. Full-cycle coverage covers stripe boundaries through 4095, timestamp ties, wrap and empty/short collections. B1 from the supplied correctness review is fixed. The insert-behind-cursor follow-up remains outside this change. ## Risk This is discovery desynchronization, not exclusive partitioning. Hash collisions and sparse/skewed timestamp ranges can still share starting rows. Startup adds one scoped head read per scanner; a seed beyond the newest result retries the head immediately. Throughput and claim-busy impact require a separately authorized production canary. No production access, merge or deployment in this lane. ## Rollback The switch defaults on. Set `BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED=off` and restart scanners to recover the exact pre-change cursor sequence and query budget. Pagination off also bypasses seeding. Failure-Class: none This is cross-instance discovery contention; the blocked-page class covers dependency starvation and the scheduler-shard class covers shared control mutations. Transaction fences remain unchanged.
2 parents b257681 + 125ae15 commit f8edd96

12 files changed

Lines changed: 197 additions & 13 deletions

‎backend/config/batch_pipeline.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
from config.refactor_switches import default_off as rollout_default_off, default_on as rollout_default_on
44

55
from enum import IntEnum
6+
import hashlib
7+
import logging
68
import math
79
import os
10+
import secrets
11+
import threading
812

913
from config.rollout_cohorts import rollout_cohort_members
1014
from typing import Callable, TypeVar
@@ -515,6 +519,38 @@ def projection_pagination_enabled() -> bool:
515519
return kill_switch('BATCH_PIPELINE_PROJECTION_PAGINATION_ENABLED')
516520

517521

522+
def projection_striping_enabled() -> bool:
523+
from config.policy import kill_switch
524+
525+
return kill_switch('BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED')
526+
527+
528+
PROJECTION_DISCOVERY_STRIPES = 4096
529+
_projection_discovery_stripe_value: int | None = None
530+
_PROJECTION_DISCOVERY_LOCK = threading.Lock()
531+
532+
533+
def projection_discovery_stripe() -> int:
534+
"""Choose and log one process-local random phase, without network I/O.
535+
536+
Replicas may share hostname/PID. Fresh entropy separates them; 4096 buckets
537+
reduce collisions at 30-50 instances. Restarts may choose a different phase.
538+
"""
539+
global _projection_discovery_stripe_value
540+
with _PROJECTION_DISCOVERY_LOCK:
541+
if _projection_discovery_stripe_value is None:
542+
identity = f'{os.getenv("K_REVISION", "")}:{secrets.randbits(128)}'
543+
_projection_discovery_stripe_value = (
544+
int.from_bytes(hashlib.sha256(identity.encode()).digest()[:8], 'big') % PROJECTION_DISCOVERY_STRIPES
545+
)
546+
logging.getLogger(__name__).info(
547+
'event=batch_projection_discovery action=start stripe=%d stripes=%d',
548+
_projection_discovery_stripe_value,
549+
PROJECTION_DISCOVERY_STRIPES,
550+
)
551+
return _projection_discovery_stripe_value
552+
553+
518554
def owner_chain_projection_enabled() -> bool:
519555
from config.policy import kill_switch
520556

‎backend/config/flag_budget.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
},
2525
"seed_review_by": "2026-11-15",
2626
"budget": 169,
27-
"count": 249,
27+
"count": 251,
2828
"names": {
2929
"PROJECTION_DRAIN_CLAIM_AWARE": {"owner": "two-lane lead", "review_by": "2026-12-05", "registry": "config/feature-flags.yaml"},
30+
"BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED": {"owner": "omi-pipeline", "review_by": "2026-12-06", "registry": "config/feature-flags.yaml"},
31+
"K_REVISION": {"owner": "omi-pipeline", "review_by": "2026-12-06"},
3032
"PARAKEET_SHAPE_CANARY_FILE": {"owner": "two-lane lead", "review_by": "2026-12-04"},
3133
"PARAKEET_LID_MODEL": {"owner": "planscost", "review_by": "2026-11-30"},
3234
"PARAKEET_LID_ONLY": {"owner": "planscost", "review_by": "2026-11-30"},

‎backend/config/policy/kill_switches.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
'BATCH_PIPELINE_LEADER_LEASE_ENABLED': True,
1212
'BATCH_PIPELINE_RECOVERY_TASKS_ENABLED': True,
1313
'BATCH_PIPELINE_PROJECTION_PAGINATION_ENABLED': True,
14+
'BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED': True,
1415
'BATCH_PIPELINE_OWNER_CHAIN_PROJECTION_ENABLED': True,
1516
'BATCH_PIPELINE_RECOVERED_MERGE_RELEASE_ENABLED': True,
1617
'BATCH_PIPELINE_RECOVERY_OWNER_ONLY_ENABLED': True,
@@ -35,16 +36,15 @@ def kill_switch(name: str, *, policy_id: str = 'control', flags=None) -> bool:
3536
raise ValueError('unregistered live batch switch')
3637
env = os.environ if flags is None else flags
3738
value = env.get(name, str(KILL_SWITCHES[name])).strip().lower()
38-
if name == 'PROJECTION_DRAIN_CLAIM_AWARE':
39+
if name in {'BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED', 'PROJECTION_DRAIN_CLAIM_AWARE'}:
3940
enabled = value != 'off'
4041
elif name == 'PARAKEET_WINDOW_STRANDED_DRIFT_SETTLE_ENABLED':
4142
enabled = value not in {'false', '0', 'off'}
4243
else:
4344
enabled = value != 'false' if name in DISABLE_ONLY_SWITCHES else value == 'true'
4445
from config.refactor_switches import default_on
4546

46-
# This switch keeps its narrower off-only grammar in every parser mode.
47-
if name != 'PROJECTION_DRAIN_CLAIM_AWARE':
47+
if name not in {'BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED', 'PROJECTION_DRAIN_CLAIM_AWARE'}:
4848
enabled = default_on(name, enabled, environ=env)
4949
logger.info('event=batch_policy_kill_switch policy_id=%s switch=%s enabled=%s count=1', policy_id, name, enabled)
5050
return enabled

‎backend/docs/feature-flag-registry.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@ and an explicit empty literal renders as `''`.
543543
| `BATCH_PIPELINE_PROJECTION_PREDECESSOR_REPLAY_ENABLED` | Default on; a blocked projection successor schedules its owner's missing predecessor through the existing ordered walk | backend | env | open | — | — | — | — | keep | keep | — | dazheng |
544544
| `BATCH_PIPELINE_PROJECTION_PREFETCH` | Default 4; an owner-chain walk reads the next N rows' successor links, job bindings, result payloads and audio manifests ahead of the chain, and runs the candidate lookup beside context and build | backend | env | open | — | — | — | — | keep | keep | — | dazheng |
545545
| `BATCH_PIPELINE_PROJECTION_SOURCE_DELETED_ENABLED` | Default on; settle deleted original sources without a tombstone or redirect as source_deleted, without content publication | backend | env | open | — | — | — | — | keep | keep | — | cl2-cost / lead2 |
546+
| `BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED` | Default on unless off; seed projection discovery at an instance-specific committed_at stripe, then wrap through every row | backend | env | open | — | — | — | — | keep | keep | — | omi-pipeline |
546547
| `BATCH_PIPELINE_RAMP_EXCLUDE_HEAVY_OWNERS` | Ship-on partial-ramp admission isolation; only off disables; owners over 30 open jobs or six hours pending audio use legacy | backend | env | closed | — | — | — | — | keep | keep | — | dazheng |
547548
| `BATCH_PIPELINE_RECOVERED_MERGE_RELEASE_ENABLED` | Default on unless false; the post-recovery owner wake commits the recovered unit, then its content's ready siblings, at once past an unready merge head (the unit itself also past the catch-up depth hold), so the same wake's chain walk projects them | backend | env | open | — | — | — | — | keep | keep | — | dazheng |
548549
| `BATCH_PIPELINE_RECOVERY_DISPATCH_INTERVAL_SECONDS` | Default 5; batch serving dispatches quarantine recovery on its own loop at this interval, on the maintenance leader only | backend | env | closed | — | — | — | — | keep | keep | — | dazheng |
@@ -1104,6 +1105,7 @@ their code default (`fail` tells you which way a missing value resolves).
11041105
- `BATCH_PIPELINE_PROJECTION_PREDECESSOR_REPLAY_ENABLED` — Default on; a blocked projection successor schedules its owner's missing predecessor through the existing ordered walk (fail: open)
11051106
- `BATCH_PIPELINE_PROJECTION_PREFETCH` — Default 4; an owner-chain walk reads the next N rows' successor links, job bindings, result payloads and audio manifests ahead of the chain, and runs the candidate lookup beside context and build (fail: open)
11061107
- `BATCH_PIPELINE_PROJECTION_SOURCE_DELETED_ENABLED` — Default on; settle deleted original sources without a tombstone or redirect as source_deleted, without content publication (fail: open)
1108+
- `BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED` — Default on unless off; seed projection discovery at an instance-specific committed_at stripe, then wrap through every row (fail: open)
11071109
- `BATCH_PIPELINE_PUBLISH_CONCURRENCY` — GPU worker post-flight publication concurrency: bounded fenced transitions and one ordered drain per owner per flight; default 4, 1 = legacy serial kill switch, max 16 (fail: closed)
11081110
- `BATCH_PIPELINE_RAMP_EXCLUDE_HEAVY_OWNERS` — Ship-on partial-ramp admission isolation; only off disables; owners over 30 open jobs or six hours pending audio use legacy (fail: closed)
11091111
- `BATCH_PIPELINE_RECORD_PROTOCOL_READY` — One-time native record protocol cutover; default closed retains record_pending obligations without expiry (fail: closed)
@@ -1461,6 +1463,7 @@ reason:
14611463

14621464
| Key | Reason | owner | decision | review_by | notes |
14631465
| --- | --- | --- | --- | --- | --- |
1466+
| `K_REVISION` | Cloud Run revision identity for process-local discovery phase, not a behavior flag | — | — | — | — |
14641467
| `DD_TRACE_ENABLED` | Datadog telemetry instrumentation switch, not a product behavior flag | — | — | — | — |
14651468
| `DD_LOGS_ENABLED` | Datadog telemetry log routing switch, not a product behavior flag | — | — | — | — |
14661469
| `OMI_FORCE_CONTEXT_BUCKETS` | Local-only dev override, not shipped remote authority | — | — | — | — |

‎backend/tests/unit/test_batch_pipeline_followup_desync.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ def keep(entry):
6363

6464

6565
def runner(rows, monkeypatch, *, fraction=0.0, now=None):
66+
# Isolate the pre-existing follow-up offset/backoff switch from global striping.
67+
monkeypatch.setenv('BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED', 'off')
6668
query = ResultQuery(rows)
6769
store = Mock()
6870
store.projection_page.side_effect = lambda **kw: projection_page(query, 'batch_pipeline_dev', **kw)

‎backend/tests/unit/test_batch_pipeline_followup_reads.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def stream(self):
5555

5656
@pytest.fixture(autouse=True)
5757
def head_first_discovery(monkeypatch):
58+
monkeypatch.setenv('BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED', 'off')
5859
# These read budgets pin the head-first pass (desync kill switch). The
5960
# desync seed adds one 1-row read per process: test_batch_pipeline_followup_desync.py.
6061
monkeypatch.setenv('BATCH_PIPELINE_PROJECTION_FOLLOWUP_BACKOFF_SECONDS', '0')

‎backend/tests/unit/test_batch_pipeline_projection_discovery.py‎

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,19 @@
22

33
from unittest.mock import Mock
44

5+
import pytest
6+
57
from utils.batch_pipeline.projection_discovery import ProjectionDiscovery
8+
from config import batch_pipeline
9+
from config.policy import kill_switch
10+
from database.batch_pipeline_store import projection_page
11+
from tests.unit.test_batch_pipeline_followup_desync import ResultQuery
12+
from utils.batch_pipeline import projection_discovery
613

714

815
def test_default_on_independent_scans_wrap_and_kill_switch_restores_first_page(monkeypatch):
916
monkeypatch.delenv('BATCH_PIPELINE_PROJECTION_PAGINATION_ENABLED', raising=False)
17+
monkeypatch.setenv('BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED', 'off')
1018
store = Mock()
1119
store.projection_page.side_effect = lambda *, limit, after: (
1220
(['ready'] * 100, (2.0, 'ready-100')) if after is None else (['sibling', 'other-owner'], None)
@@ -23,7 +31,8 @@ def test_default_on_independent_scans_wrap_and_kill_switch_restores_first_page(m
2331
assert assignment.after is followup.after is None
2432

2533

26-
def test_failed_query_retries_cursor_without_losing_page():
34+
def test_failed_query_retries_cursor_without_losing_page(monkeypatch):
35+
monkeypatch.setenv('BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED', 'off')
2736
store = Mock()
2837
store.projection_page.side_effect = [(['head'], (2.0, 'head')), RuntimeError('query unavailable'), (['tail'], None)]
2938
scan = ProjectionDiscovery(store)
@@ -40,3 +49,97 @@ def test_failed_query_retries_cursor_without_losing_page():
4049
(2.0, 'head'),
4150
(2.0, 'head'),
4251
]
52+
53+
54+
def scanner(monkeypatch, rows, stripe):
55+
monkeypatch.delenv('BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED', raising=False)
56+
monkeypatch.delenv('BATCH_PIPELINE_PROJECTION_PAGINATION_ENABLED', raising=False)
57+
monkeypatch.setattr(projection_discovery, 'projection_discovery_stripe', lambda: stripe)
58+
monkeypatch.setattr(projection_discovery.time, 'time', lambda: 1256.0)
59+
query = ResultQuery(rows)
60+
store = Mock()
61+
store.projection_page.side_effect = lambda **kw: projection_page(query, 'synthetic', **kw)
62+
return ProjectionDiscovery(store, states=('pending',)), query
63+
64+
65+
@pytest.mark.parametrize('stripe', [0, 1, 1024, 2048, 4095])
66+
@pytest.mark.parametrize('limit', [1, 17, 100])
67+
def test_striped_cycle_visits_every_row_exactly_once_including_timestamp_ties(monkeypatch, stripe, limit):
68+
rows = {f'r{i:03d}': {'committed_at': 1000.0 + i // 2, 'projection_state': 'pending'} for i in range(512)}
69+
scan, query = scanner(monkeypatch, rows, stripe)
70+
seen = []
71+
while len(seen) < 2 * len(rows):
72+
seen.extend(scan.next(limit))
73+
ordered = sorted(rows)
74+
first = 2 * ((stripe + 15) // 16)
75+
rotated = ordered[first:] + ordered[:first]
76+
assert seen[: len(rows)] == seen[len(rows) : 2 * len(rows)] == rotated
77+
assert query.calls[0] == (None, 1) # one scoped startup head read
78+
assert query.calls[1] == ((1000.0 + stripe / 16,), limit)
79+
80+
81+
def test_process_entropy_is_sampled_and_logged_once_per_process(monkeypatch, caplog):
82+
monkeypatch.setenv('K_REVISION', 'same-revision-across-replicas')
83+
entropy = Mock(side_effect=[11, 22])
84+
monkeypatch.setattr(batch_pipeline.secrets, 'randbits', entropy)
85+
monkeypatch.setattr(batch_pipeline, '_projection_discovery_stripe_value', None)
86+
try:
87+
with caplog.at_level('INFO', logger=batch_pipeline.__name__):
88+
first = ProjectionDiscovery(Mock()).stripe
89+
assert ProjectionDiscovery(Mock()).stripe == first
90+
assert entropy.call_count == 1
91+
# Simulate another process with identical revision/hostname/PID,
92+
# injecting a fresh seed rather than relying on probabilistic tests.
93+
monkeypatch.setattr(batch_pipeline, '_projection_discovery_stripe_value', None)
94+
second = ProjectionDiscovery(Mock()).stripe
95+
assert ProjectionDiscovery(Mock()).stripe == second
96+
assert first != second
97+
assert 0 <= first < 4096 and 0 <= second < 4096
98+
assert entropy.call_count == 2
99+
assert caplog.text.count('event=batch_projection_discovery action=start') == 2
100+
assert caplog.text.count('stripes=4096') == 2
101+
finally:
102+
monkeypatch.setattr(batch_pipeline, '_projection_discovery_stripe_value', None)
103+
104+
105+
@pytest.mark.parametrize('raw', ['', 'on', 'false', 'garbage', 'off', ' OFF '])
106+
@pytest.mark.parametrize('mode', ['off', 'serve'])
107+
def test_only_off_disables_striping(monkeypatch, raw, mode):
108+
monkeypatch.setenv('CAPTURE_COVERAGE_MODE', mode)
109+
monkeypatch.setenv('BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED', raw)
110+
assert batch_pipeline.projection_striping_enabled() == (raw.strip().lower() != 'off')
111+
assert kill_switch('BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED', flags={})
112+
113+
114+
@pytest.mark.parametrize('count', [0, 1, 3])
115+
def test_empty_short_or_stale_collection_returns_head_without_tick_delay(monkeypatch, count):
116+
rows = {f'r{i}': {'committed_at': 1000.0, 'projection_state': 'pending'} for i in range(count)}
117+
scan, _ = scanner(monkeypatch, rows, 4095)
118+
assert scan.next() == sorted(rows)
119+
assert scan.next() == sorted(rows)
120+
121+
122+
def test_stripe_seed_query_failure_retries_without_advancing(monkeypatch):
123+
rows = {'r0': {'committed_at': 1000.0, 'projection_state': 'pending'}}
124+
scan, _ = scanner(monkeypatch, rows, 128)
125+
page = scan.store.projection_page.side_effect
126+
scan.store.projection_page.side_effect = RuntimeError('unavailable')
127+
with pytest.raises(RuntimeError):
128+
scan.next()
129+
assert scan.after is None and not scan.seeded
130+
scan.store.projection_page.side_effect = page
131+
assert scan.next() == ['r0']
132+
133+
134+
def test_pagination_off_bypasses_startup_seed(monkeypatch):
135+
scan, query = scanner(monkeypatch, {}, 128)
136+
monkeypatch.setenv('BATCH_PIPELINE_PROJECTION_PAGINATION_ENABLED', 'false')
137+
assert scan.next() == []
138+
assert query.calls == [(None, 100)]
139+
140+
141+
def test_existing_followup_seed_is_preserved(monkeypatch):
142+
scan, query = scanner(monkeypatch, {}, 128)
143+
scan.after = (1200.0, '')
144+
assert scan.next() == []
145+
assert query.calls == [((1200.0,), 100)]

‎backend/tests/unit/test_batch_pipeline_sweep_owner_chain.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
@pytest.fixture(autouse=True)
3131
def claims(monkeypatch):
3232
"""Advisory owner claim that records nesting; a busy set refuses entry."""
33+
# Trace assertions exercise the exact head-first discovery rollback.
34+
monkeypatch.setenv('BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED', 'off')
3335
state = SimpleClaims()
3436

3537
@contextmanager

‎backend/tests/unit/test_batch_unit_policy.py‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,16 @@ def test_cohort_registry_and_live_switch_contract():
8787
for key in KILL_SWITCHES:
8888
assert by_key[key]['lifecycle'] == 'ops_kill'
8989
assert kill_switch(key, flags={})
90-
disabled = 'off' if key == 'PROJECTION_DRAIN_CLAIM_AWARE' else 'false'
91-
assert not kill_switch(key, flags={key: disabled})
90+
assert not kill_switch(
91+
key,
92+
flags={
93+
key: (
94+
'off'
95+
if key in {'BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED', 'PROJECTION_DRAIN_CLAIM_AWARE'}
96+
else 'false'
97+
)
98+
},
99+
)
92100
assert 'nested' in by_key['BATCH_LIVE_STT_RETIRE_PERCENT']['notes'].lower()
93101
with pytest.raises(ValueError):
94102
kill_switch('BATCH_PIPELINE_ENABLED')

‎backend/tests/unit/test_flag_budget.py‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
SEED_BUDGET = 170 # 2026-10-25 seed; lower it when the surface shrinks, never raise it.
5151
HELPER_KILL_SWITCHES = {
5252
'PROJECTION_DRAIN_CLAIM_AWARE',
53+
'BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED',
5354
'CONVERSATION_OCCASION_RELEVANCE_ENABLED',
5455
'LATE_AUDIO_SYNC_INTAKE_ENABLED',
5556
'EPISODE_RECORDED_TIME_ENABLED',
@@ -217,14 +218,15 @@ def test_helper_kill_switch_budget_entries_match_the_default_on_registry():
217218
assert entry['owner'] == 'two-lane lead'
218219
assert entry['review_by'] == '2026-12-05'
219220
else:
220-
assert entry['owner'] == 'rollout-meetings / occasion architecture'
221-
assert entry['review_by'] == '2026-11-28'
222-
assert flag['owner'] == 'dazheng'
221+
striped = name == 'BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED'
222+
assert entry['owner'] == ('omi-pipeline' if striped else 'rollout-meetings / occasion architecture')
223+
assert entry['review_by'] == ('2026-12-06' if striped else '2026-11-28')
224+
assert flag['owner'] == ('omi-pipeline' if name == 'BATCH_PIPELINE_PROJECTION_STRIPING_ENABLED' else 'dazheng')
223225
assert flag['kind'] == 'env'
224226
assert flag['lifecycle'] == 'ops_kill'
225227
assert 'backend' in flag['surfaces']
226228
assert flag['fail'] == 'open'
227-
assert flag['summary'].startswith('Default on;')
229+
assert flag['summary'].startswith('Default on')
228230

229231

230232
def _data(retired=None, **names):

0 commit comments

Comments
 (0)