Skip to content

Commit 57944e4

Browse files
antiguruclaude
andcommitted
test/launchdarkly: cover per-cluster and per-replica scoped flags
Extend the LaunchDarkly integration test with cluster-coherent and replica-local cases. Adds rule-based targeting (clauses on context-kind attributes) and boolean flags, then demonstrates: * a cluster-coherent optimizer feature served `true` to builtin clusters (targeted by the `cluster` context's `is_builtin`) while user clusters fall through to `false`, observed via `mz_cluster_system_parameters`; * `enable_lgalloc` served `false` only to `legacy` size-family replicas (targeted by the `replica` context's `replica_size_family`) while `cc` replicas keep the env-wide value, observed via `mz_replica_system_parameters`; * durability of both across a restart with the sync loop disabled. The LAUNCHDARKLY_KEY_MAP entries are `;`-separated (the CLI arg's value delimiter). The LaunchDarkly project/environment keys are overridable via LAUNCHDARKLY_PROJECT_KEY / LAUNCHDARKLY_ENVIRONMENT_KEY for running against a non-CI project. Verified end-to-end against a live LaunchDarkly test project. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 612f975 commit 57944e4

1 file changed

Lines changed: 207 additions & 11 deletions

File tree

test/launchdarkly/mzcompose.py

Lines changed: 207 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,39 @@
5050
LD_CONTEXT_KEY = DEFAULT_MZ_ENVIRONMENT_ID
5151
# A unique feature flag key to use for this test.
5252
LD_FEATURE_FLAG_KEY = f"ci-test-{BUILDKITE_JOB_ID}"
53+
# Unique feature flag keys for the scoped (per-cluster / per-replica) cases.
54+
LD_OPTIMIZER_FLAG_KEY = f"ci-test-optimizer-{BUILDKITE_JOB_ID}"
55+
LD_LGALLOC_FLAG_KEY = f"ci-test-lgalloc-{BUILDKITE_JOB_ID}"
56+
# A cluster-coherent (optimizer) parameter and a replica-local parameter, both
57+
# declared scoped in their definitions.
58+
OPTIMIZER_PARAM = "enable_eager_delta_joins"
59+
LGALLOC_PARAM = "enable_lgalloc"
60+
61+
62+
def context_rule(
63+
context_kind: str, attribute: str, values: list[str], variation: int
64+
) -> dict[str, Any]:
65+
"""Builds a LaunchDarkly targeting rule that serves `variation` to contexts
66+
of `context_kind` whose `attribute` is in `values`."""
67+
return {
68+
"variation": variation,
69+
"clauses": [
70+
{
71+
"contextKind": context_kind,
72+
"attribute": attribute,
73+
"op": "in",
74+
"values": values,
75+
"negate": False,
76+
}
77+
],
78+
}
79+
80+
81+
# Boolean flag variations: index 0 is `false`, index 1 is `true`.
82+
BOOL_VARIATIONS: list[Any] = [
83+
Variation(value=False, name="false"),
84+
Variation(value=True, name="true"),
85+
]
5386

5487
SERVICES = [
5588
Materialized(
@@ -79,8 +112,8 @@ def workflow_default(c: Composition) -> None:
79112
configuration=launchdarkly_api.Configuration(
80113
api_key=dict(ApiKey=LAUNCHDARKLY_API_TOKEN),
81114
),
82-
project_key="default",
83-
environment_key="ci-cd",
115+
project_key=environ.get("LAUNCHDARKLY_PROJECT_KEY", "default"),
116+
environment_key=environ.get("LAUNCHDARKLY_ENVIRONMENT_KEY", "ci-cd"),
84117
)
85118

86119
try:
@@ -229,6 +262,9 @@ def sys(command: str) -> None:
229262
# turned off).
230263
c.testdrive("\n".join(["> SHOW max_result_size", "1GB"]))
231264
c.stop("materialized")
265+
266+
# Exercise the scoped (per-cluster / per-replica) feature flags.
267+
run_scoped_feature_flag_cases(c, ld_client)
232268
except launchdarkly_api.ApiException as e:
233269
raise UIError(dedent(f"""
234270
Error when calling the Launch Darkly API.
@@ -242,6 +278,145 @@ def sys(command: str) -> None:
242278
pass # ignore exceptions on cleanup
243279

244280

281+
def run_scoped_feature_flag_cases(
282+
c: Composition, ld_client: "LaunchDarklyClient"
283+
) -> None:
284+
"""Demonstrates per-cluster (cluster-coherent) and per-replica
285+
(replica-local) LaunchDarkly overrides, plus their durability across a
286+
restart.
287+
288+
Cluster case: a cluster-coherent optimizer feature is served `true` to the
289+
builtin clusters (e.g. `mz_catalog_server`) and `false` to user clusters,
290+
targeted by the `cluster` context's `is_builtin` attribute.
291+
292+
Replica case: the replica-local `enable_lgalloc` flag is served `false` only
293+
to replicas whose `replica_size_family` is `legacy`, while modern (`cc`)
294+
replicas keep the environment-wide value, targeted by the `replica`
295+
context's `replica_size_family` attribute.
296+
"""
297+
# A materialized that syncs the scoped flags. The key map ties the
298+
# cluster-coherent optimizer parameter and the replica-local lgalloc
299+
# parameter to their LD flags, alongside the original max_result_size flag.
300+
# MZ_LAUNCHDARKLY_KEY_MAP entries are `;`-separated (the CLI arg's value
301+
# delimiter), not comma-separated.
302+
key_map = ";".join(
303+
[
304+
f"max_result_size={LD_FEATURE_FLAG_KEY}",
305+
f"{OPTIMIZER_PARAM}={LD_OPTIMIZER_FLAG_KEY}",
306+
f"{LGALLOC_PARAM}={LD_LGALLOC_FLAG_KEY}",
307+
]
308+
)
309+
scoped_mz = Materialized(
310+
environment_extra=[
311+
f"MZ_LAUNCHDARKLY_SDK_KEY={LAUNCHDARKLY_SDK_KEY}",
312+
f"MZ_LAUNCHDARKLY_KEY_MAP={key_map}",
313+
"MZ_CONFIG_SYNC_LOOP_INTERVAL=1s",
314+
],
315+
additional_system_parameter_defaults={
316+
"log_filter": "mz_adapter::catalog=debug,mz_adapter::config=debug",
317+
},
318+
external_metadata_store=True,
319+
)
320+
321+
try:
322+
# Cluster-coherent optimizer flag: fallthrough serves `false`, a rule
323+
# serves `true` to builtin clusters.
324+
ld_client.create_flag(
325+
LD_OPTIMIZER_FLAG_KEY,
326+
tags=["ci-test"],
327+
variations=BOOL_VARIATIONS,
328+
off_variation=0,
329+
on_variation=0,
330+
)
331+
ld_client.update_targeting(
332+
LD_OPTIMIZER_FLAG_KEY,
333+
on=True,
334+
rules=[context_rule("cluster", "is_builtin", ["true"], 1)],
335+
)
336+
337+
# Replica-local lgalloc flag: fallthrough serves `true` (the env-wide
338+
# default), a rule serves `false` to the legacy size family.
339+
ld_client.create_flag(
340+
LD_LGALLOC_FLAG_KEY,
341+
tags=["ci-test"],
342+
variations=BOOL_VARIATIONS,
343+
off_variation=0,
344+
on_variation=1,
345+
)
346+
ld_client.update_targeting(
347+
LD_LGALLOC_FLAG_KEY,
348+
on=True,
349+
rules=[context_rule("replica", "replica_size_family", ["legacy"], 0)],
350+
)
351+
352+
with c.override(scoped_mz):
353+
c.up("materialized")
354+
355+
# A user cluster with a legacy-family replica, to contrast with the
356+
# builtin/`cc` defaults.
357+
c.testdrive(
358+
"\n".join(
359+
[
360+
"$ postgres-connect name=mz_system url=postgres://mz_system:materialize@${testdrive.materialize-internal-sql-addr}",
361+
"$ postgres-execute connection=mz_system",
362+
"CREATE CLUSTER ld_legacy SIZE 'scale=1,workers=1,legacy'",
363+
]
364+
)
365+
)
366+
367+
# Allow a few sync ticks to evaluate the new cluster/replica.
368+
sleep(5)
369+
370+
# Cluster case: the catalog server gets the cluster-scoped `true`,
371+
# while the user cluster falls through to `false`.
372+
c.testdrive(
373+
"\n".join(
374+
[
375+
f"> SELECT c.name, p.value FROM mz_internal.mz_cluster_system_parameters p JOIN mz_clusters c ON c.id = p.cluster_id WHERE p.name = '{OPTIMIZER_PARAM}' AND c.name IN ('mz_catalog_server', 'quickstart') ORDER BY c.name",
376+
"mz_catalog_server true",
377+
"quickstart false",
378+
]
379+
)
380+
)
381+
382+
# Replica case: the legacy-family replica gets `false`, while the
383+
# `cc`-family quickstart replica keeps the env-wide `true`.
384+
c.testdrive(
385+
"\n".join(
386+
[
387+
f"> SELECT cr.name, p.value FROM mz_internal.mz_replica_system_parameters p JOIN mz_cluster_replicas cr ON cr.id = p.replica_id JOIN mz_clusters c ON c.id = cr.cluster_id WHERE p.name = '{LGALLOC_PARAM}' AND c.name IN ('ld_legacy', 'quickstart') ORDER BY c.name",
388+
"r1 false",
389+
"r1 true",
390+
]
391+
)
392+
)
393+
c.stop("materialized")
394+
395+
# Durability: restart without the sync loop and assert the scoped
396+
# values are restored from the durable cache.
397+
with c.override(Materialized(external_metadata_store=True)):
398+
c.up("materialized")
399+
c.testdrive(
400+
"\n".join(
401+
[
402+
f"> SELECT c.name, p.value FROM mz_internal.mz_cluster_system_parameters p JOIN mz_clusters c ON c.id = p.cluster_id WHERE p.name = '{OPTIMIZER_PARAM}' AND c.name IN ('mz_catalog_server', 'quickstart') ORDER BY c.name",
403+
"mz_catalog_server true",
404+
"quickstart false",
405+
f"> SELECT cr.name, p.value FROM mz_internal.mz_replica_system_parameters p JOIN mz_cluster_replicas cr ON cr.id = p.replica_id JOIN mz_clusters c ON c.id = cr.cluster_id WHERE p.name = '{LGALLOC_PARAM}' AND c.name IN ('ld_legacy', 'quickstart') ORDER BY c.name",
406+
"r1 false",
407+
"r1 true",
408+
]
409+
)
410+
)
411+
c.stop("materialized")
412+
finally:
413+
for flag in (LD_OPTIMIZER_FLAG_KEY, LD_LGALLOC_FLAG_KEY):
414+
try:
415+
ld_client.delete_flag(flag)
416+
except:
417+
pass # ignore exceptions on cleanup
418+
419+
245420
class LaunchDarklyClient:
246421
"""
247422
A test-specific LaunchDarkly client that simulates a client modifying
@@ -258,7 +433,21 @@ def __init__(
258433
self.project_key = project_key
259434
self.environment_key = environment_key
260435

261-
def create_flag(self, feature_flag_key: str, tags: list[str] = []) -> Any:
436+
def create_flag(
437+
self,
438+
feature_flag_key: str,
439+
tags: list[str] = [],
440+
variations: list[Any] | None = None,
441+
off_variation: int = 0,
442+
on_variation: int = 1,
443+
) -> Any:
444+
if variations is None:
445+
variations = [
446+
Variation(value=1073741824, name="1 GiB"),
447+
Variation(value=2147483648, name="2 GiB"),
448+
Variation(value=3221225472, name="3 GiB"),
449+
Variation(value=4294967295, name="4 GiB - 1 (max size)"),
450+
]
262451
with launchdarkly_api.ApiClient(self.configuration) as api_client:
263452
api = feature_flags_api.FeatureFlagsApi(api_client)
264453
return api.post_feature_flag(
@@ -270,17 +459,12 @@ def create_flag(self, feature_flag_key: str, tags: list[str] = []) -> Any:
270459
using_environment_id=True,
271460
using_mobile_key=True,
272461
),
273-
variations=[
274-
Variation(value=1073741824, name="1 GiB"),
275-
Variation(value=2147483648, name="2 GiB"),
276-
Variation(value=3221225472, name="3 GiB"),
277-
Variation(value=4294967295, name="4 GiB - 1 (max size)"),
278-
],
462+
variations=variations,
279463
temporary=False,
280464
tags=tags,
281465
defaults=Defaults(
282-
off_variation=0,
283-
on_variation=1,
466+
off_variation=off_variation,
467+
on_variation=on_variation,
284468
),
285469
),
286470
)
@@ -290,6 +474,7 @@ def update_targeting(
290474
feature_flag_key: str,
291475
on: bool | None = None,
292476
contextTargets: list[Any] | None = None,
477+
rules: list[Any] | None = None,
293478
) -> Any:
294479
with launchdarkly_api.ApiClient(self.configuration) as api_client:
295480
api = feature_flags_api.FeatureFlagsApi(api_client)
@@ -322,6 +507,17 @@ def update_targeting(
322507
if contextTargets is not None
323508
else []
324509
),
510+
(
511+
[
512+
PatchOperation(
513+
op="replace",
514+
path=f"/environments/{self.environment_key}/rules",
515+
value=rules,
516+
),
517+
]
518+
if rules is not None
519+
else []
520+
),
325521
)
326522
)
327523
)

0 commit comments

Comments
 (0)