Coverage for /home/agp/Documents/me/code/uswarm/uswarm/loop.py : 57%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2# TODO: automatic registering url with factory at definition time
3# TODO: use external YAML configuration for STM and late-binding
4# TODO: STM definition can be merged using merge_config()
5# TODO: Dump STM definition to YAML file.
6# TODO: Check better way to store compile info without replacing serializable definition
7# TODO: a simple function to add a transition or function to state
8# TODO: add a layer/stm to an existing working protocol
9# TODO: protocols are closed when no layer is attached.
10# TODO: extend the event protocol with target, sender, etc?
11# TODO: maybe not, isolating many reactors in threads with transports
12# TODO: and one single supervisor. Seems to be better approach.
13# TODO: use a external Loop class to handle with transport and timers
14# TODO: isolate same event group of STM in a reactor.
17"""
18STM, Reactor and Loop provide a similar concept as docker, kubernetes, etc
19but instead of using light-virtualization using container, using
20logic and group of logic applications that can be packaged, retrieving
22- [ ] Test Driven (ptw --pdb) and Log approach, try not to debug so much
23- [ ] Optimize for speed, but friendly user interface
24- [ ] Register speed as we make changes in code
25- [ ] Have in mind that this support may be compiled in C++ or any other language
26 but all the logic, structure, etc must works seamless.
27- [ ] Check which approach is faster for regular cases: need to code and implement both approaches
28- [ ] All activity relay on transport (fake or not) and timers.
29- [ ] select() exclusive approach
30- [ ] developer user friendly
31- [ ] if a method does not exist, just ignore (but strict=True)
32- [ ] when compiling a STM, don't remove textual definition (save STM)
33- [ ] separate STM definition and implementation, so we can isolated each other
34- [ ] transport: source, endpoint are named and can be located
35- [ ] can attach a STM to a protocol at any moment
36- [ ] None means 'any': for reactors, source, endpoints, protocols
37- [ ] can attach a STM to a protocol at any moment
38- [ ] None means 'any': for reactors, source, endpoints
39- [ ] Isolate logic in Reactors (Apps), and have as many as necessary.
40- [ ] This may change the philosophy of current design, oriented to a single Reactor\
41 with a huge event table.
42- [ ] Can attach a (STM+Reactor) to a protocol by URL. If is already connected, just attach
43- [ ] If is not connect, try to connect for 1st time.
44- [ ] Listen by url, and register one protocol (as factory) to URL
45- [ ] Add as many STM to the URL, so Loop can instantiate a (STM/Reactor)
46 when someone connect with us.
47- [ ] Package format to save App and send remotely.
48- [ ] Rename Reactor by App
49- [ ] Layer/STM inventory, search, get, etc, similar to public dockers containers
50- [ ] STM server inventory, from where to pull STM/APP/SWARM
51"""
52import re
53from time import time
54from itertools import chain
56from gutools.tools import _call
57# --------------------------------------------------
58# logger
59# --------------------------------------------------
60from gutools.loggers import logger, trace, debug, info, warn, error, exception
61log = logger(__name__)
63# --------------------------------------------------
64# Definitions
65# --------------------------------------------------
66STATE_INIT = 'INIT'
67STATE_READY = 'READY'
68STATE_END = 'END'
69EVENT_TERM = '<term>' # term alike
70EVENT_QUIT = '<quit>' # kill alike, but a chance to do something before die
72MERGE_ADD = 'add'
73MERGE_REPLACE_EXISTING = 'replace_existing'
74MERGE_REPLACE_ALL = 'replace_all'
76def _merge(states, transitions, mode=MERGE_ADD):
77 """Merge states and transitions to create hierarchies of layers"""
78 _states = states.pop(0) if states else dict()
79 _transitions = transitions.pop(0) if transitions else dict()
81 if mode in (MERGE_ADD, ):
82 pass
83 elif mode in (MERGE_REPLACE_EXISTING, ):
84 for st in states:
85 for state, new_functions in st.items():
86 _states.pop(state, None)
88 for tr in transitions:
89 for source, new_info in tr.items():
90 info = _transitions.setdefault(source, dict())
91 for event, new_trx in new_info.items():
92 trx = info.setdefault(event, list())
93 for new_t in new_trx:
94 for i, t in reversed(list(enumerate(trx))):
95 if t[:1] == new_t[:1]:
96 trx.pop(i)
98 elif mode in (MERGE_REPLACE_ALL, ):
99 states.clear()
100 for tr in transitions:
101 for source, new_info in tr.items():
102 _transitions.pop(source, None)
103 else:
104 raise RuntimeError(f"Unknown '{mode}' MERGE MODE")
106 # merge states
107 for st in states:
108 for state, new_functions in st.items():
109 functions = _states.setdefault(state, list([[], [], []]))
110 for i, func in enumerate(new_functions):
111 for f in func:
112 if f not in functions[i]:
113 functions[i].append(f)
115 # merge transitions
116 for tr in transitions:
117 for source, new_info in tr.items():
118 info = _transitions.setdefault(source, dict())
119 for event, new_trx in new_info.items():
120 trx = info.setdefault(event, list())
121 for new_t in new_trx:
122 for t in trx:
123 if t[:2] == new_t[:2]:
124 t[-1].extend(new_t[-1])
125 break
126 else:
127 trx.append(new_t)
129 return _states, _transitions
131# --------------------------------------------------
132# Layer
133# --------------------------------------------------
134class Layer():
135 """"Minimal event logic"""
136 def __init__(self, states=None, transitions=None, context=None):
137 self.states = states if states is not None else dict()
138 self.transitions = transitions if transitions is not None else dict()
139 self.context = context if context is not None else dict()
141 self.app = None
142 self.state = None
144 # Setup layer logic
145 for name, states, transitions, mode, _ in self._get_layer_setups():
146 self._merge(states, transitions, mode)
148 def _merge(self, states, transitions, mode=MERGE_ADD):
149 """Merge states and transitions to create hierarchies of layers"""
150 _merge([self.states, states], [self.transitions, transitions], mode=mode)
152 def _get_layer_setups(self, include=None, skip=None):
153 include = include or []
154 skip = skip or []
156 match = re.compile('_setup_(?P<name>.*)').match
157 names = [name for name in dir(self) if match(name)]
158 names.sort()
159 for name in names:
160 logic = match(name).groupdict()['name']
162 if logic in skip:
163 continue
164 if include and logic not in include:
165 continue
167 func = getattr(self, name)
168 states, transitions, mode = _call(func, **self.context)
169 yield logic, states, transitions, mode, func.__doc__
171 # -----------------------------------------------
172 # Layer definitions
173 # -----------------------------------------------
174 def _setup_term(self):
175 """Set TERM and QUIT logic for the base layer."""
176 states = {
177 STATE_INIT: [[], [], ['start']],
178 STATE_READY: [[], [], []],
179 STATE_END: [[], ['bye'], []],
180 }
181 transitions = {
182 STATE_INIT: {
183 None: [
184 [STATE_READY, [], []],
185 ],
186 },
187 STATE_READY: {
188 EVENT_TERM: [
189 [STATE_READY, [], ['term']],
190 ],
191 EVENT_QUIT: [
192 [STATE_END, [], ['quit']],
193 ],
194 },
195 STATE_END: {
196 },
197 }
198 return states, transitions, MERGE_ADD
201# --------------------------------------------------
202# STM
203# --------------------------------------------------
204class STM():
205 """Shared context for some layers"""
207# --------------------------------------------------
208# App
209# --------------------------------------------------
211class App():
212 """A Layer / STM container that represent a minimal application"""
213 def __init__(self, loop):
214 self.loop = None
215 self.layers = dict()
216 loop.attach(self)
218# --------------------------------------------------
219# Loop
220# --------------------------------------------------
222class Loop():
223 """The reactor loop"""
224 def __init__(self):
225 self._ready_transitions = dict() # The events that can be processed at this time
226 self.running = False
227 self.t0 = 0
229 def attach(self, logic):
230 """Attach a Layer or App to loop"""
231 if isinstance(logic, App):
232 for layer in logic.layers:
233 self.attach(layer)
234 elif isinstance(logic, Layer):
235 foo = 1
237 logic.loop = self
239 def run(self):
240 """Main loop
242 while is running:
243 - get next event: [timer, None, tramsport]
244 - check preconditions
245 -
247 """
248 self.t0 = time()
249 self.running = True
250 while self.running:
251 event = self.next_event() # blocking
252 info = self._ready_transitions.get(event, [])
254 # iterate over all ready transitions
255 for layer, transitions in chain(info.items()):
256 ctx = layer.context
257 ctx['key'] = key
258 ctx['data'] = data
259 # to allow callbacks receive the params directly
260 isinstance(data, dict) and ctx.update(data)
262 # try to execute the transition if preconditions are True
263 # func_names are not used, but we want to keep for serialization
264 for (new_state, preconds, func_names, func_methods, comp_preconds) in transitions:
265 try:
266 # Check preconditions
267 for pre in comp_preconds:
268 if not eval(pre, ctx):
269 break
270 else:
271 # all preconditions are True or missing
272 if new_state != layer.state: # fast self-transitions
273 # remove current state events
274 for ev in layer.transitions[layer.state]:
275 self.events[ev].pop(layer) # must exists!!
276 # add new state events
277 for ev, trx in layer.transitions[new_state].items():
278 self.events[ev][layer] = trx
280 # execute EXIT state functions (old state)
281 for func in layer.states[layer.state][GROUP_EXIT]:
282 func(**ctx)
284 # execute transition functions
285 for func in func_methods:
286 func(**ctx)
288 layer.state = new_state
290 # execute ENTRY state functions (new state)
291 for func in layer.states[new_state][GROUP_ENTRY]:
292 func(**ctx)
294 # execute DO state functions (new state)
295 for func in layer.states[new_state][GROUP_DO]:
296 func(**ctx)
298 break # assume there's only 1 transition possible each time
300 except Exception as why:
301 exception()
302 #print()
303 #print(f"- Reactor {'-'*70}")
304 #print(f"*** ERROR: {why} ***")
305 #traceback.print_exc()
306 #print("-" * 80)
307 foo = 1
309 pass