-
Notifications
You must be signed in to change notification settings - Fork 80
Add self-service overrides to table feature toggles #666
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
cbb330
merged 1 commit into
linkedin:main
from
cbb330:chbush/table-feature-override-gate
Jul 31, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
63 changes: 57 additions & 6 deletions
63
services/tables/src/main/java/com/linkedin/openhouse/tables/toggle/TableFeatureToggle.java
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,65 @@ | ||
| package com.linkedin.openhouse.tables.toggle; | ||
|
|
||
| /** Interface to check if a feature is toggled-on for a table */ | ||
| import com.linkedin.openhouse.tables.model.TableDto; | ||
| import java.util.Map; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** | ||
| * Interface to check if a feature is toggled-on for a table. | ||
| * | ||
| * <p>TODO: the two forms below differ in who may influence the decision, which today is conveyed by | ||
| * their names and javadoc rather than enforced. The intended model declares that per feature | ||
| * instead of per call site — {@code TableFeature.capability(id)} vs {@code | ||
| * TableFeature.rollout(id)} — so a permission-bearing feature cannot acquire self-service opt-in by | ||
| * calling the wrong method. The same change would carry a decision's cause for metrics, give rules | ||
| * an explicit effect, priority and expiry rather than presence-implies-active, and evaluate rules | ||
| * from a locally replicated snapshot so a table read no longer blocks on HouseTables. | ||
| */ | ||
| public interface TableFeatureToggle { | ||
| Logger LOG = LoggerFactory.getLogger(TableFeatureToggle.class); | ||
|
|
||
| /** Suffix appended to a feature id to form its self-service table property. */ | ||
| String ENABLED_PROPERTY_SUFFIX = ".enabled"; | ||
|
|
||
| /** | ||
| * Determine if given feature is activated for the table. | ||
| * Determines the server-side activation decision for a table. | ||
| * | ||
| * @param databaseId databaseId | ||
| * @param tableId tableId | ||
| * @param featureId featureId | ||
| * @return True if the feature is activated for the table. | ||
| * <p>Authorization gates — features deciding whether a user may write an otherwise preserved | ||
| * property, like {@code enable_mor} — must use this form, since the table property honored by | ||
| * {@link #isFeatureActivatedWithOverride(TableDto, String)} is writable by the user being gated. | ||
| */ | ||
| boolean isFeatureActivated(String databaseId, String tableId, String featureId); | ||
|
|
||
| /** | ||
| * Determines activation, letting the table override the server-side decision. | ||
| * | ||
| * <p>An explicit {@code <featureId>.enabled} property opts the table in or out; when absent, the | ||
| * server-side toggle decides. An unparseable value fails closed, so a typo cannot make the table | ||
| * unusable. | ||
| */ | ||
| default boolean isFeatureActivatedWithOverride(TableDto tableDto, String featureId) { | ||
| Map<String, String> properties = tableDto.getTableProperties(); | ||
| String tableProperty = featureId + ENABLED_PROPERTY_SUFFIX; | ||
| String override = properties == null ? null : properties.get(tableProperty); | ||
| if (override == null) { | ||
| return isFeatureActivated(tableDto.getDatabaseId(), tableDto.getTableId(), featureId); | ||
| } | ||
|
|
||
| String normalized = override.trim(); | ||
| if ("true".equalsIgnoreCase(normalized)) { | ||
| return true; | ||
| } | ||
| if ("false".equalsIgnoreCase(normalized)) { | ||
| return false; | ||
| } | ||
| LOG.warn( | ||
| "Ignoring unparseable table property {}={} for {}.{}; treating feature {} as inactive", | ||
| tableProperty, | ||
| override, | ||
| tableDto.getDatabaseId(), | ||
| tableDto.getTableId(), | ||
| featureId); | ||
| return false; | ||
| } | ||
| } | ||
98 changes: 98 additions & 0 deletions
98
...ces/tables/src/test/java/com/linkedin/openhouse/tables/toggle/TableFeatureToggleTest.java
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| package com.linkedin.openhouse.tables.toggle; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertFalse; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
|
||
| import com.linkedin.openhouse.tables.model.TableDto; | ||
| import java.util.Collections; | ||
| import java.util.Map; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| class TableFeatureToggleTest { | ||
| private static final String DATABASE = "database"; | ||
| private static final String TABLE = "table"; | ||
| private static final String FEATURE = "feature"; | ||
| private static final String PROPERTY = "feature.enabled"; | ||
|
|
||
| private TestTableFeatureToggle featureToggle; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| featureToggle = new TestTableFeatureToggle(); | ||
| } | ||
|
|
||
| @Test | ||
| void testUsesServerToggleWhenPropertyIsAbsent() { | ||
| TableDto tableDto = tableWithProperties(Collections.emptyMap()); | ||
| featureToggle.serverDecision = true; | ||
|
|
||
| assertTrue(featureToggle.isFeatureActivatedWithOverride(tableDto, FEATURE)); | ||
| assertEquals(1, featureToggle.serverInvocationCount); | ||
| } | ||
|
|
||
| @Test | ||
| void testUsesServerToggleWhenPropertiesAreNull() { | ||
| TableDto tableDto = tableWithProperties(null); | ||
| featureToggle.serverDecision = false; | ||
|
|
||
| assertFalse(featureToggle.isFeatureActivatedWithOverride(tableDto, FEATURE)); | ||
| assertEquals(1, featureToggle.serverInvocationCount); | ||
| } | ||
|
|
||
| @Test | ||
| void testTruePropertyOptsInWithoutServerToggle() { | ||
| TableDto tableDto = tableWithProperties(Collections.singletonMap(PROPERTY, "true")); | ||
|
|
||
| assertTrue(featureToggle.isFeatureActivatedWithOverride(tableDto, FEATURE)); | ||
| assertEquals(0, featureToggle.serverInvocationCount); | ||
| } | ||
|
|
||
| @Test | ||
| void testFalsePropertyOptsOutWithoutServerToggle() { | ||
| TableDto tableDto = tableWithProperties(Collections.singletonMap(PROPERTY, "false")); | ||
|
|
||
| assertFalse(featureToggle.isFeatureActivatedWithOverride(tableDto, FEATURE)); | ||
| assertEquals(0, featureToggle.serverInvocationCount); | ||
| } | ||
|
|
||
| @Test | ||
| void testPropertyParsingIgnoresCaseAndWhitespace() { | ||
| TableDto tableDto = tableWithProperties(Collections.singletonMap(PROPERTY, " TRUE ")); | ||
|
|
||
| assertTrue(featureToggle.isFeatureActivatedWithOverride(tableDto, FEATURE)); | ||
| assertEquals(0, featureToggle.serverInvocationCount); | ||
| } | ||
|
|
||
| @Test | ||
| void testUnparseablePropertyFailsClosed() { | ||
| TableDto tableDto = tableWithProperties(Collections.singletonMap(PROPERTY, "sometimes")); | ||
| featureToggle.serverDecision = true; | ||
|
|
||
| assertFalse(featureToggle.isFeatureActivatedWithOverride(tableDto, FEATURE)); | ||
| assertEquals(0, featureToggle.serverInvocationCount); | ||
| } | ||
|
|
||
| private static TableDto tableWithProperties(Map<String, String> properties) { | ||
| return TableDto.builder() | ||
| .databaseId(DATABASE) | ||
| .tableId(TABLE) | ||
| .tableProperties(properties) | ||
| .build(); | ||
| } | ||
|
|
||
| private static class TestTableFeatureToggle implements TableFeatureToggle { | ||
| private boolean serverDecision; | ||
| private int serverInvocationCount; | ||
|
|
||
| @Override | ||
| public boolean isFeatureActivated(String databaseId, String tableId, String featureId) { | ||
| assertEquals(DATABASE, databaseId); | ||
| assertEquals(TABLE, tableId); | ||
| assertEquals(FEATURE, featureId); | ||
| serverInvocationCount++; | ||
| return serverDecision; | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.