eventdispatch 0.2.29__tar.gz → 0.2.30__tar.gz

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 (23) hide show
  1. {eventdispatch-0.2.29/eventdispatch.egg-info → eventdispatch-0.2.30}/PKG-INFO +1 -1
  2. {eventdispatch-0.2.29 → eventdispatch-0.2.30}/eventdispatch/common1.py +40 -2
  3. {eventdispatch-0.2.29 → eventdispatch-0.2.30}/eventdispatch/composite_semaphore.py +83 -40
  4. {eventdispatch-0.2.29 → eventdispatch-0.2.30}/eventdispatch/core.py +43 -14
  5. eventdispatch-0.2.29/eventdispatch/example1.py → eventdispatch-0.2.30/eventdispatch/example_core.py +11 -2
  6. eventdispatch-0.2.29/eventdispatch/example2.py → eventdispatch-0.2.30/eventdispatch/example_cs.py +13 -3
  7. eventdispatch-0.2.30/eventdispatch/example_sim_mujoco.py +130 -0
  8. eventdispatch-0.2.30/eventdispatch/learning.py +213 -0
  9. eventdispatch-0.2.30/eventdispatch/sim.py +354 -0
  10. {eventdispatch-0.2.29 → eventdispatch-0.2.30/eventdispatch.egg-info}/PKG-INFO +1 -1
  11. {eventdispatch-0.2.29 → eventdispatch-0.2.30}/eventdispatch.egg-info/SOURCES.txt +5 -2
  12. eventdispatch-0.2.30/eventdispatch.egg-info/entry_points.txt +5 -0
  13. {eventdispatch-0.2.29 → eventdispatch-0.2.30}/pyproject.toml +1 -1
  14. {eventdispatch-0.2.29 → eventdispatch-0.2.30}/setup.py +4 -3
  15. eventdispatch-0.2.29/eventdispatch.egg-info/entry_points.txt +0 -4
  16. {eventdispatch-0.2.29 → eventdispatch-0.2.30}/LICENSE +0 -0
  17. {eventdispatch-0.2.29 → eventdispatch-0.2.30}/NOTICE +0 -0
  18. {eventdispatch-0.2.29 → eventdispatch-0.2.30}/README.md +0 -0
  19. {eventdispatch-0.2.29 → eventdispatch-0.2.30}/eventdispatch/__init__.py +0 -0
  20. {eventdispatch-0.2.29 → eventdispatch-0.2.30}/eventdispatch.egg-info/dependency_links.txt +0 -0
  21. {eventdispatch-0.2.29 → eventdispatch-0.2.30}/eventdispatch.egg-info/requires.txt +0 -0
  22. {eventdispatch-0.2.29 → eventdispatch-0.2.30}/eventdispatch.egg-info/top_level.txt +0 -0
  23. {eventdispatch-0.2.29 → eventdispatch-0.2.30}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eventdispatch
3
- Version: 0.2.29
3
+ Version: 0.2.30
4
4
  Summary: Event Dispatch: discrete time synchronization
5
5
  Home-page: http://github.com/cyan-at/python-eventdispatch
6
6
  Author: Charlie Yan
@@ -31,6 +31,7 @@ if sys.version_info.major == 2:
31
31
  makedirs_wrapper = python2_makedirs_wrapper
32
32
 
33
33
  class bcolors:
34
+ '''ANSI color constants, for colored logs'''
34
35
  # https://godoc.org/github.com/whitedevops/colors
35
36
  DEFAULT = "\033[39m"
36
37
  BLACK = "\033[30m"
@@ -80,6 +81,7 @@ def nonempty_queue_exists(
80
81
  blackboard,
81
82
  admissible_nonempty_keys,
82
83
  verbose = False):
84
+ '''True if any *_queue on the blackboard, outside the admissible set, is nonempty'''
83
85
  for k in blackboard.keys():
84
86
  if k[-6:] == "_queue":
