karton-core 5.6.1__py3-none-any.whl → 5.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- karton/core/__version__.py +1 -1
- karton/core/asyncio/__init__.py +21 -0
- karton/core/asyncio/backend.py +370 -0
- karton/core/asyncio/base.py +133 -0
- karton/core/asyncio/karton.py +359 -0
- karton/core/asyncio/logger.py +57 -0
- karton/core/asyncio/resource.py +384 -0
- karton/core/backend.py +160 -114
- karton/core/base.py +123 -94
- karton/core/config.py +11 -8
- karton/core/karton.py +14 -12
- karton/core/logger.py +33 -15
- karton/core/resource.py +44 -30
- karton/core/task.py +24 -2
- karton/core/test.py +3 -2
- karton/system/system.py +22 -7
- {karton_core-5.6.1.dist-info → karton_core-5.8.0.dist-info}/METADATA +3 -2
- karton_core-5.8.0.dist-info/RECORD +33 -0
- karton_core-5.6.1.dist-info/RECORD +0 -27
- /karton_core-5.6.1-nspkg.pth → /karton_core-5.8.0-nspkg.pth +0 -0
- {karton_core-5.6.1.dist-info → karton_core-5.8.0.dist-info}/LICENSE +0 -0
- {karton_core-5.6.1.dist-info → karton_core-5.8.0.dist-info}/WHEEL +0 -0
- {karton_core-5.6.1.dist-info → karton_core-5.8.0.dist-info}/entry_points.txt +0 -0
- {karton_core-5.6.1.dist-info → karton_core-5.8.0.dist-info}/namespace_packages.txt +0 -0
- {karton_core-5.6.1.dist-info → karton_core-5.8.0.dist-info}/top_level.txt +0 -0
karton/core/__version__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "5.
|
1
|
+
__version__ = "5.8.0"
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import sys
|
2
|
+
|
3
|
+
if sys.version_info < (3, 11, 0):
|
4
|
+
raise ImportError("karton.core.asyncio is only compatible with Python 3.11+")
|
5
|
+
|
6
|
+
from karton.core.config import Config
|
7
|
+
from karton.core.task import Task
|
8
|
+
|
9
|
+
from .karton import Consumer, Karton, Producer
|
10
|
+
from .resource import LocalResource, RemoteResource, Resource
|
11
|
+
|
12
|
+
__all__ = [
|
13
|
+
"Karton",
|
14
|
+
"Producer",
|
15
|
+
"Consumer",
|
16
|
+
"Task",
|
17
|
+
"Config",
|
18
|
+
"LocalResource",
|
19
|
+
"Resource",
|
20
|
+
"RemoteResource",
|
21
|
+
]
|
@@ -0,0 +1,370 @@
|
|
1
|
+
import json
|
2
|
+
import logging
|
3
|
+
import time
|
4
|
+
from typing import IO, Any, Dict, List, Optional, Tuple, Union
|
5
|
+
|
6
|
+
import aioboto3
|
7
|
+
from aiobotocore.credentials import ContainerProvider, InstanceMetadataProvider
|
8
|
+
from aiobotocore.session import ClientCreatorContext, get_session
|
9
|
+
from aiobotocore.utils import InstanceMetadataFetcher
|
10
|
+
from redis.asyncio import Redis
|
11
|
+
from redis.asyncio.client import Pipeline
|
12
|
+
from redis.exceptions import AuthenticationError
|
13
|
+
|
14
|
+
from karton.core import Config, Task
|
15
|
+
from karton.core.asyncio.resource import RemoteResource
|
16
|
+
from karton.core.backend import (
|
17
|
+
KARTON_BINDS_HSET,
|
18
|
+
KARTON_TASK_NAMESPACE,
|
19
|
+
KARTON_TASKS_QUEUE,
|
20
|
+
KartonBackendBase,
|
21
|
+
KartonBind,
|
22
|
+
KartonMetrics,
|
23
|
+
KartonServiceInfo,
|
24
|
+
)
|
25
|
+
from karton.core.task import TaskState
|
26
|
+
|
27
|
+
logger = logging.getLogger(__name__)
|
28
|
+
|
29
|
+
|
30
|
+
class KartonAsyncBackend(KartonBackendBase):
|
31
|
+
def __init__(
|
32
|
+
self,
|
33
|
+
config: Config,
|
34
|
+
identity: Optional[str] = None,
|
35
|
+
service_info: Optional[KartonServiceInfo] = None,
|
36
|
+
) -> None:
|
37
|
+
super().__init__(config, identity, service_info)
|
38
|
+
self._redis: Optional[Redis] = None
|
39
|
+
self._s3_session: Optional[aioboto3.Session] = None
|
40
|
+
self._s3_iam_auth = False
|
41
|
+
|
42
|
+
@property
|
43
|
+
def redis(self) -> Redis:
|
44
|
+
if not self._redis:
|
45
|
+
raise RuntimeError("Call connect() first before using KartonAsyncBackend")
|
46
|
+
return self._redis
|
47
|
+
|
48
|
+
@property
|
49
|
+
def s3(self) -> ClientCreatorContext:
|
50
|
+
if not self._s3_session:
|
51
|
+
raise RuntimeError("Call connect() first before using KartonAsyncBackend")
|
52
|
+
endpoint = self.config.get("s3", "address")
|
53
|
+
if self._s3_iam_auth:
|
54
|
+
return self._s3_session.client(
|
55
|
+
"s3",
|
56
|
+
endpoint_url=endpoint,
|
57
|
+
)
|
58
|
+
else:
|
59
|
+
access_key = self.config.get("s3", "access_key")
|
60
|
+
secret_key = self.config.get("s3", "secret_key")
|
61
|
+
return self._s3_session.client(
|
62
|
+
"s3",
|
63
|
+
endpoint_url=endpoint,
|
64
|
+
aws_access_key_id=access_key,
|
65
|
+
aws_secret_access_key=secret_key,
|
66
|
+
)
|
67
|
+
|
68
|
+
async def connect(self):
|
69
|
+
if self._redis is not None or self._s3_session is not None:
|
70
|
+
# Already connected
|
71
|
+
return
|
72
|
+
self._redis = await self.make_redis(
|
73
|
+
self.config, identity=self.identity, service_info=self.service_info
|
74
|
+
)
|
75
|
+
|
76
|
+
endpoint = self.config.get("s3", "address")
|
77
|
+
access_key = self.config.get("s3", "access_key")
|
78
|
+
secret_key = self.config.get("s3", "secret_key")
|
79
|
+
iam_auth = self.config.getboolean("s3", "iam_auth")
|
80
|
+
|
81
|
+
if not endpoint:
|
82
|
+
raise RuntimeError("Attempting to get S3 client without an endpoint set")
|
83
|
+
|
84
|
+
if access_key and secret_key and iam_auth:
|
85
|
+
logger.warning(
|
86
|
+
"Warning: iam is turned on and both S3 access key and secret key are"
|
87
|
+
" provided"
|
88
|
+
)
|
89
|
+
|
90
|
+
if iam_auth:
|
91
|
+
s3_client_creator = await self.iam_auth_s3()
|
92
|
+
if s3_client_creator:
|
93
|
+
self._s3_iam_auth = True
|
94
|
+
self._s3_session = s3_client_creator
|
95
|
+
return
|
96
|
+
|
97
|
+
if access_key is None or secret_key is None:
|
98
|
+
raise RuntimeError(
|
99
|
+
"Attempting to get S3 client without an access_key/secret_key set"
|
100
|
+
)
|
101
|
+
|
102
|
+
session = aioboto3.Session()
|
103
|
+
self._s3_session = session
|
104
|
+
|
105
|
+
async def iam_auth_s3(self):
|
106
|
+
boto_session = get_session()
|
107
|
+
iam_providers = [
|
108
|
+
ContainerProvider(),
|
109
|
+
InstanceMetadataProvider(
|
110
|
+
iam_role_fetcher=InstanceMetadataFetcher(timeout=1000, num_attempts=2)
|
111
|
+
),
|
112
|
+
]
|
113
|
+
|
114
|
+
for provider in iam_providers:
|
115
|
+
creds = await provider.load()
|
116
|
+
if creds:
|
117
|
+
boto_session._credentials = creds # type: ignore
|
118
|
+
return aioboto3.Session(botocore_session=boto_session)
|
119
|
+
|
120
|
+
@staticmethod
|
121
|
+
async def make_redis(
|
122
|
+
config,
|
123
|
+
identity: Optional[str] = None,
|
124
|
+
service_info: Optional[KartonServiceInfo] = None,
|
125
|
+
) -> Redis:
|
126
|
+
"""
|
127
|
+
Create and test a Redis connection.
|
128
|
+
|
129
|
+
:param config: The karton configuration
|
130
|
+
:param identity: Karton service identity
|
131
|
+
:param service_info: Additional service identity metadata
|
132
|
+
:return: Redis connection
|
133
|
+
"""
|
134
|
+
if service_info is not None:
|
135
|
+
client_name: Optional[str] = service_info.make_client_name()
|
136
|
+
else:
|
137
|
+
client_name = identity
|
138
|
+
|
139
|
+
redis_args = {
|
140
|
+
"host": config["redis"]["host"],
|
141
|
+
"port": config.getint("redis", "port", 6379),
|
142
|
+
"db": config.getint("redis", "db", 0),
|
143
|
+
"username": config.get("redis", "username"),
|
144
|
+
"password": config.get("redis", "password"),
|
145
|
+
"client_name": client_name,
|
146
|
+
# set socket_timeout to None if set to 0
|
147
|
+
"socket_timeout": config.getint("redis", "socket_timeout", 30) or None,
|
148
|
+
"decode_responses": True,
|
149
|
+
}
|
150
|
+
try:
|
151
|
+
rs = Redis(**redis_args)
|
152
|
+
await rs.ping()
|
153
|
+
except AuthenticationError:
|
154
|
+
# Maybe we've sent a wrong password.
|
155
|
+
# Or maybe the server is not (yet) password protected
|
156
|
+
# To make smooth transition possible, try to login insecurely
|
157
|
+
del redis_args["password"]
|
158
|
+
rs = Redis(**redis_args)
|
159
|
+
await rs.ping()
|
160
|
+
return rs
|
161
|
+
|
162
|
+
def unserialize_resource(self, resource_spec: Dict[str, Any]) -> RemoteResource:
|
163
|
+
"""
|
164
|
+
Unserializes resource into a RemoteResource object bound with current backend
|
165
|
+
|
166
|
+
:param resource_spec: Resource specification
|
167
|
+
:return: RemoteResource object
|
168
|
+
"""
|
169
|
+
return RemoteResource.from_dict(resource_spec, backend=self)
|
170
|
+
|
171
|
+
async def register_task(self, task: Task, pipe: Optional[Pipeline] = None) -> None:
|
172
|
+
"""
|
173
|
+
Register or update task in Redis.
|
174
|
+
|
175
|
+
:param task: Task object
|
176
|
+
:param pipe: Optional pipeline object if operation is a part of pipeline
|
177
|
+
"""
|
178
|
+
rs = pipe or self.redis
|
179
|
+
await rs.set(f"{KARTON_TASK_NAMESPACE}:{task.uid}", task.serialize())
|
180
|
+
|
181
|
+
async def set_task_status(
|
182
|
+
self, task: Task, status: TaskState, pipe: Optional[Pipeline] = None
|
183
|
+
) -> None:
|
184
|
+
"""
|
185
|
+
Request task status change to be applied by karton-system
|
186
|
+
|
187
|
+
:param task: Task object
|
188
|
+
:param status: New task status (TaskState)
|
189
|
+
:param pipe: Optional pipeline object if operation is a part of pipeline
|
190
|
+
"""
|
191
|
+
if task.status == status:
|
192
|
+
return
|
193
|
+
task.status = status
|
194
|
+
task.last_update = time.time()
|
195
|
+
await self.register_task(task, pipe=pipe)
|
196
|
+
|
197
|
+
async def register_bind(self, bind: KartonBind) -> Optional[KartonBind]:
|
198
|
+
"""
|
199
|
+
Register bind for Karton service and return the old one
|
200
|
+
|
201
|
+
:param bind: KartonBind object with bind definition
|
202
|
+
:return: Old KartonBind that was registered under this identity
|
203
|
+
"""
|
204
|
+
async with self.redis.pipeline(transaction=True) as pipe:
|
205
|
+
await pipe.hget(KARTON_BINDS_HSET, bind.identity)
|
206
|
+
await pipe.hset(KARTON_BINDS_HSET, bind.identity, self.serialize_bind(bind))
|
207
|
+
old_serialized_bind, _ = await pipe.execute()
|
208
|
+
|
209
|
+
if old_serialized_bind:
|
210
|
+
return self.unserialize_bind(bind.identity, old_serialized_bind)
|
211
|
+
else:
|
212
|
+
return None
|
213
|
+
|
214
|
+
async def get_bind(self, identity: str) -> KartonBind:
|
215
|
+
"""
|
216
|
+
Get bind object for given identity
|
217
|
+
|
218
|
+
:param identity: Karton service identity
|
219
|
+
:return: KartonBind object
|
220
|
+
"""
|
221
|
+
return self.unserialize_bind(
|
222
|
+
identity, await self.redis.hget(KARTON_BINDS_HSET, identity)
|
223
|
+
)
|
224
|
+
|
225
|
+
async def produce_unrouted_task(self, task: Task) -> None:
|
226
|
+
"""
|
227
|
+
Add given task to unrouted task (``karton.tasks``) queue
|
228
|
+
|
229
|
+
Task must be registered before with :py:meth:`register_task`
|
230
|
+
|
231
|
+
:param task: Task object
|
232
|
+
"""
|
233
|
+
await self.redis.rpush(KARTON_TASKS_QUEUE, task.uid)
|
234
|
+
|
235
|
+
async def consume_queues(
|
236
|
+
self, queues: Union[str, List[str]], timeout: int = 0
|
237
|
+
) -> Optional[Tuple[str, str]]:
|
238
|
+
"""
|
239
|
+
Get item from queues (ordered from the most to the least prioritized)
|
240
|
+
If there are no items, wait until one appear.
|
241
|
+
|
242
|
+
:param queues: Redis queue name or list of names
|
243
|
+
:param timeout: Waiting for item timeout (default: 0 = wait forever)
|
244
|
+
:return: Tuple of [queue_name, item] objects or None if timeout has been reached
|
245
|
+
"""
|
246
|
+
return await self.redis.blpop(queues, timeout=timeout)
|
247
|
+
|
248
|
+
async def get_task(self, task_uid: str) -> Optional[Task]:
|
249
|
+
"""
|
250
|
+
Get task object with given identifier
|
251
|
+
|
252
|
+
:param task_uid: Task identifier
|
253
|
+
:return: Task object
|
254
|
+
"""
|
255
|
+
task_data = await self.redis.get(f"{KARTON_TASK_NAMESPACE}:{task_uid}")
|
256
|
+
if not task_data:
|
257
|
+
return None
|
258
|
+
return Task.unserialize(
|
259
|
+
task_data, resource_unserializer=self.unserialize_resource
|
260
|
+
)
|
261
|
+
|
262
|
+
async def consume_routed_task(
|
263
|
+
self, identity: str, timeout: int = 5
|
264
|
+
) -> Optional[Task]:
|
265
|
+
"""
|
266
|
+
Get routed task for given consumer identity.
|
267
|
+
|
268
|
+
If there are no tasks, blocks until new one appears or timeout is reached.
|
269
|
+
|
270
|
+
:param identity: Karton service identity
|
271
|
+
:param timeout: Waiting for task timeout (default: 5)
|
272
|
+
:return: Task object
|
273
|
+
"""
|
274
|
+
item = await self.consume_queues(
|
275
|
+
self.get_queue_names(identity),
|
276
|
+
timeout=timeout,
|
277
|
+
)
|
278
|
+
if not item:
|
279
|
+
return None
|
280
|
+
queue, data = item
|
281
|
+
return await self.get_task(data)
|
282
|
+
|
283
|
+
async def increment_metrics(
|
284
|
+
self, metric: KartonMetrics, identity: str, pipe: Optional[Pipeline] = None
|
285
|
+
) -> None:
|
286
|
+
"""
|
287
|
+
Increments metrics for given operation type and identity
|
288
|
+
|
289
|
+
:param metric: Operation metric type
|
290
|
+
:param identity: Related Karton service identity
|
291
|
+
:param pipe: Optional pipeline object if operation is a part of pipeline
|
292
|
+
"""
|
293
|
+
rs = pipe or self.redis
|
294
|
+
await rs.hincrby(metric.value, identity, 1)
|
295
|
+
|
296
|
+
async def upload_object(
|
297
|
+
self,
|
298
|
+
bucket: str,
|
299
|
+
object_uid: str,
|
300
|
+
content: Union[bytes, IO[bytes]],
|
301
|
+
) -> None:
|
302
|
+
"""
|
303
|
+
Upload resource object to underlying object storage (S3)
|
304
|
+
|
305
|
+
:param bucket: Bucket name
|
306
|
+
:param object_uid: Object identifier
|
307
|
+
:param content: Object content as bytes or file-like stream
|
308
|
+
"""
|
309
|
+
async with self.s3 as client:
|
310
|
+
await client.put_object(Bucket=bucket, Key=object_uid, Body=content)
|
311
|
+
|
312
|
+
async def upload_object_from_file(
|
313
|
+
self, bucket: str, object_uid: str, path: str
|
314
|
+
) -> None:
|
315
|
+
"""
|
316
|
+
Upload resource object file to underlying object storage
|
317
|
+
|
318
|
+
:param bucket: Bucket name
|
319
|
+
:param object_uid: Object identifier
|
320
|
+
:param path: Path to the object content
|
321
|
+
"""
|
322
|
+
async with self.s3 as client:
|
323
|
+
with open(path, "rb") as f:
|
324
|
+
await client.put_object(Bucket=bucket, Key=object_uid, Body=f)
|
325
|
+
|
326
|
+
async def download_object(self, bucket: str, object_uid: str) -> bytes:
|
327
|
+
"""
|
328
|
+
Download resource object from object storage.
|
329
|
+
|
330
|
+
:param bucket: Bucket name
|
331
|
+
:param object_uid: Object identifier
|
332
|
+
:return: Content bytes
|
333
|
+
"""
|
334
|
+
async with self.s3 as client:
|
335
|
+
obj = await client.get_object(Bucket=bucket, Key=object_uid)
|
336
|
+
return await obj["Body"].read()
|
337
|
+
|
338
|
+
async def download_object_to_file(
|
339
|
+
self, bucket: str, object_uid: str, path: str
|
340
|
+
) -> None:
|
341
|
+
"""
|
342
|
+
Download resource object from object storage to file
|
343
|
+
|
344
|
+
:param bucket: Bucket name
|
345
|
+
:param object_uid: Object identifier
|
346
|
+
:param path: Target file path
|
347
|
+
"""
|
348
|
+
async with self.s3 as client:
|
349
|
+
await client.download_file(Bucket=bucket, Key=object_uid, Filename=path)
|
350
|
+
|
351
|
+
async def produce_log(
|
352
|
+
self,
|
353
|
+
log_record: Dict[str, Any],
|
354
|
+
logger_name: str,
|
355
|
+
level: str,
|
356
|
+
) -> bool:
|
357
|
+
"""
|
358
|
+
Push new log record to the logs channel
|
359
|
+
|
360
|
+
:param log_record: Dict with log record
|
361
|
+
:param logger_name: Logger name
|
362
|
+
:param level: Log level
|
363
|
+
:return: True if any active log consumer received log record
|
364
|
+
"""
|
365
|
+
return (
|
366
|
+
await self.redis.publish(
|
367
|
+
self._log_channel(logger_name, level), json.dumps(log_record)
|
368
|
+
)
|
369
|
+
> 0
|
370
|
+
)
|
@@ -0,0 +1,133 @@
|
|
1
|
+
import abc
|
2
|
+
import asyncio
|
3
|
+
import signal
|
4
|
+
from asyncio import CancelledError
|
5
|
+
from typing import Optional
|
6
|
+
|
7
|
+
from karton.core import Task
|
8
|
+
from karton.core.__version__ import __version__
|
9
|
+
from karton.core.backend import KartonServiceInfo
|
10
|
+
from karton.core.base import ConfigMixin, LoggingMixin
|
11
|
+
from karton.core.config import Config
|
12
|
+
from karton.core.task import get_current_task, set_current_task
|
13
|
+
from karton.core.utils import StrictClassMethod
|
14
|
+
|
15
|
+
from .backend import KartonAsyncBackend
|
16
|
+
from .logger import KartonAsyncLogHandler
|
17
|
+
|
18
|
+
|
19
|
+
class KartonAsyncBase(abc.ABC, ConfigMixin, LoggingMixin):
|
20
|
+
"""
|
21
|
+
Base class for all Karton services
|
22
|
+
|
23
|
+
You can set an informative version information by setting the ``version`` class
|
24
|
+
attribute.
|
25
|
+
"""
|
26
|
+
|
27
|
+
#: Karton service identity
|
28
|
+
identity: str = ""
|
29
|
+
#: Karton service version
|
30
|
+
version: Optional[str] = None
|
31
|
+
#: Include extended service information for non-consumer services
|
32
|
+
with_service_info: bool = False
|
33
|
+
|
34
|
+
def __init__(
|
35
|
+
self,
|
36
|
+
config: Optional[Config] = None,
|
37
|
+
identity: Optional[str] = None,
|
38
|
+
backend: Optional[KartonAsyncBackend] = None,
|
39
|
+
) -> None:
|
40
|
+
ConfigMixin.__init__(self, config, identity)
|
41
|
+
|
42
|
+
self.service_info = None
|
43
|
+
if self.identity is not None and self.with_service_info:
|
44
|
+
self.service_info = KartonServiceInfo(
|
45
|
+
identity=self.identity,
|
46
|
+
karton_version=__version__,
|
47
|
+
service_version=self.version,
|
48
|
+
)
|
49
|
+
|
50
|
+
self.backend = backend or KartonAsyncBackend(
|
51
|
+
self.config, identity=self.identity, service_info=self.service_info
|
52
|
+
)
|
53
|
+
|
54
|
+
log_handler = KartonAsyncLogHandler(backend=self.backend, channel=self.identity)
|
55
|
+
LoggingMixin.__init__(
|
56
|
+
self,
|
57
|
+
log_handler,
|
58
|
+
log_format="[%(asctime)s][%(levelname)s][%(task_id)s] %(message)s",
|
59
|
+
)
|
60
|
+
|
61
|
+
async def connect(self) -> None:
|
62
|
+
await self.backend.connect()
|
63
|
+
|
64
|
+
@property
|
65
|
+
def current_task(self) -> Optional[Task]:
|
66
|
+
return get_current_task()
|
67
|
+
|
68
|
+
@current_task.setter
|
69
|
+
def current_task(self, task: Optional[Task]):
|
70
|
+
set_current_task(task)
|
71
|
+
|
72
|
+
|
73
|
+
class KartonAsyncServiceBase(KartonAsyncBase):
|
74
|
+
"""
|
75
|
+
Karton base class for looping services.
|
76
|
+
|
77
|
+
You can set an informative version information by setting the ``version`` class
|
78
|
+
attribute
|
79
|
+
|
80
|
+
:param config: Karton config to use for service configuration
|
81
|
+
:param identity: Karton service identity to use
|
82
|
+
:param backend: Karton backend to use
|
83
|
+
"""
|
84
|
+
|
85
|
+
def __init__(
|
86
|
+
self,
|
87
|
+
config: Optional[Config] = None,
|
88
|
+
identity: Optional[str] = None,
|
89
|
+
backend: Optional[KartonAsyncBackend] = None,
|
90
|
+
) -> None:
|
91
|
+
super().__init__(
|
92
|
+
config=config,
|
93
|
+
identity=identity,
|
94
|
+
backend=backend,
|
95
|
+
)
|
96
|
+
self.setup_logger()
|
97
|
+
self._loop_coro: Optional[asyncio.Task] = None
|
98
|
+
|
99
|
+
def _do_shutdown(self) -> None:
|
100
|
+
self.log.info("Got signal, shutting down...")
|
101
|
+
if self._loop_coro is not None:
|
102
|
+
self._loop_coro.cancel()
|
103
|
+
|
104
|
+
@abc.abstractmethod
|
105
|
+
async def _loop(self) -> None:
|
106
|
+
raise NotImplementedError
|
107
|
+
|
108
|
+
# Base class for Karton services
|
109
|
+
async def loop(self) -> None:
|
110
|
+
if self.enable_publish_log and hasattr(self.log_handler, "start_consuming"):
|
111
|
+
self.log_handler.start_consuming()
|
112
|
+
await self.connect()
|
113
|
+
event_loop = asyncio.get_event_loop()
|
114
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
115
|
+
event_loop.add_signal_handler(sig, self._do_shutdown)
|
116
|
+
self._loop_coro = asyncio.create_task(self._loop())
|
117
|
+
try:
|
118
|
+
await self._loop_coro
|
119
|
+
finally:
|
120
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
121
|
+
event_loop.remove_signal_handler(sig)
|
122
|
+
if self.enable_publish_log and hasattr(self.log_handler, "stop_consuming"):
|
123
|
+
await self.log_handler.stop_consuming()
|
124
|
+
|
125
|
+
@StrictClassMethod
|
126
|
+
def main(cls) -> None:
|
127
|
+
"""Main method invoked from CLI."""
|
128
|
+
service = cls.karton_from_args()
|
129
|
+
try:
|
130
|
+
asyncio.run(service.loop())
|
131
|
+
except CancelledError:
|
132
|
+
# Swallow cancellation, we're done!
|
133
|
+
pass
|