1
1
import path from "path" ;
2
2
import fs from "fs/promises" ;
3
+ import { lock } from "proper-lockfile" ;
3
4
import { ACCURACY_RESULTS_DIR , LATEST_ACCURACY_RUN_NAME } from "../constants.js" ;
4
5
import {
5
6
AccuracyResult ,
@@ -25,27 +26,43 @@ export class DiskBasedResultStorage implements AccuracyResultStorage {
25
26
const raw = await fs . readFile ( filePath , "utf8" ) ;
26
27
return JSON . parse ( raw ) as AccuracyResult ;
27
28
} catch ( error ) {
28
- if ( ( error as { code : string } ) . code === "ENOENT" ) {
29
+ if ( ( error as NodeJS . ErrnoException ) . code === "ENOENT" ) {
29
30
return null ;
30
31
}
31
32
throw error ;
32
33
}
33
34
}
34
35
35
36
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 {
37
40
const accuracyResult = await this . getAccuracyResult ( commitSHA , runId ) ;
38
41
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!" ) ;
42
43
}
43
44
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
+ }
49
66
50
67
// This bit is important to mark the current run as the latest run for a
51
68
// commit so that we can use that during baseline comparison.
@@ -63,10 +80,11 @@ export class DiskBasedResultStorage implements AccuracyResultStorage {
63
80
prompt : string ,
64
81
modelResponse : ModelResponse
65
82
) : 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
+ {
70
88
runId,
71
89
runStatus : AccuracyRunStatus . InProgress ,
72
90
createdOn : Date . now ( ) ,
@@ -77,64 +95,82 @@ export class DiskBasedResultStorage implements AccuracyResultStorage {
77
95
modelResponses : [ modelResponse ] ,
78
96
} ,
79
97
] ,
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" ) ;
81
113
}
82
114
83
115
const existingPromptIdx = accuracyResult . promptResults . findIndex ( ( result ) => result . prompt === prompt ) ;
84
116
const promptResult = accuracyResult . promptResults [ existingPromptIdx ] ;
85
117
if ( ! promptResult ) {
86
- return {
87
- ...accuracyResult ,
88
- promptResults : [
89
- ...accuracyResult . promptResults ,
118
+ return await fs . writeFile (
119
+ resultFilePath ,
120
+ JSON . stringify (
90
121
{
91
- prompt,
92
- modelResponses : [ modelResponse ] ,
122
+ ...accuracyResult ,
123
+ promptResults : [
124
+ ...accuracyResult . promptResults ,
125
+ {
126
+ prompt,
127
+ modelResponses : [ modelResponse ] ,
128
+ } ,
129
+ ] ,
93
130
} ,
94
- ] ,
95
- } ;
131
+ null ,
132
+ 2
133
+ )
134
+ ) ;
96
135
}
97
136
98
137
accuracyResult . promptResults . splice ( existingPromptIdx , 1 , {
99
138
prompt : promptResult . prompt ,
100
139
modelResponses : [ ...promptResult . modelResponses , modelResponse ] ,
101
140
} ) ;
102
141
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
+ }
105
149
}
106
150
107
151
close ( ) : Promise < void > {
108
152
return Promise . resolve ( ) ;
109
153
}
110
154
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
+ } ;
137
172
}
173
+ throw error ;
138
174
}
139
175
}
140
176
0 commit comments