canvas 0.35.0__py3-none-any.whl → 0.36.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.
Potentially problematic release.
This version of canvas might be problematic. Click here for more details.
- {canvas-0.35.0.dist-info → canvas-0.36.0.dist-info}/METADATA +2 -2
- {canvas-0.35.0.dist-info → canvas-0.36.0.dist-info}/RECORD +42 -34
- canvas_cli/apps/plugin/plugin.py +3 -2
- canvas_cli/utils/validators/manifest_schema.py +15 -1
- canvas_generated/messages/effects_pb2.py +2 -2
- canvas_generated/messages/effects_pb2.pyi +8 -0
- canvas_generated/messages/events_pb2.py +2 -2
- canvas_generated/messages/events_pb2.pyi +14 -0
- canvas_sdk/caching/__init__.py +1 -0
- canvas_sdk/caching/base.py +127 -0
- canvas_sdk/caching/client.py +24 -0
- canvas_sdk/caching/exceptions.py +21 -0
- canvas_sdk/caching/plugins.py +20 -0
- canvas_sdk/caching/utils.py +28 -0
- canvas_sdk/commands/__init__.py +2 -0
- canvas_sdk/commands/commands/chart_section_review.py +23 -0
- canvas_sdk/effects/launch_modal.py +1 -0
- canvas_sdk/effects/simple_api.py +61 -2
- canvas_sdk/handlers/simple_api/websocket.py +79 -0
- canvas_sdk/value_set/custom.py +1 -1
- canvas_sdk/value_set/v2022/allergy.py +1 -1
- canvas_sdk/value_set/v2022/assessment.py +1 -1
- canvas_sdk/value_set/v2022/communication.py +1 -1
- canvas_sdk/value_set/v2022/condition.py +1 -1
- canvas_sdk/value_set/v2022/device.py +1 -1
- canvas_sdk/value_set/v2022/diagnostic_study.py +1 -1
- canvas_sdk/value_set/v2022/encounter.py +1 -1
- canvas_sdk/value_set/v2022/immunization.py +1 -1
- canvas_sdk/value_set/v2022/individual_characteristic.py +1 -1
- canvas_sdk/value_set/v2022/intervention.py +1 -1
- canvas_sdk/value_set/v2022/laboratory_test.py +1 -1
- canvas_sdk/value_set/v2022/medication.py +1 -1
- canvas_sdk/value_set/v2022/physical_exam.py +1 -1
- canvas_sdk/value_set/v2022/procedure.py +1 -1
- plugin_runner/plugin_runner.py +72 -90
- plugin_runner/sandbox.py +1 -0
- protobufs/canvas_generated/messages/effects.proto +6 -0
- protobufs/canvas_generated/messages/events.proto +8 -12
- pubsub/pubsub.py +10 -1
- settings.py +32 -0
- {canvas-0.35.0.dist-info → canvas-0.36.0.dist-info}/WHEEL +0 -0
- {canvas-0.35.0.dist-info → canvas-0.36.0.dist-info}/entry_points.txt +0 -0
plugin_runner/plugin_runner.py
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
import json
|
|
3
2
|
import os
|
|
4
3
|
import pathlib
|
|
5
4
|
import pickle
|
|
6
5
|
import pkgutil
|
|
7
6
|
import sys
|
|
7
|
+
import threading
|
|
8
8
|
import traceback
|
|
9
9
|
from collections import defaultdict
|
|
10
|
-
from collections.abc import
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
11
12
|
from http import HTTPStatus
|
|
13
|
+
from time import sleep
|
|
12
14
|
from typing import Any, TypedDict
|
|
13
15
|
|
|
14
16
|
import grpc
|
|
15
|
-
import redis
|
|
17
|
+
import redis
|
|
16
18
|
import sentry_sdk
|
|
17
|
-
from
|
|
18
|
-
from
|
|
19
|
+
from django.core.signals import request_finished, request_started
|
|
20
|
+
from redis.backoff import ExponentialBackoff
|
|
21
|
+
from redis.exceptions import ConnectionError, TimeoutError
|
|
22
|
+
from redis.retry import Retry
|
|
19
23
|
from sentry_sdk.integrations.logging import ignore_logger
|
|
20
24
|
|
|
25
|
+
import settings
|
|
21
26
|
from canvas_generated.messages.effects_pb2 import EffectType
|
|
22
27
|
from canvas_generated.messages.plugins_pb2 import ReloadPluginsRequest, ReloadPluginsResponse
|
|
23
28
|
from canvas_generated.services.plugin_runner_pb2_grpc import (
|
|
@@ -27,9 +32,10 @@ from canvas_generated.services.plugin_runner_pb2_grpc import (
|
|
|
27
32
|
from canvas_sdk.effects import Effect
|
|
28
33
|
from canvas_sdk.effects.simple_api import Response
|
|
29
34
|
from canvas_sdk.events import Event, EventRequest, EventResponse, EventType
|
|
35
|
+
from canvas_sdk.handlers.simple_api.websocket import DenyConnection
|
|
30
36
|
from canvas_sdk.protocols import ClinicalQualityMeasure
|
|
31
37
|
from canvas_sdk.utils import metrics
|
|
32
|
-
from canvas_sdk.utils.metrics import measured
|
|
38
|
+
from canvas_sdk.utils.metrics import measured
|
|
33
39
|
from logger import log
|
|
34
40
|
from plugin_runner.authentication import token_for_plugin
|
|
35
41
|
from plugin_runner.installation import install_plugins
|
|
@@ -39,7 +45,6 @@ from settings import (
|
|
|
39
45
|
CUSTOMER_IDENTIFIER,
|
|
40
46
|
ENV,
|
|
41
47
|
IS_PRODUCTION_CUSTOMER,
|
|
42
|
-
IS_TESTING,
|
|
43
48
|
MANIFEST_FILE_NAME,
|
|
44
49
|
PLUGIN_DIRECTORY,
|
|
45
50
|
REDIS_ENDPOINT,
|
|
@@ -149,33 +154,12 @@ class PluginManifest(TypedDict):
|
|
|
149
154
|
readme: str
|
|
150
155
|
|
|
151
156
|
|
|
152
|
-
@sync_to_async
|
|
153
|
-
def reconnect_if_needed() -> None:
|
|
154
|
-
"""
|
|
155
|
-
Reconnect to the database if the connection has been closed.
|
|
156
|
-
|
|
157
|
-
NOTE: Django database functions are not async-safe and must be called
|
|
158
|
-
via e.g. sync_to_async. They will silently fail and block the handler
|
|
159
|
-
if called in an async context directly.
|
|
160
|
-
"""
|
|
161
|
-
if IS_TESTING:
|
|
162
|
-
return
|
|
163
|
-
|
|
164
|
-
connection = connections["default"]
|
|
165
|
-
|
|
166
|
-
if not connection.is_usable():
|
|
167
|
-
log.debug("Connection was unusable, reconnecting...")
|
|
168
|
-
connection.connect()
|
|
169
|
-
|
|
170
|
-
|
|
171
157
|
class PluginRunner(PluginRunnerServicer):
|
|
172
158
|
"""This process runs provided plugins that register interest in incoming events."""
|
|
173
159
|
|
|
174
160
|
sandbox: Sandbox
|
|
175
161
|
|
|
176
|
-
|
|
177
|
-
self, request: EventRequest, context: Any
|
|
178
|
-
) -> AsyncGenerator[EventResponse, None]:
|
|
162
|
+
def HandleEvent(self, request: EventRequest, context: Any) -> Iterable[EventResponse]:
|
|
179
163
|
"""This is invoked when an event comes in."""
|
|
180
164
|
event = Event(request)
|
|
181
165
|
with metrics.measure(
|
|
@@ -190,13 +174,18 @@ class PluginRunner(PluginRunnerServicer):
|
|
|
190
174
|
sentry_sdk.set_tag("event-name", event_name)
|
|
191
175
|
|
|
192
176
|
if relevant_plugins:
|
|
193
|
-
|
|
177
|
+
# Send the Django request_started signal
|
|
178
|
+
request_started.send(sender=self.__class__)
|
|
194
179
|
|
|
195
180
|
if event_type in [EventType.PLUGIN_CREATED, EventType.PLUGIN_UPDATED]:
|
|
196
181
|
plugin_name = event.target.id
|
|
197
182
|
# filter only for the plugin(s) that were created/updated
|
|
198
183
|
relevant_plugins = [p for p in relevant_plugins if p.startswith(f"{plugin_name}:")]
|
|
199
|
-
elif event_type in {
|
|
184
|
+
elif event_type in {
|
|
185
|
+
EventType.SIMPLE_API_AUTHENTICATE,
|
|
186
|
+
EventType.SIMPLE_API_REQUEST,
|
|
187
|
+
EventType.SIMPLE_API_WEBSOCKET_AUTHENTICATE,
|
|
188
|
+
}:
|
|
200
189
|
# The target plugin's name will be part of the home-app URL path, so other plugins that
|
|
201
190
|
# respond to SimpleAPI request events are not relevant
|
|
202
191
|
plugin_name = event.context["plugin_name"]
|
|
@@ -238,7 +227,7 @@ class PluginRunner(PluginRunnerServicer):
|
|
|
238
227
|
"event": event_name,
|
|
239
228
|
},
|
|
240
229
|
):
|
|
241
|
-
_effects =
|
|
230
|
+
_effects = handler.compute()
|
|
242
231
|
effects = [
|
|
243
232
|
Effect(
|
|
244
233
|
type=effect.type,
|
|
@@ -283,27 +272,42 @@ class PluginRunner(PluginRunnerServicer):
|
|
|
283
272
|
f" {event.context['path']}"
|
|
284
273
|
)
|
|
285
274
|
effect_list = [Response(status_code=HTTPStatus.INTERNAL_SERVER_ERROR).apply()]
|
|
275
|
+
if event.type == EventType.SIMPLE_API_WEBSOCKET_AUTHENTICATE:
|
|
276
|
+
if len(relevant_plugin_handlers) == 0:
|
|
277
|
+
effect_list = [DenyConnection().apply()]
|
|
278
|
+
elif len(relevant_plugin_handlers) > 1:
|
|
279
|
+
log.error(
|
|
280
|
+
f"Multiple handlers responded to {EventType.Name(EventType.SIMPLE_API_WEBSOCKET_AUTHENTICATE)}"
|
|
281
|
+
f" {event.context['channel']}"
|
|
282
|
+
)
|
|
283
|
+
effect_list = [DenyConnection().apply()]
|
|
286
284
|
|
|
287
285
|
# Don't log anything if a plugin handler didn't actually run.
|
|
288
286
|
if relevant_plugins:
|
|
287
|
+
# Send the Django request_finished signal
|
|
288
|
+
request_finished.send(sender=self.__class__)
|
|
289
|
+
|
|
289
290
|
log.info(f"Responded to Event {event_name}.")
|
|
290
291
|
|
|
291
292
|
yield EventResponse(success=True, effects=effect_list)
|
|
292
293
|
|
|
293
|
-
|
|
294
|
+
def ReloadPlugins(
|
|
294
295
|
self, request: ReloadPluginsRequest, context: Any
|
|
295
|
-
) ->
|
|
296
|
+
) -> Iterable[ReloadPluginsResponse]:
|
|
296
297
|
"""This is invoked when we need to reload plugins."""
|
|
297
298
|
log.info("Reloading plugins...")
|
|
298
299
|
try:
|
|
299
|
-
|
|
300
|
+
publish_message(message={"action": "reload"})
|
|
300
301
|
except ImportError:
|
|
301
302
|
yield ReloadPluginsResponse(success=False)
|
|
302
303
|
else:
|
|
303
304
|
yield ReloadPluginsResponse(success=True)
|
|
304
305
|
|
|
305
306
|
|
|
306
|
-
|
|
307
|
+
STOP_SYNCHRONIZER = threading.Event()
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def synchronize_plugins(run_once: bool = False) -> None:
|
|
307
311
|
"""
|
|
308
312
|
Listen for messages on the pubsub channel that will indicate it is
|
|
309
313
|
necessary to reinstall and reload plugins.
|
|
@@ -312,16 +316,10 @@ async def synchronize_plugins(run_once: bool = False) -> None:
|
|
|
312
316
|
|
|
313
317
|
_, pubsub = get_client()
|
|
314
318
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
while True:
|
|
318
|
-
message = await pubsub.get_message(ignore_subscribe_messages=True, timeout=5.0)
|
|
319
|
+
pubsub.psubscribe(CHANNEL_NAME)
|
|
319
320
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if not pubsub.connection.is_connected: # type: ignore
|
|
323
|
-
log.info("synchronize_plugins: reconnecting to Redis")
|
|
324
|
-
await pubsub.connection.connect() # type: ignore
|
|
321
|
+
while not STOP_SYNCHRONIZER.is_set():
|
|
322
|
+
message = pubsub.get_message(ignore_subscribe_messages=True, timeout=5.0)
|
|
325
323
|
|
|
326
324
|
if message is None:
|
|
327
325
|
continue
|
|
@@ -357,21 +355,21 @@ async def synchronize_plugins(run_once: bool = False) -> None:
|
|
|
357
355
|
break
|
|
358
356
|
|
|
359
357
|
|
|
360
|
-
|
|
358
|
+
def synchronize_plugins_and_report_errors() -> None:
|
|
361
359
|
"""
|
|
362
360
|
Run synchronize_plugins() in perpetuity and report any encountered errors.
|
|
363
361
|
"""
|
|
364
362
|
log.info("synchronize_plugins: starting loop...")
|
|
365
363
|
|
|
366
|
-
while
|
|
364
|
+
while not STOP_SYNCHRONIZER.is_set():
|
|
367
365
|
try:
|
|
368
|
-
|
|
366
|
+
synchronize_plugins()
|
|
369
367
|
except Exception as e:
|
|
370
368
|
log.error(f"synchronize_plugins: error: {e}")
|
|
371
369
|
sentry_sdk.capture_exception(e)
|
|
372
370
|
|
|
373
371
|
# don't crush redis if we're retrying in a tight loop
|
|
374
|
-
|
|
372
|
+
sleep(0.5)
|
|
375
373
|
|
|
376
374
|
|
|
377
375
|
def validate_effects(effects: list[Effect]) -> list[Effect]:
|
|
@@ -434,17 +432,22 @@ def find_modules(base_path: pathlib.Path, prefix: str | None = None) -> list[str
|
|
|
434
432
|
return modules
|
|
435
433
|
|
|
436
434
|
|
|
437
|
-
|
|
435
|
+
def publish_message(message: dict) -> None:
|
|
438
436
|
"""Publish a message to the pubsub channel."""
|
|
439
437
|
log.info(f'Publishing message to pubsub channel "{CHANNEL_NAME}"')
|
|
440
438
|
client, _ = get_client()
|
|
441
439
|
|
|
442
|
-
|
|
440
|
+
client.publish(CHANNEL_NAME, pickle.dumps(message))
|
|
443
441
|
|
|
444
442
|
|
|
445
443
|
def get_client() -> tuple[redis.Redis, redis.client.PubSub]:
|
|
446
|
-
"""Return
|
|
447
|
-
client = redis.
|
|
444
|
+
"""Return a Redis client and pubsub object."""
|
|
445
|
+
client = redis.from_url(
|
|
446
|
+
REDIS_ENDPOINT,
|
|
447
|
+
retry=Retry(backoff=ExponentialBackoff(), retries=10),
|
|
448
|
+
retry_on_error=[ConnectionError, TimeoutError, ConnectionResetError],
|
|
449
|
+
health_check_interval=1,
|
|
450
|
+
)
|
|
448
451
|
pubsub = client.pubsub()
|
|
449
452
|
|
|
450
453
|
return client, pubsub
|
|
@@ -594,62 +597,41 @@ def load_plugins(specified_plugin_paths: list[str] | None = None) -> None:
|
|
|
594
597
|
|
|
595
598
|
refresh_event_type_map()
|
|
596
599
|
|
|
597
|
-
for key in EventType.keys(): # noqa: SIM118
|
|
598
|
-
value = len(EVENT_HANDLER_MAP[key]) if key in EVENT_HANDLER_MAP else 0
|
|
599
|
-
statsd_client.gauge("plugins.event_handler_count", value, tags={"event": key})
|
|
600
600
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
async def serve(specified_plugin_paths: list[str] | None = None) -> None:
|
|
606
|
-
"""Run the server."""
|
|
601
|
+
# NOTE: specified_plugin_paths powers the `canvas run-plugins` command
|
|
602
|
+
def main(specified_plugin_paths: list[str] | None = None) -> None:
|
|
603
|
+
"""Run the server and the synchronize_plugins loop."""
|
|
607
604
|
port = "50051"
|
|
608
605
|
|
|
609
|
-
|
|
606
|
+
executor = ThreadPoolExecutor(max_workers=settings.PLUGIN_RUNNER_MAX_WORKERS)
|
|
607
|
+
server = grpc.server(thread_pool=executor)
|
|
610
608
|
server.add_insecure_port("127.0.0.1:" + port)
|
|
611
609
|
|
|
612
610
|
add_PluginRunnerServicer_to_server(PluginRunner(), server)
|
|
613
611
|
|
|
614
612
|
log.info(f"Starting server, listening on port {port}")
|
|
615
613
|
|
|
616
|
-
# Only install plugins if the plugin runner was not started
|
|
614
|
+
# Only install plugins and start the synchronizer thread if the plugin runner was not started
|
|
615
|
+
# from the CLI
|
|
616
|
+
synchronizer_thread = threading.Thread(target=synchronize_plugins_and_report_errors)
|
|
617
617
|
if specified_plugin_paths is None:
|
|
618
618
|
install_plugins()
|
|
619
|
+
STOP_SYNCHRONIZER.clear()
|
|
620
|
+
synchronizer_thread.start()
|
|
619
621
|
|
|
620
622
|
load_plugins(specified_plugin_paths)
|
|
621
623
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
async def server_graceful_shutdown() -> None:
|
|
625
|
-
log.info("Starting graceful shutdown...")
|
|
626
|
-
await server.stop(5)
|
|
627
|
-
|
|
628
|
-
_cleanup_coroutines.append(server_graceful_shutdown())
|
|
629
|
-
|
|
630
|
-
await server.wait_for_termination()
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
# NOTE: specified_plugin_paths powers the `canvas run-plugins` command
|
|
634
|
-
def main(specified_plugin_paths: list[str] | None = None) -> None:
|
|
635
|
-
"""Run the server and the synchronize_plugins loop."""
|
|
636
|
-
loop = asyncio.new_event_loop()
|
|
637
|
-
|
|
638
|
-
asyncio.set_event_loop(loop)
|
|
624
|
+
server.start()
|
|
639
625
|
|
|
640
626
|
try:
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
# Only start the synchronizer if the plugin runner was not started from the CLI
|
|
644
|
-
if specified_plugin_paths is None:
|
|
645
|
-
coroutines.append(synchronize_plugins_and_report_errors())
|
|
646
|
-
|
|
647
|
-
loop.run_until_complete(asyncio.gather(*coroutines))
|
|
627
|
+
server.wait_for_termination()
|
|
648
628
|
except KeyboardInterrupt:
|
|
649
629
|
pass
|
|
650
630
|
finally:
|
|
651
|
-
|
|
652
|
-
|
|
631
|
+
executor.shutdown(wait=True, cancel_futures=True)
|
|
632
|
+
if synchronizer_thread.is_alive():
|
|
633
|
+
STOP_SYNCHRONIZER.set()
|
|
634
|
+
synchronizer_thread.join()
|
|
653
635
|
|
|
654
636
|
|
|
655
637
|
if __name__ == "__main__":
|
plugin_runner/sandbox.py
CHANGED
|
@@ -233,6 +233,8 @@ enum EffectType {
|
|
|
233
233
|
COMMIT_ADJUST_PRESCRIPTION_COMMAND = 1303;
|
|
234
234
|
ENTER_IN_ERROR_ADJUST_PRESCRIPTION_COMMAND = 1304;
|
|
235
235
|
|
|
236
|
+
ORIGINATE_CHART_SECTION_REVIEW_COMMAND = 1400;
|
|
237
|
+
|
|
236
238
|
SHOW_ACTION_BUTTON = 1000;
|
|
237
239
|
|
|
238
240
|
PATIENT_PORTAL__FORM_RESULT = 2000;
|
|
@@ -260,6 +262,9 @@ enum EffectType {
|
|
|
260
262
|
LAUNCH_MODAL = 3000;
|
|
261
263
|
|
|
262
264
|
SIMPLE_API_RESPONSE = 4000;
|
|
265
|
+
SIMPLE_API_WEBSOCKET_ACCEPT = 4001;
|
|
266
|
+
SIMPLE_API_WEBSOCKET_DENY = 4002;
|
|
267
|
+
SIMPLE_API_WEBSOCKET_BROADCAST = 4003;
|
|
263
268
|
|
|
264
269
|
UPDATE_USER = 5000;
|
|
265
270
|
|
|
@@ -268,6 +273,7 @@ enum EffectType {
|
|
|
268
273
|
CREATE_SCHEDULE_EVENT = 6002;
|
|
269
274
|
|
|
270
275
|
CREATE_PATIENT = 6003;
|
|
276
|
+
|
|
271
277
|
}
|
|
272
278
|
|
|
273
279
|
message Effect {
|
|
@@ -244,18 +244,12 @@ enum EventType {
|
|
|
244
244
|
CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__PRE_SEARCH = 8012;
|
|
245
245
|
CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__POST_SEARCH = 8013;
|
|
246
246
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// CHART_SECTION_REVIEW_COMMAND__PRE_DELETE = 9006;
|
|
254
|
-
// CHART_SECTION_REVIEW_COMMAND__POST_DELETE = 9007;
|
|
255
|
-
// CHART_SECTION_REVIEW_COMMAND__PRE_ENTER_IN_ERROR = 9008;
|
|
256
|
-
// CHART_SECTION_REVIEW_COMMAND__POST_ENTER_IN_ERROR = 9009;
|
|
257
|
-
// CHART_SECTION_REVIEW_COMMAND__PRE_EXECUTE_ACTION = 9010;
|
|
258
|
-
// CHART_SECTION_REVIEW_COMMAND__POST_EXECUTE_ACTION = 9011;
|
|
247
|
+
CHART_SECTION_REVIEW_COMMAND__PRE_ORIGINATE = 9000;
|
|
248
|
+
CHART_SECTION_REVIEW_COMMAND__POST_ORIGINATE = 9001;
|
|
249
|
+
CHART_SECTION_REVIEW_COMMAND__PRE_ENTER_IN_ERROR = 9002;
|
|
250
|
+
CHART_SECTION_REVIEW_COMMAND__POST_ENTER_IN_ERROR = 9003;
|
|
251
|
+
CHART_SECTION_REVIEW_COMMAND__PRE_EXECUTE_ACTION = 9004;
|
|
252
|
+
CHART_SECTION_REVIEW_COMMAND__POST_EXECUTE_ACTION = 9005;
|
|
259
253
|
|
|
260
254
|
|
|
261
255
|
CLIPBOARD_COMMAND__PRE_ORIGINATE = 53000;
|
|
@@ -1093,6 +1087,8 @@ enum EventType {
|
|
|
1093
1087
|
|
|
1094
1088
|
SIMPLE_API_AUTHENTICATE = 130000;
|
|
1095
1089
|
SIMPLE_API_REQUEST = 130001;
|
|
1090
|
+
SIMPLE_API_WEBSOCKET_AUTHENTICATE = 130002;
|
|
1091
|
+
|
|
1096
1092
|
}
|
|
1097
1093
|
|
|
1098
1094
|
message Event {
|
pubsub/pubsub.py
CHANGED
|
@@ -2,6 +2,9 @@ import os
|
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
4
|
import redis
|
|
5
|
+
from redis.backoff import ExponentialBackoff
|
|
6
|
+
from redis.exceptions import ConnectionError, TimeoutError
|
|
7
|
+
from redis.retry import Retry
|
|
5
8
|
|
|
6
9
|
REDIS_ENDPOINT = os.getenv("REDIS_ENDPOINT")
|
|
7
10
|
CUSTOMER_IDENTIFIER = os.getenv("CUSTOMER_IDENTIFIER")
|
|
@@ -24,7 +27,13 @@ class PubSubBase:
|
|
|
24
27
|
|
|
25
28
|
def _create_client(self) -> redis.Redis | None:
|
|
26
29
|
if self.redis_endpoint and self.channel:
|
|
27
|
-
return redis.Redis.from_url(
|
|
30
|
+
return redis.Redis.from_url(
|
|
31
|
+
self.redis_endpoint,
|
|
32
|
+
decode_responses=True,
|
|
33
|
+
retry=Retry(backoff=ExponentialBackoff(), retries=10),
|
|
34
|
+
retry_on_error=[ConnectionError, TimeoutError, ConnectionResetError],
|
|
35
|
+
health_check_interval=1,
|
|
36
|
+
)
|
|
28
37
|
|
|
29
38
|
return None
|
|
30
39
|
|
settings.py
CHANGED
|
@@ -9,6 +9,7 @@ from env_tools import env_to_bool
|
|
|
9
9
|
load_dotenv()
|
|
10
10
|
|
|
11
11
|
BASE_DIR = Path(__file__).resolve().parent.resolve()
|
|
12
|
+
FOURTEEN_DAYS = 60 * 60 * 24 * 14
|
|
12
13
|
|
|
13
14
|
ENV = os.getenv("ENV", "development")
|
|
14
15
|
IS_PRODUCTION = ENV == "production"
|
|
@@ -24,6 +25,10 @@ INTEGRATION_TEST_CLIENT_SECRET = os.getenv("INTEGRATION_TEST_CLIENT_SECRET")
|
|
|
24
25
|
|
|
25
26
|
GRAPHQL_ENDPOINT = os.getenv("GRAPHQL_ENDPOINT", "http://localhost:8000/plugins-graphql")
|
|
26
27
|
REDIS_ENDPOINT = os.getenv("REDIS_ENDPOINT", f"redis://{APP_NAME}-redis:6379")
|
|
28
|
+
CANVAS_SDK_PLUGINS_CACHE_LOCATION = os.getenv(
|
|
29
|
+
"CANVAS_SDK_PLUGINS_CACHE_LOCATION", "plugin_io_plugins_cache"
|
|
30
|
+
)
|
|
31
|
+
CANVAS_SDK_CACHE_TIMEOUT_SECONDS = int(os.getenv("CANVAS_SDK_CACHE_TIMEOUT", FOURTEEN_DAYS))
|
|
27
32
|
|
|
28
33
|
|
|
29
34
|
METRICS_ENABLED = env_to_bool("PLUGINS_METRICS_ENABLED", not IS_SCRIPT)
|
|
@@ -46,12 +51,15 @@ CANVAS_SDK_DB_PASSWORD = os.getenv("CANVAS_SDK_DB_PASSWORD", "app")
|
|
|
46
51
|
CANVAS_SDK_DB_HOST = os.getenv("CANVAS_SDK_DB_HOST", "home-app-db")
|
|
47
52
|
CANVAS_SDK_DB_PORT = os.getenv("CANVAS_SDK_DB_PORT", "5432")
|
|
48
53
|
|
|
54
|
+
PLUGIN_RUNNER_MAX_WORKERS = int(os.getenv("PLUGIN_RUNNER_MAX_WORKERS", 5))
|
|
55
|
+
|
|
49
56
|
if os.getenv("DATABASE_URL"):
|
|
50
57
|
parsed_url = parse.urlparse(os.getenv("DATABASE_URL"))
|
|
51
58
|
|
|
52
59
|
DATABASES = {
|
|
53
60
|
"default": {
|
|
54
61
|
"ENGINE": "django.db.backends.postgresql",
|
|
62
|
+
"OPTIONS": {"pool": {"min_size": 2, "max_size": PLUGIN_RUNNER_MAX_WORKERS}},
|
|
55
63
|
"NAME": parsed_url.path[1:],
|
|
56
64
|
"USER": os.getenv("CANVAS_SDK_DATABASE_ROLE"),
|
|
57
65
|
"PASSWORD": os.getenv("CANVAS_SDK_DATABASE_ROLE_PASSWORD"),
|
|
@@ -63,6 +71,7 @@ else:
|
|
|
63
71
|
DATABASES = {
|
|
64
72
|
"default": {
|
|
65
73
|
"ENGINE": "django.db.backends.postgresql",
|
|
74
|
+
"OPTIONS": {"pool": {"min_size": 2, "max_size": PLUGIN_RUNNER_MAX_WORKERS}},
|
|
66
75
|
"NAME": CANVAS_SDK_DB_NAME,
|
|
67
76
|
"USER": CANVAS_SDK_DB_USERNAME,
|
|
68
77
|
"PASSWORD": CANVAS_SDK_DB_PASSWORD,
|
|
@@ -106,3 +115,26 @@ TEMPLATES = [
|
|
|
106
115
|
"OPTIONS": {},
|
|
107
116
|
},
|
|
108
117
|
]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if IS_TESTING:
|
|
121
|
+
CACHES = {
|
|
122
|
+
"default": {
|
|
123
|
+
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
|
124
|
+
},
|
|
125
|
+
"plugins": {
|
|
126
|
+
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
|
127
|
+
"KEY_PREFIX": CUSTOMER_IDENTIFIER,
|
|
128
|
+
"LOCATION": "plugins_cache",
|
|
129
|
+
"TIMEOUT": CANVAS_SDK_CACHE_TIMEOUT_SECONDS,
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
else:
|
|
133
|
+
CACHES = {
|
|
134
|
+
"plugins": {
|
|
135
|
+
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
|
|
136
|
+
"KEY_PREFIX": CUSTOMER_IDENTIFIER,
|
|
137
|
+
"LOCATION": CANVAS_SDK_PLUGINS_CACHE_LOCATION,
|
|
138
|
+
"TIMEOUT": CANVAS_SDK_CACHE_TIMEOUT_SECONDS,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
File without changes
|
|
File without changes
|