11import { describe , expect , spyOn , test } from "bun:test" ;
2+ import { spawn , type ChildProcess } from "node:child_process" ;
23import fs from "node:fs" ;
34import { tmpdir } from "node:os" ;
45import { join } from "node:path" ;
@@ -15,6 +16,14 @@ function tempLogFile(): { dir: string; file: string } {
1516 return { dir, file : join ( dir , "corbits.log" ) } ;
1617}
1718
19+ function liveLockHolder ( ) : ChildProcess {
20+ const holder = spawn ( "sleep" , [ "30" ] , { stdio : "ignore" } ) ;
21+ if ( holder . pid === undefined ) {
22+ throw new Error ( "lock holder did not start" ) ;
23+ }
24+ return holder ;
25+ }
26+
1827describe ( "corbitsLogFilePath" , ( ) => {
1928 test ( "nests under the settings dir, not directly in home" , ( ) => {
2029 expect ( corbitsLogFilePath ( "/home/dev" ) ) . toBe (
@@ -226,6 +235,129 @@ describe("installFileLogSink", () => {
226235 }
227236 } ) ;
228237
238+ test ( "a crash after .1 is removed restores the rotating temp on the next flush" , ( ) => {
239+ const { dir, file } = tempLogFile ( ) ;
240+ try {
241+ const temp = `${ file } .${ process . pid } .7.rotating` ;
242+ const marker = "crashed-rotation-bytes\n" ;
243+ fs . writeFileSync ( temp , marker ) ;
244+ installFileLogSink ( file , { maxBytes : 10_000 } ) ;
245+ getLogger ( [ "logging" , "recover" ] ) . info ( "after-crash" ) ;
246+ flushFileLogSinkSync ( ) ;
247+ const active = fs . existsSync ( file ) ? fs . readFileSync ( file , "utf8" ) : "" ;
248+ const rotated = fs . existsSync ( `${ file } .1` )
249+ ? fs . readFileSync ( `${ file } .1` , "utf8" )
250+ : "" ;
251+ expect ( `${ active } ${ rotated } ` ) . toContain ( "crashed-rotation-bytes" ) ;
252+ expect ( fs . existsSync ( temp ) ) . toBe ( false ) ;
253+ } finally {
254+ fs . rmSync ( dir , { recursive : true , force : true } ) ;
255+ }
256+ } ) ;
257+
258+ test ( "a second reclaimer does not delete the lock a winner just created" , ( ) => {
259+ const { dir, file } = tempLogFile ( ) ;
260+ const holder = liveLockHolder ( ) ;
261+ const lockPath = `${ file } .lock` ;
262+ const realRead = fs . readFileSync . bind ( fs ) ;
263+ let lied = false ;
264+ try {
265+ installFileLogSink ( file , { maxBytes : 300 } ) ;
266+ const first = getLogger ( [ "logging" , "reclaim-a" ] ) ;
267+ first . info ( "first-marker" ) ;
268+ first . info ( "first-marker-b" ) ;
269+ first . info ( "short-active" ) ;
270+ flushFileLogSinkSync ( ) ;
271+ const rotated = `${ file } .1` ;
272+ const marker = fs . readFileSync ( rotated , "utf8" ) ;
273+ fs . writeFileSync ( lockPath , "999999:dead" ) ;
274+ const hooked = spyOn ( fs , "readFileSync" ) . mockImplementation ( ( (
275+ path : fs . PathOrFileDescriptor ,
276+ ) => {
277+ if ( String ( path ) === lockPath && ! lied ) {
278+ lied = true ;
279+ fs . writeFileSync ( lockPath , `${ holder . pid } :winner` ) ;
280+ return "999999:dead" ;
281+ }
282+ return realRead ( path , "utf8" ) ;
283+ } ) as unknown as typeof fs . readFileSync ) ;
284+ try {
285+ getLogger ( [ "logging" , "reclaim-b" ] ) . info ( `b-wide-${ "x" . repeat ( 80 ) } ` ) ;
286+ flushFileLogSinkSync ( ) ;
287+ expect ( fs . readFileSync ( rotated , "utf8" ) ) . toBe ( marker ) ;
288+ expect ( fs . readFileSync ( lockPath , "utf8" ) ) . toContain ( "winner" ) ;
289+ } finally {
290+ hooked . mockRestore ( ) ;
291+ }
292+ } finally {
293+ holder . kill ( ) ;
294+ fs . rmSync ( dir , { recursive : true , force : true } ) ;
295+ }
296+ } ) ;
297+
298+ test ( "a flush that cannot acquire the lock does not rotate" , ( ) => {
299+ const { dir, file } = tempLogFile ( ) ;
300+ const holder = liveLockHolder ( ) ;
301+ try {
302+ installFileLogSink ( file , { maxBytes : 300 } ) ;
303+ const first = getLogger ( [ "logging" , "lock-a" ] ) ;
304+ first . info ( "first-marker" ) ;
305+ first . info ( "first-marker-b" ) ;
306+ first . info ( "short-active" ) ;
307+ flushFileLogSinkSync ( ) ;
308+ const rotated = `${ file } .1` ;
309+ const marker = fs . readFileSync ( rotated , "utf8" ) ;
310+ const activeBefore = fs . readFileSync ( file , "utf8" ) ;
311+ fs . writeFileSync ( `${ file } .lock` , `${ holder . pid } :held` ) ;
312+ getLogger ( [ "logging" , "lock-b" ] ) . info ( `b-wide-${ "x" . repeat ( 80 ) } ` ) ;
313+ flushFileLogSinkSync ( ) ;
314+ expect ( fs . readFileSync ( rotated , "utf8" ) ) . toBe ( marker ) ;
315+ expect ( fs . readFileSync ( file , "utf8" ) ) . toBe ( activeBefore ) ;
316+ } finally {
317+ holder . kill ( ) ;
318+ fs . rmSync ( dir , { recursive : true , force : true } ) ;
319+ }
320+ } ) ;
321+
322+ test ( "a lock that cannot be read is not stolen" , ( ) => {
323+ const { dir, file } = tempLogFile ( ) ;
324+ const holder = liveLockHolder ( ) ;
325+ const lockPath = `${ file } .lock` ;
326+ const realRead = fs . readFileSync . bind ( fs ) ;
327+ try {
328+ installFileLogSink ( file , { maxBytes : 300 } ) ;
329+ const first = getLogger ( [ "logging" , "lock-read-a" ] ) ;
330+ first . info ( "first-marker" ) ;
331+ first . info ( "first-marker-b" ) ;
332+ first . info ( "short-active" ) ;
333+ flushFileLogSinkSync ( ) ;
334+ const rotated = `${ file } .1` ;
335+ const marker = fs . readFileSync ( rotated , "utf8" ) ;
336+ const activeBefore = fs . readFileSync ( file , "utf8" ) ;
337+ fs . writeFileSync ( lockPath , `${ holder . pid } :held` ) ;
338+ const hooked = spyOn ( fs , "readFileSync" ) . mockImplementation ( ( (
339+ path : fs . PathOrFileDescriptor ,
340+ ) => {
341+ if ( String ( path ) === lockPath ) {
342+ throw Object . assign ( new Error ( "EIO" ) , { code : "EIO" } ) ;
343+ }
344+ return realRead ( path , "utf8" ) ;
345+ } ) as unknown as typeof fs . readFileSync ) ;
346+ try {
347+ getLogger ( [ "logging" , "lock-read-b" ] ) . info ( `b-wide-${ "x" . repeat ( 80 ) } ` ) ;
348+ flushFileLogSinkSync ( ) ;
349+ } finally {
350+ hooked . mockRestore ( ) ;
351+ }
352+ expect ( fs . readFileSync ( rotated , "utf8" ) ) . toBe ( marker ) ;
353+ expect ( fs . readFileSync ( file , "utf8" ) ) . toBe ( activeBefore ) ;
354+ expect ( fs . readFileSync ( lockPath , "utf8" ) ) . toContain ( "held" ) ;
355+ } finally {
356+ holder . kill ( ) ;
357+ fs . rmSync ( dir , { recursive : true , force : true } ) ;
358+ }
359+ } ) ;
360+
229361 test ( "a failed rotation keeps already-written records in the active file" , ( ) => {
230362 const { dir, file } = tempLogFile ( ) ;
231363 try {
0 commit comments