mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 17:15:03 -07:00
Remove Redis client presence
This commit is contained in:
@@ -11,7 +11,7 @@ describe('UserAgentReportService', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('uses Redis live presence in API-only processes when workers are connected', async () => {
|
||||
it('uses active database clients for live API-only reports', async () => {
|
||||
process.env.API_ONLY = 'true';
|
||||
const userAgentReport = {
|
||||
find: jest.fn().mockResolvedValue([
|
||||
@@ -23,26 +23,17 @@ describe('UserAgentReportService', () => {
|
||||
},
|
||||
]),
|
||||
};
|
||||
const clientRepository = {
|
||||
find: jest.fn().mockResolvedValue([{ id: 'client-1' }]),
|
||||
};
|
||||
const clientRepository = createClientRepository([
|
||||
{
|
||||
userAgent: 'unknown/sv2',
|
||||
count: '1',
|
||||
bestDifficulty: 456,
|
||||
totalHashRate: '123',
|
||||
},
|
||||
]);
|
||||
const redisMessagingService = {
|
||||
getJsonCache: jest.fn().mockResolvedValue(null),
|
||||
getAllClientPresence: jest.fn().mockResolvedValue([
|
||||
{
|
||||
clientId: 'client-1',
|
||||
address: 'tb1qworker',
|
||||
clientName: 'sv2gateway',
|
||||
sessionId: 'session',
|
||||
userAgent: 'unknown/sv2',
|
||||
startTime: '2026-01-01T00:00:00.000Z',
|
||||
lastSeen: '2026-01-01T00:00:10.000Z',
|
||||
hashRate: 123,
|
||||
bestDifficulty: 456,
|
||||
},
|
||||
]),
|
||||
setJsonCache: jest.fn().mockResolvedValue(undefined),
|
||||
removeClientPresence: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const service = new UserAgentReportService(
|
||||
userAgentReport as any,
|
||||
@@ -59,9 +50,10 @@ describe('UserAgentReportService', () => {
|
||||
},
|
||||
]);
|
||||
expect(userAgentReport.find).not.toHaveBeenCalled();
|
||||
expect(clientRepository.createQueryBuilder).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to the materialized view in API-only processes when live presence is empty', async () => {
|
||||
it('falls back to the materialized view when no active database clients exist', async () => {
|
||||
process.env.API_ONLY = 'true';
|
||||
const viewRows = [
|
||||
{
|
||||
@@ -74,14 +66,10 @@ describe('UserAgentReportService', () => {
|
||||
const userAgentReport = {
|
||||
find: jest.fn().mockResolvedValue(viewRows),
|
||||
};
|
||||
const clientRepository = {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
const clientRepository = createClientRepository([]);
|
||||
const redisMessagingService = {
|
||||
getJsonCache: jest.fn().mockResolvedValue([]),
|
||||
getAllClientPresence: jest.fn().mockResolvedValue([]),
|
||||
setJsonCache: jest.fn().mockResolvedValue(undefined),
|
||||
removeClientPresence: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const service = new UserAgentReportService(
|
||||
userAgentReport as any,
|
||||
@@ -92,42 +80,23 @@ describe('UserAgentReportService', () => {
|
||||
await expect(service.getReport()).resolves.toEqual(viewRows);
|
||||
});
|
||||
|
||||
it('filters and removes stale Redis presence rows with no active database client', async () => {
|
||||
it('returns a non-empty cached live report before querying the database', async () => {
|
||||
process.env.API_ONLY = 'true';
|
||||
const cachedRows = [
|
||||
{
|
||||
userAgent: 'cached',
|
||||
count: '2',
|
||||
bestDifficulty: 8,
|
||||
totalHashRate: '16',
|
||||
},
|
||||
];
|
||||
const userAgentReport = {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
const clientRepository = {
|
||||
find: jest.fn().mockResolvedValue([{ id: 'active-client' }]),
|
||||
find: jest.fn(),
|
||||
};
|
||||
const clientRepository = createClientRepository([]);
|
||||
const redisMessagingService = {
|
||||
getJsonCache: jest.fn().mockResolvedValue(null),
|
||||
getAllClientPresence: jest.fn().mockResolvedValue([
|
||||
{
|
||||
clientId: 'active-client',
|
||||
address: 'tb1qactive',
|
||||
clientName: 'worker',
|
||||
sessionId: 'active',
|
||||
userAgent: 'bitaxe',
|
||||
startTime: '2026-01-01T00:00:00.000Z',
|
||||
lastSeen: '2026-01-01T00:00:10.000Z',
|
||||
hashRate: 100,
|
||||
bestDifficulty: 200,
|
||||
},
|
||||
{
|
||||
clientId: 'stale-client',
|
||||
address: 'tb1qstale',
|
||||
clientName: 'worker',
|
||||
sessionId: 'stale',
|
||||
userAgent: 'bitaxe',
|
||||
startTime: '2026-01-01T00:00:00.000Z',
|
||||
lastSeen: '2026-01-01T00:00:10.000Z',
|
||||
hashRate: 300,
|
||||
bestDifficulty: 400,
|
||||
},
|
||||
]),
|
||||
getJsonCache: jest.fn().mockResolvedValue(cachedRows),
|
||||
setJsonCache: jest.fn().mockResolvedValue(undefined),
|
||||
removeClientPresence: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const service = new UserAgentReportService(
|
||||
userAgentReport as any,
|
||||
@@ -135,15 +104,22 @@ describe('UserAgentReportService', () => {
|
||||
redisMessagingService as any,
|
||||
);
|
||||
|
||||
await expect(service.getReport()).resolves.toEqual([
|
||||
{
|
||||
userAgent: 'bitaxe',
|
||||
count: '1',
|
||||
bestDifficulty: 200,
|
||||
totalHashRate: '100',
|
||||
},
|
||||
]);
|
||||
expect(redisMessagingService.removeClientPresence)
|
||||
.toHaveBeenCalledWith('stale-client', 'tb1qstale');
|
||||
await expect(service.getReport()).resolves.toEqual(cachedRows);
|
||||
expect(clientRepository.createQueryBuilder).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
function createClientRepository(rows: unknown[]) {
|
||||
const queryBuilder = {
|
||||
select: jest.fn().mockReturnThis(),
|
||||
addSelect: jest.fn().mockReturnThis(),
|
||||
where: jest.fn().mockReturnThis(),
|
||||
groupBy: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
getRawMany: jest.fn().mockResolvedValue(rows),
|
||||
};
|
||||
|
||||
return {
|
||||
createQueryBuilder: jest.fn().mockReturnValue(queryBuilder),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ClientEntity } from '../../client/client.entity';
|
||||
import { UserAgentReportView } from './user-agent-report.view';
|
||||
@@ -60,76 +60,28 @@ export class UserAgentReportService {
|
||||
}
|
||||
|
||||
private async buildLiveReport() {
|
||||
const presences = await this.redisMessagingService.getAllClientPresence();
|
||||
const activePresences = await this.filterActivePresences(presences);
|
||||
if (activePresences.length === 0) {
|
||||
const rows = await this.clientRepository
|
||||
.createQueryBuilder('client')
|
||||
.select('COALESCE(NULLIF(client.userAgent, \'\'), \'Other\')', 'userAgent')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.addSelect('MAX(client.bestDifficulty)', 'bestDifficulty')
|
||||
.addSelect('COALESCE(SUM(client.hashRate), 0)', 'totalHashRate')
|
||||
.where('client.deletedAt IS NULL')
|
||||
.groupBy('COALESCE(NULLIF(client.userAgent, \'\'), \'Other\')')
|
||||
.orderBy('"totalHashRate"', 'DESC')
|
||||
.getRawMany<UserAgentReportView>();
|
||||
|
||||
if (rows.length === 0) {
|
||||
return await this.userAgentReport.find();
|
||||
}
|
||||
const rows = new Map<string, {
|
||||
userAgent: string;
|
||||
count: number;
|
||||
bestDifficulty: number;
|
||||
totalHashRate: number;
|
||||
}>();
|
||||
|
||||
activePresences.forEach(presence => {
|
||||
const userAgent = presence.userAgent == null || presence.userAgent.length === 0
|
||||
? 'Other'
|
||||
: presence.userAgent;
|
||||
const row = rows.get(userAgent) ?? {
|
||||
userAgent,
|
||||
count: 0,
|
||||
bestDifficulty: 0,
|
||||
totalHashRate: 0,
|
||||
};
|
||||
row.count++;
|
||||
row.bestDifficulty = Math.max(
|
||||
row.bestDifficulty,
|
||||
Number(presence.bestDifficulty ?? 0),
|
||||
);
|
||||
row.totalHashRate += Number(presence.hashRate ?? 0);
|
||||
rows.set(userAgent, row);
|
||||
});
|
||||
|
||||
const report = [...rows.values()]
|
||||
.sort((left, right) => right.totalHashRate - left.totalHashRate)
|
||||
.map(row => ({
|
||||
userAgent: row.userAgent,
|
||||
count: row.count.toString(),
|
||||
bestDifficulty: row.bestDifficulty,
|
||||
totalHashRate: row.totalHashRate.toString(),
|
||||
}));
|
||||
|
||||
await this.redisMessagingService
|
||||
.setJsonCache(this.liveReportCacheKey, report, 60 * 1000)
|
||||
.setJsonCache(this.liveReportCacheKey, rows, 60 * 1000)
|
||||
.catch(error => {
|
||||
console.error(`Live user-agent report cache write failed: ${error.message}`);
|
||||
});
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
private async filterActivePresences(presences: Awaited<ReturnType<RedisMessagingService['getAllClientPresence']>>) {
|
||||
if (presences.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const activeClients = await this.clientRepository.find({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
id: In(presences.map(presence => presence.clientId)),
|
||||
},
|
||||
});
|
||||
const activeIds = new Set(activeClients.map(client => client.id));
|
||||
const stalePresences = presences.filter(presence => !activeIds.has(presence.clientId));
|
||||
void Promise.all(stalePresences.map(presence => {
|
||||
return this.redisMessagingService.removeClientPresence(presence.clientId, presence.address)
|
||||
.catch(() => undefined);
|
||||
}));
|
||||
|
||||
return presences.filter(presence => activeIds.has(presence.clientId));
|
||||
return rows;
|
||||
}
|
||||
|
||||
public async refreshReport() {
|
||||
|
||||
Reference in New Issue
Block a user