Skip to content

Local and remote flags with the same key are not isolated (shared snapshot + shared provider chain) #294

Description

@kirich1409

Problem

localFlags { } and remoteFlags { } are declared in separate DSL containers and generate
separate objects (GeneratedLocalFlags* / GeneratedRemoteFlags*), which strongly implies the
two are isolated. They are not isolated at runtime:

  1. ConfigValues keys its in-memory snapshot by ConfigParam.key (the raw string). A local flag
    and a remote flag declared with the same name/key map to the same snapshot slot
    (documented last-write-wins), so they overwrite each other.
  2. More fundamentally, ConfigValues.getValue(param) resolves any key through the same
    provider chain (local → remote → default) regardless of which container declared it. A flag
    authored in localFlags { } will therefore pick up a remote value if the remote provider
    serves that key, and a remoteFlags { } flag will pick up a local override. The
    local/remote split is purely a codegen/organizational concern today, with no runtime effect on
    resolution.

Expectation

Because they live in separate containers and separate generated classes, a local flag and a
remote flag should be runtime-isolated:

  • a localFlags flag resolves only from the local provider (+ its default), never from remote;
  • a remoteFlags flag resolves only from the remote provider (+ its default), never from a local
    override of a same-named local flag;
  • a local flag and a remote flag with the same name do not collide in the snapshot.

Use case that hits this

The "client-readiness AND server-rollout" gating pattern: a feature is enabled only when both
the locally bundled flag (the client build has shipped/enabled it) and the server flag are
trueisEnabled(localFlag) && isEnabled(remoteFlag). This is a common hybrid (an analogue of
a bundled local_feature_toggle.json AND'd with remote config).

Today this is impossible to express cleanly: a local twin must be given an artificial distinct
key
(e.g. a _LOCAL suffix) so the server never serves it and it doesn't collide with the
remote flag. That is a workaround, not a design.

Repro (1.2.0)

featured {
    localFlags  { boolean("FEATURE_X", default = true) }   // client gate
    remoteFlags { boolean("FEATURE_X", default = false) }  // server rollout
}
// GeneratedLocalFlags.featureX and GeneratedRemoteFlags.featureX both have key "FEATURE_X"
// -> same snapshot slot; getValue(local) also reads the remote provider's "FEATURE_X".

Possible directions (maintainer's call)

  • Namespace the snapshot key by container (e.g. prefix local keys internally), keeping the public
    flag name unchanged.
  • Route localFlags strictly through the local provider and remoteFlags strictly through the
    remote provider (carry the container on ConfigParam, resolve accordingly).
  • Separate snapshots per container.

Version: 1.2.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions