tracdap-runtime 0.8.0rc2__py3-none-any.whl → 0.9.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. tracdap/rt/_impl/core/data.py +578 -33
  2. tracdap/rt/_impl/core/repos.py +7 -0
  3. tracdap/rt/_impl/core/storage.py +10 -3
  4. tracdap/rt/_impl/core/util.py +54 -11
  5. tracdap/rt/_impl/exec/dev_mode.py +122 -100
  6. tracdap/rt/_impl/exec/engine.py +178 -109
  7. tracdap/rt/_impl/exec/functions.py +218 -257
  8. tracdap/rt/_impl/exec/graph.py +140 -125
  9. tracdap/rt/_impl/exec/graph_builder.py +411 -449
  10. tracdap/rt/_impl/grpc/codec.py +4 -2
  11. tracdap/rt/_impl/grpc/server.py +7 -7
  12. tracdap/rt/_impl/grpc/tracdap/api/internal/runtime_pb2.py +25 -18
  13. tracdap/rt/_impl/grpc/tracdap/api/internal/runtime_pb2.pyi +27 -9
  14. tracdap/rt/_impl/grpc/tracdap/metadata/common_pb2.py +1 -1
  15. tracdap/rt/_impl/grpc/tracdap/metadata/config_pb2.py +1 -1
  16. tracdap/rt/_impl/grpc/tracdap/metadata/custom_pb2.py +1 -1
  17. tracdap/rt/_impl/grpc/tracdap/metadata/data_pb2.py +1 -1
  18. tracdap/rt/_impl/grpc/tracdap/metadata/file_pb2.py +1 -1
  19. tracdap/rt/_impl/grpc/tracdap/metadata/flow_pb2.py +1 -1
  20. tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.py +67 -63
  21. tracdap/rt/_impl/grpc/tracdap/metadata/job_pb2.pyi +11 -2
  22. tracdap/rt/_impl/grpc/tracdap/metadata/model_pb2.py +1 -1
  23. tracdap/rt/_impl/grpc/tracdap/metadata/object_id_pb2.py +1 -1
  24. tracdap/rt/_impl/grpc/tracdap/metadata/object_pb2.py +1 -1
  25. tracdap/rt/_impl/grpc/tracdap/metadata/resource_pb2.py +1 -1
  26. tracdap/rt/_impl/grpc/tracdap/metadata/search_pb2.py +1 -1
  27. tracdap/rt/_impl/grpc/tracdap/metadata/storage_pb2.py +11 -9
  28. tracdap/rt/_impl/grpc/tracdap/metadata/storage_pb2.pyi +11 -2
  29. tracdap/rt/_impl/grpc/tracdap/metadata/tag_pb2.py +1 -1
  30. tracdap/rt/_impl/grpc/tracdap/metadata/tag_update_pb2.py +1 -1
  31. tracdap/rt/_impl/grpc/tracdap/metadata/type_pb2.py +1 -1
  32. tracdap/rt/_impl/runtime.py +8 -0
  33. tracdap/rt/_plugins/repo_git.py +56 -11
  34. tracdap/rt/_version.py +1 -1
  35. tracdap/rt/config/__init__.py +6 -6
  36. tracdap/rt/config/common.py +5 -0
  37. tracdap/rt/config/job.py +13 -3
  38. tracdap/rt/config/result.py +8 -4
  39. tracdap/rt/config/runtime.py +2 -0
  40. tracdap/rt/metadata/__init__.py +37 -36
  41. tracdap/rt/metadata/job.py +2 -0
  42. tracdap/rt/metadata/storage.py +9 -0
  43. {tracdap_runtime-0.8.0rc2.dist-info → tracdap_runtime-0.9.0b1.dist-info}/METADATA +3 -1
  44. {tracdap_runtime-0.8.0rc2.dist-info → tracdap_runtime-0.9.0b1.dist-info}/RECORD +47 -47
  45. {tracdap_runtime-0.8.0rc2.dist-info → tracdap_runtime-0.9.0b1.dist-info}/WHEEL +1 -1
  46. {tracdap_runtime-0.8.0rc2.dist-info → tracdap_runtime-0.9.0b1.dist-info}/licenses/LICENSE +0 -0
  47. {tracdap_runtime-0.8.0rc2.dist-info → tracdap_runtime-0.9.0b1.dist-info}/top_level.txt +0 -0
@@ -91,11 +91,32 @@ class _JobResultSpec:
91
91
  result_format: str = None
92
92
 
93
93
 
94
+ @dc.dataclass
95
+ class _JobLog:
96
+
97
+ log_init: "dc.InitVar[tp.Optional[_JobLog]]" = None
98
+
99
+ log_file_needed: bool = True
100
+
101
+ log_buffer: io.BytesIO = None
102
+ log_provider: _logging.LogProvider = None
103
+
104
+ def __post_init__(self, log_init):
105
+
106
+ if log_init is not None:
107
+ self.log_provider = log_init.log_provider
108
+ self.log_file_needed = False
109
+ elif self.log_file_needed:
110
+ self.log_buffer = io.BytesIO()
111
+ self.log_provider = _logging.job_log_provider(self.log_buffer)
112
+ else:
113
+ self.log_provider = _logging.LogProvider()
114
+
115
+
94
116
  @dc.dataclass
95
117
  class _JobState:
96
118
 
97
119
  job_id: _meta.TagHeader
98
- log_init: dc.InitVar[tp.Optional[_logging.LogProvider]] = None
99
120
 
100
121
  actor_id: _actors.ActorId = None
101
122
  monitors: tp.List[_actors.ActorId] = dc.field(default_factory=list)
@@ -107,19 +128,7 @@ class _JobState:
107
128
  parent_key: str = None
108
129
  result_spec: _JobResultSpec = None
109
130
 
110
- log_buffer: io.BytesIO = None
111
- log_provider: _logging.LogProvider = None
112
- log: _logging.Logger = None
113
-
114
- def __post_init__(self, log_init):
115
-
116
- if isinstance(self.log, _logging.LogProvider):
117
- self.log_provider = log_init
118
- else:
119
- self.log_buffer = io.BytesIO()
120
- self.log_provider = _logging.job_log_provider(self.log_buffer)
121
-
122
- self.log = self.log_provider.logger_for_class(TracEngine)
131
+ job_log: _JobLog = None
123
132
 
124
133
 
125
134
  class TracEngine(_actors.Actor):
@@ -194,16 +203,19 @@ class TracEngine(_actors.Actor):
194
203
  job_result_format: str):
195
204
 
196
205
  job_key = _util.object_key(job_config.jobId)
197
- job_state = _JobState(job_config.jobId)
198
206
 
199
- job_state.log.info(f"Job submitted: [{job_key}]")
207
+ self._log.info(f"Received a new job: [{job_key}]")
200
208
 
201
209
  result_needed = bool(job_result_dir)
202
210
  result_spec = _JobResultSpec(result_needed, job_result_dir, job_result_format)
211
+ job_log = _JobLog(log_file_needed=result_needed)
212
+
213
+ job_state = _JobState(job_config.jobId)
214
+ job_state.job_log = job_log
203
215
 
204
216
  job_processor = JobProcessor(
205
- self._sys_config, self._models, self._storage, job_state.log_provider,
206
- job_key, job_config, graph_spec=None)
217
+ self._sys_config, self._models, self._storage,
218
+ job_key, job_config, graph_spec=None, job_log=job_state.job_log)
207
219
 
208
220
  job_actor_id = self.actors().spawn(job_processor)
209
221
 
@@ -231,13 +243,18 @@ class TracEngine(_actors.Actor):
231
243
 
232
244
  child_key = _util.object_key(child_id)
233
245
 