85
87
  mutex_k = k[:-6] + "_mutex"
@@ -109,6 +111,7 @@ def wrap_with_prints(pre_msg, post_msg):
109
111
  return decorator
110
112
 
111
113
  def replace_with_func(other_func):
114
+ '''decorator: swap the wrapped function for other_func'''
112
115
  def decorator(func):
113
116
  def wrapper(*args, **kwargs):
114
117
  other_func(*args, **kwargs)
@@ -116,6 +119,12 @@ def replace_with_func(other_func):
116
119
  return decorator
117
120
 
118
121
  class CommonEvent(Event):
122
+ '''
123
+ an Event with the conventions BlackboardQueueCVED expects:
124
+ the constructor's first arg after event_id is the blackboard,
125
+ and deserialize() (below) is how a queue item becomes one.
126
+ log / internal_log print in debug_color.
127
+ '''
119
128
  debug_color = bcolors.LIGHTCYAN
120
129
 
121
130
  def __init__(self, event_id, *args, **kwargs):
@@ -139,6 +148,7 @@ class CommonEvent(Event):
139
148
 
140
149
  @staticmethod
141
150
  def deserialize(ed, blackboard, *args, **kwargs):
151
+ '''[class name, *tokens] -> ((event_id, blackboard), tokens)'''
142
152
  tokens = args[0]
143
153
  # ed.log("TOKENS! {}".format(tokens))
144
154
  if len(tokens) < 1:
@@ -150,6 +160,12 @@ TIMERWAIT_TIMEOUT = -1
150
160
  TIMERWAIT_BAILED = 1
151
161
 
152
162
  class TimerWait(CommonEvent):
163
+ '''
164
+ sleeps args[0] seconds on its own thread; on timeout, finish
165
+ enqueues the pending events (a timed CSRelease). An actor can
166
+ set the blackboard status to TIMERWAIT_BAILED to make the
167
+ finish a noop instead.
168
+ '''
153
169
  debug_color = bcolors.CYAN
154
170
 
155
171
  def get_pending(self, args):
@@ -177,7 +193,7 @@ class TimerWait(CommonEvent):
177
193
  self.log("TimerWait sleeping for {} to CSRelease {}".format(
178
194
  args[0], args[1]))
179
195
 
180
- self.blackboard[self.k]['timer'] .wait(args[0])
196
+ self.blackboard[self.k]['timer'].wait(args[0])
181
197
 
182
198
  def finish(self, event_dispatch, *args, **kwargs):
183
199
  if self.blackboard[self.k]['status'] == TIMERWAIT_TIMEOUT:
@@ -193,11 +209,23 @@ class TimerWait(CommonEvent):
193
209
  self.blackboard[event_dispatch.cv_name].notify(1)
194
210
  self.blackboard[event_dispatch.cv_name].release()
195
211
 
196
- self.blackboard.pop(self.k)
197
212
  elif self.blackboard[self.k]['status'] == TIMERWAIT_BAILED:
198
213
  self.log("TimerWait bailed! noop")
199
214
 
215
+ self.blackboard.pop(self.k)
216
+
200
217
  class BlackboardQueueCVED(EventDispatch):
218
+ '''
219
+ a buffer-and-drain dispatch: producers append serialized
220
+ [class name, *args] items to <name>_queue on the blackboard
221
+ and notify <name>_cv; run() (a thread target) drains the
222
+ queue completely each wake, deserializing and dispatching
223
+ every item.
224
+
225
+ subclass hooks: prior_cb / post_cb around each drain.
226
+ do_dispatch is the funnel from the queue to dispatch().
227
+ event ids cycle % 30000; release_event_id is a no-op here.
228
+ '''
201
229
  def __init__(self, blackboard, name):
202
230
  super(BlackboardQueueCVED, self).__init__(
203
231
  blackboard, name + "_dispatch")
@@ -221,6 +249,7 @@ class BlackboardQueueCVED(EventDispatch):
221
249
  len(blackboard[self.queue_name])))
