1
+ using System . Buffers ;
1
2
using System . Collections . Immutable ;
2
3
using System . IO . Hashing ;
3
4
using System . IO . Pipelines ;
5
+ using System . Runtime . InteropServices ;
6
+ using System . Text . Json ;
7
+ using System . Threading . Channels ;
4
8
using HotChocolate . Buffers ;
9
+ using HotChocolate . Fusion . Packaging ;
5
10
using HotChocolate . Language ;
6
11
using HotChocolate . Utilities ;
7
12
using IOPath = System . IO . Path ;
8
13
9
14
namespace HotChocolate . Fusion . Configuration ;
10
15
11
- public class FileSystemFusionConfigurationProvider : IFusionSchemaDocumentProvider
16
+ public class FileSystemFusionConfigurationProvider : IFusionConfigurationProvider
12
17
{
13
18
#if NET9_0_OR_GREATER
14
19
private readonly Lock _syncRoot = new ( ) ;
@@ -17,11 +22,20 @@ public class FileSystemFusionConfigurationProvider : IFusionSchemaDocumentProvid
17
22
#endif
18
23
private readonly string _fileName ;
19
24
private readonly FileSystemWatcher _watcher ;
20
- private readonly SemaphoreSlim _semaphore ;
25
+
26
+ private readonly Channel < bool > _schemaUpdateEvents =
27
+ Channel . CreateBounded < bool > (
28
+ new BoundedChannelOptions ( 1 )
29
+ {
30
+ FullMode = BoundedChannelFullMode . DropNewest , SingleReader = true , SingleWriter = false
31
+ } ) ;
32
+
21
33
private readonly CancellationTokenSource _cts ;
22
- private readonly CancellationToken _ct ;
23
34
private ImmutableArray < ObserverSession > _sessions = [ ] ;
35
+ private readonly bool _isPackage ;
24
36
private ulong _schemaDocumentHash ;
37
+ private ulong _settingsHash ;
38
+ private ulong _packageHash ;
25
39
private bool _disposed ;
26
40
27
41
public FileSystemFusionConfigurationProvider ( string fileName )
@@ -38,15 +52,13 @@ public FileSystemFusionConfigurationProvider(string fileName)
38
52
throw new FileNotFoundException ( "The file must contain a path." , fileName ) ;
39
53
}
40
54
41
- _semaphore = new SemaphoreSlim ( 1 , 1 ) ;
55
+ _isPackage = IOPath . GetExtension ( fileName ) ? . ToLowerInvariant ( ) is ".far" ;
42
56
_cts = new CancellationTokenSource ( ) ;
43
- _ct = _cts . Token ;
44
57
45
58
_watcher = new FileSystemWatcher
46
59
{
47
60
Path = directory ,
48
61
Filter = "*.*" ,
49
-
50
62
NotifyFilter =
51
63
NotifyFilters . FileName
52
64
| NotifyFilters . DirectoryName
@@ -60,27 +72,30 @@ public FileSystemFusionConfigurationProvider(string fileName)
60
72
{
61
73
if ( fullPath . Equals ( e . FullPath , StringComparison . Ordinal ) )
62
74
{
63
- BeginLoadSchemaDocument ( ) ;
75
+ _schemaUpdateEvents . Writer . TryWrite ( true ) ;
64
76
}
65
77
} ;
66
78
67
79
_watcher . Changed += ( _ , e ) =>
68
80
{
69
81
if ( fullPath . Equals ( e . FullPath , StringComparison . Ordinal ) )
70
82
{
71
- BeginLoadSchemaDocument ( ) ;
83
+ _schemaUpdateEvents . Writer . TryWrite ( true ) ;
72
84
}
73
85
} ;
74
86
75
87
_watcher . EnableRaisingEvents = true ;
76
- BeginLoadSchemaDocument ( ) ;
88
+ _schemaUpdateEvents . Writer . TryWrite ( true ) ;
89
+
90
+ SchemaUpdateProcessorAsync ( _cts . Token ) . FireAndForget ( ) ;
77
91
}
78
92
79
- public DocumentNode ? SchemaDocument { get ; private set ; }
93
+ public FusionConfiguration ? Configuration { get ; private set ; }
80
94
81
- public IDisposable Subscribe ( IObserver < DocumentNode > observer )
95
+ public IDisposable Subscribe ( IObserver < FusionConfiguration > observer )
82
96
{
83
97
ArgumentNullException . ThrowIfNull ( observer ) ;
98
+ ObjectDisposedException . ThrowIf ( _disposed , this ) ;
84
99
85
100
var session = new ObserverSession ( this , observer ) ;
86
101
@@ -89,11 +104,11 @@ public IDisposable Subscribe(IObserver<DocumentNode> observer)
89
104
_sessions = _sessions . Add ( session ) ;
90
105
}
91
106
92
- var schemaDocument = SchemaDocument ;
107
+ var configuration = Configuration ;
93
108
94
- if ( schemaDocument is not null )
109
+ if ( configuration is not null )
95
110
{
96
- observer . OnNext ( SchemaDocument ! ) ;
111
+ observer . OnNext ( configuration ) ;
97
112
}
98
113
99
114
return session ;
@@ -107,61 +122,118 @@ private void Unsubscribe(ObserverSession session)
107
122
}
108
123
}
109
124
110
- private void BeginLoadSchemaDocument ( )
111
- => LoadSchemaDocumentAsync ( _ct ) . FireAndForget ( ) ;
112
-
113
- private async Task LoadSchemaDocumentAsync ( CancellationToken cancellationToken )
125
+ private async Task SchemaUpdateProcessorAsync ( CancellationToken ct )
114
126
{
115
- await _semaphore . WaitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
127
+ var defaultSettings = JsonDocument . Parse ( "{ }" ) ;
128
+ var defaultSettingsHash = XxHash64 . HashToUInt64 ( JsonMarshal . GetRawUtf8Value ( defaultSettings . RootElement ) ) ;
116
129
117
- try
130
+ await foreach ( var _ in _schemaUpdateEvents . Reader . ReadAllAsync ( ct ) )
118
131
{
119
- using var buffer = new PooledArrayWriter ( ) ;
120
- await using var fileStream = File . OpenRead ( _fileName ) ;
121
- var pipeReader = PipeReader . Create ( fileStream ) ;
122
-
123
- while ( true )
132
+ try
124
133
{
125
- var result = await pipeReader . ReadAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
126
- var readBuffer = result . Buffer ;
134
+ var settings = new JsonDocumentOwner ( defaultSettings , EmptyMemoryOwner . Instance ) ;
135
+ DocumentNode schema ;
136
+ ulong settingsHash ;
137
+ ulong schemaHash ;
127
138
128
- foreach ( var segment in readBuffer )
139
+ if ( _isPackage )
129
140
{
130
- var span = segment . Span ;
131
- span . CopyTo ( buffer . GetSpan ( span . Length ) ) ;
132
- buffer . Advance ( span . Length ) ;
141
+ await using ( var fileStream = File . OpenRead ( _fileName ) )
142
+ {
143
+ var hash = new XxHash64 ( ) ;
144
+ await hash . AppendAsync ( fileStream , ct ) ;
145
+ var packageHash = hash . GetCurrentHashAsUInt64 ( ) ;
146
+
147
+ if ( packageHash == _packageHash )
148
+ {
149
+ continue ;
150
+ }
151
+
152
+ _packageHash = packageHash ;
153
+ }
154
+
155
+ using var archive = FusionArchive . Open ( _fileName ) ;
156
+ using var config = await archive . TryGetGatewayConfigurationAsync ( new Version ( 2 , 0 , 0 ) , ct ) ;
157
+
158
+ if ( config is null )
159
+ {
160
+ // ignore and wait for next update
161
+ continue ;
162
+ }
163
+
164
+ await using var stream = await config . OpenReadSchemaAsync ( ct ) ;
165
+ ( schema , schemaHash ) = await ReadSchemaDocumentAsync ( stream , ct ) ;
166
+ var settingsSpan = JsonMarshal . GetRawUtf8Value ( config . Settings . RootElement ) ;
167
+ var buffer = new PooledArrayWriter ( settingsSpan . Length ) ;
168
+ buffer . Write ( settingsSpan ) ;
169
+ settingsHash = XxHash64 . HashToUInt64 ( settingsSpan ) ;
170
+ settings = new JsonDocumentOwner ( JsonDocument . Parse ( buffer . WrittenMemory ) , buffer ) ;
171
+ }
172
+ else
173
+ {
174
+ await using var stream = File . OpenRead ( _fileName ) ;
175
+ ( schema , schemaHash ) = await ReadSchemaDocumentAsync ( stream , ct ) ;
176
+ settingsHash = defaultSettingsHash ;
133
177
}
134
178
135
- pipeReader . AdvanceTo ( readBuffer . End ) ;
136
-
137
- if ( result . IsCompleted )
179
+ if ( _schemaDocumentHash == schemaHash && _settingsHash == settingsHash )
138
180
{
139
- break ;
181
+ settings . Dispose ( ) ;
182
+ continue ;
140
183
}
184
+
185
+ _settingsHash = settingsHash ;
186
+ _schemaDocumentHash = schemaHash ;
187
+ NotifyObservers ( new FusionConfiguration ( schema , settings ) ) ;
141
188
}
189
+ catch
190
+ {
191
+ // ignore and wait for next update
192
+ }
193
+ }
194
+ }
142
195
143
- await pipeReader . CompleteAsync ( ) . ConfigureAwait ( false ) ;
196
+ private async ValueTask < ( DocumentNode , ulong ) > ReadSchemaDocumentAsync ( Stream stream , CancellationToken ct )
197
+ {
198
+ using var buffer = new PooledArrayWriter ( ) ;
199
+ var pipeReader = PipeReader . Create ( stream ) ;
144
200
145
- var hash = XxHash64 . HashToUInt64 ( buffer . WrittenSpan ) ;
201
+ while ( true )
202
+ {
203
+ var result = await pipeReader . ReadAsync ( ct ) . ConfigureAwait ( false ) ;
204
+ var readBuffer = result . Buffer ;
146
205
147
- if ( _schemaDocumentHash != hash )
206
+ foreach ( var segment in readBuffer )
148
207
{
149
- _schemaDocumentHash = hash ;
208
+ var span = segment . Span ;
209
+ span . CopyTo ( buffer . GetSpan ( span . Length ) ) ;
210
+ buffer . Advance ( span . Length ) ;
211
+ }
212
+
213
+ pipeReader . AdvanceTo ( readBuffer . End ) ;
150
214
151
- var schemaDocument = Utf8GraphQLParser . Parse ( buffer . WrittenSpan ) ;
152
- SchemaDocument = schemaDocument ;
153
- NotifyObservers ( schemaDocument ) ;
215
+ if ( result . IsCompleted )
216
+ {
217
+ break ;
154
218
}
155
219
}
156
- finally
157
- {
158
- _semaphore . Release ( ) ;
159
- }
220
+
221
+ await pipeReader . CompleteAsync ( ) . ConfigureAwait ( false ) ;
222
+
223
+ var hash = XxHash64 . HashToUInt64 ( buffer . WrittenSpan ) ;
224
+ var document = Utf8GraphQLParser . Parse ( buffer . WrittenSpan ) ;
225
+ return ( document , hash ) ;
160
226
}
161
227
162
- private void NotifyObservers ( DocumentNode schemaDocument )
228
+ private void NotifyObservers ( FusionConfiguration configuration )
163
229
{
164
- var sessions = _sessions ;
230
+ ImmutableArray < ObserverSession > sessions ;
231
+
232
+ lock ( _syncRoot )
233
+ {
234
+ sessions = _sessions ;
235
+ Configuration = configuration ;
236
+ }
165
237
166
238
if ( sessions . IsEmpty )
167
239
{
@@ -170,7 +242,7 @@ private void NotifyObservers(DocumentNode schemaDocument)
170
242
171
243
foreach ( var session in sessions )
172
244
{
173
- session . Notify ( schemaDocument ) ;
245
+ session . Notify ( configuration ) ;
174
246
}
175
247
}
176
248
@@ -187,48 +259,43 @@ public ValueTask DisposeAsync()
187
259
_watcher . Dispose ( ) ;
188
260
189
261
_cts . Cancel ( ) ;
190
-
191
- _semaphore . Dispose ( ) ;
192
262
_cts . Dispose ( ) ;
193
263
194
264
foreach ( var session in _sessions )
195
265
{
196
266
session . Complete ( ) ;
197
267
}
198
268
269
+ // drain events
270
+ while ( _schemaUpdateEvents . Reader . TryRead ( out _ ) )
271
+ {
272
+ }
273
+
199
274
return ValueTask . CompletedTask ;
200
275
}
201
276
202
- private sealed class ObserverSession : IDisposable
277
+ private sealed class ObserverSession (
278
+ FileSystemFusionConfigurationProvider provider ,
279
+ IObserver < FusionConfiguration > observer )
280
+ : IDisposable
203
281
{
204
- private readonly FileSystemFusionConfigurationProvider _provider ;
205
- private readonly IObserver < DocumentNode > _observer ;
206
-
207
- public ObserverSession (
208
- FileSystemFusionConfigurationProvider provider ,
209
- IObserver < DocumentNode > observer )
210
- {
211
- _observer = observer ;
212
- _provider = provider ;
213
- }
214
-
215
- public void Notify ( DocumentNode schemaDocument )
282
+ public void Notify ( FusionConfiguration schemaDocument )
216
283
{
217
284
try
218
285
{
219
- _observer . OnNext ( schemaDocument ) ;
286
+ observer . OnNext ( schemaDocument ) ;
220
287
}
221
288
catch ( Exception ex )
222
289
{
223
- _observer . OnError ( ex ) ;
290
+ observer . OnError ( ex ) ;
224
291
}
225
292
}
226
293
227
294
public void Complete ( )
228
295
{
229
296
try
230
297
{
231
- _observer . OnCompleted ( ) ;
298
+ observer . OnCompleted ( ) ;
232
299
}
233
300
catch
234
301
{
@@ -238,6 +305,17 @@ public void Complete()
238
305
}
239
306
240
307
public void Dispose ( )
241
- => _provider . Unsubscribe ( this ) ;
308
+ => provider . Unsubscribe ( this ) ;
309
+ }
310
+
311
+ private sealed class EmptyMemoryOwner : IMemoryOwner < byte >
312
+ {
313
+ public static readonly EmptyMemoryOwner Instance = new ( ) ;
314
+
315
+ public Memory < byte > Memory => default ;
316
+
317
+ public void Dispose ( )
318
+ {
319
+ }
242
320
}
243
321
}
0 commit comments