246
+ self._log.info(f"Received a child job: [{child_key}] for parent [{parent_key}]")
247
+
248
+ child_job_log = _JobLog(parent_state.job_log)
249
+
234
250
  child_processor = JobProcessor(
235
- self._sys_config, self._models, self._storage, parent_state.log_provider,
236
- child_key, None, graph_spec=child_graph)
251
+ self._sys_config, self._models, self._storage,
252
+ child_key, None, graph_spec=child_graph, job_log=child_job_log)
237
253
 
238
254
  child_actor_id = self.actors().spawn(child_processor)
239
255
 
240
- child_state = _JobState(child_id, parent_state.log_provider)
256
+ child_state = _JobState(child_id)
257
+ child_state.job_log = child_job_log
241
258
  child_state.actor_id = child_actor_id
242
259
  child_state.monitors.append(monitor_id)
243
260
  child_state.parent_key = parent_key
@@ -265,9 +282,9 @@ class TracEngine(_actors.Actor):
265
282
  self._log.warning(f"Ignoring [job_succeeded] message, job [{job_key}] has already completed")
266
283
  return
267
284
 
268
- job_state = self._jobs[job_key]
269
- job_state.log.info(f"Recording job as successful: {job_key}")
285
+ self._log.info(f"Marking job as successful: {job_key}")
270
286
 
287
+ job_state = self._jobs[job_key]
271
288
  job_state.job_result = job_result
272
289
 
273
290
  for monitor_id in job_state.monitors:
@@ -276,36 +293,30 @@ class TracEngine(_actors.Actor):
276
293
  self._finalize_job(job_key)
277
294
 
278
295
  @_actors.Message
279
- def job_failed(self, job_key: str, error: Exception):
296
+ def job_failed(self, job_key: str, error: Exception, job_result: tp.Optional[_cfg.JobResult] = None):
280
297
 
281
298
  # Ignore duplicate messages from the job processor (can happen in unusual error cases)
282
299
  if job_key not in self._jobs:
283
300
  self._log.warning(f"Ignoring [job_failed] message, job [{job_key}] has already completed")
284
301
  return
285
302
 
286
- job_state = self._jobs[job_key]
287
- job_state.log.error(f"Recording job as failed: {job_key}")
288
-
289
- job_state.job_error = error
290
-
291
- # Create a failed result so there is something to report
292
- result_id = job_state.job_config.resultMapping.get("trac_job_result")
303
+ self._log.error(f"Marking job as failed: {job_key}")
293
304
 
294
- if result_id is not None:
295
-
296
- job_state.job_result = _cfg.JobResult(
297
- jobId=job_state.job_id,
298
- statusCode=_meta.JobStatusCode.FAILED,
299
- statusMessage=str(error))
305
+ job_state = self._jobs[job_key]
300
306
 
307
+ # Build a failed result if none is supplied by the job processor (should not normally happen)
308
+ # In this case, no job log will be included in the output
309
+ if job_result is None and job_state.job_config is not None:
310
+ job_id = job_state.job_id
311
+ result_id = job_state.job_config.resultId
301
312
  result_def = _meta.ResultDefinition()
302
313
  result_def.jobId = _util.selector_for(job_state.job_id)
303
314
  result_def.statusCode = _meta.JobStatusCode.FAILED
315
+ result_def.statusMessage = str(error)
316
+ job_result = _cfg.JobResult(job_id, result_id, result_def)
304
317
 
305
- result_key = _util.object_key(result_id)
306
- result_obj = _meta.ObjectDefinition(objectType=_meta.ObjectType.RESULT, result=result_def)
307
-
308
- job_state.job_result.results[result_key] = result_obj
318
+ job_state.job_result = job_result
319
+ job_state.job_error = error
309
320
 
310
321
  for monitor_id in job_state.monitors:
311
322
  self.actors().send(monitor_id, "job_failed", error)
@@ -323,10 +334,6 @@ class TracEngine(_actors.Actor):
323
334
 
324
335
  # Record output metadata if required (not needed for local runs or when using API server)
325
336
  if job_state.parent_key is None and job_state.result_spec.save_result:
326
-
327
- if "trac_job_log_file" in job_state.job_config.resultMapping:
328
- self._save_job_log_file(job_key, job_state)
329
-
330
337
  self._save_job_result(job_key, job_state)
