Skip to content

Tables: add FeatureFlagsResolver seam to stamp featureFlags server-side#10

Open
cbb330 wants to merge 3 commits into
chbush/runtime-policy-responsefrom
chbush/feature-flags-resolver
Open

Tables: add FeatureFlagsResolver seam to stamp featureFlags server-side#10
cbb330 wants to merge 3 commits into
chbush/runtime-policy-responsefrom
chbush/feature-flags-resolver

Conversation

@cbb330

@cbb330 cbb330 commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a server-side FeatureFlagsResolver seam so a deployment can stamp per-(database, table) runtime featureFlags onto table responses without forking the core handler. Stacked on top of linkedin#644 (which adds the featureFlags field + client capture). The open-source default resolves nothing, so this stays behaviorless until a deployment supplies its own resolver.

Changes

  • Client-facing API Changes
  • Internal API Changes
  • Bug Fixes
  • New Features
  • Performance Improvements
  • Code Style
  • Refactoring
  • Documentation
  • Tests

New Features / Internal API Changes

  • FeatureFlagsResolver interface: FeatureFlags resolve(String databaseId, String tableId, TableDto tableDto). Implementations must be cheap and never throw on the response path; returning null is the safe default.
  • NoOpFeatureFlagsResolver: open-source default, returns null.
  • ApiConfig registers the no-op as the default bean via @ConditionalOnMissingBean(FeatureFlagsResolver.class), so a deployment can override it with its own bean (e.g. @Primary/@Component).
  • OpenHouseTablesApiHandler calls the resolver right after tablesMapper.toGetTableResponseBody(...) and stamps the result via body.toBuilder().featureFlags(...).build() on getTable, createTable, and updateTable. The mapper still ignores featureFlags — it is a request-time decision, not sourced from TableDto.

Why the default lives in ApiConfig (not a @Component)
The default resolver is registered in ApiConfig (package com.linkedin.openhouse.tables.api, already scanned by the prod app and the e2e SpringH2Application) instead of relying on component-scan of a new package, so the enumerated test scanners keep resolving the bean.

Example deployment override (e.g. li-openhouse)

@Primary @Component @Profile("!dev")
class LiFeatureFlagsResolver implements FeatureFlagsResolver {
  public FeatureFlags resolve(String databaseId, String tableId, TableDto tableDto) {
    // e.g. if tableId.equals("prod_db_changes_venice_test") -> stamp read-bridge ON
  }
}

Testing Done

  • Manually Tested on local docker setup. Please include commands ran, and their output.
  • Added new tests for the changes made.
  • Updated existing tests to reflect the changes made.
  • No tests added or updated. Please explain why. If unsure, please feel free to ask for help.
  • Some other form of testing like staging or soak time in production. Please explain.

New FeatureFlagsResolverTest:

  • the no-op resolver returns null;
  • getTable stamps the resolver's FeatureFlags onto the response body;
  • with the no-op resolver, getTable leaves featureFlags null.

Regression: :services:tables:test --tests TablesControllerTest (real handler via H2) passes, confirming the new @Autowired resolver resolves in the e2e Spring context.

Additional Information

  • Breaking Changes
  • Deprecations
  • Large PR broken into smaller PRs, and PR plan linked in the description.

Sequence: linkedin#644 (response field + client capture) → this PR (server-side resolver seam) → deployment-specific resolver (e.g. li-openhouse) + a client consumer that acts on a recognized flag. Not stamped here: the snapshot/commit response path (OpenHouseIcebergSnapshotsApiHandler) and searchTables; can be added if flags are needed there.

Made with Cursor

