hat-event 0.9.27__cp310.cp311.cp312.cp313-abi3-win_amd64.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.
- hat/event/__init__.py +1 -0
- hat/event/adminer/__init__.py +18 -0
- hat/event/adminer/client.py +124 -0
- hat/event/adminer/common.py +27 -0
- hat/event/adminer/server.py +111 -0
- hat/event/backends/__init__.py +0 -0
- hat/event/backends/dummy.py +49 -0
- hat/event/backends/lmdb/__init__.py +9 -0
- hat/event/backends/lmdb/backend.py +319 -0
- hat/event/backends/lmdb/common.py +277 -0
- hat/event/backends/lmdb/conditions.py +102 -0
- hat/event/backends/lmdb/convert/__init__.py +0 -0
- hat/event/backends/lmdb/convert/__main__.py +8 -0
- hat/event/backends/lmdb/convert/convert_v06_to_v07.py +213 -0
- hat/event/backends/lmdb/convert/convert_v07_to_v09.py +175 -0
- hat/event/backends/lmdb/convert/main.py +88 -0
- hat/event/backends/lmdb/convert/v06.py +216 -0
- hat/event/backends/lmdb/convert/v07.py +508 -0
- hat/event/backends/lmdb/convert/v09.py +50 -0
- hat/event/backends/lmdb/convert/version.py +63 -0
- hat/event/backends/lmdb/environment.py +100 -0
- hat/event/backends/lmdb/latestdb.py +116 -0
- hat/event/backends/lmdb/manager/__init__.py +0 -0
- hat/event/backends/lmdb/manager/__main__.py +8 -0
- hat/event/backends/lmdb/manager/common.py +45 -0
- hat/event/backends/lmdb/manager/copy.py +92 -0
- hat/event/backends/lmdb/manager/main.py +34 -0
- hat/event/backends/lmdb/manager/query.py +215 -0
- hat/event/backends/lmdb/refdb.py +234 -0
- hat/event/backends/lmdb/systemdb.py +102 -0
- hat/event/backends/lmdb/timeseriesdb.py +486 -0
- hat/event/backends/memory.py +178 -0
- hat/event/common/__init__.py +144 -0
- hat/event/common/backend.py +91 -0
- hat/event/common/collection/__init__.py +8 -0
- hat/event/common/collection/common.py +28 -0
- hat/event/common/collection/list.py +19 -0
- hat/event/common/collection/tree.py +62 -0
- hat/event/common/common.py +176 -0
- hat/event/common/encoder.py +305 -0
- hat/event/common/json_schema_repo.json +1 -0
- hat/event/common/matches.py +44 -0
- hat/event/common/module.py +142 -0
- hat/event/common/sbs_repo.json +1 -0
- hat/event/common/subscription/__init__.py +22 -0
- hat/event/common/subscription/_csubscription.abi3.pyd +0 -0
- hat/event/common/subscription/common.py +145 -0
- hat/event/common/subscription/csubscription.py +47 -0
- hat/event/common/subscription/pysubscription.py +97 -0
- hat/event/component.py +284 -0
- hat/event/eventer/__init__.py +28 -0
- hat/event/eventer/client.py +260 -0
- hat/event/eventer/common.py +27 -0
- hat/event/eventer/server.py +286 -0
- hat/event/manager/__init__.py +0 -0
- hat/event/manager/__main__.py +8 -0
- hat/event/manager/common.py +48 -0
- hat/event/manager/main.py +387 -0
- hat/event/server/__init__.py +0 -0
- hat/event/server/__main__.py +8 -0
- hat/event/server/adminer_server.py +43 -0
- hat/event/server/engine.py +216 -0
- hat/event/server/engine_runner.py +127 -0
- hat/event/server/eventer_client.py +205 -0
- hat/event/server/eventer_client_runner.py +152 -0
- hat/event/server/eventer_server.py +119 -0
- hat/event/server/main.py +84 -0
- hat/event/server/main_runner.py +212 -0
- hat_event-0.9.27.dist-info/LICENSE +202 -0
- hat_event-0.9.27.dist-info/METADATA +108 -0
- hat_event-0.9.27.dist-info/RECORD +73 -0
- hat_event-0.9.27.dist-info/WHEEL +7 -0
- hat_event-0.9.27.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from hat import aio
|
|
6
|
+
from hat import json
|
|
7
|
+
|
|
8
|
+
from hat.event import common
|
|
9
|
+
from hat.event.server.engine import create_engine
|
|
10
|
+
from hat.event.server.eventer_client import SyncedState
|
|
11
|
+
from hat.event.server.eventer_client_runner import EventerClientRunner
|
|
12
|
+
from hat.event.server.eventer_server import EventerServer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
mlog: logging.Logger = logging.getLogger(__name__)
|
|
16
|
+
"""Module logger"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EngineRunner(aio.Resource):
|
|
20
|
+
|
|
21
|
+
def __init__(self,
|
|
22
|
+
conf: json.Data,
|
|
23
|
+
backend: common.Backend,
|
|
24
|
+
eventer_server: EventerServer,
|
|
25
|
+
eventer_client_runner: EventerClientRunner | None,
|
|
26
|
+
reset_monitor_ready_cb: Callable[[], None]):
|
|
27
|
+
self._conf = conf
|
|
28
|
+
self._backend = backend
|
|
29
|
+
self._eventer_server = eventer_server
|
|
30
|
+
self._eventer_client_runner = eventer_client_runner
|
|
31
|
+
self._reset_monitor_ready_cb = reset_monitor_ready_cb
|
|
32
|
+
self._async_group = aio.Group()
|
|
33
|
+
self._engine = None
|
|
34
|
+
self._restart = asyncio.Event()
|
|
35
|
+
|
|
36
|
+
self.async_group.spawn(self._run)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def async_group(self) -> aio.Group:
|
|
40
|
+
return self._async_group
|
|
41
|
+
|
|
42
|
+
async def set_synced(self,
|
|
43
|
+
server_id: common.ServerId,
|
|
44
|
+
state: SyncedState,
|
|
45
|
+
count: int | None):
|
|
46
|
+
if not self._engine or not self._engine.is_open:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
data = {'state': state.name}
|
|
50
|
+
if state == SyncedState.SYNCED:
|
|
51
|
+
data['count'] = count
|
|
52
|
+
|
|
53
|
+
source = common.Source(type=common.SourceType.SERVER, id=0)
|
|
54
|
+
event = common.RegisterEvent(
|
|
55
|
+
type=('event', str(self._conf['server_id']), 'synced',
|
|
56
|
+
str(server_id)),
|
|
57
|
+
source_timestamp=None,
|
|
58
|
+
payload=common.EventPayloadJson(data))
|
|
59
|
+
|
|
60
|
+
await self._engine.register(source, [event])
|
|
61
|
+
|
|
62
|
+
async def _run(self):
|
|
63
|
+
try:
|
|
64
|
+
mlog.debug("staring engine runner loop")
|
|
65
|
+
while True:
|
|
66
|
+
await self._wait_while_remote_active()
|
|
67
|
+
|
|
68
|
+
self._restart.clear()
|
|
69
|
+
|
|
70
|
+
await self._eventer_server.set_status(common.Status.STARTING,
|
|
71
|
+
None)
|
|
72
|
+
|
|
73
|
+
mlog.debug("creating engine")
|
|
74
|
+
self._engine = await create_engine(
|
|
75
|
+
backend=self._backend,
|
|
76
|
+
eventer_server=self._eventer_server,
|
|
77
|
+
module_confs=self._conf['modules'],
|
|
78
|
+
server_id=self._conf['server_id'],
|
|
79
|
+
restart_cb=self._restart.set,
|
|
80
|
+
reset_monitor_ready_cb=self._reset_monitor_ready_cb)
|
|
81
|
+
await self._eventer_server.set_status(
|
|
82
|
+
common.Status.OPERATIONAL, self._engine)
|
|
83
|
+
|
|
84
|
+
async with self._async_group.create_subgroup() as subgroup:
|
|
85
|
+
await asyncio.wait(
|
|
86
|
+
[subgroup.spawn(self._engine.wait_closing),
|
|
87
|
+
subgroup.spawn(self._restart.wait)],
|
|
88
|
+
return_when=asyncio.FIRST_COMPLETED)
|
|
89
|
+
|
|
90
|
+
if not self._engine.is_open:
|
|
91
|
+
break
|
|
92
|
+
|
|
93
|
+
await self._close()
|
|
94
|
+
|
|
95
|
+
except Exception as e:
|
|
96
|
+
mlog.error("engine runner loop error: %s", e, exc_info=e)
|
|
97
|
+
|
|
98
|
+
finally:
|
|
99
|
+
mlog.debug("closing engine runner loop")
|
|
100
|
+
self.close()
|
|
101
|
+
await aio.uncancellable(self._close())
|
|
102
|
+
|
|
103
|
+
async def _close(self):
|
|
104
|
+
if self._engine:
|
|
105
|
+
self._engine.close()
|
|
106
|
+
|
|
107
|
+
await self._eventer_server.set_status(common.Status.STOPPING, None)
|
|
108
|
+
|
|
109
|
+
if self._engine:
|
|
110
|
+
await self._engine.async_close()
|
|
111
|
+
|
|
112
|
+
await self._backend.flush()
|
|
113
|
+
|
|
114
|
+
# TODO not needed with _wait_while_remote_active
|
|
115
|
+
# await self._eventer_server.notify_events([], True, True)
|
|
116
|
+
|
|
117
|
+
await self._eventer_server.set_status(common.Status.STANDBY, None)
|
|
118
|
+
|
|
119
|
+
async def _wait_while_remote_active(self):
|
|
120
|
+
if not self._eventer_client_runner:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
while self._eventer_client_runner.remote_active:
|
|
124
|
+
event = asyncio.Event()
|
|
125
|
+
with self._eventer_client_runner.register_remote_active_cb(
|
|
126
|
+
lambda _: event.set()):
|
|
127
|
+
await event.wait()
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
import contextlib
|
|
3
|
+
import enum
|
|
4
|
+
import logging
|
|
5
|
+
import typing
|
|
6
|
+
|
|
7
|
+
from hat import aio
|
|
8
|
+
from hat.drivers import tcp
|
|
9
|
+
|
|
10
|
+
from hat.event import common
|
|
11
|
+
from hat.event import eventer
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
mlog: logging.Logger = logging.getLogger(__name__)
|
|
15
|
+
"""Module logger"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SyncedState(enum.Enum):
|
|
19
|
+
"""Synced state"""
|
|
20
|
+
CONNECTED = 0
|
|
21
|
+
SYNCING = 1
|
|
22
|
+
SYNCED = 2
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
StatusCb: typing.TypeAlias = aio.AsyncCallable[[common.Status], None]
|
|
26
|
+
"""Status callback"""
|
|
27
|
+
|
|
28
|
+
SyncedCb: typing.TypeAlias = aio.AsyncCallable[[SyncedState, int | None], None]
|
|
29
|
+
"""Synced callback"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def create_eventer_client(addr: tcp.Address,
|
|
33
|
+
client_name: str,
|
|
34
|
+
local_server_id: common.ServerId,
|
|
35
|
+
remote_server_id: common.ServerId,
|
|
36
|
+
backend: common.Backend,
|
|
37
|
+
*,
|
|
38
|
+
client_token: str | None = None,
|
|
39
|
+
status_cb: StatusCb | None = None,
|
|
40
|
+
synced_cb: SyncedCb | None = None,
|
|
41
|
+
**kwargs
|
|
42
|
+
) -> 'EventerClient':
|
|
43
|
+
"""Create eventer client"""
|
|
44
|
+
client = EventerClient()
|
|
45
|
+
client._local_server_id = local_server_id
|
|
46
|
+
client._remote_server_id = remote_server_id
|
|
47
|
+
client._backend = backend
|
|
48
|
+
client._status_cb = status_cb
|
|
49
|
+
client._synced_cb = synced_cb
|
|
50
|
+
client._synced = None
|
|
51
|
+
client._events_queue = collections.deque()
|
|
52
|
+
|
|
53
|
+
client._client = await eventer.connect(addr=addr,
|
|
54
|
+
client_name=client_name,
|
|
55
|
+
client_token=client_token,
|
|
56
|
+
subscriptions=[('*', )],
|
|
57
|
+
server_id=remote_server_id,
|
|
58
|
+
persisted=True,
|
|
59
|
+
status_cb=client._on_status,
|
|
60
|
+
events_cb=client._on_events,
|
|
61
|
+
**kwargs)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
client.async_group.spawn(client._synchronize)
|
|
65
|
+
|
|
66
|
+
except BaseException:
|
|
67
|
+
await aio.uncancellable(client.async_close())
|
|
68
|
+
raise
|
|
69
|
+
|
|
70
|
+
return client
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class EventerClient(aio.Resource):
|
|
74
|
+
"""Eventer client
|
|
75
|
+
|
|
76
|
+
For creating new client see `create_eventer_client` coroutine.
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def async_group(self) -> aio.Group:
|
|
82
|
+
"""Async group"""
|
|
83
|
+
return self._client.async_group
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def status(self) -> common.Status:
|
|
87
|
+
"""Status"""
|
|
88
|
+
return self._client.status
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def synced(self) -> SyncedState | None:
|
|
92
|
+
"""Synced state"""
|
|
93
|
+
return self._synced
|
|
94
|
+
|
|
95
|
+
async def _on_status(self, client, status):
|
|
96
|
+
if status == common.Status.OPERATIONAL and self._synced:
|
|
97
|
+
data = {'state': self._synced.name}
|
|
98
|
+
if self._synced == SyncedState.SYNCED:
|
|
99
|
+
data['count'] = None
|
|
100
|
+
|
|
101
|
+
with contextlib.suppress(Exception):
|
|
102
|
+
await self._client.register([
|
|
103
|
+
common.RegisterEvent(
|
|
104
|
+
type=('event', str(self._local_server_id), 'synced',
|
|
105
|
+
str(self._remote_server_id)),
|
|
106
|
+
source_timestamp=None,
|
|
107
|
+
payload=common.EventPayloadJson(data))])
|
|
108
|
+
|
|
109
|
+
if not self._status_cb:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
await aio.call(self._status_cb, status)
|
|
113
|
+
|
|
114
|
+
async def _on_events(self, client, events):
|
|
115
|
+
mlog.debug("received %s notify events", len(events))
|
|
116
|
+
|
|
117
|
+
if self._events_queue is not None:
|
|
118
|
+
self._events_queue.append(events)
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
await self._backend.register(events)
|
|
122
|
+
|
|
123
|
+
async def _synchronize(self):
|
|
124
|
+
mlog.debug("starting synchronization")
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
last_event_id = await self._backend.get_last_event_id(
|
|
128
|
+
self._remote_server_id)
|
|
129
|
+
events = collections.deque()
|
|
130
|
+
result = common.QueryResult([], True)
|
|
131
|
+
synced_counter = 0
|
|
132
|
+
|
|
133
|
+
await self._set_synced(SyncedState.CONNECTED, None)
|
|
134
|
+
|
|
135
|
+
while result.more_follows:
|
|
136
|
+
params = common.QueryServerParams(
|
|
137
|
+
server_id=self._remote_server_id,
|
|
138
|
+
persisted=True,
|
|
139
|
+
last_event_id=last_event_id)
|
|
140
|
+
result = await self._client.query(params)
|
|
141
|
+
|
|
142
|
+
mlog.debug("received %s query events", len(result.events))
|
|
143
|
+
events.extend(result.events)
|
|
144
|
+
|
|
145
|
+
if result.events and synced_counter == 0:
|
|
146
|
+
await self._set_synced(SyncedState.SYNCING, None)
|
|
147
|
+
|
|
148
|
+
synced_counter += len(result.events)
|
|
149
|
+
if not events:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
last_event_id = events[-1].id
|
|
153
|
+
|
|
154
|
+
while events[0].id.session != events[-1].id.session:
|
|
155
|
+
session_id = events[0].id.session
|
|
156
|
+
session_events = collections.deque()
|
|
157
|
+
|
|
158
|
+
while events[0].id.session == session_id:
|
|
159
|
+
session_events.append(events.popleft())
|
|
160
|
+
|
|
161
|
+
await self._backend.register(session_events)
|
|
162
|
+
|
|
163
|
+
if events:
|
|
164
|
+
await self._backend.register(events)
|
|
165
|
+
|
|
166
|
+
mlog.debug("processing cached notify events")
|
|
167
|
+
while self._events_queue:
|
|
168
|
+
events = [event for event in self._events_queue.popleft()
|
|
169
|
+
if event.id > last_event_id]
|
|
170
|
+
if not events:
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
await self._backend.register(events)
|
|
174
|
+
|
|
175
|
+
self._events_queue = None
|
|
176
|
+
|
|
177
|
+
mlog.debug("synchronized %s events", synced_counter)
|
|
178
|
+
await self._set_synced(SyncedState.SYNCED, synced_counter)
|
|
179
|
+
|
|
180
|
+
except ConnectionError:
|
|
181
|
+
mlog.debug("connection closed")
|
|
182
|
+
self.close()
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
mlog.error("synchronization error: %s", e, exc_info=e)
|
|
186
|
+
self.close()
|
|
187
|
+
|
|
188
|
+
async def _set_synced(self, state, count):
|
|
189
|
+
self._synced = state
|
|
190
|
+
|
|
191
|
+
data = {'state': state.name}
|
|
192
|
+
if state == SyncedState.SYNCED:
|
|
193
|
+
data['count'] = count
|
|
194
|
+
|
|
195
|
+
await self._client.register([
|
|
196
|
+
common.RegisterEvent(
|
|
197
|
+
type=('event', str(self._local_server_id), 'synced',
|
|
198
|
+
str(self._remote_server_id)),
|
|
199
|
+
source_timestamp=None,
|
|
200
|
+
payload=common.EventPayloadJson(data))])
|
|
201
|
+
|
|
202
|
+
if not self._synced_cb:
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
await aio.call(self._synced_cb, state, count)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
import asyncio
|
|
3
|
+
import functools
|
|
4
|
+
import logging
|
|
5
|
+
import types
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
from hat import aio
|
|
9
|
+
from hat import json
|
|
10
|
+
from hat import util
|
|
11
|
+
from hat.drivers import tcp
|
|
12
|
+
import hat.monitor.component
|
|
13
|
+
|
|
14
|
+
from hat.event import common
|
|
15
|
+
from hat.event.server.eventer_client import (SyncedState,
|
|
16
|
+
create_eventer_client)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
mlog: logging.Logger = logging.getLogger(__name__)
|
|
20
|
+
"""Module logger"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
SyncedCb: typing.TypeAlias = aio.AsyncCallable[
|
|
24
|
+
[common.ServerId, SyncedState, int | None],
|
|
25
|
+
None]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class EventerServerData(typing.NamedTuple):
|
|
29
|
+
server_id: common.ServerId
|
|
30
|
+
addr: tcp.Address
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EventerClientRunner(aio.Resource):
|
|
34
|
+
|
|
35
|
+
def __init__(self,
|
|
36
|
+
conf: json.Data,
|
|
37
|
+
backend: common.Backend,
|
|
38
|
+
synced_cb: SyncedCb,
|
|
39
|
+
reconnect_delay: float = 5):
|
|
40
|
+
self._conf = conf
|
|
41
|
+
self._backend = backend
|
|
42
|
+
self._synced_cb = synced_cb
|
|
43
|
+
self._reconnect_delay = reconnect_delay
|
|
44
|
+
self._async_group = aio.Group()
|
|
45
|
+
self._remote_active = False
|
|
46
|
+
self._remote_active_cbs = util.CallbackRegistry()
|
|
47
|
+
self._valid_server_data = set()
|
|
48
|
+
self._connecting_server_data = set()
|
|
49
|
+
self._remote_active_server_data = set()
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def async_group(self) -> aio.Group:
|
|
53
|
+
return self._async_group
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def remote_active(self) -> bool:
|
|
57
|
+
return self._remote_active
|
|
58
|
+
|
|
59
|
+
def register_remote_active_cb(self,
|
|
60
|
+
cb: Callable[[bool], None]
|
|
61
|
+
) -> util.RegisterCallbackHandle:
|
|
62
|
+
return self._remote_active_cbs.register(cb)
|
|
63
|
+
|
|
64
|
+
def set_monitor_state(self, state: hat.monitor.component.State):
|
|
65
|
+
self._valid_server_data = set(_get_eventer_server_data(
|
|
66
|
+
group=self._conf['monitor_component']['group'],
|
|
67
|
+
server_token=self._conf.get('server_token'),
|
|
68
|
+
state=state))
|
|
69
|
+
|
|
70
|
+
for server_data in self._valid_server_data:
|
|
71
|
+
if server_data in self._connecting_server_data:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
self.async_group.spawn(self._client_loop, server_data)
|
|
75
|
+
self._connecting_server_data.add(server_data)
|
|
76
|
+
|
|
77
|
+
async def _client_loop(self, server_data):
|
|
78
|
+
try:
|
|
79
|
+
mlog.debug("staring eventer client runner loop")
|
|
80
|
+
while server_data in self._valid_server_data:
|
|
81
|
+
self._set_client_status(server_data, None)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
mlog.debug("creating eventer client")
|
|
85
|
+
eventer_client = await create_eventer_client(
|
|
86
|
+
addr=server_data.addr,
|
|
87
|
+
client_name=f"event/{self._conf['name']}",
|
|
88
|
+
local_server_id=self._conf['server_id'],
|
|
89
|
+
remote_server_id=server_data.server_id,
|
|
90
|
+
backend=self._backend,
|
|
91
|
+
client_token=self._conf.get('server_token'),
|
|
92
|
+
status_cb=functools.partial(self._set_client_status,
|
|
93
|
+
server_data),
|
|
94
|
+
synced_cb=functools.partial(self._synced_cb,
|
|
95
|
+
server_data.server_id))
|
|
96
|
+
|
|
97
|
+
except Exception:
|
|
98
|
+
await asyncio.sleep(self._reconnect_delay)
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
self._set_client_status(server_data, eventer_client.status)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
await eventer_client.wait_closing()
|
|
105
|
+
|
|
106
|
+
finally:
|
|
107
|
+
await aio.uncancellable(eventer_client.async_close())
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
mlog.error("eventer client runner loop error: %s", e, exc_info=e)
|
|
111
|
+
self.close()
|
|
112
|
+
|
|
113
|
+
finally:
|
|
114
|
+
mlog.debug("closing eventer client runner loop")
|
|
115
|
+
self._connecting_server_data.remove(server_data)
|
|
116
|
+
self._set_client_status(server_data, None)
|
|
117
|
+
|
|
118
|
+
def _set_client_status(self, server_data, status):
|
|
119
|
+
if status is None or status == common.Status.STANDBY:
|
|
120
|
+
self._remote_active_server_data.discard(server_data)
|
|
121
|
+
|
|
122
|
+
else:
|
|
123
|
+
self._remote_active_server_data.add(server_data)
|
|
124
|
+
|
|
125
|
+
remote_active = bool(self._remote_active_server_data)
|
|
126
|
+
if remote_active == self._remote_active:
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
self._remote_active = remote_active
|
|
130
|
+
self._remote_active_cbs.notify(remote_active)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _get_eventer_server_data(group, server_token, state):
|
|
134
|
+
for info in state.components:
|
|
135
|
+
if info == state.info or info.group != group:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
server_id = json.get(info.data, 'server_id')
|
|
139
|
+
host = json.get(info.data, ['eventer_server', 'host'])
|
|
140
|
+
port = json.get(info.data, ['eventer_server', 'port'])
|
|
141
|
+
token = json.get(info.data, 'server_token')
|
|
142
|
+
if (not isinstance(server_id, int) or
|
|
143
|
+
not isinstance(host, str) or
|
|
144
|
+
not isinstance(port, int) or
|
|
145
|
+
not isinstance(token, (str, types.NoneType))):
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
if server_token is not None and token != server_token:
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
yield EventerServerData(server_id=server_id,
|
|
152
|
+
addr=tcp.Address(host, port))
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Eventer server"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Collection, Iterable
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from hat import aio
|
|
7
|
+
from hat.drivers import tcp
|
|
8
|
+
|
|
9
|
+
from hat.event import common
|
|
10
|
+
from hat.event import eventer
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
mlog: logging.Logger = logging.getLogger(__name__)
|
|
14
|
+
"""Module logger"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def create_eventer_server(addr: tcp.Address,
|
|
18
|
+
backend: common.Backend,
|
|
19
|
+
server_id: int,
|
|
20
|
+
*,
|
|
21
|
+
server_token: str | None = None,
|
|
22
|
+
**kwargs
|
|
23
|
+
) -> 'EventerServer':
|
|
24
|
+
"""Create eventer server"""
|
|
25
|
+
server = EventerServer()
|
|
26
|
+
server._backend = backend
|
|
27
|
+
server._server_id = server_id
|
|
28
|
+
server._server_token = server_token
|
|
29
|
+
server._engine = None
|
|
30
|
+
|
|
31
|
+
server._srv = await eventer.listen(addr,
|
|
32
|
+
connected_cb=server._on_connected,
|
|
33
|
+
disconnected_cb=server._on_disconnected,
|
|
34
|
+
register_cb=server._on_register,
|
|
35
|
+
query_cb=server._on_query,
|
|
36
|
+
**kwargs)
|
|
37
|
+
|
|
38
|
+
return server
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class EventerServer(aio.Resource):
|
|
42
|
+
"""Eventer server
|
|
43
|
+
|
|
44
|
+
For creating new server see `create_eventer_server` coroutine.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def async_group(self) -> aio.Group:
|
|
50
|
+
"""Async group"""
|
|
51
|
+
return self._srv.async_group
|
|
52
|
+
|
|
53
|
+
def get_client_names(self) -> Iterable[tuple[common.Source, str]]:
|
|
54
|
+
"""Get client names"""
|
|
55
|
+
for info in self._srv.get_conn_infos():
|
|
56
|
+
yield _get_source(info.id), info.client_name
|
|
57
|
+
|
|
58
|
+
async def set_status(self,
|
|
59
|
+
status: common.Status,
|
|
60
|
+
engine: common.Engine | None):
|
|
61
|
+
"""Set status"""
|
|
62
|
+
if status == common.Status.OPERATIONAL:
|
|
63
|
+
if not engine:
|
|
64
|
+
raise ValueError('invalid status/engine')
|
|
65
|
+
|
|
66
|
+
else:
|
|
67
|
+
if engine:
|
|
68
|
+
raise ValueError('invalid status/engine')
|
|
69
|
+
|
|
70
|
+
self._engine = engine
|
|
71
|
+
await self._srv.set_status(status)
|
|
72
|
+
|
|
73
|
+
async def notify_events(self,
|
|
74
|
+
events: Collection[common.Event],
|
|
75
|
+
persisted: bool,
|
|
76
|
+
with_ack: bool = False):
|
|
77
|
+
"""Notify events"""
|
|
78
|
+
await self._srv.notify_events(events, persisted, with_ack)
|
|
79
|
+
|
|
80
|
+
async def _on_connected(self, info):
|
|
81
|
+
if (info.client_token is not None and
|
|
82
|
+
info.client_token != self._server_token):
|
|
83
|
+
raise Exception('invalid client token')
|
|
84
|
+
|
|
85
|
+
if not self._engine or not self._engine.is_open:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
source = _get_source(info.id)
|
|
89
|
+
register_event = self._create_eventer_event(info, 'CONNECTED')
|
|
90
|
+
await self._engine.register(source, [register_event])
|
|
91
|
+
|
|
92
|
+
async def _on_disconnected(self, info):
|
|
93
|
+
if not self._engine or not self._engine.is_open:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
source = _get_source(info.id)
|
|
97
|
+
register_event = self._create_eventer_event(info, 'DISCONNECTED')
|
|
98
|
+
await self._engine.register(source, [register_event])
|
|
99
|
+
|
|
100
|
+
async def _on_register(self, info, register_events):
|
|
101
|
+
if not self._engine:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
source = _get_source(info.id)
|
|
105
|
+
return await self._engine.register(source, register_events)
|
|
106
|
+
|
|
107
|
+
async def _on_query(self, info, params):
|
|
108
|
+
return await self._backend.query(params)
|
|
109
|
+
|
|
110
|
+
def _create_eventer_event(self, info, status):
|
|
111
|
+
return common.RegisterEvent(
|
|
112
|
+
type=('event', str(self._server_id), 'eventer', info.client_name),
|
|
113
|
+
source_timestamp=None,
|
|
114
|
+
payload=common.EventPayloadJson(status))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _get_source(source_id):
|
|
118
|
+
return common.Source(type=common.SourceType.EVENTER,
|
|
119
|
+
id=source_id)
|
hat/event/server/main.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Event server main"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import argparse
|
|
5
|
+
import asyncio
|
|
6
|
+
import contextlib
|
|
7
|
+
import logging.config
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
import appdirs
|
|
11
|
+
|
|
12
|
+
from hat import aio
|
|
13
|
+
from hat import json
|
|
14
|
+
|
|
15
|
+
from hat.event import common
|
|
16
|
+
from hat.event.server.main_runner import MainRunner
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
mlog: logging.Logger = logging.getLogger('hat.event.server.main')
|
|
20
|
+
"""Module logger"""
|
|
21
|
+
|
|
22
|
+
user_conf_dir: Path = Path(appdirs.user_config_dir('hat'))
|
|
23
|
+
"""User configuration directory path"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_argument_parser() -> argparse.ArgumentParser:
|
|
27
|
+
"""Create argument parser"""
|
|
28
|
+
parser = argparse.ArgumentParser()
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
'--conf', metavar='PATH', type=Path, default=None,
|
|
31
|
+
help="configuration defined by hat-event://server.yaml "
|
|
32
|
+
"(default $XDG_CONFIG_HOME/hat/event.{yaml|yml|toml|json})")
|
|
33
|
+
return parser
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main():
|
|
37
|
+
"""Event Server"""
|
|
38
|
+
parser = create_argument_parser()
|
|
39
|
+
args = parser.parse_args()
|
|
40
|
+
conf = json.read_conf(args.conf, user_conf_dir / 'event')
|
|
41
|
+
sync_main(conf)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def sync_main(conf: json.Data):
|
|
45
|
+
"""Sync main entry point"""
|
|
46
|
+
aio.init_asyncio()
|
|
47
|
+
|
|
48
|
+
common.json_schema_repo.validate('hat-event://server.yaml', conf)
|
|
49
|
+
|
|
50
|
+
info = common.import_backend_info(conf['backend']['module'])
|
|
51
|
+
if info.json_schema_repo and info.json_schema_id:
|
|
52
|
+
info.json_schema_repo.validate(info.json_schema_id, conf['backend'])
|
|
53
|
+
|
|
54
|
+
for module_conf in conf['modules']:
|
|
55
|
+
info = common.import_module_info(module_conf['module'])
|
|
56
|
+
if info.json_schema_repo and info.json_schema_id:
|
|
57
|
+
info.json_schema_repo.validate(info.json_schema_id, module_conf)
|
|
58
|
+
|
|
59
|
+
log_conf = conf.get('log')
|
|
60
|
+
if log_conf:
|
|
61
|
+
logging.config.dictConfig(log_conf)
|
|
62
|
+
|
|
63
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
64
|
+
aio.run_asyncio(async_main(conf))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def async_main(conf: json.Data):
|
|
68
|
+
"""Async main entry point"""
|
|
69
|
+
main_runner = MainRunner(conf)
|
|
70
|
+
|
|
71
|
+
async def cleanup():
|
|
72
|
+
await main_runner.async_close()
|
|
73
|
+
await asyncio.sleep(0.1)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
await main_runner.wait_closing()
|
|
77
|
+
|
|
78
|
+
finally:
|
|
79
|
+
await aio.uncancellable(cleanup())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == '__main__':
|
|
83
|
+
sys.argv[0] = 'hat-event-server'
|
|
84
|
+
sys.exit(main())
|