Tables: add FeatureFlagsResolver seam to stamp featureFlags server-side#10
Open
cbb330 wants to merge 3 commits into
Open
Tables: add FeatureFlagsResolver seam to stamp featureFlags server-side#10cbb330 wants to merge 3 commits into
cbb330 wants to merge 3 commits into
Conversation
9b5e906 to
14c82c3
Compare
7c276f0 to
fdfaf48
Compare
7df59f3 to
747c7ef
Compare
… 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>
747c7ef to
112cdab
Compare
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>
112cdab to
2f2fccb
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a server-side
FeatureFlagsResolverseam so a deployment can stamp per-(database, table) runtimefeatureFlagsonto table responses without forking the core handler. Stacked on top of linkedin#644 (which adds thefeatureFlagsfield + client capture). The open-source default resolves nothing, so this stays behaviorless until a deployment supplies its own resolver.Changes
New Features / Internal API Changes
FeatureFlagsResolverinterface:FeatureFlags resolve(String databaseId, String tableId, TableDto tableDto). Implementations must be cheap and never throw on the response path; returningnullis the safe default.NoOpFeatureFlagsResolver: open-source default, returnsnull.ApiConfigregisters 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).OpenHouseTablesApiHandlercalls the resolver right aftertablesMapper.toGetTableResponseBody(...)and stamps the result viabody.toBuilder().featureFlags(...).build()ongetTable,createTable, andupdateTable. The mapper still ignoresfeatureFlags— it is a request-time decision, not sourced fromTableDto.Why the default lives in
ApiConfig(not a@Component)The default resolver is registered in
ApiConfig(packagecom.linkedin.openhouse.tables.api, already scanned by the prod app and the e2eSpringH2Application) 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)
Testing Done
New
FeatureFlagsResolverTest:null;getTablestamps the resolver'sFeatureFlagsonto the response body;getTableleavesfeatureFlagsnull.Regression:
:services:tables:test --tests TablesControllerTest(real handler via H2) passes, confirming the new@Autowiredresolver resolves in the e2e Spring context.Additional Information
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) andsearchTables; can be added if flags are needed there.Made with Cursor