django-nativemojo 0.1.15__py3-none-any.whl → 0.1.17__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.
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/METADATA +3 -2
- django_nativemojo-0.1.17.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/commands/serializer_admin.py +121 -1
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +279 -0
- mojo/apps/account/models/group.py +294 -8
- mojo/apps/account/models/member.py +14 -1
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +190 -17
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +8 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +95 -5
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +6 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/backends/s3.py +209 -0
- mojo/apps/fileman/models/file.py +45 -9
- mojo/apps/fileman/models/manager.py +269 -3
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/incident.py +2 -0
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -3
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/models/log.py +3 -0
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +17 -0
- mojo/decorators/http.py +40 -1
- mojo/helpers/aws/__init__.py +11 -7
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +8 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +271 -57
- mojo/models/secrets.py +86 -0
- mojo/serializers/__init__.py +16 -10
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/{manager.py → core/manager.py} +53 -4
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +14 -0
- testit/runner.py +23 -6
- django_nativemojo-0.1.15.dist-info/RECORD +0 -234
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -44
- mojo/apps/tasks/manager.py +0 -644
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -76
- mojo/apps/tasks/runner.py +0 -439
- mojo/apps/tasks/task.py +0 -99
- mojo/apps/tasks/tq_handlers.py +0 -132
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/serializers/advanced/README.md +0 -363
- mojo/serializers/advanced/__init__.py +0 -247
- mojo/serializers/advanced/formats/__init__.py +0 -28
- mojo/serializers/advanced/formats/excel.py +0 -516
- mojo/serializers/advanced/formats/json.py +0 -239
- mojo/serializers/advanced/formats/response.py +0 -485
- mojo/serializers/advanced/serializer.py +0 -568
- mojo/serializers/optimized.py +0 -618
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
- /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
- /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
- /mojo/serializers/{advanced/formats → formats}/localizers.py +0 -0
mojo/apps/tasks/rest/tasks.py
DELETED
@@ -1,76 +0,0 @@
|
|
1
|
-
from mojo import decorators as md
|
2
|
-
from mojo.helpers.response import JsonResponse
|
3
|
-
# from django.http import JsonResponse
|
4
|
-
from mojo.apps import tasks
|
5
|
-
|
6
|
-
@md.GET('status')
|
7
|
-
def api_status(request):
|
8
|
-
tman = tasks.get_manager()
|
9
|
-
return JsonResponse(dict(status=True, data=tman.get_status()))
|
10
|
-
|
11
|
-
|
12
|
-
@md.GET('runners')
|
13
|
-
def api_task_runners(request):
|
14
|
-
tman = tasks.get_manager()
|
15
|
-
runners = [r for r in tman.get_active_runners().values()]
|
16
|
-
for r in runners:
|
17
|
-
r['id'] = r['hostname']
|
18
|
-
return JsonResponse(dict(status=True, data=runners, size=len(runners), count=len(runners)))
|
19
|
-
|
20
|
-
|
21
|
-
@md.URL('pending')
|
22
|
-
def api_pending(request):
|
23
|
-
tman = tasks.get_manager()
|
24
|
-
pending = tman.get_all_pending()
|
25
|
-
size = len(pending)
|
26
|
-
response = {
|
27
|
-
'status': True,
|
28
|
-
'count': size,
|
29
|
-
'page': 0,
|
30
|
-
'size': size,
|
31
|
-
'data': pending
|
32
|
-
}
|
33
|
-
return JsonResponse(response)
|
34
|
-
|
35
|
-
@md.URL('completed')
|
36
|
-
def api_completed(request):
|
37
|
-
tman = tasks.get_manager()
|
38
|
-
completed = tman.get_all_completed(include_data=True)
|
39
|
-
size = len(completed)
|
40
|
-
response = {
|
41
|
-
'status': True,
|
42
|
-
'count': size,
|
43
|
-
'page': 0,
|
44
|
-
'size': size,
|
45
|
-
'data': completed
|
46
|
-
}
|
47
|
-
return JsonResponse(response)
|
48
|
-
|
49
|
-
@md.URL('running')
|
50
|
-
def api_running(request):
|
51
|
-
tman = tasks.get_manager()
|
52
|
-
running = tman.get_all_running(include_data=True)
|
53
|
-
size = len(running)
|
54
|
-
response = {
|
55
|
-
'status': True,
|
56
|
-
'count': size,
|
57
|
-
'page': 0,
|
58
|
-
'size': size,
|
59
|
-
'data': running
|
60
|
-
}
|
61
|
-
return JsonResponse(response)
|
62
|
-
|
63
|
-
|
64
|
-
@md.URL('errors')
|
65
|
-
def api_errors(request):
|
66
|
-
tman = tasks.get_manager()
|
67
|
-
errors = tman.get_all_errors()
|
68
|
-
size = len(errors)
|
69
|
-
response = {
|
70
|
-
'status': True,
|
71
|
-
'count': size,
|
72
|
-
'page': 0,
|
73
|
-
'size': size,
|
74
|
-
'data': errors
|
75
|
-
}
|
76
|
-
return JsonResponse(response)
|
mojo/apps/tasks/runner.py
DELETED
@@ -1,439 +0,0 @@
|
|
1
|
-
from importlib import import_module
|
2
|
-
from concurrent.futures import ThreadPoolExecutor
|
3
|
-
from .manager import TaskManager
|
4
|
-
from mojo.apps.tasks import manager
|
5
|
-
import os
|
6
|
-
from mojo.helpers import logit
|
7
|
-
from mojo.helpers import daemon
|
8
|
-
from mojo.helpers import paths
|
9
|
-
from mojo.apps import metrics
|
10
|
-
import time
|
11
|
-
import socket
|
12
|
-
import threading
|
13
|
-
import json
|
14
|
-
|
15
|
-
|
16
|
-
class TaskEngine(daemon.Daemon):
|
17
|
-
"""
|
18
|
-
The TaskEngine is responsible for managing and executing tasks across different channels.
|
19
|
-
It leverages a thread pool to execute tasks concurrently and uses a task manager to maintain task states.
|
20
|
-
"""
|
21
|
-
def __init__(self, channels=["broadcast"], max_workers=5):
|
22
|
-
"""
|
23
|
-
Initialize the TaskEngine.
|
24
|
-
|
25
|
-
Args:
|
26
|
-
channels (list): A list of channel names where tasks are queued.
|
27
|
-
max_workers (int, optional): The maximum number of threads available for task execution. Defaults to 5.
|
28
|
-
"""
|
29
|
-
super().__init__("taskit", os.path.join(paths.VAR_ROOT, "taskit"))
|
30
|
-
self.hostname = socket.gethostname()
|
31
|
-
self.manager = manager.TaskManager(channels)
|
32
|
-
self.channels = channels
|
33
|
-
if "broadcast" not in self.channels:
|
34
|
-
self.channels.append("broadcast")
|
35
|
-
|
36
|
-
# Add hostname-specific channel for this runner
|
37
|
-
self.runner_channel = f"runner_{self.hostname}"
|
38
|
-
if self.runner_channel not in self.channels:
|
39
|
-
self.channels.append(self.runner_channel)
|
40
|
-
|
41
|
-
self.max_workers = max_workers
|
42
|
-
self.executor = None
|
43
|
-
self.logger = logit.get_logger("tasks", "tasks.log")
|
44
|
-
self.ping_thread = None
|
45
|
-
self.ping_interval = 30 # seconds
|
46
|
-
self.started_at = time.time()
|
47
|
-
|
48
|
-
def register_runner(self):
|
49
|
-
"""
|
50
|
-
Register this runner as active in the system.
|
51
|
-
"""
|
52
|
-
runner_data = {
|
53
|
-
'hostname': self.hostname,
|
54
|
-
'started_at': self.started_at,
|
55
|
-
'max_workers': self.max_workers,
|
56
|
-
'channels': self.channels,
|
57
|
-
'last_ping': time.time(),
|
58
|
-
'status': 'active'
|
59
|
-
}
|
60
|
-
self.manager.redis.hset(
|
61
|
-
self.manager.get_runners_key(),
|
62
|
-
self.hostname,
|
63
|
-
json.dumps(runner_data)
|
64
|
-
)
|
65
|
-
self.logger.info(f"Registered runner {self.hostname}")
|
66
|
-
|
67
|
-
def unregister_runner(self):
|
68
|
-
"""
|
69
|
-
Unregister this runner from the active runners list.
|
70
|
-
"""
|
71
|
-
self.manager.redis.hdel(self.manager.get_runners_key(), self.hostname)
|
72
|
-
self.logger.info(f"Unregistered runner {self.hostname}")
|
73
|
-
|
74
|
-
def update_runner_status(self, status_data=None):
|
75
|
-
"""
|
76
|
-
Update the status of this runner.
|
77
|
-
"""
|
78
|
-
if status_data is None:
|
79
|
-
status_data = {}
|
80
|
-
|
81
|
-
runner_data = {
|
82
|
-
'hostname': self.hostname,
|
83
|
-
'last_ping': time.time(),
|
84
|
-
'status': 'active',
|
85
|
-
'started_at': self.started_at,
|
86
|
-
'max_workers': self.max_workers,
|
87
|
-
'channels': self.channels,
|
88
|
-
**status_data
|
89
|
-
}
|
90
|
-
self.manager.redis.hset(
|
91
|
-
self.manager.get_runners_key(),
|
92
|
-
self.hostname,
|
93
|
-
json.dumps(runner_data)
|
94
|
-
)
|
95
|
-
|
96
|
-
|
97
|
-
def ping_runners(self):
|
98
|
-
"""
|
99
|
-
Send ping messages to all active runners to check their status.
|
100
|
-
"""
|
101
|
-
active_runners = self.manager.get_active_runners()
|
102
|
-
for hostname in active_runners.keys():
|
103
|
-
if hostname != self.hostname: # Don't ping ourselves
|
104
|
-
ping_message = {
|
105
|
-
'type': 'ping',
|
106
|
-
'from': self.hostname,
|
107
|
-
'timestamp': time.time()
|
108
|
-
}
|
109
|
-
runner_channel = f"runner_{hostname}"
|
110
|
-
self.manager.redis.publish(
|
111
|
-
self.manager.get_channel_key(runner_channel),
|
112
|
-
json.dumps(ping_message)
|
113
|
-
)
|
114
|
-
|
115
|
-
def handle_ping_request(self, message_data):
|
116
|
-
"""
|
117
|
-
Handle incoming ping requests and send response.
|
118
|
-
"""
|
119
|
-
ping_data = json.loads(message_data)
|
120
|
-
response = {
|
121
|
-
'type': 'ping_response',
|
122
|
-
'from': self.hostname,
|
123
|
-
'to': ping_data['from'],
|
124
|
-
'timestamp': time.time(),
|
125
|
-
'status': self.get_runner_status()
|
126
|
-
}
|
127
|
-
|
128
|
-
# Send response to the requesting runner's channel
|
129
|
-
requester_channel = f"runner_{ping_data['from']}"
|
130
|
-
self.manager.redis.publish(
|
131
|
-
self.manager.get_channel_key(requester_channel),
|
132
|
-
json.dumps(response)
|
133
|
-
)
|
134
|
-
|
135
|
-
def handle_ping_response(self, message_data):
|
136
|
-
"""
|
137
|
-
Handle ping responses from other runners.
|
138
|
-
"""
|
139
|
-
response_data = json.loads(message_data)
|
140
|
-
self.logger.info(f"Received ping response from {response_data['from']}")
|
141
|
-
# Update the runner's status in our active runners list
|
142
|
-
self.manager.redis.hset(
|
143
|
-
self.manager.get_runners_key(),
|
144
|
-
response_data['from'],
|
145
|
-
json.dumps(response_data['status'])
|
146
|
-
)
|
147
|
-
|
148
|
-
def get_runner_status(self):
|
149
|
-
"""
|
150
|
-
Get the current status of this runner.
|
151
|
-
|
152
|
-
Returns:
|
153
|
-
dict: Status information for this runner.
|
154
|
-
"""
|
155
|
-
active_threads = 0
|
156
|
-
if self.executor and hasattr(self.executor, '_threads'):
|
157
|
-
active_threads = len([t for t in self.executor._threads if t.is_alive()])
|
158
|
-
|
159
|
-
return {
|
160
|
-
'hostname': self.hostname,
|
161
|
-
'status': 'active',
|
162
|
-
'max_workers': self.max_workers,
|
163
|
-
'active_threads': active_threads,
|
164
|
-
'channels': self.channels,
|
165
|
-
'last_ping': time.time(),
|
166
|
-
'uptime': time.time() - getattr(self, 'start_time', time.time())
|
167
|
-
}
|
168
|
-
|
169
|
-
def start_ping_thread(self):
|
170
|
-
"""
|
171
|
-
Start the background thread that periodically pings other runners.
|
172
|
-
"""
|
173
|
-
def ping_loop():
|
174
|
-
while self.running:
|
175
|
-
try:
|
176
|
-
self.ping_runners()
|
177
|
-
self.update_runner_status()
|
178
|
-
time.sleep(self.ping_interval)
|
179
|
-
except Exception as e:
|
180
|
-
self.logger.error(f"Error in ping loop: {e}")
|
181
|
-
time.sleep(5)
|
182
|
-
|
183
|
-
self.ping_thread = threading.Thread(target=ping_loop, daemon=True)
|
184
|
-
self.ping_thread.start()
|
185
|
-
|
186
|
-
def cleanup_stale_runners(self):
|
187
|
-
"""
|
188
|
-
Remove runners that haven't been seen for a while.
|
189
|
-
"""
|
190
|
-
cutoff_time = time.time() - (self.ping_interval * 3) # 3 missed pings
|
191
|
-
active_runners = self.manager.get_active_runners()
|
192
|
-
|
193
|
-
for hostname, runner_data in active_runners.items():
|
194
|
-
last_ping = runner_data.get('last_ping', 0)
|
195
|
-
if last_ping < cutoff_time:
|
196
|
-
self.logger.info(f"Removing stale runner: {hostname}")
|
197
|
-
self.manager.redis.hdel(self.manager.get_runners_key(), hostname)
|
198
|
-
|
199
|
-
def reset_running_tasks(self):
|
200
|
-
"""
|
201
|
-
Reset tasks that are stuck in a running state by moving them back to the pending state.
|
202
|
-
"""
|
203
|
-
for channel in self.channels:
|
204
|
-
for task_id in self.manager.get_running_ids(channel):
|
205
|
-
self.logger.info(f"moving task {task_id} from running to pending")
|
206
|
-
self.manager.remove_from_running(task_id, channel)
|
207
|
-
self.manager.add_to_pending(task_id, channel)
|
208
|
-
|
209
|
-
def queue_pending_tasks(self):
|
210
|
-
"""
|
211
|
-
Queue all the pending tasks for execution.
|
212
|
-
"""
|
213
|
-
for channel in self.channels:
|
214
|
-
for task_id in self.manager.get_pending_ids(channel):
|
215
|
-
self.queue_task(task_id)
|
216
|
-
|
217
|
-
def handle_message(self, message):
|
218
|
-
"""
|
219
|
-
Handle incoming messages from the channels, decoding task identifiers and queuing them for execution.
|
220
|
-
|
221
|
-
Args:
|
222
|
-
message (dict): A dictionary with message data containing task information.
|
223
|
-
"""
|
224
|
-
message_data = message['data'].decode()
|
225
|
-
|
226
|
-
# Check if this is a ping/status message
|
227
|
-
try:
|
228
|
-
parsed_message = json.loads(message_data)
|
229
|
-
if isinstance(parsed_message, dict) and 'type' in parsed_message:
|
230
|
-
if parsed_message['type'] == 'ping':
|
231
|
-
self.handle_ping_request(message_data)
|
232
|
-
return
|
233
|
-
elif parsed_message['type'] == 'ping_response':
|
234
|
-
self.handle_ping_response(message_data)
|
235
|
-
return
|
236
|
-
except (json.JSONDecodeError, TypeError):
|
237
|
-
pass
|
238
|
-
|
239
|
-
# If not a ping message, treat as a task
|
240
|
-
self.queue_task(message_data)
|
241
|
-
|
242
|
-
def on_run_task(self, task_id):
|
243
|
-
"""
|
244
|
-
Execute a task based on its identifier by locating the relevant function and executing it.
|
245
|
-
|
246
|
-
Args:
|
247
|
-
task_id (str): The identifier of the task to be executed.
|
248
|
-
"""
|
249
|
-
# this is a keep it thread safe with the redis connection
|
250
|
-
tman = TaskManager([])
|
251
|
-
task_data = tman.get_task(task_id)
|
252
|
-
if not task_data:
|
253
|
-
# this task has expired or no longer exists
|
254
|
-
self.logger.info(f"Task {task_id} has expired or no longer exists")
|
255
|
-
metrics.record("tasks_expired", category="tasks")
|
256
|
-
# try and remove any pending dead tasks
|
257
|
-
self.manager.channels = self.channels
|
258
|
-
self.manager.take_out_the_dead(local=True)
|
259
|
-
return
|
260
|
-
self.logger.info(f"Executing task {task_id}")
|
261
|
-
function_path = task_data.get('function')
|
262
|
-
module_name, func_name = function_path.rsplit('.', 1)
|
263
|
-
module = import_module(module_name)
|
264
|
-
func = getattr(module, func_name)
|
265
|
-
self.manager.remove_from_pending(task_id, task_data.channel)
|
266
|
-
self.manager.add_to_running(task_id, task_data.channel)
|
267
|
-
|
268
|
-
try:
|
269
|
-
task_data.started_at = time.time()
|
270
|
-
task_data._thread_id = threading.current_thread().ident
|
271
|
-
tdata = task_data.get("data", {})
|
272
|
-
if tdata and "args" in tdata and "kwargs" in tdata:
|
273
|
-
args = tdata["args"]
|
274
|
-
kwargs = tdata["kwargs"]
|
275
|
-
# self.logger.info(f"Executing task {task_id} with args {args} and kwargs {kwargs}")
|
276
|
-
func(*args, **kwargs)
|
277
|
-
else:
|
278
|
-
# self.logger.info(f"Executing task {task_id} with no arguments")
|
279
|
-
func(task_data)
|
280
|
-
task_data.completed_at = time.time()
|
281
|
-
task_data.elapsed_time = task_data.completed_at - task_data.started_at
|
282
|
-
if "_thread_id" in task_data:
|
283
|
-
del task_data["_thread_id"]
|
284
|
-
tman.save_task(task_data)
|
285
|
-
tman.add_to_completed(task_data)
|
286
|
-
metrics.record("tasks_completed", category="tasks")
|
287
|
-
self.logger.info(f"Task {task_id} completed after {task_data.elapsed_time} seconds")
|
288
|
-
except Exception as e:
|
289
|
-
self.logger.exception(f"Error executing task {task_id}: {str(e)}")
|
290
|
-
tman.add_to_errors(task_data, str(e))
|
291
|
-
metrics.record("tasks_errors", category="tasks")
|
292
|
-
finally:
|
293
|
-
tman.remove_from_running(task_id, task_data.channel)
|
294
|
-
|
295
|
-
def queue_task(self, task_id):
|
296
|
-
"""
|
297
|
-
Submit a task for execution in the thread pool.
|
298
|
-
|
299
|
-
Args:
|
300
|
-
task_id (str): The identifier of the task to be queued.
|
301
|
-
"""
|
302
|
-
self.logger.info(f"adding task {task_id}")
|
303
|
-
self.executor.submit(self.on_run_task, task_id)
|
304
|
-
|
305
|
-
|
306
|
-
def _clear_queued_tasks(self):
|
307
|
-
import queue
|
308
|
-
q = self.executor._work_queue
|
309
|
-
removed = 0
|
310
|
-
try:
|
311
|
-
while True:
|
312
|
-
q.get_nowait()
|
313
|
-
removed += 1
|
314
|
-
except queue.Empty:
|
315
|
-
pass
|
316
|
-
return removed
|
317
|
-
|
318
|
-
def _wait_for_active_tasks(self, timeout=5.0):
|
319
|
-
"""
|
320
|
-
Waits up to `timeout` seconds for active executor threads to finish.
|
321
|
-
Returns True if all threads completed, False if timeout hit.
|
322
|
-
"""
|
323
|
-
start_time = time.time()
|
324
|
-
while time.time() - start_time < timeout:
|
325
|
-
active = self.manager.get_all_running_ids(local=True)
|
326
|
-
if len(active) == 0:
|
327
|
-
return True
|
328
|
-
time.sleep(0.01)
|
329
|
-
return False
|
330
|
-
|
331
|
-
def wait_for_all_tasks_to_complete(self, timeout=5):
|
332
|
-
"""
|
333
|
-
Wait for all tasks submitted to the executor to complete with graceful degradation.
|
334
|
-
"""
|
335
|
-
if not self.executor:
|
336
|
-
return
|
337
|
-
|
338
|
-
self.logger.info(f"Initiating graceful shutdown with {timeout}s timeout")
|
339
|
-
self.executor.shutdown(wait=False)
|
340
|
-
self._clear_queued_tasks()
|
341
|
-
result = self._wait_for_active_tasks(timeout)
|
342
|
-
if not result:
|
343
|
-
self.logger.warning("Timeout reached while waiting for active tasks to complete")
|
344
|
-
return result
|
345
|
-
|
346
|
-
def start_listening(self):
|
347
|
-
"""
|
348
|
-
Listen for messages on the subscribed channels and handle them as they arrive.
|
349
|
-
"""
|
350
|
-
self.logger.info("starting with channels...", self.channels)
|
351
|
-
self.start_time = time.time()
|
352
|
-
self.register_runner()
|
353
|
-
self.manager.take_out_the_dead(local=True)
|
354
|
-
self.reset_running_tasks()
|
355
|
-
self.queue_pending_tasks()
|
356
|
-
self.start_ping_thread()
|
357
|
-
|
358
|
-
pubsub = self.manager.redis.pubsub()
|
359
|
-
channel_keys = {self.manager.get_channel_key(channel): self.handle_message for channel in self.channels}
|
360
|
-
pubsub.subscribe(**channel_keys)
|
361
|
-
|
362
|
-
for message in pubsub.listen():
|
363
|
-
if not self.running:
|
364
|
-
self.logger.info("shutting down, waiting for tasks to complete")
|
365
|
-
self.wait_for_all_tasks_to_complete()
|
366
|
-
self.unregister_runner()
|
367
|
-
self.logger.info("shutdown complete")
|
368
|
-
return
|
369
|
-
if message['type'] != 'message':
|
370
|
-
continue
|
371
|
-
self.handle_message(message)
|
372
|
-
|
373
|
-
def run(self):
|
374
|
-
self.executor = ThreadPoolExecutor(max_workers=self.max_workers)
|
375
|
-
self.start_listening()
|
376
|
-
|
377
|
-
|
378
|
-
# HELPERS FOR RUNNING VIA CLI
|
379
|
-
def get_args():
|
380
|
-
"""
|
381
|
-
Setup the argument parser for command-line interface.
|
382
|
-
|
383
|
-
Returns:
|
384
|
-
Namespace: Parsed command-line arguments.
|
385
|
-
"""
|
386
|
-
import argparse
|
387
|
-
parser = argparse.ArgumentParser(description="TaskEngine Background Service")
|
388
|
-
parser.add_argument("--start", action="store_true", help="Start the daemon")
|
389
|
-
parser.add_argument("--stop", action="store_true", help="Stop the daemon")
|
390
|
-
parser.add_argument("--foreground", "-f", action="store_true", help="Run in foreground mode")
|
391
|
-
parser.add_argument("--status", action="store_true", help="Show status of all runners")
|
392
|
-
parser.add_argument("-v", "--verbose", action="store_true",
|
393
|
-
help="Enable verbose logging")
|
394
|
-
return parser, parser.parse_args()
|
395
|
-
|
396
|
-
|
397
|
-
def main():
|
398
|
-
from mojo.helpers.settings import settings
|
399
|
-
parser, args = get_args()
|
400
|
-
daemon = TaskEngine(settings.TASK_CHANNELS)
|
401
|
-
|
402
|
-
if args.status:
|
403
|
-
runners = daemon.manager.get_active_runners()
|
404
|
-
if runners:
|
405
|
-
print("Active TaskEngine Runners:")
|
406
|
-
for hostname, data in runners.items():
|
407
|
-
print(f" {hostname}: {data.get('status', 'unknown')} "
|
408
|
-
f"(last ping: {time.time() - data.get('last_ping', 0):.1f}s ago)")
|
409
|
-
else:
|
410
|
-
print("No active runners found")
|
411
|
-
elif args.start:
|
412
|
-
daemon.start()
|
413
|
-
elif args.stop:
|
414
|
-
daemon.stop()
|
415
|
-
elif args.foreground:
|
416
|
-
print("Running in foreground mode...")
|
417
|
-
daemon.run()
|
418
|
-
else:
|
419
|
-
parser.print_help()
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
def kill_thread(thread):
|
424
|
-
import ctypes
|
425
|
-
if not thread.is_alive():
|
426
|
-
return False
|
427
|
-
|
428
|
-
tid = thread.ident
|
429
|
-
if tid is None:
|
430
|
-
return False
|
431
|
-
|
432
|
-
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
|
433
|
-
ctypes.c_long(tid), ctypes.py_object(SystemExit)
|
434
|
-
)
|
435
|
-
if res > 1:
|
436
|
-
# Undo if multiple threads were affected
|
437
|
-
ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), 0)
|
438
|
-
return False
|
439
|
-
return True
|
mojo/apps/tasks/task.py
DELETED
@@ -1,99 +0,0 @@
|
|
1
|
-
from objict import objict
|
2
|
-
import time
|
3
|
-
|
4
|
-
|
5
|
-
class Task(objict):
|
6
|
-
"""
|
7
|
-
Task model for the Django Mojo task system.
|
8
|
-
|
9
|
-
This class represents a task that can be queued, executed, and tracked
|
10
|
-
through various states (pending, running, completed, error, cancelled).
|
11
|
-
"""
|
12
|
-
|
13
|
-
def __init__(self, id=None, function=None, data=None, channel="default",
|
14
|
-
expires=None, created=None, status="pending", error=None,
|
15
|
-
completed_at=None, **kwargs):
|
16
|
-
"""
|
17
|
-
Initialize a new Task instance.
|
18
|
-
|
19
|
-
Args:
|
20
|
-
id (str): Unique identifier for the task
|
21
|
-
function (str): Function name to be executed
|
22
|
-
data (dict): Data to be passed to the function
|
23
|
-
channel (str): Channel name for task routing
|
24
|
-
expires (float): Expiration timestamp
|
25
|
-
created (float): Creation timestamp
|
26
|
-
status (str): Current task status
|
27
|
-
error (str): Error message if task failed
|
28
|
-
completed_at (float): Completion timestamp
|
29
|
-
**kwargs: Additional attributes
|
30
|
-
"""
|
31
|
-
super().__init__(**kwargs)
|
32
|
-
|
33
|
-
self.id = id
|
34
|
-
self.function = function
|
35
|
-
self.data = data or {}
|
36
|
-
self.channel = channel
|
37
|
-
self.expires = expires
|
38
|
-
self.created = created or time.time()
|
39
|
-
self.status = status
|
40
|
-
self.error = error
|
41
|
-
self.completed_at = completed_at
|
42
|
-
|
43
|
-
def is_expired(self):
|
44
|
-
"""
|
45
|
-
Check if the task has expired.
|
46
|
-
|
47
|
-
Returns:
|
48
|
-
bool: True if task has expired, False otherwise
|
49
|
-
"""
|
50
|
-
if self.expires is None:
|
51
|
-
return False
|
52
|
-
return time.time() > self.expires
|
53
|
-
|
54
|
-
def is_pending(self):
|
55
|
-
"""Check if task is in pending state."""
|
56
|
-
return self.status == "pending"
|
57
|
-
|
58
|
-
def is_running(self):
|
59
|
-
"""Check if task is in running state."""
|
60
|
-
return self.status == "running"
|
61
|
-
|
62
|
-
def is_completed(self):
|
63
|
-
"""Check if task is in completed state."""
|
64
|
-
return self.status == "completed"
|
65
|
-
|
66
|
-
def is_error(self):
|
67
|
-
"""Check if task is in error state."""
|
68
|
-
return self.status == "error"
|
69
|
-
|
70
|
-
def is_cancelled(self):
|
71
|
-
"""Check if task is in cancelled state."""
|
72
|
-
return self.status == "cancelled"
|
73
|
-
|
74
|
-
def mark_as_running(self):
|
75
|
-
"""Mark task as running."""
|
76
|
-
self.status = "running"
|
77
|
-
|
78
|
-
def mark_as_completed(self):
|
79
|
-
"""Mark task as completed."""
|
80
|
-
self.status = "completed"
|
81
|
-
self.completed_at = time.time()
|
82
|
-
|
83
|
-
def mark_as_error(self, error_message):
|
84
|
-
"""Mark task as error with error message."""
|
85
|
-
self.status = "error"
|
86
|
-
self.error = error_message
|
87
|
-
|
88
|
-
def mark_as_cancelled(self):
|
89
|
-
"""Mark task as cancelled."""
|
90
|
-
self.status = "cancelled"
|
91
|
-
|
92
|
-
def __str__(self):
|
93
|
-
"""String representation of the task."""
|
94
|
-
return f"Task({self.id}, {self.function}, {self.status})"
|
95
|
-
|
96
|
-
def __repr__(self):
|
97
|
-
"""Detailed string representation of the task."""
|
98
|
-
return (f"Task(id='{self.id}', function='{self.function}', "
|
99
|
-
f"status='{self.status}', channel='{self.channel}')")
|