@@ -101,7 +101,22 @@ def __init__(self, workflows_mgr, log, max_threads=10):
101101 self .log = log
102102 self .data = {}
103103 self .w_subs : Dict [str , WorkflowSubscriber ] = {}
104- self .topics = {ALL_DELTAS .encode ('utf-8' ), b'shutdown' }
104+ self .topics = {
105+ ALL_DELTAS .encode ('utf-8' ),
106+ WORKFLOW .encode ('utf-8' ),
107+ b'shutdown'
108+ }
109+ # If fragments in graphql sub for minimal sync
110+ self .min_sync_fragments = {
111+ 'AddedDelta' ,
112+ 'WorkflowData' ,
113+ 'UpdatedDelta'
114+ }
115+ # set of workflows to sync all data
116+ self .full_sync_workflows = set ()
117+ self .full_sync_gql_subs = set ()
118+ # dict of workflow full sync subscriber IDs
119+ self .full_sync_workflow_gql_subs = {}
105120 self .loop = None
106121 self .executor = ThreadPoolExecutor (max_threads )
107122 self .delta_queues = {}
@@ -161,6 +176,9 @@ async def connect_workflow(self, w_id, contact_data):
161176
162177 self .delta_queues [w_id ] = {}
163178
179+ # setup sync subscriber set
180+ self .full_sync_workflow_gql_subs [w_id ] = set ()
181+
164182 # Might be options other than threads to achieve
165183 # non-blocking subscriptions, but this works.
166184 self .executor .submit (
@@ -170,17 +188,33 @@ async def connect_workflow(self, w_id, contact_data):
170188 contact_data [CFF .HOST ],
171189 contact_data [CFF .PUBLISH_PORT ]
172190 )
173- successful_updates = await self ._entire_workflow_update (ids = [w_id ])
191+
192+ result = await self .workflow_data_update (w_id , minimal = True )
193+
194+ if result :
195+ # don't update the contact data until we have successfully updated
196+ self ._update_contact (w_id , contact_data )
197+
198+ @log_call
199+ async def workflow_data_update (
200+ self ,
201+ w_id : str ,
202+ minimal : Optional [bool ] = None
203+ ):
204+ if minimal is None :
205+ minimal = w_id in self .full_sync_workflows
206+ successful_updates = await self ._entire_workflow_update (
207+ ids = [w_id ],
208+ minimal = minimal
209+ )
174210
175211 if w_id not in successful_updates :
176212 # something went wrong, undo any changes to allow for subsequent
177213 # connection attempts
178214 self .log .info (f'failed to connect to { w_id } ' )
179215 self .disconnect_workflow (w_id )
180216 return False
181- else :
182- # don't update the contact data until we have successfully updated
183- self ._update_contact (w_id , contact_data )
217+ return True
184218
185219 @log_call
186220 def disconnect_workflow (self , w_id , update_contact = True ):
@@ -207,6 +241,9 @@ def disconnect_workflow(self, w_id, update_contact=True):
207241 if w_id in self .w_subs :
208242 self .w_subs [w_id ].stop ()
209243 del self .w_subs [w_id ]
244+ if w_id in self .full_sync_workflow_gql_subs :
245+ del self .full_sync_workflow_gql_subs [w_id ]
246+ self .full_sync_workflows .discard (w_id )
210247
211248 def get_workflows (self ):
212249 """Return all workflows the data store is currently tracking.
@@ -283,8 +320,18 @@ def _update_workflow_data(self, topic, delta, w_id):
283320 # close connections
284321 self .disconnect_workflow (w_id )
285322 return
286- self ._apply_all_delta (w_id , delta )
287- self ._delta_store_to_queues (w_id , topic , delta )
323+ elif topic == WORKFLOW :
324+ if w_id in self .full_sync_workflows :
325+ return
326+ self ._apply_delta (w_id , WORKFLOW , delta )
327+ # might seem clunky, but as with contact update, making it look
328+ # like an ALL_DELTA avoids changing the resolver in cylc-flow
329+ all_deltas = DELTAS_MAP [ALL_DELTAS ]()
330+ all_deltas .workflow .CopyFrom (delta )
331+ self ._delta_store_to_queues (w_id , ALL_DELTAS , all_deltas )
332+ elif w_id in self .full_sync_workflows :
333+ self ._apply_all_delta (w_id , delta )
334+ self ._delta_store_to_queues (w_id , topic , delta )
288335
289336 def _clear_data_field (self , w_id , field_name ):
290337 if field_name == WORKFLOW :
@@ -295,22 +342,26 @@ def _clear_data_field(self, w_id, field_name):
295342 def _apply_all_delta (self , w_id , delta ):
296343 """Apply the AllDeltas delta."""
297344 for field , sub_delta in delta .ListFields ():
298- delta_time = getattr (sub_delta , 'time' , 0.0 )
299- # If the workflow has reloaded clear the data before
300- # delta application.
301- if sub_delta .reloaded :
302- self ._clear_data_field (w_id , field .name )
303- self .data [w_id ]['delta_times' ][field .name ] = 0.0
304- # hard to catch errors in a threaded async app, so use try-except.
305- try :
306- # Apply the delta if newer than the previously applied.
307- if delta_time >= self .data [w_id ]['delta_times' ][field .name ]:
308- apply_delta (field .name , sub_delta , self .data [w_id ])
309- self .data [w_id ]['delta_times' ][field .name ] = delta_time
310- if not sub_delta .reloaded :
311- self ._reconcile_update (field .name , sub_delta , w_id )
312- except Exception as exc :
313- self .log .exception (exc )
345+ self ._apply_delta (w_id , field .name , sub_delta )
346+
347+ def _apply_delta (self , w_id , name , delta ):
348+ """Apply delta."""
349+ delta_time = getattr (delta , 'time' , 0.0 )
350+ # If the workflow has reloaded clear the data before
351+ # delta application.
352+ if delta .reloaded :
353+ self ._clear_data_field (w_id , name )
354+ self .data [w_id ]['delta_times' ][name ] = 0.0
355+ # hard to catch errors in a threaded async app, so use try-except.
356+ try :
357+ # Apply the delta if newer than the previously applied.
358+ if delta_time >= self .data [w_id ]['delta_times' ][name ]:
359+ apply_delta (name , delta , self .data [w_id ])
360+ self .data [w_id ]['delta_times' ][name ] = delta_time
361+ if not delta .reloaded :
362+ self ._reconcile_update (name , delta , w_id )
363+ except Exception as exc :
364+ self .log .exception (exc )
314365
315366 def _delta_store_to_queues (self , w_id , topic , delta ):
316367 # Queue delta for graphql subscription resolving
@@ -369,7 +420,9 @@ def _reconcile_update(self, topic, delta, w_id):
369420 self .log .exception (exc )
370421
371422 async def _entire_workflow_update (
372- self , ids : Optional [list ] = None
423+ self ,
424+ ids : Optional [list ] = None ,
425+ minimal : Optional [bool ] = False
373426 ) -> Set [str ]:
374427 """Update entire local data-store of workflow(s).
375428
@@ -414,6 +467,8 @@ async def _entire_workflow_update(
414467 for key in DATA_TEMPLATE
415468 }
416469 continue
470+ elif minimal :
471+ continue
417472 new_data [field .name ] = {n .id : n for n in value }
418473 self .data [w_id ] = new_data
419474 successes .add (w_id )
@@ -502,3 +557,33 @@ def _get_status_msg(self, w_id: str, is_active: bool) -> str:
502557 else :
503558 # the workflow has not yet run
504559 return 'not yet run'
560+
561+ def graphql_sub_interrogate (self , sub_id , info ):
562+ """Scope data requirements."""
563+ fragments = set (info .fragments .keys ())
564+ minimal = (
565+ fragments <= self .min_sync_fragments
566+ and bool (fragments )
567+ )
568+ if not minimal :
569+ self .full_sync_gql_subs .add (sub_id )
570+ return minimal
571+
572+ async def graphql_sub_data_match (self , w_id , sub_id ):
573+ """Match store data level to requested graphql subscription."""
574+ if (
575+ sub_id in self .full_sync_gql_subs
576+ and sub_id not in self .full_sync_workflow_gql_subs [w_id ]
577+ ):
578+ self .full_sync_workflow_gql_subs [w_id ].add (sub_id )
579+ await self .workflow_data_update (w_id , minimal = False )
580+
581+ self .full_sync_workflows .add (w_id )
582+
583+ def graphql_sub_discard (self , sub_id ):
584+ """Discard graphql subscription references."""
585+ self .full_sync_gql_subs .discard (sub_id )
586+ for w_id in self .full_sync_workflow_gql_subs :
587+ self .full_sync_workflow_gql_subs [w_id ].discard (w_id )
588+ if not self .full_sync_workflow_gql_subs [w_id ]:
589+ self .full_sync_workflows .discard (w_id )
0 commit comments