mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(workspace): stabilise the development stack (#2024)
This commit is contained in:
1 parent
cf1a7d7a8a
commit
5d2e5932a4
55 files changed
+1848
-3396
No files matched your search
@@ -39,7 +39,7 @@ function AbuseAwareAppErrorHandler(err: Error, ctx: Context<HonoEnv>): Response
|
||||
|
||||
export async function createAPIApp(options: CreateAPIAppOptions): Promise<APIAppResult> {
|
||||
const {config, logger} = options;
|
||||
const shutdownApiLifecycle = createShutdown(logger);
|
||||
const shutdownApiLifecycle = createShutdown(config, logger);
|
||||
setIsDevelopment(config.nodeEnv === 'development');
|
||||
const routes = new Hono<HonoEnv>({strict: true});
|
||||
configureMiddleware(routes, {
|
||||
|
||||
@@ -306,6 +306,7 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
|
||||
apiSecret: master.integrations.voice.api_secret,
|
||||
webhookUrl: master.integrations.voice.webhook_url,
|
||||
url: master.integrations.voice.url,
|
||||
internalUrl: master.integrations.voice.internal_url,
|
||||
defaultRegion: master.integrations.voice.default_region,
|
||||
},
|
||||
stripe: {
|
||||
|
||||
@@ -36,6 +36,11 @@ import {JetStreamWorkerQueue} from '../worker/JetStreamWorkerQueue';
|
||||
import {WorkerService} from '../worker/WorkerService';
|
||||
|
||||
let jsConnectionManager: JetStreamConnectionManager | null = null;
|
||||
|
||||
function unsupportedDatabaseBackend(backend: never): never {
|
||||
throw new Error(`Unsupported database backend during shutdown: ${String(backend)}`);
|
||||
}
|
||||
|
||||
export function createInitializer(config: APIConfig, logger: ILogger): () => Promise<void> {
|
||||
return async (): Promise<void> => {
|
||||
try {
|
||||
@@ -131,7 +136,7 @@ export function createInitializer(config: APIConfig, logger: ILogger): () => Pro
|
||||
try {
|
||||
const kvDeletionQueue = getKVAccountDeletionQueue();
|
||||
if (await kvDeletionQueue.needsRebuild()) {
|
||||
logger.warn('KV deletion queue needs rebuild, rebuilding...');
|
||||
logger.info('KV deletion queue needs rebuild, rebuilding...');
|
||||
await kvDeletionQueue.rebuildState();
|
||||
} else {
|
||||
logger.info('KV deletion queue state is healthy');
|
||||
@@ -204,13 +209,13 @@ export function createInitializer(config: APIConfig, logger: ILogger): () => Pro
|
||||
logger.info('API service initialization complete');
|
||||
} catch (error) {
|
||||
logger.error({error}, 'API service initialization failed');
|
||||
await createShutdown(logger)();
|
||||
await createShutdown(config, logger)();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createShutdown(logger: ILogger): () => Promise<void> {
|
||||
export function createShutdown(config: APIConfig, logger: ILogger): () => Promise<void> {
|
||||
return async (): Promise<void> => {
|
||||
logger.info('Shutting down API service...');
|
||||
if (jsConnectionManager) {
|
||||
@@ -258,18 +263,26 @@ export function createShutdown(logger: ILogger): () => Promise<void> {
|
||||
} catch (error) {
|
||||
logger.error({error}, 'Error shutting down report service');
|
||||
}
|
||||
try {
|
||||
setDatabaseQueryExecutor(null);
|
||||
await shutdownPostgres();
|
||||
logger.info('Postgres client shut down');
|
||||
} catch (error) {
|
||||
logger.error({error}, 'Error shutting down Postgres client');
|
||||
}
|
||||
try {
|
||||
await shutdownCassandra();
|
||||
logger.info('Cassandra client shut down');
|
||||
} catch (error) {
|
||||
logger.error({error}, 'Error shutting down Cassandra client');
|
||||
setDatabaseQueryExecutor(null);
|
||||
switch (config.database.backend) {
|
||||
case 'postgres':
|
||||
try {
|
||||
await shutdownPostgres();
|
||||
logger.info('Postgres client shut down');
|
||||
} catch (error) {
|
||||
logger.error({error}, 'Error shutting down Postgres client');
|
||||
}
|
||||
break;
|
||||
case 'cassandra':
|
||||
try {
|
||||
await shutdownCassandra();
|
||||
logger.info('Cassandra client shut down');
|
||||
} catch (error) {
|
||||
logger.error({error}, 'Error shutting down Cassandra client');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
unsupportedDatabaseBackend(config.database.backend);
|
||||
}
|
||||
logger.info('API service shutdown complete');
|
||||
};
|
||||
|
||||
@@ -66,7 +66,9 @@ export function registerControllers(routes: HonoApp, config: APIConfig): void {
|
||||
TestHarnessController(routes);
|
||||
}
|
||||
UserController(routes);
|
||||
registerInboundSmsWebhook(routes);
|
||||
if (config.sms.enabled) {
|
||||
registerInboundSmsWebhook(routes);
|
||||
}
|
||||
WebhookController(routes);
|
||||
OAuth2Controller(routes);
|
||||
OAuth2ApplicationsController(routes);
|
||||
|
||||
@@ -204,6 +204,7 @@ export interface APIConfig {
|
||||
apiSecret?: string;
|
||||
webhookUrl?: string;
|
||||
url?: string;
|
||||
internalUrl?: string;
|
||||
defaultRegion?: {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -19,6 +19,8 @@ interface PageState {
|
||||
|
||||
const VALUE_SEPARATOR = '\u001f';
|
||||
const ENCODED_TYPE_KEY = '__fluxer_type';
|
||||
const POSTGRES_KV_SCHEMA_LOCK_NAMESPACE = 0x46584b56;
|
||||
const POSTGRES_KV_SCHEMA_LOCK_TIMEOUT = '120s';
|
||||
|
||||
function normalizeCql(cql: string): string {
|
||||
return cql.replace(/\s+/g, ' ').trim();
|
||||
@@ -354,8 +356,13 @@ function parseEqWhere(whereSql: string, cql: string): ReadonlyArray<EqWhereExpr>
|
||||
}
|
||||
|
||||
export async function ensurePostgresKvSchema(client: IPostgresClient): Promise<void> {
|
||||
const table = quoteIdentifier(client.kvTable());
|
||||
await client.query(`
|
||||
const kvTable = client.kvTable();
|
||||
const table = quoteIdentifier(kvTable);
|
||||
await client.transaction(async (db) => {
|
||||
await db.query("SELECT set_config('statement_timeout', $1, true)", [POSTGRES_KV_SCHEMA_LOCK_TIMEOUT]);
|
||||
await db.query('SELECT pg_advisory_xact_lock($1, hashtext($2))', [POSTGRES_KV_SCHEMA_LOCK_NAMESPACE, kvTable]);
|
||||
await db.query("SELECT set_config('statement_timeout', '0', true)");
|
||||
await db.query(`
|
||||
CREATE TABLE IF NOT EXISTS ${table} (
|
||||
table_name text NOT NULL,
|
||||
partition_key text NOT NULL,
|
||||
@@ -365,28 +372,29 @@ CREATE TABLE IF NOT EXISTS ${table} (
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (table_name, row_key)
|
||||
)`);
|
||||
await client.query(
|
||||
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${client.kvTable()}_partition_row_idx`)} ON ${table} (table_name, partition_key, row_key)`,
|
||||
);
|
||||
await client.query(
|
||||
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${client.kvTable()}_row_key_c_idx`)} ON ${table} (table_name, row_key COLLATE "C")`,
|
||||
);
|
||||
await client.query(
|
||||
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${client.kvTable()}_expires_idx`)} ON ${table} (expires_at) WHERE expires_at IS NOT NULL`,
|
||||
);
|
||||
await client.query(
|
||||
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${client.kvTable()}_messages_message_idx`)} ON ${table} (partition_key, ((CASE WHEN row_data -> 'message_id' ->> 'value' ~ '^-?[0-9]+$' THEN (row_data -> 'message_id' ->> 'value')::bigint END))) WHERE table_name = 'messages'`,
|
||||
);
|
||||
await client.query(
|
||||
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${client.kvTable()}_message_reactions_message_idx`)} ON ${table} (partition_key, ((CASE WHEN row_data -> 'message_id' ->> 'value' ~ '^-?[0-9]+$' THEN (row_data -> 'message_id' ->> 'value')::bigint END))) WHERE table_name = 'message_reactions'`,
|
||||
);
|
||||
await client.query(`
|
||||
await db.query(
|
||||
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${kvTable}_partition_row_idx`)} ON ${table} (table_name, partition_key, row_key)`,
|
||||
);
|
||||
await db.query(
|
||||
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${kvTable}_row_key_c_idx`)} ON ${table} (table_name, row_key COLLATE "C")`,
|
||||
);
|
||||
await db.query(
|
||||
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${kvTable}_expires_idx`)} ON ${table} (expires_at) WHERE expires_at IS NOT NULL`,
|
||||
);
|
||||
await db.query(
|
||||
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${kvTable}_messages_message_idx`)} ON ${table} (partition_key, ((CASE WHEN row_data -> 'message_id' ->> 'value' ~ '^-?[0-9]+$' THEN (row_data -> 'message_id' ->> 'value')::bigint END))) WHERE table_name = 'messages'`,
|
||||
);
|
||||
await db.query(
|
||||
`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${kvTable}_message_reactions_message_idx`)} ON ${table} (partition_key, ((CASE WHEN row_data -> 'message_id' ->> 'value' ~ '^-?[0-9]+$' THEN (row_data -> 'message_id' ->> 'value')::bigint END))) WHERE table_name = 'message_reactions'`,
|
||||
);
|
||||
await db.query(`
|
||||
UPDATE ${table}
|
||||
SET partition_key = split_part(row_key, chr(31), 1) || chr(31) || split_part(row_key, chr(31), 2)
|
||||
WHERE table_name = 'messages'
|
||||
AND partition_key = row_key
|
||||
AND split_part(row_key, chr(31), 3) <> ''`);
|
||||
await client.query(`DROP INDEX IF EXISTS ${quoteIdentifier(`${client.kvTable()}_partition_idx`)}`);
|
||||
await db.query(`DROP INDEX IF EXISTS ${quoteIdentifier(`${kvTable}_partition_idx`)}`);
|
||||
});
|
||||
}
|
||||
|
||||
export async function pruneExpiredPostgresKvRows(client: IPostgresClient, batchSize = 5000): Promise<number> {
|
||||
|
||||
@@ -60,7 +60,7 @@ export class KVAccountDeletionQueueService {
|
||||
}
|
||||
|
||||
async rebuildState(): Promise<void> {
|
||||
Logger.info('Starting deletion queue rebuild from Cassandra');
|
||||
Logger.info('Starting deletion queue rebuild from primary database');
|
||||
try {
|
||||
await this.kvClient.del(QUEUE_KEY);
|
||||
await this.kvClient.del(STATE_VERSION_KEY);
|
||||
|
||||
@@ -109,6 +109,18 @@ function createRoomServiceClient(endpoint: string, apiKey: string, apiSecret: st
|
||||
return client;
|
||||
}
|
||||
|
||||
function resolveRoomServiceEndpoint(server: VoiceServerRecord): string {
|
||||
const defaultRegion = Config.voice.defaultRegion;
|
||||
const internalUrl = Config.voice.internalUrl;
|
||||
if (!defaultRegion || !internalUrl) {
|
||||
return server.endpoint;
|
||||
}
|
||||
if (server.regionId !== defaultRegion.id || server.serverId !== `${defaultRegion.id}-server-1`) {
|
||||
return server.endpoint;
|
||||
}
|
||||
return internalUrl;
|
||||
}
|
||||
|
||||
export class LiveKitService extends ILiveKitService {
|
||||
private serverClients: Map<string, Map<string, ServerClientConfig>> = new Map();
|
||||
private topology: VoiceTopology;
|
||||
@@ -453,12 +465,13 @@ export class LiveKitService extends ILiveKitService {
|
||||
const servers = this.topology.getServersForRegion(region.id);
|
||||
const serverMap: Map<string, ServerClientConfig> = new Map();
|
||||
for (const server of servers) {
|
||||
const roomServiceEndpoint = resolveRoomServiceEndpoint(server);
|
||||
serverMap.set(server.serverId, {
|
||||
endpoint: server.endpoint,
|
||||
apiKey: server.apiKey,
|
||||
apiSecret: server.apiSecret,
|
||||
isActive: server.isActive,
|
||||
roomServiceClient: createRoomServiceClient(server.endpoint, server.apiKey, server.apiSecret),
|
||||
roomServiceClient: createRoomServiceClient(roomServiceEndpoint, server.apiKey, server.apiSecret),
|
||||
});
|
||||
}
|
||||
newMap.set(region.id, serverMap);
|
||||
|
||||
@@ -284,7 +284,7 @@ export function setInjectedAccountPolicyEvaluator(evaluator: IAccountPolicyEvalu
|
||||
function getRegistrationRiskEvaluator(): IRegistrationRiskEvaluator {
|
||||
if (_registrationRiskEvaluator) return _registrationRiskEvaluator;
|
||||
if (!Config.risk.enabled) {
|
||||
Logger.warn(
|
||||
Logger.info(
|
||||
{},
|
||||
'[ServiceMiddleware] integrations.risk_integration.enabled is false — account risk scoring is disabled',
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user