222
250
 
223
251
  def register_blackboard_assets(self, blackboard, name):
252
+ '''create (if absent) <name>_hb / _mutex / _cv / _queue on the blackboard'''
224
253
  self.name = name
225
254
 
226
255
  self.hb_key = name + "_hb"
@@ -269,6 +298,9 @@ class BlackboardQueueCVED(EventDispatch):
269
298
  stubbed out to support CSBQCVED
270
299
 
271
300
  avoid overriding this
301
+
302
+ deserialize one queue item and dispatch it;
303
+ True on success, False (logged) on any failure
272
304
  '''
273
305
 
274
306
  # s (serialized_event) expected to be
@@ -300,6 +332,12 @@ class BlackboardQueueCVED(EventDispatch):
300
332
  prefix, # expected, str
301
333
  empty_cv_name = None, # expected, str
302
334
  debug_color = None):
335
+ '''
336
+ thread target: wait on the cv, drain the queue
337
+ (prior_cb, do_dispatch each item, post_cb), notify
338
+ empty_cv_name when drained. Exits when <name>_hb goes
339
+ False and the cv is notified.
340
+ '''
303
341
  assert(blackboard is not None)
304
342
 
305
343
  if (debug_color is not None):
@@ -9,26 +9,19 @@ from .common1 import *
9
9
  # from core import *
10
10
  # from common1 import *
11
11
 
12
- '''
13
- a thread-safe data structure
14
-
15
- on the left ('releasing/left')
16
- a set that you 'tick' down
17
- but resetting it takes O(1) complexity
18
-
19
- the set is also dynamic:
20
- when the set is completely full
21
- you can change its values / keys
12
+ class CompositeSemaphore(object):
13
+ '''
14
+ a thread-safe semaphore with named lefts and many rights
22
15
 
23
- once it is partially tick'd
24
- this is forbidden
16
+ left: a dynamic set of keys. release(k) ticks one key down;
17
+ when every key has been ticked in the current round, all
18
+ waiting rights are released and the round resets (O(1)).
19
+ Keys can be added / cleared between rounds, not mid-round.
25
20
 
26
- on the right ('acquiring/right')
27
- a dynamics set of callbacks to fire
28
- it is free-form here
29
- per free-form thing, release underlying(s) semaphore
30
- '''
31
- class CompositeSemaphore(object):
21
+ right: any number of acquirers, each with its own semaphore;
22
+ before releasing it, the releaser writes the outcome into
23
+ the acquirer's mutable_shared (the status).
24
+ '''
32
25
  # not really colloidal
33
26
  def __init__(self, initial_keys):
34
27
  self.left_lock = threading.Lock()
@@ -56,6 +49,7 @@ class CompositeSemaphore(object):
56
49
  }
57
50
 
58
51
  def add_left(self, k):
52
+ '''add a key to the left set (between rounds)'''
59
53
  with self.semaphore_lock:
60
54
  if not self.mutable_hb["hb"]:
61
55
  # print("cs killed")
@@ -67,6 +61,7 @@ class CompositeSemaphore(object):
67
61
  self.key_count += 1
68
62
 
69
63
  def clear_left(self, k):
64
+ '''remove a key from the left set (between rounds)'''
70
65
  with self.semaphore_lock:
71
66
  if not self.mutable_hb["hb"]:
72
67
  # print("cs killed")
@@ -77,6 +72,7 @@ class CompositeSemaphore(object):
77
72
 
78
73
  def wraparound_idempotent_increment(
79
74
  self, k, identifier=None):
75
+ '''advance k's counter to this round's tick; False if already ticked'''
80
76
  if self.tick > self.counters[k][0]:
81
77
  self.counters[k][0] += 1
82
78
  self.counters[k][1] = identifier
@@ -94,6 +90,11 @@ class CompositeSemaphore(object):
94
90
  return False
95
91
 
96
92
  def release(self, k, identifier=None):
93
+ '''
94
+ tick key k down (once per round). When the last key
95
+ ticks, write the status into every right's mutable_shared,
96
+ release them all, and reset the round.
97
+ '''
97
98
  go = True
98
99
  with self.mutable_hb["hb_lock"]:
99
100
  if not self.mutable_hb["hb"]:
@@ -137,6 +138,11 @@ class CompositeSemaphore(object):
137
138
  # print("not found", k)
138
139
 
139
140
  def acquire(self, identifier, mutable_shared):
141
+ '''
142
+ register (identifier, mutable_shared) and block until a
143
+ full round of lefts releases it.
144
+ An identifier may acquire only once at a time.
145
+ '''
140
146
  # decorator?
141
147
  go = True
142
148
  with self.mutable_hb["hb_lock"]:
@@ -225,6 +231,7 @@ def consume_target(sem, x, delay, c, e):
225
231
 
226
232
 
227
233
  class Collector(object):
234
+ '''shared bookkeeping for main()'s producer / consumer demo'''
228
235
  def __init__(self, l):
229
236
  self.heartbeat = True
230
237
 
@@ -239,6 +246,15 @@ class Collector(object):
239
246
  #######################################
240
247
 
241
248
  class CSWait(CommonEvent):
249
+ '''
250
+ wait for a set of left tokens before a pending event may run:
251
+ dispatch finds / creates the CompositeSemaphore for its lefts
252
+ and blocks its own thread acquiring it; finish then enqueues
253
+ the pending event (it never dispatches it directly).
254
+
255
+ args: [lefts csv, requester name, <pending event serialization>]
256
+ lefts must not overlap another live CSWait's.
257
+ '''
242
258
  debug_color = bcolors.BLUE
243
259
 
244
260
  def prior_cb(self, args):
@@ -263,7 +279,8 @@ class CSWait(CommonEvent):
263
279
  @staticmethod
264
280
  def parse_lefts(s):
265
281
  '''
266
- identifier is just a serialization of tokens, not a new thing
282
+ "b,a,b" -> (["a", "b"], "a,b"): deduped, sorted.
283
+ The identifier is just the joined serialization, not a new thing.
267
284
  '''
268
285
  tokens = sorted(list(set(s.split(","))))
269
286
 
@@ -361,6 +378,7 @@ class CSWait(CommonEvent):
361
378
  )
362
379
 
363
380
  def cleanup(self):
381
+ '''remove this wait's semaphore + identifier from the registry'''
364
382
  self.internal_log("cleaning up {}".format(self.ls))
365
383
  with self.blackboard["volatile"]["cs_registry_l"] and self.blackboard["volatile"]["cs_cv_l"]:
366
384
  for l in self.ls:
@@ -386,6 +404,7 @@ class CSWait(CommonEvent):
386
404
  )
387
405
 
388
406
  def finish(self, event_dispatch, *args, **kwargs):
407
+ '''cleanup, then enqueue the pending event (status -1: noop)'''
389
408
  # if self.exception:
390
409
  # self.internal_log("noop")
391
410
  # return
@@ -415,6 +434,9 @@ class CSWait(CommonEvent):
415
434
 
416
435
  class CSRelease(CommonEvent):
417
436
  '''
437
+ release some tokens, possibly unblocking CSWaits.
438
+ The release happens entirely in dispatch; finish is a noop.
439
+
418
440
  example:
419
441
  ["CSRelease", "0,1,2", 0, 0]
420
442
  the last 2 indices don't really matter
@@ -459,6 +481,7 @@ class CSRelease(CommonEvent):
459
481
  self.internal_log("CSRelease dispatch done!")
460
482
 
461
483
  def finish(self, event_dispatch, *args, **kwargs):
484
+ '''noop: CSRelease is a signal, not a process'''
462
485
  pass
463
486
 
464
487
  #######################################
@@ -470,6 +493,7 @@ class CSBQCVED(BlackboardQueueCVED):
470
493
  of its peers
471
494
  '''