@cbb330 cbb330 force-pushed the chbush/feature-flags-resolver branch 5 times, most recently from 9b5e906 to 14c82c3 Compare June 29, 2026 05:27
@cbb330 cbb330 force-pushed the chbush/runtime-policy-response branch from 7c276f0 to fdfaf48 Compare June 29, 2026 22:26
@cbb330 cbb330 force-pushed the chbush/feature-flags-resolver branch 5 times, most recently from 7df59f3 to 747c7ef Compare July 1, 2026 21:29
… GetTableResponseBody and capture it client-side (linkedin#644)

## Summary

Adds a generic, server-stamped, per-table client **`config`** map to
`GetTableResponseBody` and captures it in `OpenHouseTableOperations` so
subclasses can read it. It follows the **Iceberg REST
`LoadTableResponse.config` convention** — a `Map<String,String>` of
client-side behavior overrides the server controls at runtime, without
re-rolling the slow-to-upgrade Java client fleet. **No behavior
change**: the map is null/empty until a server stamps it, and the base
client has nothing to act on.

## Changes

- [x] Client-facing API Changes
- [x] Internal API Changes
- [ ] Bug Fixes
- [x] New Features
- [x] Tests

**Client-facing API Changes**
- READ_ONLY, nullable `config` field (`Map<String,String>`) on
`GetTableResponseBody`, modeled on the Iceberg REST load-table `config`.
Namespaced keys (e.g. `openhouse.read-bridge`); clients ignore keys they
do not understand.
- Why a response field: `doRefresh` already fetches a live
`GetTableResponseBody` on every table load (and on commit responses) but
discarded all but `getTableLocation()` — it is the natural,
already-present, server-controlled, zero-staleness delivery channel. The
generated client sets `FAIL_ON_UNKNOWN_PROPERTIES = false`, so a new
response field cannot break older clients (and unknown config keys are
simply carried). Same additive, nullable, READ_ONLY pattern as
`sortOrder` etc.

**Internal API Changes**
- `OpenHouseTableOperations.doRefresh` keeps the full response and
stashes `config` in an `AtomicReference`, exposed to subclasses via the
new `protected currentConfig()`. READ_ONLY + side-channel: never sent
back on writes.
- `TablesMapper` (the table DTO serializer into gettableresponsebody)
ignores `config` (`@Mapping(target = "config", ignore = true)`) — it is
stamped separately, not sourced from `TableDto`.

**New Features**
- A flat string map keyed by namespaced keys means new features become
new `config` entries rather than an API/schema change or client regen.

## Testing Done
- [x] Added new tests.

Compile/codegen verified (`:services:tables`, `:client:tableclient`,
`:integrations:java:iceberg-1.2:openhouse-java-runtime`); the client
regenerates with `getConfig()` returning `Map<String,String>`.
`OpenHouseTableOperationsTest` covers: `currentConfig()` null before
refresh; `doRefresh` captures/clears `config`; the REST-style string map
deserializes from a response and tolerates unknown fields.

## Stack
**linkedin#644 (channel)** → linkedin#645 (read-bridge mechanism) → [li-openhouse
#2166](https://github.com/linkedin-multiproduct/li-openhouse/pull/2166)
(li column-default source from avro.schema.literal)

This is the substrate PR — intentionally behaviorless on its own.

## Next steps (this PR)
- [ ] Review + merge first — it is the base of the stack; linkedin#645 and #2166
depend on it.

This PR delivers only the channel; it stays inert until two further
pieces exist, both **required**, and both delivered by the next PR in
the stack:
- **A server stamp** to populate `config` (it is null until something
stamps it) — required, delivered by linkedin#645 via `ReadBridgeConfigResolver`
+ the `ColumnDefaultsSource` seam.
- **A client consumer** that reads `currentConfig()` and acts on it —
required, delivered by linkedin#645's read-bridge metadata overlay, driven by
the column defaults that #2166 supplies.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
@cbb330 cbb330 force-pushed the chbush/feature-flags-resolver branch from 747c7ef to 112cdab Compare July 4, 2026 15:36
Adds the open-source read-bridge feature on top of the per-table `config`
channel (linkedin#644):

- ColumnDefaultsSource: the single open-source/closed-source seam (field-id ->
  Iceberg single-value JSON). Open-source default is a no-op lambda in ApiConfig;
  a deployment overrides this bean (e.g. li-openhouse, from avro.schema.literal).
- ReadBridgeConfigResolver: server-side encoder that stamps each default as a
  flat namespaced config entry (openhouse.read-bridge.column-default.<fieldId> =
  single-value JSON) — no envelope/POJO; the config map carries the structure.
- ReadBridge (client): decodes those entries and applies the overlay at
  metadata-load time (the column-default transform is a marked TODO, and the
  place further V3 features get backported). Keeps loadMetadata to one call.

Behaviorless until a ColumnDefaultsSource is supplied; fail-closed throughout.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cbb330 cbb330 force-pushed the chbush/feature-flags-resolver branch from 112cdab to 2f2fccb Compare July 4, 2026 15:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants