eventdispatch 0.2.28.2__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.
- {eventdispatch-0.2.28.2/eventdispatch.egg-info → eventdispatch-0.2.30}/PKG-INFO +1 -1
- {eventdispatch-0.2.28.2 → eventdispatch-0.2.30}/eventdispatch/common1.py +40 -2
- {eventdispatch-0.2.28.2 → eventdispatch-0.2.30}/eventdispatch/composite_semaphore.py +83 -40
- {eventdispatch-0.2.28.2 → eventdispatch-0.2.30}/eventdispatch/core.py +43 -14
- eventdispatch-0.2.28.2/eventdispatch/example1.py → eventdispatch-0.2.30/eventdispatch/example_core.py +11 -2
- eventdispatch-0.2.28.2/eventdispatch/example2.py → eventdispatch-0.2.30/eventdispatch/example_cs.py +13 -3
- eventdispatch-0.2.30/eventdispatch/example_sim_mujoco.py +130 -0
- eventdispatch-0.2.30/eventdispatch/learning.py +213 -0
- eventdispatch-0.2.30/eventdispatch/sim.py +354 -0
- {eventdispatch-0.2.28.2 → eventdispatch-0.2.30/eventdispatch.egg-info}/PKG-INFO +1 -1
- {eventdispatch-0.2.28.2 → eventdispatch-0.2.30}/eventdispatch.egg-info/SOURCES.txt +5 -2
- eventdispatch-0.2.30/eventdispatch.egg-info/entry_points.txt +5 -0
- {eventdispatch-0.2.28.2 → eventdispatch-0.2.30}/pyproject.toml +1 -1
- {eventdispatch-0.2.28.2 → eventdispatch-0.2.30}/setup.py +4 -3
- eventdispatch-0.2.28.2/eventdispatch.egg-info/entry_points.txt +0 -4
- {eventdispatch-0.2.28.2 → eventdispatch-0.2.30}/LICENSE +0 -0
- {eventdispatch-0.2.28.2 → eventdispatch-0.2.30}/NOTICE +0 -0
- {eventdispatch-0.2.28.2 → eventdispatch-0.2.30}/README.md +0 -0
- {eventdispatch-0.2.28.2 → eventdispatch-0.2.30}/eventdispatch/__init__.py +0 -0
- {eventdispatch-0.2.28.2 → eventdispatch-0.2.30}/eventdispatch.egg-info/dependency_links.txt +0 -0
- {eventdispatch-0.2.28.2 → eventdispatch-0.2.30}/eventdispatch.egg-info/requires.txt +0 -0
- {eventdispatch-0.2.28.2 → eventdispatch-0.2.30}/eventdispatch.egg-info/top_level.txt +0 -0
- {eventdispatch-0.2.28.2 → eventdispatch-0.2.30}/setup.cfg +0 -0
|
@@ -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']
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
|
491
|
-
'''
|
|
492
|
-
|
|
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
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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')
|
eventdispatch-0.2.28.2/eventdispatch/example2.py → eventdispatch-0.2.30/eventdispatch/example_cs.py
RENAMED
|
@@ -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 .
|
|
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
|
|
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
|
|
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')
|