revert stale job retention

This commit is contained in:
Ben
2026-05-07 09:07:04 -04:00
parent 8424638081
commit 9878fa42b9
4 changed files with 28 additions and 142 deletions
-1
View File
@@ -25,7 +25,6 @@ export class MiningJob {
public jobTemplateId: string; public jobTemplateId: string;
public networkDifficulty: number; public networkDifficulty: number;
public creation: number; public creation: number;
public retiredAt?: number;
constructor( constructor(
private network: bitcoinjs.networks.Network, private network: bitcoinjs.networks.Network,
-13
View File
@@ -550,19 +550,6 @@ export class StratumV1Client {
return false; return false;
} }
const classification = this.stratumV1JobsService.classifyJobForShare(job);
if (classification === 'stale-rejected') {
const err = new StratumErrorMessage(
submission.id,
eStratumErrorCode.JobNotFound,
'stale').response();
const success = await this.write(err);
if (!success) {
return false;
}
return false;
}
const submissionHash = [ const submissionHash = [
submission.jobId, submission.jobId,
submission.extraNonce2, submission.extraNonce2,
+11 -31
View File
@@ -47,7 +47,7 @@ describe('StratumV1JobsService', () => {
expect(service.getJobTemplateById('1')).toBe(jobTemplate); expect(service.getJobTemplateById('1')).toBe(jobTemplate);
}); });
it('should retire jobs when the block height changes', async () => { it('should clear jobs when the block height changes', async () => {
const firstTemplate = await firstValueFrom(service.newMiningJob$); const firstTemplate = await firstValueFrom(service.newMiningJob$);
service.addJob({ jobId: 'old-job', creation: Date.now() } as any); service.addJob({ jobId: 'old-job', creation: Date.now() } as any);
@@ -57,27 +57,18 @@ describe('StratumV1JobsService', () => {
const jobTemplate = await nextTemplate; const jobTemplate = await nextTemplate;
expect(jobTemplate.blockData.clearJobs).toBe(true); expect(jobTemplate.blockData.clearJobs).toBe(true);
expect(service.getJobById('old-job')).toEqual(expect.objectContaining({ expect(service.getJobById('old-job')).toBeUndefined();
jobId: 'old-job', expect(service.getJobTemplateById(firstTemplate.blockData.id)).toBeUndefined();
retiredAt: Date.now()
}));
expect(service.getJobTemplateById(firstTemplate.blockData.id).blockData.retiredAt).toBe(Date.now());
expect(service.getJobTemplateById(jobTemplate.blockData.id)).toBe(jobTemplate); expect(service.getJobTemplateById(jobTemplate.blockData.id)).toBe(jobTemplate);
}); });
it('should age retired jobs and templates after the retention window', async () => { it('should age old jobs and templates after five minutes', async () => {
const firstTemplate = await firstValueFrom(service.newMiningJob$); await firstValueFrom(service.newMiningJob$);
const oldCreation = Date.now() - (1000 * 60 * 11); const oldCreation = Date.now() - (1000 * 60 * 11);
const retiredAt = Date.now() - (1000 * 60 * 11); service.jobs['old-job'] = { jobId: 'old-job', creation: oldCreation } as any;
firstTemplate.blockData.retiredAt = retiredAt; service.blocks['old-template'] = {
for (let i = 4; i >= 0; i--) { blockData: { creation: oldCreation }
service.jobs[`old-job-${i}`] = { jobId: `old-job-${i}`, creation: oldCreation - i, retiredAt } as any; } as any;
(service as any).trackJob(`old-job-${i}`);
service.blocks[`old-template-${i}`] = {
blockData: { creation: oldCreation - i, retiredAt }
} as any;
(service as any).trackBlock(`old-template-${i}`);
}
bitcoinRpcService.miningInfo.blocks = MockRecording1.BLOCK_TEMPLATE.height; bitcoinRpcService.miningInfo.blocks = MockRecording1.BLOCK_TEMPLATE.height;
const nextTemplate = firstValueFrom(service.newMiningJob$.pipe(skip(1))); const nextTemplate = firstValueFrom(service.newMiningJob$.pipe(skip(1)));
@@ -85,22 +76,11 @@ describe('StratumV1JobsService', () => {
const jobTemplate = await nextTemplate; const jobTemplate = await nextTemplate;
expect(jobTemplate.blockData.clearJobs).toBe(false); expect(jobTemplate.blockData.clearJobs).toBe(false);
expect(service.getJobById('old-job-4')).toBeUndefined(); expect(service.getJobById('old-job')).toBeUndefined();
expect(service.getJobTemplateById('old-template-4')).toBeUndefined(); expect(service.getJobTemplateById('old-template')).toBeUndefined();
expect(service.getJobTemplateById(jobTemplate.blockData.id)).toBe(jobTemplate); expect(service.getJobTemplateById(jobTemplate.blockData.id)).toBe(jobTemplate);
}); });
it('should classify retired jobs inside and outside the stale grace window', () => {
const job = { jobId: '1', creation: Date.now(), retiredAt: Date.now() - 1000 } as any;
expect(service.classifyJobForShare(job, Date.now())).toBe('stale-creditable');
job.retiredAt = Date.now() - 6000;
expect(service.classifyJobForShare(job, Date.now())).toBe('stale-rejected');
expect(service.classifyJobForShare({ jobId: '2', creation: Date.now() } as any, Date.now())).toBe('active');
});
it('should increment job ids when jobs are added', () => { it('should increment job ids when jobs are added', () => {
expect(service.getNextId()).toBe('1'); expect(service.getNextId()).toBe('1');
+17 -97
View File
@@ -18,14 +18,9 @@ export interface IJobTemplate {
networkDifficulty: number; networkDifficulty: number;
height: number; height: number;
clearJobs: boolean; clearJobs: boolean;
retiredAt?: number;
}; };
} }
const STALE_GRACE_MS = parseInt(process.env.STRATUM_STALE_GRACE_MS) || 5000;
const MIN_RETAINED = 3;
export { STALE_GRACE_MS };
@Injectable() @Injectable()
export class StratumV1JobsService { export class StratumV1JobsService {
@@ -36,11 +31,6 @@ export class StratumV1JobsService {
public blocks: { [id: number]: IJobTemplate } = {}; public blocks: { [id: number]: IJobTemplate } = {};
private lastBlockHeight = 0; private lastBlockHeight = 0;
private jobRetentionMs = parseInt(process.env.JOB_RETENTION_MS) || 600000;
private jobOrder: string[] = [];
private blockOrder: string[] = [];
private jobOrderSet = new Set<string>();
private blockOrderSet = new Set<string>();
constructor( constructor(
private readonly bitcoinRpcService: BitcoinRpcService private readonly bitcoinRpcService: BitcoinRpcService
@@ -121,7 +111,6 @@ export class StratumV1JobsService {
tap((data) => { tap((data) => {
this.cleanup(data.blockData.clearJobs); this.cleanup(data.blockData.clearJobs);
this.blocks[data.blockData.id] = data; this.blocks[data.blockData.id] = data;
this.trackBlock(data.blockData.id);
}), }),
shareReplay({ refCount: true, bufferSize: 1 }) shareReplay({ refCount: true, bufferSize: 1 })
) )
@@ -153,78 +142,32 @@ export class StratumV1JobsService {
public cleanup(clearJobs: boolean, now: number = Date.now()) { public cleanup(clearJobs: boolean, now: number = Date.now()) {
if (clearJobs) { if (clearJobs) {
for (const id of this.blockOrder) { this.blocks = {};
const block = this.blocks[id]; this.jobs = {};
if (block != null && block.blockData.retiredAt === undefined) { return;
block.blockData.retiredAt = now; }
}
} let templatesDeleted = 0;
for (const jobId of this.jobOrder) { let jobsDeleted = 0;
const job = this.jobs[jobId];
if (job != null && job.retiredAt === undefined) { for (const templateId in this.blocks) {
job.retiredAt = now; if (now - this.blocks[templateId].blockData.creation > (1000 * 60 * 5)) {
} delete this.blocks[templateId];
templatesDeleted++;
} }
} }
this.ageEntries( for (const jobId in this.jobs) {
this.blocks, if (now - this.jobs[jobId].creation > (1000 * 60 * 5)) {
this.blockOrder, delete this.jobs[jobId];
this.blockOrderSet, jobsDeleted++;
now,
entry => entry.blockData.creation,
entry => entry.blockData.retiredAt,
);
this.ageEntries(
this.jobs,
this.jobOrder,
this.jobOrderSet,
now,
entry => entry.creation,
entry => entry.retiredAt,
);
}
private ageEntries<T>(
map: Record<string, T>,
order: string[],
orderSet: Set<string>,
now: number,
getCreation: (entry: T) => number,
getRetiredAt: (entry: T) => number | undefined,
): void {
while (order.length > MIN_RETAINED) {
const id = order[0];
const entry = map[id];
if (entry == null) {
order.shift();
orderSet.delete(id);
continue;
} }
const retiredAt = getRetiredAt(entry);
if (retiredAt !== undefined && now - retiredAt > this.jobRetentionMs) {
delete map[id];
order.shift();
orderSet.delete(id);
continue;
}
if (retiredAt === undefined && now - getCreation(entry) > this.jobRetentionMs * 2) {
delete map[id];
order.shift();
orderSet.delete(id);
continue;
}
break;
} }
//console.log(`Deleted ${templatesDeleted} templates and ${jobsDeleted} jobs.`)
} }
public addJob(job: MiningJob) { public addJob(job: MiningJob) {
this.jobs[job.jobId] = job; this.jobs[job.jobId] = job;
this.trackJob(job.jobId);
this.latestJobId++; this.latestJobId++;
} }
@@ -232,13 +175,6 @@ export class StratumV1JobsService {
return this.jobs[jobId]; return this.jobs[jobId];
} }
public classifyJobForShare(job: MiningJob, now: number = Date.now()): 'active' | 'stale-creditable' | 'stale-rejected' {
if (job.retiredAt === undefined) {
return 'active';
}
return (now - job.retiredAt) <= STALE_GRACE_MS ? 'stale-creditable' : 'stale-rejected';
}
public getNextTemplateId() { public getNextTemplateId() {
return this.latestJobTemplateId.toString(16); return this.latestJobTemplateId.toString(16);
} }
@@ -246,20 +182,4 @@ export class StratumV1JobsService {
return this.latestJobId.toString(16); return this.latestJobId.toString(16);
} }
private trackJob(jobId: string): void {
if (this.jobOrderSet.has(jobId)) {
return;
}
this.jobOrderSet.add(jobId);
this.jobOrder.push(jobId);
}
private trackBlock(blockId: string): void {
if (this.blockOrderSet.has(blockId)) {
return;
}
this.blockOrderSet.add(blockId);
this.blockOrder.push(blockId);
}
} }