331
338
 
332
339
  # Stop any monitors that were created directly by the engine
@@ -341,37 +348,6 @@ class TracEngine(_actors.Actor):
341
348
  self.actors().stop(job_state.actor_id)
342
349
  job_state.actor_id = None
343
350
 
344
- def _save_job_log_file(self, job_key: str, job_state: _JobState):
345
-
346
- self._log.info(f"Saving job log file for [{job_key}]")
347
-
348
- # Saving log files could go into a separate actor, perhaps a job monitor along with _save_job_result()
349
-
350
- file_id = job_state.job_config.resultMapping["trac_job_log_file"]
351
- storage_id = job_state.job_config.resultMapping["trac_job_log_file:STORAGE"]
352
-
353
- file_type = _meta.FileType("TXT", "text/plain")
354
- file_def, storage_def = _graph.GraphBuilder.build_output_file_and_storage(
355
- "trac_job_log_file", file_type,
356
- self._sys_config, job_state.job_config)
357
-
358
- storage_item = storage_def.dataItems[file_def.dataItem].incarnations[0].copies[0]
359
- storage = self._storage.get_file_storage(storage_item.storageKey)
360
-
361
- with storage.write_byte_stream(storage_item.storagePath) as stream:
362
- stream.write(job_state.log_buffer.getbuffer())
363
- file_def.size = stream.tell()
364
-
365
- result_id = job_state.job_config.resultMapping["trac_job_result"]
366
- result_def = job_state.job_result.results[_util.object_key(result_id)].result
367
- result_def.logFileId = _util.selector_for(file_id)
368
-
369
- file_obj = _meta.ObjectDefinition(objectType=_meta.ObjectType.FILE, file=file_def)
370
- storage_obj = _meta.ObjectDefinition(objectType=_meta.ObjectType.STORAGE, storage=storage_def)
371
-
372
- job_state.job_result.results[_util.object_key(file_id)] = file_obj
373
- job_state.job_result.results[_util.object_key(storage_id)] = storage_obj
374
-
375
351
  def _save_job_result(self, job_key: str, job_state: _JobState):
376
352
 
377
353
  self._log.info(f"Saving job result for [{job_key}]")
@@ -398,24 +374,28 @@ class TracEngine(_actors.Actor):
398
374
 
399
375
  job_result = _cfg.JobResult()
400
376
  job_result.jobId = job_state.job_id
377
+ job_result.resultId = job_state.job_config.resultId
378
+ job_result.result = _meta.ResultDefinition()
379
+ job_result.result.jobId = _util.selector_for(job_state.job_id)
401
380
 
402
381
  if job_state.actor_id is not None:
403
- job_result.statusCode = _meta.JobStatusCode.RUNNING
382
+ job_result.result.statusCode = _meta.JobStatusCode.RUNNING
404
383
 
405
384
  elif job_state.job_result is not None:
406
- job_result.statusCode = job_state.job_result.statusCode
407
- job_result.statusMessage = job_state.job_result.statusMessage
385
+ job_result.result.statusCode = job_state.job_result.result.statusCode
386
+ job_result.result.statusMessage = job_state.job_result.result.statusMessage
408
387
  if details:
409
- job_result.results = job_state.job_result.results or dict()
388
+ job_result.objectIds = job_state.job_result.objectIds or list()
389
+ job_result.objects = job_state.job_result.objects or dict()
410
390
 
411
391
  elif job_state.job_error is not None:
412
- job_result.statusCode = _meta.JobStatusCode.FAILED
413
- job_result.statusMessage = str(job_state.job_error.args[0])
392
+ job_result.result.statusCode = _meta.JobStatusCode.FAILED
393
+ job_result.result.statusMessage = str(job_state.job_error.args[0])
414
394
 
415
395
  else:
416
396
  # Alternatively return UNKNOWN status or throw an error here
