-
Notifications
You must be signed in to change notification settings - Fork 306
Expand file tree
/
Copy pathreputation-recompute-processor.ts
More file actions
144 lines (127 loc) · 4.71 KB
/
Copy pathreputation-recompute-processor.ts
File metadata and controls
144 lines (127 loc) · 4.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/**
* Reputation Recompute Processor
*
* Handles periodic recomputation of reputation scores with checkpointing.
* Subject IDs are streamed in pages from the database so the job never loads
* the entire table into memory at once.
*/
import { ReputationRecomputePayload, JobResult } from "../types";
import { ReputationService } from "../../services/reputation.service";
import { reputationStore } from "../../models/reputation.store";
import {
reputationCheckpointStore,
RecomputeCheckpoint,
} from "../../models/reputation-checkpoint.store";
import { ReputationRepository } from "../../repositories/reputationRepository";
import { createLogger } from "../../logger";
/**
* Async generator that yields one page of distinct target IDs at a time from
* the reputation_entries table, stopping when the repository returns an empty page.
*
* @param repo - Instantiated ReputationRepository.
* @param pageSize - Rows per page (matches job batchSize).
*/
async function* targetIdPages(
repo: ReputationRepository,
pageSize: number,
): AsyncGenerator<string[]> {
let pageNumber = 0;
while (true) {
const offset = pageNumber * pageSize;
const page = repo.getDistinctTargetIdPage(pageSize, offset);
if (page.length === 0) break;
yield page;
if (page.length < pageSize) break; // last page
pageNumber += 1;
}
}
/**
* Process a reputation recompute job.
*
* Iterates all distinct subject IDs from the database in pages, delegates
* score aggregation to `ReputationService.getProfile`, and persists a
* checkpoint after every successfully processed subject. A single subject
* failure is logged and skipped — it does not abort the batch.
*
* @param payload - Recompute configuration (batchSize, forceRecompute, etc.)
* @param repo - ReputationRepository instance; injected for testability.
* @returns JobResult with statistics for the completed run.
*/
export async function processReputationRecompute(
payload: ReputationRecomputePayload,
repo: ReputationRepository,
): Promise<JobResult> {
const jobId = `recompute-${Date.now()}`;
const batchSize = payload.batchSize ?? 100;
const forceRecompute = payload.forceRecompute ?? false;
const log = createLogger({
processor: "reputation-recompute",
...(payload.correlationId && { correlationId: payload.correlationId }),
...(payload.requestId && { requestId: payload.requestId }),
});
log.info("Starting reputation recompute job", { jobId });
// --- checkpoint wiring ---
let checkpoint: RecomputeCheckpoint | undefined;
if (payload.resumeFromCheckpoint !== false) {
const active = reputationCheckpointStore.getActiveCheckpoints();
checkpoint =
active.length > 0
? active[0]
: reputationCheckpointStore.createCheckpoint(jobId, 0);
} else {
checkpoint = reputationCheckpointStore.createCheckpoint(jobId, 0);
}
let totalProcessed = 0;
let hasAnyId = false;
for await (const page of targetIdPages(repo, batchSize)) {
hasAnyId = true;
for (const targetId of page) {
try {
const profile = ReputationService.getProfile(targetId);
if (!forceRecompute && isProfileUpToDate(profile.lastUpdated)) {
log.info("Profile up to date, skipping", { targetId });
continue;
}
// Persist the freshly computed profile back to the in-memory store
reputationStore.set(profile);
totalProcessed++;
reputationCheckpointStore.updateProgress(checkpoint.jobId, targetId);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
log.warn("Failed to recompute reputation for subject; skipping", {
msg,
});
// per-subject isolation — continue with next subject
}
}
log.info("Batch processed", { totalProcessed });
}
if (!hasAnyId) {
log.info("No subjects found to recompute");
reputationCheckpointStore.markCompleted(checkpoint.jobId);
return {
success: true,
message: "No freelancers found to recompute",
data: { totalProcessed: 0, totalFreelancers: 0 },
};
}
reputationCheckpointStore.markCompleted(checkpoint.jobId);
log.info("Reputation recompute job completed", { jobId, totalProcessed });
return {
success: true,
message: `Successfully recomputed reputation for ${totalProcessed} freelancers`,
data: {
totalProcessed,
jobId,
checkpointId: checkpoint.jobId,
},
};
}
/**
* Returns true when a profile's `lastUpdated` timestamp is within the last 24 h,
* meaning a recompute can be skipped unless `forceRecompute` is set.
*/
function isProfileUpToDate(lastUpdated: string): boolean {
const ageMs = Date.now() - new Date(lastUpdated).getTime();
return ageMs < 24 * 60 * 60 * 1000;
}