472
495
  def register_blackboard_assets(self, blackboard, name):
496
+ '''the queue assets, plus the volatile cs registry / set / cv'''
473
497
  super(CSBQCVED, self).register_blackboard_assets(
474
498
  blackboard, name)
475
499
 
@@ -487,38 +511,44 @@ class CSBQCVED(BlackboardQueueCVED):
487
511
  blackboard["volatile"]["cs_cv"] = threading.Condition(
488
512
  blackboard["volatile"]["cs_cv_l"])
489
513
 
490
- def prior_cb(self, blackboard):
491
- '''
492
- avoid overriding this
514
+ def _dispatch_cs_waits(self, blackboard):
515
+ '''partition the queue and dispatch this batch's CSWaits
516
+
517
+ The CS partition, shared by the blocking prior_cb below
518
+ and by subclasses that gate instead of blocking
519
+ (SimClockCSBQCVED): a CSWait whose identifier is already
520
+ registered, or whose lefts do not overlap a live
521
+ semaphore, is dispatched -- always on the threaded
522
+ vehicle (BlackboardQueueCVED.do_dispatch), never a
523
+ subclass's do_dispatch override. A CSWait whose lefts
524
+ overlap a live semaphore is rejected. The rest of the
525
+ queue is left in place.
526
+
527
+ Returns (cs_instances, accepted_ids, expected_ids):
528
+ expected includes rejected waits' identifiers (the
529
+ blocking prior_cb waits on it); accepted excludes them.
493
530
  '''
494
- self.internal_log("CSBQCVED: prior_cb")
495
-
496
531
  cs_instances = []
532
+ accepted_ids = set()
497
533
  expected_cs_ids = set()
498
534
  non_cs_instances = []
499
535
  for instance in blackboard[self.queue_name]:
500
536
  if instance[0] in ["CSWait"]: # CSRelease, we only demand open-the-mouth on CSWait, CSRelease we don't care
501
- # ids = CSWait.parse_lefts(instance[1])
502
- # identifier = instance[2]
503
-
504
537
  ids, identifier = CSWait.parse_lefts(instance[1])
505
538
 
506
- self.internal_log("instance[1] {}, identifier: {}".format(instance[1], identifier))
507
-
508
539
  # reject overlap at the dispatch level
509
540
  # instead of in event
510
541
  with blackboard["volatile"]["cs_registry_l"] and blackboard["volatile"]["cs_cv_l"]:
511
- if identifier in self.blackboard["volatile"]["cs_set"]:
542
+ if identifier in blackboard["volatile"]["cs_set"]:
512
543
  cs_instances.append(instance)
544
+ accepted_ids.add(identifier)
513
545
  elif len(set.intersection(
514
546
  set(blackboard["volatile"]["cs_registry"].keys()),
515
- # ids)) == 0:
516
547
  ids)) > 0:
517
548
  self.internal_log("left overlap caught, rejecting CSWait attempt")
518
549
  else:
519
550
  cs_instances.append(instance)
520
-
521
- # expected_cs_ids.update(ids)
551
+ accepted_ids.add(identifier)
522
552
 
523
553
  # we use the 'identifier' in cs_set because
524
554
  # for now, CSWaits must be non-overlapping
@@ -528,24 +558,35 @@ class CSBQCVED(BlackboardQueueCVED):
528
558
  else:
529
559
  non_cs_instances.append(instance)
530
560
 
531
- self.internal_log("CS_INSTANCES {}!!!!!".format(cs_instances))
532
- self.internal_log("non_cs_instances {}!!!!!".format(non_cs_instances))
533
-
534
561
  blackboard[self.queue_name] = non_cs_instances
535
562
 
563
+ for cs_instance in cs_instances:
564
+ BlackboardQueueCVED.do_dispatch(self, blackboard, cs_instance)
565
+
566
+ return cs_instances, accepted_ids, expected_cs_ids
567
+
568
+ def prior_cb(self, blackboard):
569
+ '''
570
+ avoid overriding this
571
+
572
+ partition the queue: CSWaits dispatch first (CSWaits
573
+ whose lefts overlap a live semaphore are rejected), and
574
+ the rest of the queue waits until every expected CSWait
575
+ has registered its semaphore.
576
+ '''
577
+ self.internal_log("CSBQCVED: prior_cb")
578
+
579
+ cs_instances, _, expected_cs_ids = self._dispatch_cs_waits(blackboard)
580
+
536
581
  if len(cs_instances) == 0:
537
582
  # self.internal_log("prior_cb DONE!!!!")
538
583
  return
539
584
 
540
- for cs_instance in cs_instances:
541
- self.do_dispatch(blackboard, cs_instance)
542
-
543
585
  self.internal_log("expected_cs_ids {}!!!!!".format(expected_cs_ids))
544
586
  if len(expected_cs_ids) > 0:
545
587
  self.blackboard["volatile"]["cs_cv_l"].acquire()
546
588
  while len(set.intersection(
547
589
  self.blackboard["volatile"]["cs_set"],
548
- # ids)) == 0:
549
590
  expected_cs_ids)) < len(expected_cs_ids):
550
591
  self.blackboard["volatile"]["cs_cv"].wait()
551
592
  self.blackboard["volatile"]["cs_cv_l"].release()
@@ -560,6 +601,7 @@ class CSBQCVED(BlackboardQueueCVED):
560
601
 
561
602
  # convienence function
562
603
  def clear_cs_waits(self, blackboard, key=None):
604
+ '''force-release waiting CSWaits (status -1: aborted); all, or one key'''
563
605
  if "volatile" not in blackboard:
564
606
  self.internal_log("blackboard missing volatile, noop")
565
607
  return
@@ -581,6 +623,7 @@ class CSBQCVED(BlackboardQueueCVED):
581
623
  s[1].release()
582
624
 
583
625
  def main():
626
+ '''standalone produce / consume demo of CompositeSemaphore'''
584
627
  s = 4
585
628
 
586
629
  sem1 = CompositeSemaphore([x for x in range(s)])
@@ -14,10 +14,15 @@ import threading, collections
14
14
  from threading import Condition, Lock, Thread
15
15
 
16
16
  def wrap_instance_method(instance, method_name, wrapper_with_args):
17
+ '''instance.<method_name> = wrapper_with_args(instance.<method_name>)'''
17
18
  wrapped_method = wrapper_with_args(getattr(instance, method_name))
18
19
  setattr(instance, method_name, wrapped_method)
19
20
 
20
21
  def call_when_switch_turned_on(obj, switch, switch_lock):
22
+ '''
23
+ decorator factory: the wrapped call raises while obj.<switch>
24
+ is off; the check and the call happen under obj.<switch_lock>
25
+ '''
21
26
  def decorator(func): # func should return void
22
27
  def wrapper(*args, **kwargs):
23
28
  lock_obj = getattr(obj, switch_lock)
@@ -33,6 +38,14 @@ def call_when_switch_turned_on(obj, switch, switch_lock):
33
38
  return decorator
34
39
 
35
40
  class Blackboard(dict):
41
+ '''
42
+ a dict with per-key condition variables
43
+
44
+ register_payload(key) rents a cv set for a key; setting that
45
+ key afterwards appends the value to the set's queue and
46
+ notifies its waiters (open-the-mouth-before-you-feed-it).
47
+ release_cv(key) returns the set to the pool.
48
+ '''
36
49
  def __init__(self, *args, **kwargs):
37
50
  # self.mutex = Lock()
38
51
  self.update(dict(*args, **kwargs)) # use the free update to set keys
@@ -65,6 +78,7 @@ class Blackboard(dict):
65
78
  # super().__delitem__(key)
66
79
 
67
80
  def release_cv(self, key):
81
+ '''return key's cv set to the pool'''
68
82
  print("RELEASING CV", key)
