Skip to content

Commit f98f2df

Browse files
committed
sync data-level of subscription
1 parent dfd80ca commit f98f2df

File tree

1 file changed

+109
-24
lines changed

1 file changed

+109
-24
lines changed

cylc/uiserver/data_store_mgr.py

Lines changed: 109 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)