feat: exponer group_sync_seconds_until_next y adaptar tests

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
main
brobert 4 weeks ago
parent a5a3d98167
commit cd453afbce

@ -84,6 +84,12 @@ export class WebhookServer {
Metrics.set('allowed_groups_total_allowed', allowed);
Metrics.set('allowed_groups_total_blocked', blocked);
} catch {}
// Exponer métrica con el tiempo restante hasta el próximo group sync (o -1 si scheduler inactivo)
try {
const secs = GroupSyncService.getSecondsUntilNextGroupSync();
const val = (secs == null || !Number.isFinite(secs)) ? -1 : secs;
Metrics.set('group_sync_seconds_until_next', val);
} catch {}
const format = (process.env.METRICS_FORMAT || 'prom').toLowerCase() === 'json' ? 'json' : 'prom';
const body = Metrics.render(format as any);
return new Response(body, {

@ -71,6 +71,8 @@ export class GroupSyncService {
private static _groupsSchedulerRunning = false;
private static _membersTimer: any = null;
private static _membersSchedulerRunning = false;
private static _groupsIntervalMs: number | null = null;
private static _groupsNextTickAt: number | null = null;
static async syncGroups(force: boolean = false): Promise<{ added: number; updated: number }> {
if (!this.shouldSync(force)) {
@ -682,8 +684,12 @@ export class GroupSyncService {
if (process.env.NODE_ENV === 'development' && interval < 10000) {
interval = 10000;
}
this._groupsIntervalMs = interval;
this._groupsNextTickAt = Date.now() + interval;
this._groupsTimer = setInterval(() => {
// Programar el siguiente tick antes de ejecutar la sincronización
this._groupsNextTickAt = Date.now() + (this._groupsIntervalMs ?? interval);
this.syncGroups().catch(err => {
console.error('❌ Groups scheduler run error:', err);
});
@ -696,6 +702,15 @@ export class GroupSyncService {
clearInterval(this._groupsTimer);
this._groupsTimer = null;
}
this._groupsIntervalMs = null;
this._groupsNextTickAt = null;
}
public static getSecondsUntilNextGroupSync(nowMs: number = Date.now()): number | null {
const next = this._groupsNextTickAt;
if (next == null) return null;
const secs = (next - nowMs) / 1000;
return secs > 0 ? secs : 0;
}
public static startMembersScheduler(): void {

@ -3,6 +3,7 @@ import { GroupSyncService } from '../../../src/services/group-sync';
const envBackup = { ...process.env };
let originalSyncMembers: any;
let originalSyncGroups: any;
function sleep(ms: number) {
return new Promise(res => setTimeout(res, ms));
@ -11,11 +12,14 @@ function sleep(ms: number) {
describe('GroupSyncService - scheduler de miembros', () => {
beforeEach(() => {
originalSyncMembers = GroupSyncService.syncMembersForActiveGroups;
originalSyncGroups = GroupSyncService.syncGroups;
});
afterEach(() => {
GroupSyncService.stopMembersScheduler();
GroupSyncService.stopGroupsScheduler();
GroupSyncService.syncMembersForActiveGroups = originalSyncMembers;
GroupSyncService.syncGroups = originalSyncGroups;
process.env = envBackup;
});
@ -45,4 +49,36 @@ describe('GroupSyncService - scheduler de miembros', () => {
GroupSyncService.stopMembersScheduler();
expect(called).toBeGreaterThanOrEqual(1);
});
test('groups scheduler no arranca en entorno de test', async () => {
process.env = { ...envBackup, NODE_ENV: 'test' };
let called = 0;
GroupSyncService.syncGroups = async () => {
called++;
return { added: 0, updated: 0 };
};
GroupSyncService.startGroupsScheduler();
await sleep(100);
expect(called).toBe(0);
expect(GroupSyncService.getSecondsUntilNextGroupSync()).toBeNull();
});
test('groups scheduler arranca en producción y programa next tick', async () => {
process.env = { ...envBackup, NODE_ENV: 'production', GROUP_SYNC_INTERVAL_MS: '30' };
let called = 0;
GroupSyncService.syncGroups = async () => {
called++;
return { added: 0, updated: 0 };
};
GroupSyncService.startGroupsScheduler();
const secs1 = GroupSyncService.getSecondsUntilNextGroupSync();
expect(secs1).not.toBeNull();
expect(Number(secs1)).toBeGreaterThan(0);
await sleep(120);
GroupSyncService.stopGroupsScheduler();
expect(called).toBeGreaterThanOrEqual(1);
});
});

@ -37,6 +37,7 @@ describe('/metrics y /health (detallado)', () => {
expect(body).toContain('last_sync_ok');
expect(body).toContain('active_groups 2');
expect(body).toContain('webhook_events_total_messages_upsert 3');
expect(body).toContain('group_sync_seconds_until_next');
});
test('/health?full=1 devuelve JSON con contadores y snapshot', async () => {

Loading…
Cancel
Save