69
83
  with self.cv_pool_lock:
70
84
  if key not in self.cvs:
@@ -76,6 +90,7 @@ class Blackboard(dict):
76
90
  self.cv_pool.append(cv_set)
77
91
 
78
92
  def register_payload(self, payload, match_target = None):
93
+ '''rent a cv set for payload (recycled from the pool, or new)'''
79
94
  with self.cv_pool_lock:
80
95
  if len(self.cv_pool) > 0:
81
96
  cv_set = self.cv_pool.pop(0)
@@ -149,33 +164,44 @@ class Event(object):
149
164
 
150
165
  # methods for the child to override
151
166
  def get_id(self):
167
+ '''the id this dispatch instance knows this event by'''
152
168
  # return self.__class__.__name__ + "@" + str(self.event_id)
153
169
  return self.event_id
154
170
 
155
171
  @staticmethod
156
172
  def deserialize(ed, blackboard, *args, **kwargs):
157
- # up to the Event class to define
158
- # returns 2 tuples, constructor_args, dispatch_args
159
- # unlike dispatch / finish, involves no instance
173
+ '''
174
+ MUST override: turn a serialized form into
175
+ (constructor_args, dispatch_args); involves no instance
176
+ '''
160
177
  raise NotImplementedError
161
178
 
162
179
  def dispatch(self, event_dispatch, *args, **kwargs):