417
- job_result.statusCode = _meta.JobStatusCode.FAILED
418
- job_result.statusMessage = "No details available"
397
+ job_result.result.statusCode = _meta.JobStatusCode.FAILED
398
+ job_result.result.statusMessage = "No details available"
419
399
 
420
400
  return job_result
421
401
 
@@ -458,8 +438,9 @@ class JobProcessor(_actors.Actor):
458
438
 
459
439
  def __init__(
460
440
  self, sys_config: _cfg.RuntimeConfig,
461
- models: _models.ModelLoader, storage: _storage.StorageManager, log_provider: _logging.LogProvider,
462
- job_key: str, job_config: tp.Optional[_cfg.JobConfig], graph_spec: tp.Optional[_graph.Graph]):
441
+ models: _models.ModelLoader, storage: _storage.StorageManager,
442
+ job_key: str, job_config: tp.Optional[_cfg.JobConfig], graph_spec: tp.Optional[_graph.Graph],
443
+ job_log: tp.Optional[_JobLog] = None):
463
444
 
464
445
  super().__init__()
465
446
 
@@ -473,9 +454,15 @@ class JobProcessor(_actors.Actor):
473
454
  self._sys_config = sys_config
474
455
  self._models = models
475
456
  self._storage = storage
476
- self._log_provider = log_provider
477
- self._resolver = _func.FunctionResolver(models, storage, log_provider)
478
- self._log = log_provider.logger_for_object(self)
457
+
458
+ self._job_log = job_log or _JobLog()
459
+ self._log_provider = self._job_log.log_provider
460
+ self._log = self._job_log.log_provider.logger_for_object(self)
461
+
462
+ self._log.info(f"New job created for [{self.job_key}]")
463
+
464
+ self._resolver = _func.FunctionResolver(models, storage, self._log_provider)
465
+ self._preallocated_ids: tp.Dict[_meta.ObjectType, tp.List[_meta.TagHeader]] = dict()
479
466
 
480
467
  def on_start(self):
481
468
 
@@ -513,7 +500,10 @@ class JobProcessor(_actors.Actor):
513
500
  return super().on_signal(signal)
514
501
 
515
502
  @_actors.Message
516
- def build_graph_succeeded(self, graph_spec: _graph.Graph):
503
+ def build_graph_succeeded(self, graph_spec: _graph.Graph, unallocated_ids = None):
504
+
505
+ # Save any unallocated IDs to use later (needed for saving the log file)
506
+ self._preallocated_ids = unallocated_ids or dict()
517
507
 
518
508
  # Build a new engine context graph from the graph spec
519
509
  engine_id = self.actors().parent
@@ -524,6 +514,7 @@ class JobProcessor(_actors.Actor):
524
514
  graph.pending_nodes.update(graph.nodes.keys())
525
515
 
526
516
  self.actors().spawn(FunctionResolver(self._resolver, self._log_provider, graph))
517
+
527
518
  if self.actors().sender != self.actors().id and self.actors().sender != self.actors().parent:
528
519
  self.actors().stop(self.actors().sender)
529
520
 
@@ -531,20 +522,100 @@ class JobProcessor(_actors.Actor):
531
522
  def resolve_functions_succeeded(self, graph: _EngineContext):
532
523
 
533
524
  self.actors().spawn(GraphProcessor(graph, self._resolver, self._log_provider))
525
+
534
526
  if self.actors().sender != self.actors().id and self.actors().sender != self.actors().parent:
535
527
  self.actors().stop(self.actors().sender)
536
528
 
537
529
  @_actors.Message
538
530
  def job_succeeded(self, job_result: _cfg.JobResult):
539
- self._log.info(f"Job succeeded {self.job_key}")
531
+
532
+ # This will be the last message in the job log file
533
+ self._log.info(f"Job succeeded [{self.job_key}]")
534
+
535
+ self._save_job_log_file(job_result)
536
+
540
537
  self.actors().stop(self.actors().sender)
541
538
  self.actors().send_parent("job_succeeded", self.job_key, job_result)
542
539
 
543
540
  @_actors.Message
