django-cfg 1.5.8__py3-none-any.whl → 1.5.20__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 django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/api/commands/serializers.py +152 -0
- django_cfg/apps/api/commands/views.py +32 -0
- django_cfg/apps/business/accounts/management/commands/otp_test.py +5 -2
- django_cfg/apps/business/accounts/serializers/profile.py +42 -0
- django_cfg/apps/business/agents/management/commands/create_agent.py +5 -194
- django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
- django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
- django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
- django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
- django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
- django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
- django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
- django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
- django_cfg/apps/business/support/serializers.py +3 -2
- django_cfg/apps/integrations/centrifugo/apps.py +2 -1
- django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
- django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +6 -6
- django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
- django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
- django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
- django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
- django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
- django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
- django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
- django_cfg/apps/integrations/centrifugo/urls.py +8 -0
- django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +0 -79
- django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +257 -0
- django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
- django_cfg/apps/integrations/grpc/admin/config.py +113 -9
- django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
- django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
- django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
- django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
- django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
- django_cfg/apps/integrations/grpc/centrifugo/__init__.py +29 -0
- django_cfg/apps/integrations/grpc/centrifugo/bridge.py +277 -0
- django_cfg/apps/integrations/grpc/centrifugo/config.py +167 -0
- django_cfg/apps/integrations/grpc/centrifugo/demo.py +626 -0
- django_cfg/apps/integrations/grpc/centrifugo/test_publish.py +229 -0
- django_cfg/apps/integrations/grpc/centrifugo/transformers.py +89 -0
- django_cfg/apps/integrations/grpc/interceptors/__init__.py +3 -1
- django_cfg/apps/integrations/grpc/interceptors/centrifugo.py +541 -0
- django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
- django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
- django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
- django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +185 -0
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +474 -95
- django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
- django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
- django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
- django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
- django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
- django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
- django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
- django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
- django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
- django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
- django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
- django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
- django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
- django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
- django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
- django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
- django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
- django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
- django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
- django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
- django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
- django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
- django_cfg/apps/integrations/grpc/services/discovery.py +7 -1
- django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
- django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
- django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
- django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
- django_cfg/apps/integrations/grpc/urls.py +8 -0
- django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
- django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
- django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
- django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +378 -0
- django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
- django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
- django_cfg/apps/integrations/grpc/views/charts.py +21 -14
- django_cfg/apps/integrations/grpc/views/config.py +8 -6
- django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
- django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
- django_cfg/apps/integrations/grpc/views/services.py +30 -21
- django_cfg/apps/integrations/grpc/views/testing.py +45 -43
- django_cfg/apps/integrations/rq/views/jobs.py +19 -9
- django_cfg/apps/integrations/rq/views/schedule.py +7 -3
- django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
- django_cfg/apps/system/dashboard/serializers/config.py +95 -9
- django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
- django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
- django_cfg/apps/system/frontend/views.py +87 -6
- django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
- django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
- django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
- django_cfg/config.py +33 -0
- django_cfg/core/builders/security_builder.py +1 -0
- django_cfg/core/generation/integration_generators/api.py +2 -0
- django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
- django_cfg/management/commands/check_endpoints.py +2 -2
- django_cfg/management/commands/check_settings.py +3 -10
- django_cfg/management/commands/clear_constance.py +3 -10
- django_cfg/management/commands/create_token.py +4 -11
- django_cfg/management/commands/list_urls.py +4 -10
- django_cfg/management/commands/migrate_all.py +18 -12
- django_cfg/management/commands/migrator.py +4 -11
- django_cfg/management/commands/script.py +4 -10
- django_cfg/management/commands/show_config.py +8 -16
- django_cfg/management/commands/show_urls.py +5 -11
- django_cfg/management/commands/superuser.py +4 -11
- django_cfg/management/commands/tree.py +5 -10
- django_cfg/management/utils/README.md +402 -0
- django_cfg/management/utils/__init__.py +29 -0
- django_cfg/management/utils/mixins.py +176 -0
- django_cfg/middleware/pagination.py +53 -54
- django_cfg/models/api/grpc/__init__.py +15 -21
- django_cfg/models/api/grpc/config.py +155 -73
- django_cfg/models/ngrok/config.py +7 -6
- django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
- django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
- django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
- django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
- django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
- django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
- django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
- django_cfg/modules/django_client/core/ir/schema.py +15 -1
- django_cfg/modules/django_client/core/parser/base.py +126 -30
- django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
- django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
- django_cfg/modules/django_email/management/commands/test_email.py +4 -10
- django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
- django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
- django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
- django_cfg/modules/django_unfold/navigation.py +6 -18
- django_cfg/pyproject.toml +1 -1
- django_cfg/registry/modules.py +1 -4
- django_cfg/requirements.txt +52 -0
- django_cfg/static/frontend/admin.zip +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/METADATA +1 -1
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/RECORD +158 -121
- django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,21 +1,53 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Django management command to run gRPC server.
|
|
2
|
+
Django management command to run async gRPC server with auto-reload support.
|
|
3
3
|
|
|
4
4
|
Usage:
|
|
5
|
+
# Development mode (with auto-reload)
|
|
5
6
|
python manage.py rungrpc
|
|
7
|
+
|
|
8
|
+
# Production mode (no auto-reload)
|
|
9
|
+
python manage.py rungrpc --noreload
|
|
10
|
+
|
|
11
|
+
# Custom host and port
|
|
6
12
|
python manage.py rungrpc --host 0.0.0.0 --port 50051
|
|
7
|
-
|
|
13
|
+
|
|
14
|
+
# With Centrifugo test event on startup
|
|
15
|
+
python manage.py rungrpc --test
|
|
16
|
+
|
|
17
|
+
# Disable specific features
|
|
18
|
+
python manage.py rungrpc --no-reflection --no-health-check
|
|
19
|
+
|
|
20
|
+
Auto-reload behavior:
|
|
21
|
+
- Enabled by default in development mode (ENV_MODE != "production")
|
|
22
|
+
- Disabled by default in production mode (ENV_MODE == "production")
|
|
23
|
+
- Use --noreload to explicitly disable auto-reload
|
|
24
|
+
- Server will restart automatically when Python files change
|
|
25
|
+
|
|
26
|
+
Test mode:
|
|
27
|
+
- Use --test to send a test Centrifugo event on server startup
|
|
28
|
+
- Useful for verifying Centrifugo integration is working
|
|
29
|
+
- Event published to: grpc#rungrpc#startup#test
|
|
8
30
|
"""
|
|
9
31
|
|
|
10
32
|
from __future__ import annotations
|
|
11
33
|
|
|
34
|
+
import asyncio
|
|
12
35
|
import logging
|
|
13
36
|
import signal
|
|
14
37
|
import sys
|
|
15
|
-
|
|
38
|
+
import threading
|
|
16
39
|
|
|
17
40
|
from django.conf import settings
|
|
18
41
|
from django.core.management.base import BaseCommand
|
|
42
|
+
from django.utils import autoreload
|
|
43
|
+
|
|
44
|
+
from django_cfg.core.config import get_current_config
|
|
45
|
+
from django_cfg.modules.django_logging import get_logger
|
|
46
|
+
from django_cfg.apps.integrations.grpc.utils.streaming_logger import (
|
|
47
|
+
setup_streaming_logger,
|
|
48
|
+
log_server_start,
|
|
49
|
+
log_server_shutdown,
|
|
50
|
+
)
|
|
19
51
|
|
|
20
52
|
# Check dependencies before importing grpc
|
|
21
53
|
from django_cfg.apps.integrations.grpc._cfg import check_grpc_dependencies
|
|
@@ -28,24 +60,46 @@ except Exception as e:
|
|
|
28
60
|
|
|
29
61
|
# Now safe to import grpc
|
|
30
62
|
import grpc
|
|
31
|
-
|
|
32
|
-
logger = logging.getLogger(__name__)
|
|
63
|
+
import grpc.aio
|
|
33
64
|
|
|
34
65
|
|
|
35
66
|
class Command(BaseCommand):
|
|
36
67
|
"""
|
|
37
|
-
Run gRPC server with auto-discovered services.
|
|
68
|
+
Run async gRPC server with auto-discovered services and hot-reload.
|
|
38
69
|
|
|
39
70
|
Features:
|
|
71
|
+
- Async server with grpc.aio
|
|
40
72
|
- Auto-discovers and registers services
|
|
41
|
-
-
|
|
73
|
+
- Hot-reload in development mode (watches for file changes)
|
|
74
|
+
- Configurable host, port
|
|
42
75
|
- Health check support
|
|
43
76
|
- Reflection support
|
|
44
77
|
- Graceful shutdown
|
|
45
78
|
- Signal handling
|
|
79
|
+
|
|
80
|
+
Hot-reload:
|
|
81
|
+
- Automatically enabled in development mode (ENV_MODE != "production")
|
|
82
|
+
- Automatically disabled in production mode (ENV_MODE == "production")
|
|
83
|
+
- Use --noreload to explicitly disable in development
|
|
84
|
+
- Works like Django's runserver - restarts on code changes
|
|
46
85
|
"""
|
|
47
86
|
|
|
48
|
-
|
|
87
|
+
# Web execution metadata
|
|
88
|
+
web_executable = False
|
|
89
|
+
requires_input = False
|
|
90
|
+
is_destructive = False
|
|
91
|
+
|
|
92
|
+
help = "Run async gRPC server with optional hot-reload support"
|
|
93
|
+
|
|
94
|
+
def __init__(self, *args, **kwargs):
|
|
95
|
+
"""Initialize with self.logger and async server reference."""
|
|
96
|
+
super().__init__(*args, **kwargs)
|
|
97
|
+
self.logger = get_logger('rungrpc')
|
|
98
|
+
self.streaming_logger = None # Will be initialized when server starts
|
|
99
|
+
self.server = None
|
|
100
|
+
self.shutdown_event = None
|
|
101
|
+
self.server_status = None
|
|
102
|
+
self.server_config = None # Store config for re-registration
|
|
49
103
|
|
|
50
104
|
def add_arguments(self, parser):
|
|
51
105
|
"""Add command arguments."""
|
|
@@ -61,12 +115,6 @@ class Command(BaseCommand):
|
|
|
61
115
|
default=None,
|
|
62
116
|
help="Server port (default: from settings or 50051)",
|
|
63
117
|
)
|
|
64
|
-
parser.add_argument(
|
|
65
|
-
"--workers",
|
|
66
|
-
type=int,
|
|
67
|
-
default=None,
|
|
68
|
-
help="Max worker threads (default: from settings or 10)",
|
|
69
|
-
)
|
|
70
118
|
parser.add_argument(
|
|
71
119
|
"--no-reflection",
|
|
72
120
|
action="store_true",
|
|
@@ -77,53 +125,190 @@ class Command(BaseCommand):
|
|
|
77
125
|
action="store_true",
|
|
78
126
|
help="Disable health check service",
|
|
79
127
|
)
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
"--asyncio-debug",
|
|
130
|
+
action="store_true",
|
|
131
|
+
help="Enable asyncio debug mode",
|
|
132
|
+
)
|
|
133
|
+
parser.add_argument(
|
|
134
|
+
"--noreload",
|
|
135
|
+
action="store_false",
|
|
136
|
+
dest="use_reloader",
|
|
137
|
+
help="Disable auto-reloader (default: enabled in dev mode)",
|
|
138
|
+
)
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"--test",
|
|
141
|
+
action="store_true",
|
|
142
|
+
help="Send test Centrifugo event on startup (for testing integration)",
|
|
143
|
+
)
|
|
80
144
|
|
|
81
145
|
def handle(self, *args, **options):
|
|
82
|
-
"""Run gRPC server."""
|
|
83
|
-
|
|
84
|
-
|
|
146
|
+
"""Run async gRPC server with optional auto-reload."""
|
|
147
|
+
config = get_current_config()
|
|
148
|
+
|
|
149
|
+
# Determine if we should use auto-reload
|
|
150
|
+
# Changed default to False for stability with bidirectional streaming
|
|
151
|
+
use_reloader = options.get("use_reloader", False)
|
|
152
|
+
|
|
153
|
+
# Check if we're in production mode (disable reloader in production)
|
|
154
|
+
if config and hasattr(config, 'is_production'):
|
|
155
|
+
is_production = config.is_production # property, not method
|
|
156
|
+
else:
|
|
157
|
+
# Fallback to settings
|
|
158
|
+
env_mode = getattr(settings, "ENV_MODE", "development").lower()
|
|
159
|
+
is_production = env_mode == "production"
|
|
160
|
+
|
|
161
|
+
# Disable reloader in production by default
|
|
162
|
+
if is_production and options.get("use_reloader") is None:
|
|
163
|
+
use_reloader = False
|
|
164
|
+
self.stdout.write(
|
|
165
|
+
self.style.WARNING(
|
|
166
|
+
"Production mode - auto-reloader disabled"
|
|
167
|
+
)
|
|
168
|
+
)
|
|
85
169
|
|
|
86
|
-
#
|
|
87
|
-
|
|
170
|
+
# Enable asyncio debug if requested
|
|
171
|
+
if options.get("asyncio_debug"):
|
|
172
|
+
asyncio.get_event_loop().set_debug(True)
|
|
173
|
+
self.logger.info("Asyncio debug mode enabled")
|
|
174
|
+
|
|
175
|
+
# Run with or without reloader
|
|
176
|
+
if use_reloader:
|
|
177
|
+
from datetime import datetime
|
|
178
|
+
current_time = datetime.now().strftime('%H:%M:%S')
|
|
179
|
+
|
|
180
|
+
self.stdout.write(
|
|
181
|
+
self.style.SUCCESS(
|
|
182
|
+
f"🔄 Auto-reloader enabled - watching for file changes [{current_time}]"
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
self.stdout.write(
|
|
186
|
+
self.style.WARNING(
|
|
187
|
+
"⚠️ Note: Active streaming connections will be dropped on reload. Use --noreload for stable bots."
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Setup autoreload to watch project directory
|
|
192
|
+
from django.utils.autoreload import autoreload_started
|
|
193
|
+
|
|
194
|
+
def watch_project_files(sender, **kwargs):
|
|
195
|
+
"""Automatically watch Django project directory for changes."""
|
|
196
|
+
base_dir = getattr(settings, 'BASE_DIR', None)
|
|
197
|
+
if base_dir:
|
|
198
|
+
sender.watch_dir(str(base_dir), '*.py')
|
|
199
|
+
self.logger.debug(f"Watching project directory: {base_dir}")
|
|
88
200
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
201
|
+
autoreload_started.connect(watch_project_files)
|
|
202
|
+
|
|
203
|
+
# Use autoreload to restart on code changes
|
|
204
|
+
autoreload.run_with_reloader(
|
|
205
|
+
lambda: asyncio.run(self._async_main(*args, **options))
|
|
206
|
+
)
|
|
207
|
+
else:
|
|
208
|
+
# Run directly without reloader
|
|
209
|
+
asyncio.run(self._async_main(*args, **options))
|
|
210
|
+
|
|
211
|
+
async def _async_main(self, *args, **options):
|
|
212
|
+
"""Main async server loop."""
|
|
213
|
+
# Setup streaming logger for detailed gRPC logging
|
|
214
|
+
self.streaming_logger = setup_streaming_logger(
|
|
215
|
+
name='grpc_rungrpc',
|
|
216
|
+
level=logging.DEBUG,
|
|
217
|
+
console_level=logging.INFO
|
|
218
|
+
)
|
|
93
219
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
220
|
+
config = get_current_config()
|
|
221
|
+
use_reloader = options.get("use_reloader", True)
|
|
222
|
+
|
|
223
|
+
# Determine production mode
|
|
224
|
+
if config and hasattr(config, 'is_production'):
|
|
225
|
+
is_production = config.is_production # property, not method
|
|
226
|
+
else:
|
|
227
|
+
env_mode = getattr(settings, "ENV_MODE", "development").lower()
|
|
228
|
+
is_production = env_mode == "production"
|
|
229
|
+
|
|
230
|
+
# Log startup using reusable function
|
|
231
|
+
start_time = log_server_start(
|
|
232
|
+
self.streaming_logger,
|
|
233
|
+
server_type="gRPC Server",
|
|
234
|
+
mode="Production" if is_production else "Development",
|
|
235
|
+
hotreload_enabled=use_reloader
|
|
97
236
|
)
|
|
98
|
-
|
|
99
|
-
|
|
237
|
+
|
|
238
|
+
# Import models here to avoid AppRegistryNotReady
|
|
239
|
+
from django_cfg.apps.integrations.grpc.models import GRPCServerStatus
|
|
240
|
+
from django_cfg.apps.integrations.grpc.services.config_helper import (
|
|
241
|
+
get_grpc_server_config,
|
|
100
242
|
)
|
|
101
243
|
|
|
244
|
+
# Get configuration
|
|
245
|
+
grpc_server_config_obj = get_grpc_server_config()
|
|
246
|
+
|
|
247
|
+
# Fallback to settings if not configured via django-cfg
|
|
248
|
+
if not grpc_server_config_obj:
|
|
249
|
+
grpc_server_config = getattr(settings, "GRPC_SERVER", {})
|
|
250
|
+
host = options["host"] or grpc_server_config.get("host", "[::]")
|
|
251
|
+
port = options["port"] or grpc_server_config.get("port", 50051)
|
|
252
|
+
max_concurrent_streams = grpc_server_config.get("max_concurrent_streams", None)
|
|
253
|
+
enable_reflection = not options["no_reflection"] and grpc_server_config.get(
|
|
254
|
+
"enable_reflection", False
|
|
255
|
+
)
|
|
256
|
+
enable_health_check = not options["no_health_check"] and grpc_server_config.get(
|
|
257
|
+
"enable_health_check", True
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
# Use django-cfg config
|
|
261
|
+
host = options["host"] or grpc_server_config_obj.host
|
|
262
|
+
port = options["port"] or grpc_server_config_obj.port
|
|
263
|
+
max_concurrent_streams = grpc_server_config_obj.max_concurrent_streams
|
|
264
|
+
enable_reflection = (
|
|
265
|
+
not options["no_reflection"] and grpc_server_config_obj.enable_reflection
|
|
266
|
+
)
|
|
267
|
+
enable_health_check = (
|
|
268
|
+
not options["no_health_check"]
|
|
269
|
+
and grpc_server_config_obj.enable_health_check
|
|
270
|
+
)
|
|
271
|
+
grpc_server_config = {
|
|
272
|
+
"host": grpc_server_config_obj.host,
|
|
273
|
+
"port": grpc_server_config_obj.port,
|
|
274
|
+
"max_concurrent_streams": grpc_server_config_obj.max_concurrent_streams,
|
|
275
|
+
"enable_reflection": grpc_server_config_obj.enable_reflection,
|
|
276
|
+
"enable_health_check": grpc_server_config_obj.enable_health_check,
|
|
277
|
+
"compression": grpc_server_config_obj.compression,
|
|
278
|
+
"max_send_message_length": grpc_server_config_obj.max_send_message_length,
|
|
279
|
+
"max_receive_message_length": grpc_server_config_obj.max_receive_message_length,
|
|
280
|
+
"keepalive_time_ms": grpc_server_config_obj.keepalive_time_ms,
|
|
281
|
+
"keepalive_timeout_ms": grpc_server_config_obj.keepalive_timeout_ms,
|
|
282
|
+
}
|
|
283
|
+
|
|
102
284
|
# gRPC options
|
|
103
285
|
grpc_options = self._build_grpc_options(grpc_server_config)
|
|
104
286
|
|
|
105
|
-
#
|
|
106
|
-
|
|
107
|
-
|
|
287
|
+
# Add max_concurrent_streams if specified
|
|
288
|
+
if max_concurrent_streams:
|
|
289
|
+
grpc_options.append(("grpc.max_concurrent_streams", max_concurrent_streams))
|
|
290
|
+
|
|
291
|
+
# Create async server
|
|
292
|
+
self.server = grpc.aio.server(
|
|
108
293
|
options=grpc_options,
|
|
109
|
-
interceptors=self.
|
|
294
|
+
interceptors=await self._build_interceptors_async(),
|
|
110
295
|
)
|
|
111
296
|
|
|
112
297
|
# Discover and register services FIRST
|
|
113
|
-
service_count = self.
|
|
298
|
+
service_count = await self._register_services_async(self.server)
|
|
114
299
|
|
|
115
300
|
# Add health check with registered services
|
|
116
301
|
health_servicer = None
|
|
117
302
|
if enable_health_check:
|
|
118
|
-
health_servicer = self.
|
|
303
|
+
health_servicer = await self._add_health_check_async(self.server)
|
|
119
304
|
|
|
120
305
|
# Add reflection
|
|
121
306
|
if enable_reflection:
|
|
122
|
-
self.
|
|
307
|
+
await self._add_reflection_async(self.server)
|
|
123
308
|
|
|
124
309
|
# Bind server
|
|
125
310
|
address = f"{host}:{port}"
|
|
126
|
-
server.add_insecure_port(address)
|
|
311
|
+
self.server.add_insecure_port(address)
|
|
127
312
|
|
|
128
313
|
# Track server status in database
|
|
129
314
|
server_status = None
|
|
@@ -131,35 +316,62 @@ class Command(BaseCommand):
|
|
|
131
316
|
import os
|
|
132
317
|
from django_cfg.apps.integrations.grpc.services import ServiceDiscovery
|
|
133
318
|
|
|
134
|
-
#
|
|
319
|
+
# Store config for re-registration
|
|
320
|
+
self.server_config = {
|
|
321
|
+
'host': host,
|
|
322
|
+
'port': port,
|
|
323
|
+
'pid': os.getpid(),
|
|
324
|
+
'max_workers': 0,
|
|
325
|
+
'enable_reflection': enable_reflection,
|
|
326
|
+
'enable_health_check': enable_health_check,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
# Get registered services metadata (run in thread to avoid blocking)
|
|
135
330
|
discovery = ServiceDiscovery()
|
|
136
|
-
services_metadata =
|
|
331
|
+
services_metadata = await asyncio.to_thread(
|
|
332
|
+
discovery.get_registered_services
|
|
333
|
+
)
|
|
137
334
|
|
|
138
|
-
server_status =
|
|
335
|
+
server_status = await asyncio.to_thread(
|
|
336
|
+
GRPCServerStatus.objects.start_server,
|
|
139
337
|
host=host,
|
|
140
338
|
port=port,
|
|
141
339
|
pid=os.getpid(),
|
|
142
|
-
max_workers=
|
|
340
|
+
max_workers=0, # Async server - no workers
|
|
143
341
|
enable_reflection=enable_reflection,
|
|
144
342
|
enable_health_check=enable_health_check,
|
|
145
343
|
)
|
|
146
344
|
|
|
147
345
|
# Store registered services in database
|
|
148
346
|
server_status.registered_services = services_metadata
|
|
149
|
-
|
|
347
|
+
await asyncio.to_thread(
|
|
348
|
+
server_status.save,
|
|
349
|
+
update_fields=["registered_services"]
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Store in instance for heartbeat
|
|
353
|
+
self.server_status = server_status
|
|
150
354
|
|
|
151
355
|
except Exception as e:
|
|
152
|
-
logger.warning(f"Could not start server status tracking: {e}")
|
|
356
|
+
self.logger.warning(f"Could not start server status tracking: {e}")
|
|
153
357
|
|
|
154
358
|
# Start server
|
|
155
|
-
server.start()
|
|
359
|
+
await self.server.start()
|
|
156
360
|
|
|
157
361
|
# Mark server as running
|
|
158
362
|
if server_status:
|
|
159
363
|
try:
|
|
160
|
-
server_status.mark_running
|
|
364
|
+
await asyncio.to_thread(server_status.mark_running)
|
|
161
365
|
except Exception as e:
|
|
162
|
-
logger.warning(f"Could not mark server as running: {e}")
|
|
366
|
+
self.logger.warning(f"Could not mark server as running: {e}")
|
|
367
|
+
|
|
368
|
+
# Start heartbeat background task
|
|
369
|
+
heartbeat_task = None
|
|
370
|
+
if server_status:
|
|
371
|
+
heartbeat_task = asyncio.create_task(
|
|
372
|
+
self._heartbeat_loop(interval=30)
|
|
373
|
+
)
|
|
374
|
+
self.logger.info("Started heartbeat background task (30s interval)")
|
|
163
375
|
|
|
164
376
|
# Display gRPC-specific startup info
|
|
165
377
|
try:
|
|
@@ -168,35 +380,100 @@ class Command(BaseCommand):
|
|
|
168
380
|
|
|
169
381
|
# Get registered service names
|
|
170
382
|
discovery = ServiceDiscovery()
|
|
171
|
-
services_metadata =
|
|
383
|
+
services_metadata = await asyncio.to_thread(
|
|
384
|
+
discovery.get_registered_services
|
|
385
|
+
)
|
|
172
386
|
service_names = [s.get('name', 'Unknown') for s in services_metadata]
|
|
173
387
|
|
|
174
388
|
# Display startup info
|
|
175
389
|
grpc_display = GRPCDisplayManager()
|
|
176
|
-
|
|
390
|
+
await asyncio.to_thread(
|
|
391
|
+
grpc_display.display_grpc_startup,
|
|
177
392
|
host=host,
|
|
178
393
|
port=port,
|
|
179
|
-
max_workers=
|
|
394
|
+
max_workers=0, # Async server
|
|
180
395
|
enable_reflection=enable_reflection,
|
|
181
396
|
enable_health_check=enable_health_check,
|
|
182
397
|
registered_services=service_count,
|
|
183
398
|
service_names=service_names,
|
|
184
399
|
)
|
|
185
400
|
except Exception as e:
|
|
186
|
-
logger.warning(f"Could not display gRPC startup info: {e}")
|
|
401
|
+
self.logger.warning(f"Could not display gRPC startup info: {e}")
|
|
187
402
|
|
|
188
403
|
# Setup signal handlers for graceful shutdown
|
|
189
|
-
self.
|
|
404
|
+
self._setup_signal_handlers_async(self.server, server_status)
|
|
190
405
|
|
|
191
406
|
# Keep server running
|
|
192
|
-
self.stdout.write(self.style.SUCCESS("\n✅ gRPC server is running..."))
|
|
407
|
+
self.stdout.write(self.style.SUCCESS("\n✅ Async gRPC server is running..."))
|
|
408
|
+
|
|
409
|
+
# Show reloader status
|
|
410
|
+
if use_reloader and not is_production:
|
|
411
|
+
self.stdout.write(
|
|
412
|
+
self.style.SUCCESS(
|
|
413
|
+
"🔄 Auto-reloader active - server will restart on code changes"
|
|
414
|
+
)
|
|
415
|
+
)
|
|
416
|
+
|
|
193
417
|
self.stdout.write("Press CTRL+C to stop\n")
|
|
194
418
|
|
|
419
|
+
# Log server ready
|
|
420
|
+
self.streaming_logger.info("✅ Server ready and accepting connections")
|
|
421
|
+
if use_reloader:
|
|
422
|
+
self.streaming_logger.info("🔄 Watching for file changes...")
|
|
423
|
+
|
|
424
|
+
# Send test Centrifugo event if --test flag is set
|
|
425
|
+
if options.get("test"):
|
|
426
|
+
self.streaming_logger.info("🧪 Sending test Centrifugo event...")
|
|
427
|
+
try:
|
|
428
|
+
from django_cfg.apps.integrations.grpc.centrifugo.demo import send_demo_event
|
|
429
|
+
|
|
430
|
+
test_result = await send_demo_event(
|
|
431
|
+
channel="grpc#rungrpc#startup#test",
|
|
432
|
+
metadata={
|
|
433
|
+
"source": "rungrpc",
|
|
434
|
+
"action": "startup_test",
|
|
435
|
+
"mode": "Development" if not is_production else "Production",
|
|
436
|
+
"host": host,
|
|
437
|
+
"port": port,
|
|
438
|
+
}
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
if test_result:
|
|
442
|
+
self.streaming_logger.info("✅ Test Centrifugo event sent successfully")
|
|
443
|
+
self.stdout.write(self.style.SUCCESS("🧪 Test event published to Centrifugo"))
|
|
444
|
+
else:
|
|
445
|
+
self.streaming_logger.warning("⚠️ Test Centrifugo event failed")
|
|
446
|
+
self.stdout.write(self.style.WARNING("⚠️ Test event failed (check Centrifugo config)"))
|
|
447
|
+
|
|
448
|
+
except Exception as e:
|
|
449
|
+
self.streaming_logger.error(f"❌ Failed to send test event: {e}")
|
|
450
|
+
self.stdout.write(
|
|
451
|
+
self.style.ERROR(f"❌ Test event error: {e}")
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
shutdown_reason = "Unknown"
|
|
195
455
|
try:
|
|
196
|
-
server.wait_for_termination()
|
|
456
|
+
await self.server.wait_for_termination()
|
|
457
|
+
shutdown_reason = "Normal termination"
|
|
197
458
|
except KeyboardInterrupt:
|
|
198
|
-
|
|
459
|
+
shutdown_reason = "Keyboard interrupt"
|
|
199
460
|
pass
|
|
461
|
+
finally:
|
|
462
|
+
# Log shutdown using reusable function
|
|
463
|
+
log_server_shutdown(
|
|
464
|
+
self.streaming_logger,
|
|
465
|
+
start_time,
|
|
466
|
+
server_type="gRPC Server",
|
|
467
|
+
reason=shutdown_reason
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
# Cancel heartbeat task
|
|
471
|
+
if heartbeat_task and not heartbeat_task.done():
|
|
472
|
+
heartbeat_task.cancel()
|
|
473
|
+
try:
|
|
474
|
+
await heartbeat_task
|
|
475
|
+
except asyncio.CancelledError:
|
|
476
|
+
pass
|
|
200
477
|
|
|
201
478
|
def _build_grpc_options(self, config: dict) -> list:
|
|
202
479
|
"""
|
|
@@ -244,12 +521,12 @@ class Command(BaseCommand):
|
|
|
244
521
|
|
|
245
522
|
return options
|
|
246
523
|
|
|
247
|
-
def
|
|
524
|
+
async def _build_interceptors_async(self) -> list:
|
|
248
525
|
"""
|
|
249
|
-
Build server interceptors from configuration.
|
|
526
|
+
Build async server interceptors from configuration.
|
|
250
527
|
|
|
251
528
|
Returns:
|
|
252
|
-
List of interceptor instances
|
|
529
|
+
List of async interceptor instances
|
|
253
530
|
"""
|
|
254
531
|
grpc_framework_config = getattr(settings, "GRPC_FRAMEWORK", {})
|
|
255
532
|
interceptor_paths = grpc_framework_config.get("SERVER_INTERCEPTORS", [])
|
|
@@ -269,22 +546,22 @@ class Command(BaseCommand):
|
|
|
269
546
|
interceptor = interceptor_class()
|
|
270
547
|
interceptors.append(interceptor)
|
|
271
548
|
|
|
272
|
-
logger.debug(f"Loaded interceptor: {class_name}")
|
|
549
|
+
self.logger.debug(f"Loaded async interceptor: {class_name}")
|
|
273
550
|
|
|
274
551
|
except Exception as e:
|
|
275
|
-
logger.error(f"Failed to load interceptor {interceptor_path}: {e}")
|
|
552
|
+
self.logger.error(f"Failed to load async interceptor {interceptor_path}: {e}")
|
|
276
553
|
|
|
277
554
|
return interceptors
|
|
278
555
|
|
|
279
|
-
def
|
|
556
|
+
async def _add_health_check_async(self, server):
|
|
280
557
|
"""
|
|
281
|
-
Add health check service
|
|
558
|
+
Add health check service to async server.
|
|
282
559
|
|
|
283
560
|
Args:
|
|
284
|
-
server: gRPC server instance
|
|
561
|
+
server: Async gRPC server instance
|
|
285
562
|
|
|
286
563
|
Returns:
|
|
287
|
-
health_servicer: Health servicer instance
|
|
564
|
+
health_servicer: Health servicer instance or None
|
|
288
565
|
"""
|
|
289
566
|
try:
|
|
290
567
|
from grpc_health.v1 import health, health_pb2, health_pb2_grpc
|
|
@@ -294,14 +571,13 @@ class Command(BaseCommand):
|
|
|
294
571
|
|
|
295
572
|
# Set overall server status
|
|
296
573
|
health_servicer.set("", health_pb2.HealthCheckResponse.SERVING)
|
|
297
|
-
logger.info("Overall server health: SERVING")
|
|
574
|
+
self.logger.info("Overall server health: SERVING")
|
|
298
575
|
|
|
299
|
-
# Get registered service names
|
|
576
|
+
# Get registered service names from async server
|
|
300
577
|
service_names = []
|
|
301
578
|
if hasattr(server, '_state') and hasattr(server._state, 'generic_handlers'):
|
|
302
579
|
for handler in server._state.generic_handlers:
|
|
303
580
|
if hasattr(handler, 'service_name'):
|
|
304
|
-
# service_name() returns a callable or list
|
|
305
581
|
names = handler.service_name()
|
|
306
582
|
if callable(names):
|
|
307
583
|
names = names()
|
|
@@ -316,44 +592,42 @@ class Command(BaseCommand):
|
|
|
316
592
|
service_name,
|
|
317
593
|
health_pb2.HealthCheckResponse.SERVING
|
|
318
594
|
)
|
|
319
|
-
logger.info(f"Service '{service_name}' health: SERVING")
|
|
595
|
+
self.logger.info(f"Service '{service_name}' health: SERVING")
|
|
320
596
|
|
|
321
|
-
# Register health service
|
|
597
|
+
# Register health service to async server
|
|
322
598
|
health_pb2_grpc.add_HealthServicer_to_server(health_servicer, server)
|
|
323
599
|
|
|
324
|
-
logger.info(
|
|
600
|
+
self.logger.info(
|
|
325
601
|
f"✅ Health check enabled for {len(service_names)} service(s)"
|
|
326
602
|
)
|
|
327
603
|
|
|
328
|
-
# Return servicer for dynamic health updates
|
|
329
604
|
return health_servicer
|
|
330
605
|
|
|
331
606
|
except ImportError:
|
|
332
|
-
logger.warning(
|
|
607
|
+
self.logger.warning(
|
|
333
608
|
"grpcio-health-checking not installed. "
|
|
334
609
|
"Install with: pip install 'django-cfg[grpc]'"
|
|
335
610
|
)
|
|
336
611
|
return None
|
|
337
612
|
except Exception as e:
|
|
338
|
-
logger.error(f"Failed to add health check service: {e}")
|
|
613
|
+
self.logger.error(f"Failed to add health check service: {e}")
|
|
339
614
|
return None
|
|
340
615
|
|
|
341
|
-
def
|
|
616
|
+
async def _add_reflection_async(self, server):
|
|
342
617
|
"""
|
|
343
|
-
Add reflection service to server.
|
|
618
|
+
Add reflection service to async server.
|
|
344
619
|
|
|
345
620
|
Args:
|
|
346
|
-
server: gRPC server instance
|
|
621
|
+
server: Async gRPC server instance
|
|
347
622
|
"""
|
|
348
623
|
try:
|
|
349
624
|
from grpc_reflection.v1alpha import reflection
|
|
350
625
|
|
|
351
|
-
# Get service names from
|
|
626
|
+
# Get service names from async server
|
|
352
627
|
service_names = []
|
|
353
628
|
if hasattr(server, '_state') and hasattr(server._state, 'generic_handlers'):
|
|
354
629
|
for handler in server._state.generic_handlers:
|
|
355
630
|
if hasattr(handler, 'service_name'):
|
|
356
|
-
# service_name() returns a callable or list
|
|
357
631
|
names = handler.service_name()
|
|
358
632
|
if callable(names):
|
|
359
633
|
names = names()
|
|
@@ -365,25 +639,25 @@ class Command(BaseCommand):
|
|
|
365
639
|
# Add grpc.reflection.v1alpha.ServerReflection service itself
|
|
366
640
|
service_names.append('grpc.reflection.v1alpha.ServerReflection')
|
|
367
641
|
|
|
368
|
-
# Add reflection
|
|
642
|
+
# Add reflection to async server
|
|
369
643
|
reflection.enable_server_reflection(service_names, server)
|
|
370
644
|
|
|
371
|
-
logger.info(f"Server reflection enabled for {len(service_names)} service(s)")
|
|
645
|
+
self.logger.info(f"Server reflection enabled for {len(service_names)} service(s)")
|
|
372
646
|
|
|
373
647
|
except ImportError:
|
|
374
|
-
logger.warning(
|
|
648
|
+
self.logger.warning(
|
|
375
649
|
"grpcio-reflection not installed. "
|
|
376
650
|
"Install with: pip install grpcio-reflection"
|
|
377
651
|
)
|
|
378
652
|
except Exception as e:
|
|
379
|
-
logger.error(f"Failed to enable server reflection: {e}")
|
|
653
|
+
self.logger.error(f"Failed to enable server reflection: {e}")
|
|
380
654
|
|
|
381
|
-
def
|
|
655
|
+
async def _register_services_async(self, server) -> int:
|
|
382
656
|
"""
|
|
383
|
-
Discover and register services to server.
|
|
657
|
+
Discover and register services to async server.
|
|
384
658
|
|
|
385
659
|
Args:
|
|
386
|
-
server: gRPC server instance
|
|
660
|
+
server: Async gRPC server instance
|
|
387
661
|
|
|
388
662
|
Returns:
|
|
389
663
|
Number of services registered
|
|
@@ -391,24 +665,115 @@ class Command(BaseCommand):
|
|
|
391
665
|
try:
|
|
392
666
|
from django_cfg.apps.integrations.grpc.services.discovery import discover_and_register_services
|
|
393
667
|
|
|
394
|
-
|
|
668
|
+
# Service registration is sync, run in thread
|
|
669
|
+
count = await asyncio.to_thread(
|
|
670
|
+
discover_and_register_services,
|
|
671
|
+
server
|
|
672
|
+
)
|
|
395
673
|
return count
|
|
396
674
|
|
|
397
675
|
except Exception as e:
|
|
398
|
-
logger.error(f"Failed to register services: {e}", exc_info=True)
|
|
676
|
+
self.logger.error(f"Failed to register services: {e}", exc_info=True)
|
|
399
677
|
self.stdout.write(
|
|
400
678
|
self.style.ERROR(f"Error registering services: {e}")
|
|
401
679
|
)
|
|
402
680
|
return 0
|
|
403
681
|
|
|
404
|
-
def
|
|
682
|
+
async def _heartbeat_loop(self, interval: int = 30):
|
|
683
|
+
"""
|
|
684
|
+
Periodically update server heartbeat with auto-recovery.
|
|
685
|
+
|
|
686
|
+
If server record is deleted from database, automatically re-registers
|
|
687
|
+
the server to maintain monitoring continuity.
|
|
688
|
+
|
|
689
|
+
Args:
|
|
690
|
+
interval: Heartbeat interval in seconds (default: 30)
|
|
691
|
+
"""
|
|
692
|
+
from django_cfg.apps.integrations.grpc.models import GRPCServerStatus
|
|
693
|
+
from asgiref.sync import sync_to_async
|
|
694
|
+
|
|
695
|
+
try:
|
|
696
|
+
while True:
|
|
697
|
+
await asyncio.sleep(interval)
|
|
698
|
+
|
|
699
|
+
if not self.server_status or not self.server_config:
|
|
700
|
+
self.logger.warning("No server status or config available")
|
|
701
|
+
continue
|
|
702
|
+
|
|
703
|
+
try:
|
|
704
|
+
# Check if record still exists
|
|
705
|
+
record_exists = await sync_to_async(
|
|
706
|
+
GRPCServerStatus.objects.filter(
|
|
707
|
+
id=self.server_status.id
|
|
708
|
+
).exists
|
|
709
|
+
)()
|
|
710
|
+
|
|
711
|
+
if not record_exists:
|
|
712
|
+
# Record was deleted - re-register server
|
|
713
|
+
self.logger.warning(
|
|
714
|
+
"Server record was deleted from database, "
|
|
715
|
+
"re-registering..."
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
# Get services metadata for re-registration
|
|
719
|
+
from django_cfg.apps.integrations.grpc.services import ServiceDiscovery
|
|
720
|
+
discovery = ServiceDiscovery()
|
|
721
|
+
services_metadata = await asyncio.to_thread(
|
|
722
|
+
discovery.get_registered_services
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
# Re-register server
|
|
726
|
+
new_server_status = await asyncio.to_thread(
|
|
727
|
+
GRPCServerStatus.objects.start_server,
|
|
728
|
+
**self.server_config
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
# Store registered services
|
|
732
|
+
new_server_status.registered_services = services_metadata
|
|
733
|
+
await asyncio.to_thread(
|
|
734
|
+
new_server_status.save,
|
|
735
|
+
update_fields=["registered_services"]
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
# Mark as running
|
|
739
|
+
await asyncio.to_thread(new_server_status.mark_running)
|
|
740
|
+
|
|
741
|
+
# Update reference
|
|
742
|
+
self.server_status = new_server_status
|
|
743
|
+
|
|
744
|
+
self.logger.warning(
|
|
745
|
+
f"✅ Successfully re-registered server (ID: {new_server_status.id})"
|
|
746
|
+
)
|
|
747
|
+
else:
|
|
748
|
+
# Record exists - just update heartbeat
|
|
749
|
+
await asyncio.to_thread(self.server_status.mark_running)
|
|
750
|
+
self.logger.debug(f"Heartbeat updated (interval: {interval}s)")
|
|
751
|
+
|
|
752
|
+
except Exception as e:
|
|
753
|
+
self.logger.warning(f"Failed to update heartbeat: {e}")
|
|
754
|
+
|
|
755
|
+
except asyncio.CancelledError:
|
|
756
|
+
self.logger.info("Heartbeat task cancelled")
|
|
757
|
+
raise
|
|
758
|
+
|
|
759
|
+
def _setup_signal_handlers_async(self, server, server_status=None):
|
|
405
760
|
"""
|
|
406
|
-
Setup signal handlers for graceful shutdown.
|
|
761
|
+
Setup signal handlers for graceful async server shutdown.
|
|
407
762
|
|
|
408
763
|
Args:
|
|
409
|
-
server: gRPC server instance
|
|
764
|
+
server: Async gRPC server instance
|
|
410
765
|
server_status: GRPCServerStatus instance (optional)
|
|
766
|
+
|
|
767
|
+
Note:
|
|
768
|
+
Signal handlers can only be set in the main thread.
|
|
769
|
+
When running with autoreload, we're in a separate thread,
|
|
770
|
+
so we skip signal handler setup.
|
|
411
771
|
"""
|
|
772
|
+
# Check if we're in the main thread
|
|
773
|
+
if threading.current_thread() is not threading.main_thread():
|
|
774
|
+
# In autoreload mode, Django handles signals - we don't need to set them up
|
|
775
|
+
return
|
|
776
|
+
|
|
412
777
|
# Flag to prevent multiple shutdown attempts
|
|
413
778
|
shutdown_initiated = {'value': False}
|
|
414
779
|
|
|
@@ -423,19 +788,33 @@ class Command(BaseCommand):
|
|
|
423
788
|
# Mark server as stopping
|
|
424
789
|
if server_status:
|
|
425
790
|
try:
|
|
426
|
-
|
|
791
|
+
import django
|
|
792
|
+
if django.VERSION >= (3, 0):
|
|
793
|
+
from asgiref.sync import sync_to_async
|
|
794
|
+
# Run in sync context
|
|
795
|
+
try:
|
|
796
|
+
server_status.mark_stopping()
|
|
797
|
+
except:
|
|
798
|
+
pass
|
|
427
799
|
except Exception as e:
|
|
428
|
-
logger.warning(f"Could not mark server as stopping: {e}")
|
|
800
|
+
self.logger.warning(f"Could not mark server as stopping: {e}")
|
|
429
801
|
|
|
430
|
-
# Stop server
|
|
431
|
-
|
|
802
|
+
# Stop async server
|
|
803
|
+
try:
|
|
804
|
+
# Create task to stop server
|
|
805
|
+
loop = asyncio.get_event_loop()
|
|
806
|
+
loop.create_task(server.stop(grace=5))
|
|
807
|
+
except Exception as e:
|
|
808
|
+
self.logger.error(f"Error stopping server: {e}")
|
|
432
809
|
|
|
433
|
-
# Mark server as stopped
|
|
810
|
+
# Mark server as stopped (async-safe)
|
|
434
811
|
if server_status:
|
|
435
812
|
try:
|
|
436
|
-
|
|
813
|
+
from asgiref.sync import sync_to_async
|
|
814
|
+
# Wrap sync DB operation in sync_to_async
|
|
815
|
+
asyncio.create_task(sync_to_async(server_status.mark_stopped)())
|
|
437
816
|
except Exception as e:
|
|
438
|
-
logger.warning(f"Could not mark server as stopped: {e}")
|
|
817
|
+
self.logger.warning(f"Could not mark server as stopped: {e}")
|
|
439
818
|
|
|
440
819
|
self.stdout.write(self.style.SUCCESS("✅ Server stopped"))
|
|
441
820
|
|