From 30c8b394fb574410a5bc2e0455ed782edff1cd6a Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Thu, 21 Sep 2023 16:42:19 +0300 Subject: [PATCH] fix: decouple metrics from ofter-read table Signed-off-by: andreas-unleash --- src/lib/db/feature-strategy-store.ts | 45 +++++++++----- src/lib/db/feature-toggle-client-store.ts | 19 +++++- src/lib/db/feature-toggle-store.ts | 62 ++++++++++--------- ...eate-feature-environments-metrics-table.js | 14 ++++- 4 files changed, 91 insertions(+), 49 deletions(-) diff --git a/src/lib/db/feature-strategy-store.ts b/src/lib/db/feature-strategy-store.ts index c7f76bc4b5..6bd2a96df4 100644 --- a/src/lib/db/feature-strategy-store.ts +++ b/src/lib/db/feature-strategy-store.ts @@ -349,7 +349,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { acc.project = r.project; acc.stale = r.stale; acc.lastSeenAt = r.last_seen_at; - acc.createdAt = r.created_at; acc.type = r.type; if (!acc.environments[r.environment]) { @@ -414,6 +413,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { }); featureToggle.archived = archived; + return featureToggle; } throw new NotFoundError( @@ -507,15 +507,21 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { 'feature_environments.feature_name', 'features.name', ) - .leftJoin( - 'feature_environments_metrics', - 'feature_environments_metrics.feature_name', - 'features.name', - ) + .leftJoin('feature_environments_metrics', function () { + this.on( + 'feature_environments_metrics.feature_name', + '=', + 'feature_environments.feature_name', + ).andOn( + 'feature_environments_metrics.environment', + '=', + 'feature_environments.environment', + ); + }) .leftJoin( 'environments', - 'feature_environments.environment', 'environments.name', + 'feature_environments.environment', ) .leftJoin('feature_tag as ft', 'ft.feature_name', 'features.name'); @@ -524,7 +530,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { 'features.description as description', 'features.type as type', 'features.created_at as created_at', - 'features.last_seen_at as last_seen_at', 'features.stale as stale', 'features.impression_data as impression_data', 'feature_environments.enabled as enabled', @@ -559,26 +564,36 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { if (rows.length > 0) { const overview = rows.reduce((acc, row) => { if (acc[row.feature_name] !== undefined) { - acc[row.feature_name].environments.push( - FeatureStrategiesStore.getEnvironment(row), - ); + const currentEnv = + FeatureStrategiesStore.getEnvironment(row); + acc[row.feature_name].environments.push(currentEnv); if (this.isNewTag(acc[row.feature_name], row)) { this.addTag(acc[row.feature_name], row); } + + if ( + new Date(currentEnv.lastSeenAt) > + new Date(acc[row.feature_name].lastSeenAt) + ) { + acc[row.feature_name].lastSeenAt = + currentEnv.lastSeenAt; + } } else { + const envOverview = + FeatureStrategiesStore.getEnvironment(row); + acc[row.feature_name] = { type: row.type, description: row.description, favorite: row.favorite, name: row.feature_name, + lastSeenAt: row.env_last_seen_at, createdAt: row.created_at, - lastSeenAt: row.last_seen_at, stale: row.stale, impressionData: row.impression_data, - environments: [ - FeatureStrategiesStore.getEnvironment(row), - ], + environments: [envOverview], }; + if (this.isNewTag(acc[row.feature_name], row)) { this.addTag(acc[row.feature_name], row); } diff --git a/src/lib/db/feature-toggle-client-store.ts b/src/lib/db/feature-toggle-client-store.ts index 77cfad5a7c..47ea68231c 100644 --- a/src/lib/db/feature-toggle-client-store.ts +++ b/src/lib/db/feature-toggle-client-store.ts @@ -77,10 +77,9 @@ export default class FeatureToggleClientStore 'features.project as project', 'features.stale as stale', 'features.impression_data as impression_data', - 'features.last_seen_at as last_seen_at', 'features.created_at as created_at', 'fe.variants as variants', - 'fe.last_seen_at as env_last_seen_at', + 'fem.last_seen_at as env_last_seen_at', 'fe.enabled as enabled', 'fe.environment as environment', 'fs.id as strategy_id', @@ -96,6 +95,15 @@ export default class FeatureToggleClientStore 'df.parent as parent', 'df.variants as parent_variants', 'df.enabled as parent_enabled', + this.db.raw(`( + SELECT + CASE + WHEN COUNT(*) > 0 THEN MAX(last_seen_at) + ELSE NULL + END + FROM feature_environments_metrics + WHERE features.name = feature_environments_metrics.feature_name + ) as last_seen_at`), ] as (string | Raw)[]; let query = this.db('features') @@ -115,13 +123,18 @@ export default class FeatureToggleClientStore 'enabled', 'environment', 'variants', - 'last_seen_at', ) .where({ environment }) .as('fe'), 'fe.feature_name', 'features.name', ) + .leftJoin('feature_environments_metrics as fem', function () { + this.on('fem.feature_name', '=', 'features.name').andOnVal( + 'fem.environment', + environment, + ); + }) .leftJoin( 'feature_strategy_segment as fss', `fss.feature_strategy_id`, diff --git a/src/lib/db/feature-toggle-store.ts b/src/lib/db/feature-toggle-store.ts index 5fd2522157..07b8929793 100644 --- a/src/lib/db/feature-toggle-store.ts +++ b/src/lib/db/feature-toggle-store.ts @@ -20,7 +20,6 @@ const FEATURE_COLUMNS = [ 'stale', 'created_at', 'impression_data', - 'last_seen_at', 'archived_at', ]; @@ -86,7 +85,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore { async get(name: string): Promise { return this.db - .first(FEATURE_COLUMNS) + .first(this.columnsWithMetrics()) .from(TABLE) .where({ name }) .then(this.rowToFeature); @@ -101,7 +100,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore { ): Promise { const { archived, ...rest } = query; const rows = await this.db - .select(FEATURE_COLUMNS) + .select(this.columnsWithMetrics()) .from(TABLE) .where(rest) .modify(FeatureToggleStore.filterByArchived, archived); @@ -109,9 +108,11 @@ export default class FeatureToggleStore implements IFeatureToggleStore { } async getAllByNames(names: string[]): Promise { - const query = this.db(TABLE).orderBy('name', 'asc'); - query.whereIn('name', names); - const rows = await query; + const rows = await this.db + .select(this.columnsWithMetrics()) + .from(TABLE) + .orderBy('name', 'asc') + .whereIn('name', names); return rows.map(this.rowToFeature); } @@ -187,18 +188,6 @@ export default class FeatureToggleStore implements IFeatureToggleStore { .forUpdate() .skipLocked(), ); - - // Updating the toggle's last_seen_at also for backwards compatibility - await this.db(TABLE) - .update({ last_seen_at: now }) - .whereIn( - 'name', - this.db(TABLE) - .select('name') - .whereIn('name', toggleNames) - .forUpdate() - .skipLocked(), - ); } } catch (err) { this.logger.error('Could not update lastSeen, error: ', err); @@ -286,7 +275,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore { try { const row = await this.db(TABLE) .insert(this.dtoToRow(project, data)) - .returning(FEATURE_COLUMNS); + .returning(this.columnsWithMetrics()); return this.rowToFeature(row[0]); } catch (err) { @@ -310,7 +299,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore { const row = await this.db(TABLE) .where({ name: data.name }) .update(this.dtoToRow(project, data)) - .returning(FEATURE_COLUMNS); + .returning(this.columnsWithMetrics()); return this.rowToFeature(row[0]); } @@ -320,7 +309,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore { const row = await this.db(TABLE) .where({ name }) .update({ archived_at: now }) - .returning(FEATURE_COLUMNS); + .returning(this.columnsWithMetrics()); return this.rowToFeature(row[0]); } @@ -329,7 +318,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore { const rows = await this.db(TABLE) .whereIn('name', names) .update({ archived_at: now }) - .returning(FEATURE_COLUMNS); + .returning(this.columnsWithMetrics()); return rows.map((row) => this.rowToFeature(row)); } @@ -340,7 +329,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore { const rows = await this.db(TABLE) .whereIn('name', names) .update({ stale }) - .returning(FEATURE_COLUMNS); + .returning(this.columnsWithMetrics()); return rows.map((row) => this.rowToFeature(row)); } @@ -362,7 +351,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore { const row = await this.db(TABLE) .where({ name }) .update({ archived_at: null }) - .returning(FEATURE_COLUMNS); + .returning(this.columnsWithMetrics()); return this.rowToFeature(row[0]); } @@ -370,7 +359,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore { const rows = await this.db(TABLE) .whereIn('name', names) .update({ archived_at: null }) - .returning(FEATURE_COLUMNS); + .returning(this.columnsWithMetrics()); return rows.map((row) => this.rowToFeature(row)); } @@ -418,11 +407,13 @@ export default class FeatureToggleStore implements IFeatureToggleStore { .update('variants', variantsString) .where('feature_name', featureName); - const row = await this.db(TABLE) - .select(FEATURE_COLUMNS) + const rows = await this.db(TABLE) + .select(this.columnsWithMetrics()) .where({ project: project, name: featureName }); - const toggle = this.rowToFeature(row[0]); + const toggle = this.rowToFeature( + (rows as unknown as FeaturesTable[])[0], + ); toggle.variants = newVariants; return toggle; @@ -483,6 +474,21 @@ export default class FeatureToggleStore implements IFeatureToggleStore { return result?.potentially_stale ?? false; } + + private columnsWithMetrics = () => { + return [ + ...FEATURE_COLUMNS, + this.db.raw(`( + SELECT + CASE + WHEN COUNT(*) > 0 THEN MAX(last_seen_at) + ELSE NULL + END + FROM feature_environments_metrics + WHERE features.name = feature_environments_metrics.feature_name + ) as last_seen_at`), + ]; + }; } module.exports = FeatureToggleStore; diff --git a/src/migrations/20230919103955-create-feature-environments-metrics-table.js b/src/migrations/20230919103955-create-feature-environments-metrics-table.js index bb942de4d3..74cb223f09 100644 --- a/src/migrations/20230919103955-create-feature-environments-metrics-table.js +++ b/src/migrations/20230919103955-create-feature-environments-metrics-table.js @@ -34,7 +34,14 @@ exports.up = function (db, cb) { features.impression_data as impression_data, features.created_at as created_at, features.archived_at as archived_at, - features.last_seen_at as last_seen_at, + ( + SELECT + CASE + WHEN COUNT(*) > 0 THEN MAX(last_seen_at) + END + FROM feature_environments_metrics + WHERE features.name = feature_environments_metrics.feature_name + ) as last_seen_at, feature_environments_metrics.last_seen_at as env_last_seen_at, feature_environments.enabled as enabled, feature_environments.environment as environment, @@ -53,9 +60,10 @@ exports.up = function (db, cb) { feature_strategies.variants as strategy_variants FROM features LEFT JOIN feature_environments ON feature_environments.feature_name = features.name - LEFT JOIN feature_environments_metrics ON feature_environments_metrics.feature_name = features.name LEFT JOIN feature_strategies ON feature_strategies.feature_name = feature_environments.feature_name - and feature_strategies.environment = feature_environments.environment + AND feature_strategies.environment = feature_environments.environment + LEFT JOIN feature_environments_metrics ON feature_environments_metrics.feature_name = feature_environments.feature_name + AND feature_environments_metrics.environment = feature_environments.environment LEFT JOIN environments ON feature_environments.environment = environments.name LEFT JOIN feature_strategy_segment as fss ON fss.feature_strategy_id = feature_strategies.id; `,