544
541
  def job_failed(self, error: Exception):
545
- self._log.error(f"Job failed {self.job_key}")
542
+
543
+ # This will be the last message in the job log file
544
+ self._log.error(f"Job failed [{self.job_key}]")
545
+
546
546
  self.actors().stop(self.actors().sender)
547
- self.actors().send_parent("job_failed", self.job_key, error)
547
+
548
+ # For top level jobs, build a failed job result and save the log file
549
+ if self.job_config is not None:
550
+
551
+ job_id = self.job_config.jobId
552
+ result_id = self.job_config.resultId
553
+ result_def = _meta.ResultDefinition()
554
+ result_def.jobId = _util.selector_for(job_id)
555
+ result_def.statusCode = _meta.JobStatusCode.FAILED
556
+ result_def.statusMessage = str(error)
557
+ job_result = _cfg.JobResult(job_id, result_id, result_def)
558
+
559
+ self._save_job_log_file(job_result)
560
+
561
+ self.actors().send_parent("job_failed", self.job_key, error, job_result)
562
+
563
+ # For child jobs, just send the error response
564
+ # Result and log file will be handled in the top level job
565
+ else:
566
+ self.actors().send_parent("job_failed", self.job_key, error)
567
+
568
+ def _save_job_log_file(self, job_result: _cfg.JobResult):
569
+
570
+ # Do not save log files for child jobs, or if a log is not available
571
+ if self._job_log.log_buffer is None:
572
+ if self._job_log.log_file_needed:
573
+ self._log.warning(f"Job log not available for [{self.job_key}]")
574
+ return
575
+
576
+ # Saving log files could go into a separate actor
577
+
578
+ file_id = self._allocate_id(_meta.ObjectType.FILE)
579
+ storage_id = self._allocate_id(_meta.ObjectType.STORAGE)
580
+
581
+ file_name = "trac_job_log_file"
582
+ file_type = _meta.FileType("TXT", "text/plain")
583
+
584
+ file_spec = _data.build_file_spec(
585
+ file_id, storage_id,
586
+ file_name, file_type,
587
+ self._sys_config.storage)
588
+
589
+ file_def = file_spec.definition
590
+ storage_def = file_spec.storage
591
+
592
+ storage_item = storage_def.dataItems[file_def.dataItem].incarnations[0].copies[0]
593
+ storage = self._storage.get_file_storage(storage_item.storageKey)
594
+
595
+ with storage.write_byte_stream(storage_item.storagePath) as stream:
596
+ stream.write(self._job_log.log_buffer.getbuffer())
597
+ file_def.size = stream.tell()
598
+
599
+ result_def = job_result.result
600
+ result_def.logFileId = _util.selector_for(file_id)
601
+
602
+ file_obj = _meta.ObjectDefinition(objectType=_meta.ObjectType.FILE, file=file_def)
603
+ storage_obj = _meta.ObjectDefinition(objectType=_meta.ObjectType.STORAGE, storage=storage_def)
604
+
605
+ job_result.objectIds.append(file_id)
606
+ job_result.objectIds.append(storage_id)
607
+ job_result.objects[_util.object_key(file_id)] = file_obj
608
+ job_result.objects[_util.object_key(storage_id)] = storage_obj
609
+
610
+ def _allocate_id(self, object_type: _meta.ObjectType):
611
+
612
+ preallocated_ids = self._preallocated_ids.get(object_type)
613
+
614
+ if preallocated_ids:
615
+ # Preallocated IDs have objectVersion = 0, use a new version to get objectVersion = 1
616
+ return _util.new_object_version(preallocated_ids.pop())
617
+ else:
618
+ return _util.new_object_id(object_type)
548
619
 
549
620
 
550
621
  class GraphBuilder(_actors.Actor):
@@ -570,8 +641,9 @@ class GraphBuilder(_actors.Actor):
570
641
 
571
642
  graph_builder = _graph.GraphBuilder(self.sys_config, job_config)
572
643
  graph_spec = graph_builder.build_job(job_config.job)