163
- # CAN EITHER PASS IN ARGS OR KWARGS HERE
164
- # OR SET THEM IN CONSTRUCTOR
165
- # OR SET THEM IN THE BLACKBOARD
166
- # unlike deserialize / finish, happens in its own thread
180
+ '''
181
+ MUST override: the work itself; runs in its own thread.
182
+ Do risky / uncertain stuff here.
183
+ Args can come from dispatch, the constructor, or the blackboard.
184
+ '''
167
185
  raise NotImplementedError
168
186
 
169
187
  def finish(self, event_dispatch, *args, **kwargs):
170
- # unlike deserialize / dispatch, involves other events
171
-
172
- # BEST PRACTICE:
173
- # you should deal with OUTCOMES here
174
- # do risky / uncertain stuff inside dispatch
175
- # and deal with the outcomes here
188
+ '''
189
+ MUST override: called when dispatch's thread returns.
190
+ Deal with the outcomes here; only the event decides
191
+ what is dispatched next.
192
+ '''
176
193
  raise NotImplementedError
177
194
 
178
195
  class EventDispatch(object):
196
+ '''
197
+ the core mechanism: dispatch(event) spawns an EventThread
198
+ running event.dispatch; when that thread returns, its callback
199
+ dispatch_finish calls event.finish and releases the event id.
200
+
201
+ dispatch is wrapped at construction with a switch guard: an
202
+ event can set self.dispatch_switch off to stop all further
203
+ dispatching until it is cleared -- the safety axis.
204
+ '''
179
205
  def __init__(self, blackboard = None, ed_id = None):
180
206
  self.thread_registry = {}
181
207
  self.mutex_registry = {}
@@ -206,6 +232,7 @@ class EventDispatch(object):
206
232
  # something child ED class can override
207
233
  # for Event.deserialize to form some event_id
208
234
  def reserve_event_id(self):
235
+ '''an id unique within this dispatch instance; recycled'''
209
236
  if len(self.event_id_pool) > 0:
210
237
  return self.event_id_pool.pop()
211
238
  else:
@@ -214,10 +241,11 @@ class EventDispatch(object):
214
241
  return new_id
215
242
 
216
243
  def release_event_id(self, event_id):
217
- # return event_id to pool
244
+ '''return event_id to the pool'''
218
245
  self.event_id_pool.add(event_id)
219
246
 
220
247
  def dispatch(self, event, *args, **kwargs):
