-
Notifications
You must be signed in to change notification settings - Fork 1
Add feature-flag system: schema, evaluation service, router gating, and admin UI #152
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
Draft
Tim-Machine
wants to merge
5
commits into
v2.2.x
Choose a base branch
from
feature/feature-flags
base: v2.2.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
7e6ccc7
Remove stray EXIT: deploy-log file (invalid path on Windows)
9542011
Add feature_flags and feature_flag_audit tables (migration 068)
646e5e6
Add FeatureFlagService evaluation helper to flask_core
f85375d
Gate router dispatch behind feature flags
8ae7e81
Add feature-flag admin surface to hub portal
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 was deleted.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| /** | ||
| * Redis Configuration | ||
| * A single lazily-connected shared client using node-redis (redis v4). | ||
| * | ||
| * The hub backend uses Redis only for lightweight pub/sub cache-invalidation | ||
| * signalling (e.g. the "feature_flags:reload" channel the Python router | ||
| * subscribes to). It is NOT a hard dependency: if REDIS_URL is unset the | ||
| * exported client is null, and if Redis is unreachable every operation fails | ||
| * soft (logged, never thrown). The app must run fine with Redis down or absent. | ||
| */ | ||
| import { createClient } from 'redis'; | ||
| import { logger } from '../utils/logger.js'; | ||
|
|
||
| // REDIS_URL example (see docker-compose.yml): redis://hub:<pw>@infra-redis:6379/0 | ||
| const redisUrl = process.env.REDIS_URL || null; | ||
|
|
||
| /** | ||
| * Build the shared client. Connection is deferred until first use so that | ||
| * importing this module never blocks startup or crashes when Redis is absent. | ||
| * Returns null when no REDIS_URL is configured. | ||
| */ | ||
| function buildClient() { | ||
| if (!redisUrl) { | ||
| logger.debug('Redis disabled: REDIS_URL is not set'); | ||
| return null; | ||
| } | ||
|
|
||
| const c = createClient({ | ||
| url: redisUrl, | ||
| socket: { | ||
| connectTimeout: 5000, | ||
| // Give up after a handful of attempts so an absent Redis does not | ||
| // produce an endless reconnect/error loop in the logs. | ||
| reconnectStrategy: (retries) => (retries > 10 ? false : Math.min(retries * 200, 3000)), | ||
| }, | ||
| }); | ||
|
|
||
| // An 'error' listener is mandatory: without one, node-redis emits on the | ||
| // process and an unhandled 'error' would crash the app. | ||
| c.on('error', (err) => logger.warn('Redis client error', { error: err.message })); | ||
| c.on('ready', () => logger.info('Redis client ready')); | ||
| c.on('end', () => logger.debug('Redis connection closed')); | ||
|
|
||
| return c; | ||
| } | ||
|
|
||
| // Single shared client instance (or null when Redis is not configured). | ||
| const client = buildClient(); | ||
|
|
||
| // De-dupe concurrent connect attempts. | ||
| let connecting = null; | ||
|
|
||
| /** | ||
| * Return a connected client, or null if Redis is unavailable/unconfigured. | ||
| * Never throws. | ||
| * @returns {Promise<import('redis').RedisClientType|null>} | ||
| */ | ||
| export async function getRedisClient() { | ||
| if (!client) return null; | ||
| if (client.isOpen) return client; | ||
|
|
||
| if (!connecting) { | ||
| connecting = client.connect().catch((err) => { | ||
| logger.warn('Redis connection failed', { error: err.message }); | ||
| return null; | ||
| }).finally(() => { | ||
| connecting = null; | ||
| }); | ||
| } | ||
| await connecting; | ||
| return client.isOpen ? client : null; | ||
| } | ||
|
|
||
| /** | ||
| * Fire-and-forget publish. Returns true if the message was handed to Redis, | ||
| * false if Redis is unavailable. Never throws. | ||
| * @param {string} channel | ||
| * @param {string} message | ||
| * @returns {Promise<boolean>} | ||
| */ | ||
| export async function publish(channel, message) { | ||
| const c = await getRedisClient(); | ||
| if (!c) return false; | ||
| await c.publish(channel, message); | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Check Redis connectivity. Never throws. | ||
| * @returns {Promise<boolean>} | ||
| */ | ||
| export async function checkConnection() { | ||
| try { | ||
| const c = await getRedisClient(); | ||
| if (!c) return false; | ||
| const pong = await c.ping(); | ||
| return pong === 'PONG'; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Close the shared client (best-effort, for graceful shutdown). | ||
| */ | ||
| export async function closeRedis() { | ||
| if (client && client.isOpen) { | ||
| try { | ||
| await client.quit(); | ||
| logger.info('Redis client closed'); | ||
| } catch (err) { | ||
| logger.warn('Error closing Redis client', { error: err.message }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export { client }; | ||
| export default { getRedisClient, publish, checkConnection, closeRedis, client }; | ||
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
have nodejs do "backend" things would be a change of architecture.. we typically use Python3.12+ / Quart , Rust for clients, and GoLang for networking to handle "backend" things and simply use nextjs for a thin web layer.
Changing languages is not out of the question, but we should talk through it first.