644
+ unallocated_ids = graph_builder.unallocated_ids()
573
645
 
574
- self.actors().reply("build_graph_succeeded", graph_spec)
646
+ self.actors().reply("build_graph_succeeded", graph_spec, unallocated_ids)
575
647
 
576
648
 
577
649
  class FunctionResolver(_actors.Actor):
@@ -704,23 +776,21 @@ class GraphProcessor(_actors.Actor):
704
776
  self.check_job_status(do_submit=False)
705
777
 
706
778
  @_actors.Message
707
- def update_graph(
708
- self, requestor_id: NodeId,
709
- new_nodes: tp.Dict[NodeId, _graph.Node],
710
- new_deps: tp.Dict[NodeId, tp.List[_graph.Dependency]]):
779
+ def update_graph(self, requestor_id: NodeId, update: _graph.GraphUpdate):
711
780
 
712
781
  new_graph = cp.copy(self.graph)
713
782
  new_graph.nodes = cp.copy(new_graph.nodes)
714
783
 
715
784
  # Attempt to insert a duplicate node is always an error
716
- node_collision = list(filter(lambda nid: nid in self.graph.nodes, new_nodes))
785
+ node_collision = list(filter(lambda nid: nid in self.graph.nodes, update.nodes))
717
786
 
718
787
  # Only allow adding deps to pending nodes for now (adding deps to active nodes will require more work)
719
- dep_collision = list(filter(lambda nid: nid not in self.graph.pending_nodes, new_deps))
788
+ dep_collision = list(filter(lambda nid: nid not in self.graph.pending_nodes, update.dependencies))
720
789
 
790
+ # Only allow adding deps to new nodes (deps to existing nodes should not be part of an update)
721
791
  dep_invalid = list(filter(
722
- lambda dds: any(filter(lambda dd: dd.node_id not in new_nodes, dds)),
723
- new_deps.values()))
792
+ lambda ds: any(filter(lambda d: d.node_id not in update.nodes, ds)),
793
+ update.dependencies.values()))
724
794
 
725
795
  if any(node_collision) or any(dep_collision) or any(dep_invalid):
726
796
 
@@ -736,18 +806,20 @@ class GraphProcessor(_actors.Actor):
736
806
  requestor.error = _ex.ETracInternal("Node collision during graph update")
737
807
  new_graph.nodes[requestor_id] = requestor
738
808
 
809
+ self.graph = new_graph
810
+
739
811
  return
740
812
 
741
813
  new_graph.pending_nodes = cp.copy(new_graph.pending_nodes)
742
814
 
743
- for node_id, node in new_nodes.items():
815
+ for node_id, node in update.nodes.items():
744
816
  self._graph_logger.log_node_add(node)
745
817
  node_func = self._resolver.resolve_node(node)
746
818
  new_node = _EngineNode(node, node_func)
747
819
  new_graph.nodes[node_id] = new_node
748
820
  new_graph.pending_nodes.add(node_id)
749
821
 
750
- for node_id, deps in new_deps.items():
822
+ for node_id, deps in update.dependencies.items():
751
823
  engine_node = cp.copy(new_graph.nodes[node_id])
752
824
  engine_node.dependencies = cp.copy(engine_node.dependencies)
753
825
  for dep in deps:
@@ -1302,8 +1374,5 @@ class NodeCallbackImpl(_func.NodeCallback):
1302
1374
  self.__actor_ctx = actor_ctx
1303
1375
  self.__node_id = node_id
1304
1376
 
1305
- def send_graph_updates(
1306
- self, new_nodes: tp.Dict[NodeId, _graph.Node],
1307
- new_deps: tp.Dict[NodeId, tp.List[_graph.Dependency]]):
1308
-
1309
- self.__actor_ctx.send_parent("update_graph", self.__node_id, new_nodes, new_deps)
1377
+ def send_graph_update(self, update: _graph.GraphUpdate):
1378
+ self.__actor_ctx.send_parent("update_graph", self.__node_id, update)