@@ -21,11 +21,95 @@ import { Client } from "@cocalc/sync/editor/generic/types";
21
21
import { once } from "@cocalc/util/async-utils" ;
22
22
import { filename_extension } from "@cocalc/util/misc" ;
23
23
import { jupyter_backend } from "../jupyter/jupyter" ;
24
+ import { EventEmitter } from "events" ;
24
25
25
26
const COCALC_EPHEMERAL_STATE : boolean =
26
27
process . env . COCALC_EPHEMERAL_STATE === "yes" ;
27
28
28
- const syncdocs : { [ path : string ] : SyncDoc } = { } ;
29
+ class SyncDocs extends EventEmitter {
30
+ private syncdocs : { [ path : string ] : SyncDoc } = { } ;
31
+ private closing : Set < string > = new Set ( ) ;
32
+
33
+ async close ( path : string , log ?) : Promise < void > {
34
+ const doc = this . get ( path ) ;
35
+ if ( doc == null ) {
36
+ log ?.( `close ${ path } -- no need, as it is not opened` ) ;
37
+ return ;
38
+ }
39
+ try {
40
+ log ?.( `close ${ path } -- starting close` ) ;
41
+ this . closing . add ( path ) ;
42
+ // As soon as this close starts, doc is in an undefined state.
43
+ // Also, this can take an **unbounded** amount of time to finish,
44
+ // since it tries to save the patches table (among other things)
45
+ // to the database, and if there is no connection from the hub
46
+ // to this project, then it will simply wait however long it takes
47
+ // until we get a connection (and there is no timeout). That is
48
+ // perfectly fine! E.g., a user closes their browser connected
49
+ // to a project, then comes back 8 hours later and tries to open
50
+ // this document when they resume their browser. During those entire
51
+ // 8 hours, the project might have been waiting to reconnect, just
52
+ // so it could send the patches from patches_list to the database.
53
+ // It does that, then finishes this async doc.close(), releases
54
+ // the lock, and finally the user gets to open their file. See
55
+ // https://github.com/sagemathinc/cocalc/issues/5823 for how not being
56
+ // careful with locking like this resulted in a very difficult to
57
+ // track down heisenbug. See also
58
+ // https://github.com/sagemathinc/cocalc/issues/5617
59
+ await doc . close ( ) ;
60
+ log ?.( `close ${ path } -- successfully closed` ) ;
61
+ } finally {
62
+ // No matter what happens above when it finishes, we clear it
63
+ // and consider it closed.
64
+ // There is perhaps a chance closing fails above (no idea how),
65
+ // but we don't want it to be impossible to attempt to open
66
+ // the path again I.e., we don't want to leave around a lock.
67
+ log ?.( `close ${ path } -- recording that close succeeded` ) ;
68
+ delete this . syncdocs [ path ] ;
69
+ this . closing . delete ( path ) ;
70
+ this . emit ( `close-${ path } ` ) ;
71
+ }
72
+ }
73
+
74
+ get ( path : string ) : SyncDoc | undefined {
75
+ return this . syncdocs [ path ] ;
76
+ }
77
+
78
+ async create ( type , opts , log ) : Promise < SyncDoc > {
79
+ const path = opts . path ;
80
+ if ( this . closing . has ( path ) ) {
81
+ log (
82
+ `create ${ path } -- waiting for previous version to completely finish closing...`
83
+ ) ;
84
+ await once ( this , `close-${ path } ` ) ;
85
+ log ( `create ${ path } -- successfully closed.` ) ;
86
+ }
87
+ let doc ;
88
+ switch ( type ) {
89
+ case "string" :
90
+ doc = new SyncString ( opts ) ;
91
+ break ;
92
+ case "db" :
93
+ doc = new SyncDB ( opts ) ;
94
+ break ;
95
+ default :
96
+ throw Error ( `unknown syncdoc type ${ type } ` ) ;
97
+ }
98
+ this . syncdocs [ path ] = doc ;
99
+ log ( `create ${ path } -- successfully created.` ) ;
100
+ return doc ;
101
+ }
102
+
103
+ async closeAll ( filename : string ) : Promise < void > {
104
+ for ( const path in this . syncdocs ) {
105
+ if ( path == filename || path . startsWith ( filename + "/" ) ) {
106
+ await this . close ( path ) ;
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ const syncDocs = new SyncDocs ( ) ;
29
113
30
114
export function init_syncdoc (
31
115
client : Client ,
@@ -47,14 +131,7 @@ export function init_syncdoc(
47
131
// return it; otherwise, return undefined. This is useful
48
132
// for getting a reference to a syncdoc, e.g., for prettier.
49
133
export function get_syncdoc ( path : string ) : SyncDoc | undefined {
50
- return syncdocs [ path ] ;
51
- }
52
-
53
- async function close_syncdoc ( path : string ) : Promise < void > {
54
- const doc = get_syncdoc ( path ) ;
55
- if ( doc == null ) return ;
56
- delete syncdocs [ path ] ;
57
- await doc . close ( ) ;
134
+ return syncDocs . get ( path ) ;
58
135
}
59
136
60
137
async function init_syncdoc_async (
@@ -74,24 +151,23 @@ async function init_syncdoc_async(
74
151
log ( "type = " , type ) ;
75
152
log ( "opts = " , JSON . stringify ( opts ) ) ;
76
153
opts . client = client ;
77
- log ( " now creating syncdoc..." ) ;
154
+ log ( ` now creating syncdoc ${ opts . path } ...` ) ;
78
155
let syncdoc ;
79
156
try {
80
- syncdoc = create_syncdoc ( type , opts ) ;
81
- syncdocs [ opts . path ] = syncdoc ;
157
+ syncdoc = await syncDocs . create ( type , opts , log ) ;
82
158
} catch ( err ) {
83
159
log ( `ERROR creating syncdoc -- ${ err . toString ( ) } ` , err . stack ) ;
84
160
// TODO: how to properly inform clients and deal with this?!
85
161
return ;
86
162
}
87
163
synctable . on ( "closed" , function ( ) {
88
164
log ( "syncstring table closed, so closing syncdoc" , opts . path ) ;
89
- close_syncdoc ( opts . path ) ;
165
+ syncDocs . close ( opts . path , log ) ;
90
166
} ) ;
91
167
92
168
syncdoc . on ( "error" , function ( err ) {
93
169
log ( `syncdoc error -- ${ err } ` ) ;
94
- close_syncdoc ( opts . path ) ;
170
+ syncDocs . close ( opts . path , log ) ;
95
171
} ) ;
96
172
97
173
// Extra backend support in some cases, e.g., Jupyter, Sage, etc.
@@ -168,32 +244,21 @@ function get_type_and_opts(synctable: SyncTable): { type: string; opts: any } {
168
244
return { type, opts } ;
169
245
}
170
246
171
- function create_syncdoc ( type , opts ) : SyncDoc {
172
- switch ( type ) {
173
- case "string" :
174
- return new SyncString ( opts ) ;
175
- case "db" :
176
- return new SyncDB ( opts ) ;
177
- default :
178
- throw Error ( `unknown syncdoc type ${ type } ` ) ;
179
- }
180
- }
181
-
182
247
export async function syncdoc_call (
183
248
path : string ,
184
249
logger : any ,
185
250
mesg : any
186
251
) : Promise < string > {
187
252
logger . debug ( "syncdoc_call" , path , mesg ) ;
188
- const doc = get_syncdoc ( path ) ;
253
+ const doc = syncDocs . get ( path ) ;
189
254
if ( doc == null ) {
190
255
logger . debug ( "syncdoc_call -- not open: " , path ) ;
191
256
return "not open" ;
192
257
}
193
258
switch ( mesg . cmd ) {
194
259
case "close" :
195
260
logger . debug ( "syncdoc_call -- now closing: " , path ) ;
196
- await close_syncdoc ( path ) ;
261
+ await syncDocs . close ( path , logger . debug ) ;
197
262
logger . debug ( "syncdoc_call -- closed: " , path ) ;
198
263
return "successfully closed" ;
199
264
default :
@@ -206,9 +271,5 @@ export async function syncdoc_call(
206
271
export async function close_all_syncdocs_in_tree (
207
272
filename : string
208
273
) : Promise < void > {
209
- for ( const path in syncdocs ) {
210
- if ( path == filename || path . indexOf ( filename + "/" ) != - 1 ) {
211
- await close_syncdoc ( path ) ;
212
- }
213
- }
274
+ return await syncDocs . closeAll ( filename ) ;
214
275
}
0 commit comments