11import path from "path" ;
22import fs from "fs/promises" ;
3+ import { lock } from "proper-lockfile" ;
34import { ACCURACY_RESULTS_DIR , LATEST_ACCURACY_RUN_NAME } from "../constants.js" ;
45import {
56 AccuracyResult ,
@@ -25,27 +26,43 @@ export class DiskBasedResultStorage implements AccuracyResultStorage {
2526 const raw = await fs . readFile ( filePath , "utf8" ) ;
2627 return JSON . parse ( raw ) as AccuracyResult ;
2728 } catch ( error ) {
28- if ( ( error as { code : string } ) . code === "ENOENT" ) {
29+ if ( ( error as NodeJS . ErrnoException ) . code === "ENOENT" ) {
2930 return null ;
3031 }
3132 throw error ;
3233 }
3334 }
3435
3536 async updateRunStatus ( commitSHA : string , runId : string , status : AccuracyRunStatuses ) : Promise < void > {
36- await this . atomicWriteResult ( commitSHA , runId , async ( ) => {
37+ const resultFilePath = this . getAccuracyResultFilePath ( commitSHA , runId ) ;
38+ const release = await lock ( resultFilePath , { retries : 10 } ) ;
39+ try {
3740 const accuracyResult = await this . getAccuracyResult ( commitSHA , runId ) ;
3841 if ( ! accuracyResult ) {
39- throw new Error (
40- `Cannot update run status to ${ status } for commit - ${ commitSHA } , runId - ${ runId } . Results not found!`
41- ) ;
42+ throw new Error ( "Results not found!" ) ;
4243 }
4344
44- return {
45- ...accuracyResult ,
46- runStatus : status ,
47- } ;
48- } ) ;
45+ await fs . writeFile (
46+ resultFilePath ,
47+ JSON . stringify (
48+ {
49+ ...accuracyResult ,
50+ runStatus : status ,
51+ } ,
52+ null ,
53+ 2
54+ ) ,
55+ { encoding : "utf8" }
56+ ) ;
57+ } catch ( error ) {
58+ console . warn (
59+ `Could not update run status to ${ status } for commit - ${ commitSHA } , runId - ${ runId } .` ,
60+ error
61+ ) ;
62+ throw error ;
63+ } finally {
64+ await release ( ) ;
65+ }
4966
5067 // This bit is important to mark the current run as the latest run for a
5168 // commit so that we can use that during baseline comparison.
@@ -63,10 +80,11 @@ export class DiskBasedResultStorage implements AccuracyResultStorage {
6380 prompt : string ,
6481 modelResponse : ModelResponse
6582 ) : Promise < void > {
66- await this . atomicWriteResult ( commitSHA , runId , async ( ) => {
67- const accuracyResult = await this . getAccuracyResult ( commitSHA , runId ) ;
68- if ( ! accuracyResult ) {
69- return {
83+ const resultFilePath = this . getAccuracyResultFilePath ( commitSHA , runId ) ;
84+ const { fileCreatedWithInitialData } = await this . ensureAccuracyResultFile (
85+ resultFilePath ,
86+ JSON . stringify (
87+ {
7088 runId,
7189 runStatus : AccuracyRunStatus . InProgress ,
7290 createdOn : Date . now ( ) ,
@@ -77,64 +95,82 @@ export class DiskBasedResultStorage implements AccuracyResultStorage {
7795 modelResponses : [ modelResponse ] ,
7896 } ,
7997 ] ,
80- } ;
98+ } ,
99+ null ,
100+ 2
101+ )
102+ ) ;
103+
104+ if ( fileCreatedWithInitialData ) {
105+ return ;
106+ }
107+
108+ const releaseLock = await lock ( resultFilePath , { retries : 10 } ) ;
109+ try {
110+ const accuracyResult = await this . getAccuracyResult ( commitSHA , runId ) ;
111+ if ( ! accuracyResult ) {
112+ throw new Error ( "Expected at-least initial accuracy result to be present" ) ;
81113 }
82114
83115 const existingPromptIdx = accuracyResult . promptResults . findIndex ( ( result ) => result . prompt === prompt ) ;
84116 const promptResult = accuracyResult . promptResults [ existingPromptIdx ] ;
85117 if ( ! promptResult ) {
86- return {
87- ...accuracyResult ,
88- promptResults : [
89- ...accuracyResult . promptResults ,
118+ return await fs . writeFile (
119+ resultFilePath ,
120+ JSON . stringify (
90121 {
91- prompt,
92- modelResponses : [ modelResponse ] ,
122+ ...accuracyResult ,
123+ promptResults : [
124+ ...accuracyResult . promptResults ,
125+ {
126+ prompt,
127+ modelResponses : [ modelResponse ] ,
128+ } ,
129+ ] ,
93130 } ,
94- ] ,
95- } ;
131+ null ,
132+ 2
133+ )
134+ ) ;
96135 }
97136
98137 accuracyResult . promptResults . splice ( existingPromptIdx , 1 , {
99138 prompt : promptResult . prompt ,
100139 modelResponses : [ ...promptResult . modelResponses , modelResponse ] ,
101140 } ) ;
102141
103- return accuracyResult ;
104- } ) ;
142+ return await fs . writeFile ( resultFilePath , JSON . stringify ( accuracyResult , null , 2 ) ) ;
143+ } catch ( error ) {
144+ console . warn ( `Could not save model response for commit - ${ commitSHA } , runId - ${ runId } .` , error ) ;
145+ throw error ;
146+ } finally {
147+ await releaseLock ?.( ) ;
148+ }
105149 }
106150
107151 close ( ) : Promise < void > {
108152 return Promise . resolve ( ) ;
109153 }
110154
111- private async atomicWriteResult (
112- commitSHA : string ,
113- runId : string ,
114- generateResult : ( ) => Promise < AccuracyResult >
115- ) : Promise < void > {
116- for ( let attempt = 0 ; attempt < 10 ; attempt ++ ) {
117- // This should happen outside the try catch to let the result
118- // generation error bubble up.
119- const result = await generateResult ( ) ;
120- const resultFilePath = this . getAccuracyResultFilePath ( commitSHA , runId ) ;
121- try {
122- const tmp = `${ resultFilePath } ~${ Date . now ( ) } ` ;
123- await fs . writeFile ( tmp , JSON . stringify ( result , null , 2 ) ) ;
124- await fs . rename ( tmp , resultFilePath ) ;
125- return ;
126- } catch ( error ) {
127- if ( ( error as { code : string } ) . code === "ENOENT" ) {
128- const baseDir = path . dirname ( resultFilePath ) ;
129- await fs . mkdir ( baseDir , { recursive : true } ) ;
130- }
131-
132- if ( attempt < 10 ) {
133- await this . waitFor ( 100 + Math . random ( ) * 200 ) ;
134- } else {
135- throw error ;
136- }
155+ private async ensureAccuracyResultFile (
156+ filePath : string ,
157+ initialData : string
158+ ) : Promise < {
159+ fileCreatedWithInitialData : boolean ;
160+ } > {
161+ try {
162+ await fs . mkdir ( path . dirname ( filePath ) , { recursive : true } ) ;
163+ await fs . writeFile ( filePath , initialData , { flag : "wx" } ) ;
164+ return {
165+ fileCreatedWithInitialData : true ,
166+ } ;
167+ } catch ( error ) {
168+ if ( ( error as NodeJS . ErrnoException ) . code === "EEXIST" ) {
169+ return {
170+ fileCreatedWithInitialData : false ,
171+ } ;
137172 }
173+ throw error ;
138174 }
139175 }
140176
0 commit comments