248
+ '''spawn the event's thread (guarded by the dispatch switch)'''
221
249
  with self.dispatch_mutex:
222
250
  # print("dispatching %s" % (event.get_id())) # debug
223
251
  self.thread_registry[event.get_id()] = EventThread(
@@ -234,6 +262,7 @@ class EventDispatch(object):
234
262
  # NOTE: events must deal with exceptions internally
235
263
 
236
264
  def dispatch_finish(self, event, *args, **kwargs):
265
+ '''thread callback: unregister, call finish, release the id'''
237
266
  with self.dispatch_mutex:
238
267
  event_id = event.get_id()
239
268
  self.thread_registry.pop(event_id, None)
@@ -6,6 +6,15 @@ Created on 2025-05-23
6
6
 
7
7
  Copyright (c) 2025, Charlie Yan
8
8
  License: Apache-2.0 (see LICENSE for details)
9
+
10
+ Example core: the core mechanism on a BlackboardQueueCVED.
11
+ WorkItemEvent's finish enqueues UncertainEvent1 / UncertainEvent2,
12
+ which write results to the Blackboard; CheckEvent1 combines them.
13
+ An interactive keyboard actor thread feeds the queue. Events run
14
+ on EventThreads, finishes enqueue what happens next.
15
+
16
+ Run: eventdispatch_example_core
17
+ (or: python -m eventdispatch.example_core)
9
18
  '''
10
19
 
11
20
  from .core import *
@@ -18,7 +27,7 @@ import signal, time, os, sys, random, threading, argparse
18
27
 
19
28
  # these are examples Event classes
20
29
  # together, they define the drift and control in a system
21
- # defined here, they are registered as key-value paris in the Blackboard instance below
30
+ # defined here, they are registered as key-value pairs in the Blackboard instance below
22
31
 
23
32
  class WorkItemEvent(CommonEvent):
24
33
  debug_color = bcolors.WARNING
@@ -239,7 +248,7 @@ class KeyboardThread(threading.Thread):
239
248
  local_hb = self.mutable_hb['hb']
240
249
 
241
250
  def main():
242
- parser = argparse.ArgumentParser(description='eventdispatch example1')
251
+ parser = argparse.ArgumentParser(description='eventdispatch example_core')
243
252
  parser.add_argument('--verbose',
244
253
  help="verbose, default=True",
245
254
  action='store_true')
@@ -6,17 +6,27 @@ Created on 2025-08-02
6
6
 
7
7
  Copyright (c) 2025, Charlie Yan
8
8
  License: Apache-2.0 (see LICENSE for details)
9
+
10
+ Example cs: the composite-semaphore mechanism on a CSBQCVED.
11
+ CSWait / CSRelease create on-the-fly associations between Events:
12
+ type a command of the form ####aaa or aaa##### (# = digits,
13
+ a = letters) -- if digits come first, that signal combination
14
+ prints the letters; if letters come first, printing the letters
15
+ produces those signals.
16
+
17
+ Run: eventdispatch_example_cs
18
+ (or: python -m eventdispatch.example_cs)
9
19
  '''
10
20
 
11
21
  from .core import *
12
22
  from .common1 import *
13
23
  from .composite_semaphore import *
14
- from .example1 import *
24
+ from .example_core import *
15
25
 
16
26
  # from core import *
17
27
  # from common1 import *
18
28
  # from composite_semaphore import *
19
- # from example1 import KeyboardThread
29
+ # from example_core import KeyboardThread
20
30
 
21
31
  import signal, time, os, sys, random, threading, argparse
22
32
 
@@ -197,7 +207,7 @@ class QuietCSRelease(CSRelease):
197
207
  pass
198
208
 
199
209
  def main():
200
- parser = argparse.ArgumentParser(description='eventdispatch example2')
210
+ parser = argparse.ArgumentParser(description='eventdispatch example_cs')
201
211
  parser.add_argument('--verbose',
202
212
  help="verbose, default=True",
203
213
  action='store_true')