Skip to content

Commit e89e3d0

Browse files
authored
log-client: check missing logs on server after flushing (#257)
* log-client: check missing logs after flushing * log-client: check for missing logs after flushing * changeset
1 parent 2b0b4ed commit e89e3d0

File tree

4 files changed

+95
-38
lines changed

4 files changed

+95
-38
lines changed

.changeset/stale-terms-stare.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@lightmill/log-client": minor
3+
---
4+
5+
`Logger#flush` now checks for missing logs on the server and fails if any are missing that were added prior to the flush call. This is considered a minor change, as it primarily surfaces existing issues (such as communication errors) earlier.

packages/log-client/__mocks__/mock-server.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,27 @@ export class MockServer {
305305

306306
'/runs/{id}': {
307307
get: async () => {
308-
throw new Error('Not implemented: GET /runs/{id}');
308+
return {
309+
status: 200,
310+
body: {
311+
data: {
312+
id: 'default-run-id',
313+
type: 'runs' as const,
314+
attributes: {
315+
name: 'Default Run',
316+
status: 'idle',
317+
lastLogNumber: 0,
318+
missingLogNumbers: [],
319+
},
320+
relationships: {
321+
experiment: {
322+
data: { id: 'default-experiment-id', type: 'experiments' },
323+
},
324+
lastLogs: { data: [] },
325+
},
326+
},
327+
},
328+
};
309329
},
310330
patch: async ({ params, body }) => {
311331
const run = this.#runs.get(params.id as string);

packages/log-client/__tests__/logger.test.ts

Lines changed: 40 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -283,26 +283,14 @@ describe('LogClient#flush', () => {
283283
body: { data: { id: `log-id-${reqManager.size() + 1}`, type: 'logs' } },
284284
});
285285
});
286-
logger.addLog({
287-
type: 'mock-log',
288-
val: 1,
289-
date: new Date('2021-06-03T02:00:00.000Z'),
290-
});
291-
logger.addLog({
292-
type: 'mock-log',
293-
val: 2,
294-
date: new Date('2021-06-03T03:00:00.000Z'),
295-
});
286+
logger.addLog({ type: 'mock-log', val: 1 });
287+
logger.addLog({ type: 'mock-log', val: 2 });
296288
let resolved = false;
297289
let flushPromise = logger.flush().then((result) => {
298290
resolved = true;
299291
return result;
300292
});
301-
logger.addLog({
302-
type: 'mock-log',
303-
val: 3,
304-
date: new Date('2021-06-03T04:00:00.000Z'),
305-
});
293+
logger.addLog({ type: 'mock-log', val: 3 });
306294
await reqManager.waitForRequests(2);
307295
expect(resolved).toBe(false);
308296
reqManager.resolveNextRequest();
@@ -328,38 +316,53 @@ describe('LogClient#flush', () => {
328316
body: { data: { id: `log-id-${defManager.size() + 1}`, type: 'logs' } },
329317
});
330318
});
331-
logger.addLog({
332-
type: 'mock-log',
333-
val: 1,
334-
date: new Date('2021-06-03T02:00:00.000Z'),
335-
});
336-
logger.addLog({
337-
type: 'mock-log',
338-
val: 2,
339-
date: new Date('2021-06-03T03:00:00.000Z'),
319+
let oldGetRun = server.handlers['/runs/{id}'].get.getMockImplementation()!;
320+
server.handlers['/runs/{id}'].get.mockImplementation(async (...args) => {
321+
let result = await oldGetRun(...args);
322+
if (result.status !== 200) return result;
323+
result.body.data.attributes = {
324+
...result.body.data.attributes,
325+
// Should be ignored by first flush call.
326+
missingLogNumbers: [3],
327+
};
328+
return result;
340329
});
330+
logger.addLog({ type: 'mock-log', val: 1 });
331+
logger.addLog({ type: 'mock-log', val: 2 });
341332
let flushPromise = logger.flush();
342-
logger
343-
.addLog({
344-
type: 'mock-log',
345-
val: 'fail',
346-
date: new Date('2021-06-03T04:00:00.000Z'),
347-
})
348-
.catch(() => {
349-
// Prevent vitest from catching the error and complaining about it.
350-
});
351-
logger.addLog({
352-
type: 'mock-log',
353-
val: 4,
354-
date: new Date('2021-06-03T03:00:00.000Z'),
333+
logger.addLog({ type: 'mock-log', val: 'fail' }).catch(() => {
334+
// Prevent vitest from catching the error and complaining about it.
355335
});
336+
logger.addLog({ type: 'mock-log', val: 4 });
356337
await defManager.waitForRequests(3);
357338
defManager.resolveAllRequests();
358339
await expect(flushPromise).resolves.toBeUndefined();
359340
await expect(logger.flush()).rejects.toThrowErrorMatchingInlineSnapshot(
360341
`[AddLogError: FORBIDDEN]`,
361342
);
362343
});
344+
345+
it('fails if there are still missing logs on the server', async ({
346+
logger,
347+
server,
348+
}) => {
349+
let oldGetRun = server.handlers['/runs/{id}'].get.getMockImplementation()!;
350+
server.handlers['/runs/{id}'].get.mockImplementation(async (...args) => {
351+
let result = await oldGetRun(...args);
352+
if (result.status !== 200) return result;
353+
result.body.data.attributes = {
354+
...result.body.data.attributes,
355+
// Should be ignored by first flush call.
356+
missingLogNumbers: [1],
357+
};
358+
return result;
359+
});
360+
logger.addLog({ type: 'mock-log', val: 1 });
361+
logger.addLog({ type: 'mock-log', val: 2 });
362+
await expect(logger.flush()).rejects.toThrowErrorMatchingInlineSnapshot(
363+
`[FlushError: There are missing logs on server after flushing. Missing logs: 1]`,
364+
);
365+
});
363366
});
364367

365368
describe('LogClient#completeRun', () => {

packages/log-client/src/logger.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,16 @@ export class LightmillLogger<
135135
},
136136
});
137137
});
138+
const missingLogs = await this.#fetchMissingLogs();
139+
if (missingLogs.some((l) => l <= lastLogNumber)) {
140+
// If we have missing logs that are before or at the last log number,
141+
// then we know that there are missing logs on the server that we
142+
// do not have in our pending logs, which means we lost some logs
143+
// in the process.
144+
throw new FlushError(
145+
`There are missing logs on server after flushing. Missing logs: ${missingLogs.join(', ')}`,
146+
);
147+
}
138148
}
139149

140150
#isTherePendingLogsBefore(logNumber: number): boolean {
@@ -145,6 +155,18 @@ export class LightmillLogger<
145155
return first != null && first <= logNumber;
146156
}
147157

158+
async #fetchMissingLogs() {
159+
const response = await this.#fetchClient.GET('/runs/{id}', {
160+
credentials: 'include',
161+
params: { path: { id: this.#runId } },
162+
headers: { 'content-type': apiMediaType },
163+
});
164+
if (response.error != null) {
165+
throw new RequestError(response);
166+
}
167+
return response.data.data.attributes.missingLogNumbers;
168+
}
169+
148170
async completeRun() {
149171
await this.#endRun('completed');
150172
}
@@ -196,3 +218,10 @@ class AddLogError extends Error {
196218
this.logNumber = logNumber;
197219
}
198220
}
221+
222+
class FlushError extends Error {
223+
name = 'FlushError' as const;
224+
constructor(message: string) {
225+
super(message);
226+
}
227+
}

0 commit comments

Comments
 (0)