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

    Fields

    No fields configured for Feature.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions