fix(vps): report stable creation progress
Build and publish Docker image / build-and-publish (push) Successful in 9s
Build and publish Docker image / build-and-publish (push) Successful in 9s
This commit is contained in:
1 parent
9822e43592
commit
522e5c5634
2 files changed
+193
-90
No files matched your search
+93
-36
@@ -48,6 +48,17 @@ import websockets
|
||||
|
||||
__all__ = ['DateTimeEncoder', 'attach_to_game_console', 'ensure_node_shell_pod', 'execute_minecraft_save_all', 'handle_2fa_disable', 'handle_2fa_enable', 'handle_2fa_login', 'handle_2fa_setup', 'handle_add_credits', 'handle_authenticate', 'handle_change_server_version', 'handle_check_demo_eligibility', 'handle_convert_demo_to_paid', 'handle_copy_path', 'handle_create_backup', 'handle_create_custom_domain', 'handle_create_directory', 'handle_create_domain', 'handle_create_server', 'handle_create_ticket', 'handle_delete_backup', 'handle_delete_custom_domain', 'handle_delete_domain', 'handle_delete_path', 'handle_delete_server', 'handle_download_file', 'handle_exec_command', 'handle_expose_port', 'handle_game_console_command', 'handle_game_console_stream', 'handle_generate_sftp', 'handle_get_backups', 'handle_get_credits', 'handle_get_minecraft_settings', 'handle_get_server_creation_progress', 'handle_get_server_logs', 'handle_get_server_metrics', 'handle_get_servers', 'handle_get_ticket', 'handle_get_ticket_stats', 'handle_get_tickets', 'handle_link_domain', 'handle_list_custom_domains', 'handle_list_domains', 'handle_list_files', 'handle_login', 'handle_move_path', 'handle_node_terminal_connect', 'handle_node_terminal_disconnect', 'handle_node_terminal_input', 'handle_pack_zip', 'handle_read_file', 'handle_register', 'handle_remove_port', 'handle_restart_server', 'handle_restore_backup', 'handle_send_ticket_message', 'handle_start_chunked_upload', 'handle_start_server', 'handle_stop_server', 'handle_sync_server_status', 'handle_unlink_domain', 'handle_unpack_zip', 'handle_update_minecraft_settings', 'handle_update_ticket_status', 'handle_upload_file', 'handle_upload_file_chunk', 'handle_verify_custom_domain', 'handle_vps_terminal_connect', 'handle_vps_terminal_disconnect', 'handle_vps_terminal_input', 'handle_websocket', 'handle_write_file', 'notify_admins', 'notify_ticket_participants', 'parse_ls_output', 'send_message', 'send_verification_email', 'start_websocket_server', 'stream_game_console_output', 'stream_metrics', 'sync_user_domains_with_dns']
|
||||
|
||||
_server_creation_progress_by_user: Dict[str, Dict] = {}
|
||||
_server_creation_progress_lock = threading.Lock()
|
||||
|
||||
|
||||
def _set_server_creation_progress(user_id: str, progress: Dict) -> Dict:
|
||||
"""Keep the latest creation status available after a WebSocket reconnect."""
|
||||
snapshot = {**progress, 'timestamp': time.time()}
|
||||
with _server_creation_progress_lock:
|
||||
_server_creation_progress_by_user[str(user_id)] = snapshot
|
||||
return snapshot
|
||||
|
||||
|
||||
|
||||
async def handle_create_server(client_id, data):
|
||||
@@ -58,6 +69,14 @@ async def handle_create_server(client_id, data):
|
||||
return
|
||||
|
||||
user_id = client_info['user_id']
|
||||
server_type = data.get('serverType', '')
|
||||
_set_server_creation_progress(user_id, {
|
||||
'phase': 'Preparing',
|
||||
'progress': 1,
|
||||
'message': 'Checking capacity and preparing your server.',
|
||||
'serverType': server_type,
|
||||
'state': 'active'
|
||||
})
|
||||
|
||||
try:
|
||||
# Check if this is a demo server request
|
||||
@@ -67,6 +86,18 @@ async def handle_create_server(client_id, data):
|
||||
server_name = data.get('name', '').strip()
|
||||
is_valid, error_msg = validate_server_name(server_name)
|
||||
if not is_valid:
|
||||
failed = _set_server_creation_progress(user_id, {
|
||||
'phase': 'Failed',
|
||||
'progress': 1,
|
||||
'message': error_msg,
|
||||
'serverType': server_type,
|
||||
'state': 'failed'
|
||||
})
|
||||
await send_message(client_id, {'type': 'server_creation_progress', **failed})
|
||||
await send_message(client_id, {
|
||||
'type': 'server_creation_failed',
|
||||
'error': error_msg
|
||||
})
|
||||
await send_message(client_id, {
|
||||
'type': 'error',
|
||||
'error': error_msg
|
||||
@@ -80,7 +111,15 @@ async def handle_create_server(client_id, data):
|
||||
capacity_check = validate_server_capacity(data)
|
||||
|
||||
if not capacity_check['success']:
|
||||
failed = _set_server_creation_progress(user_id, {
|
||||
'phase': 'Capacity check',
|
||||
'progress': 1,
|
||||
'message': capacity_check['message'],
|
||||
'serverType': server_type,
|
||||
'state': 'failed'
|
||||
})
|
||||
# Send detailed capacity error
|
||||
await send_message(client_id, {'type': 'server_creation_progress', **failed})
|
||||
await send_message(client_id, {
|
||||
'type': 'capacity_error',
|
||||
'error': capacity_check['message'],
|
||||
@@ -90,43 +129,48 @@ async def handle_create_server(client_id, data):
|
||||
})
|
||||
return
|
||||
|
||||
# Capacity check passed, proceed with server creation
|
||||
# Create progress callback that sends websocket messages
|
||||
# Capacity check passed, proceed with server creation.
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def progress_callback(phase, progress, message):
|
||||
"""Thread-safe progress callback that sends websocket messages"""
|
||||
"""Store and forward progress updates from the provisioning thread."""
|
||||
try:
|
||||
# Store progress in client info for reconnection recovery
|
||||
if client_id in connected_clients:
|
||||
connected_clients[client_id]['server_creation_progress'] = {
|
||||
'phase': phase,
|
||||
'progress': progress,
|
||||
'message': message,
|
||||
'timestamp': time.time()
|
||||
}
|
||||
with _server_creation_progress_lock:
|
||||
previous = dict(_server_creation_progress_by_user.get(str(user_id), {}))
|
||||
state = 'failed' if phase in ('Failed', 'Error', 'Timeout') else 'active'
|
||||
snapshot = _set_server_creation_progress(user_id, {
|
||||
'phase': phase,
|
||||
'progress': max(int(previous.get('progress', 0)), max(0, min(99, int(progress)))),
|
||||
'message': message,
|
||||
'serverType': server_type,
|
||||
'state': state
|
||||
})
|
||||
|
||||
# Use run_coroutine_threadsafe to send async message from executor thread
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
send_message(client_id, {
|
||||
'type': 'server_creation_progress',
|
||||
'phase': phase,
|
||||
'progress': progress,
|
||||
'message': message
|
||||
**snapshot
|
||||
}),
|
||||
loop
|
||||
)
|
||||
# Don't wait for completion to avoid blocking
|
||||
# The future will complete asynchronously
|
||||
future.result(timeout=2)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending progress update: {e}")
|
||||
|
||||
# Run blocking create_server in executor with progress callback
|
||||
# Run blocking provisioning outside the event loop.
|
||||
server = await loop.run_in_executor(None, create_server, data, user_id, progress_callback)
|
||||
|
||||
# Clear server creation progress on completion
|
||||
if client_id in connected_clients:
|
||||
connected_clients[client_id].pop('server_creation_progress', None)
|
||||
completed = _set_server_creation_progress(user_id, {
|
||||
'phase': 'Provisioned',
|
||||
'progress': 100,
|
||||
'message': 'Server resources are ready. The VPS may take a little longer to boot.',
|
||||
'serverType': server_type,
|
||||
'state': 'complete'
|
||||
})
|
||||
await send_message(client_id, {
|
||||
'type': 'server_creation_progress',
|
||||
**completed
|
||||
})
|
||||
|
||||
# Get user credits
|
||||
user = get_user_by_id(user_id)
|
||||
@@ -145,10 +189,22 @@ async def handle_create_server(client_id, data):
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating server: {e}", exc_info=True)
|
||||
|
||||
# Clear server creation progress on error
|
||||
if client_id in connected_clients:
|
||||
connected_clients[client_id].pop('server_creation_progress', None)
|
||||
previous = _server_creation_progress_by_user.get(str(user_id), {})
|
||||
failed = _set_server_creation_progress(user_id, {
|
||||
'phase': 'Failed',
|
||||
'progress': previous.get('progress', 0),
|
||||
'message': str(e),
|
||||
'serverType': data.get('serverType', ''),
|
||||
'state': 'failed'
|
||||
})
|
||||
await send_message(client_id, {
|
||||
'type': 'server_creation_progress',
|
||||
**failed
|
||||
})
|
||||
await send_message(client_id, {
|
||||
'type': 'server_creation_failed',
|
||||
'error': str(e)
|
||||
})
|
||||
|
||||
# Check if it's a capacity-related error
|
||||
error_msg = str(e)
|
||||
@@ -1269,25 +1325,25 @@ async def handle_get_server_creation_progress(client_id, data):
|
||||
await send_message(client_id, {'type': 'error', 'error': 'Not authenticated'})
|
||||
return
|
||||
|
||||
# Get stored progress if available
|
||||
progress = client_info.get('server_creation_progress')
|
||||
user_id = str(client_info['user_id'])
|
||||
with _server_creation_progress_lock:
|
||||
progress = _server_creation_progress_by_user.get(user_id)
|
||||
if progress:
|
||||
# Check if progress is recent (within last 5 minutes)
|
||||
if time.time() - progress.get('timestamp', 0) < 300:
|
||||
expiry = 1800 if progress.get('state') == 'active' else 600
|
||||
if time.time() - progress.get('timestamp', 0) < expiry:
|
||||
await send_message(client_id, {
|
||||
'type': 'server_creation_progress',
|
||||
'phase': progress.get('phase', ''),
|
||||
'progress': progress.get('progress', 0),
|
||||
'message': progress.get('message', 'Creating server...')
|
||||
**progress
|
||||
})
|
||||
else:
|
||||
# Progress is stale, clear it
|
||||
client_info.pop('server_creation_progress', None)
|
||||
with _server_creation_progress_lock:
|
||||
_server_creation_progress_by_user.pop(user_id, None)
|
||||
await send_message(client_id, {
|
||||
'type': 'server_creation_progress',
|
||||
'phase': 'Unknown',
|
||||
'progress': 0,
|
||||
'message': 'No active server creation found'
|
||||
'message': 'No recent server creation status is available.',
|
||||
'state': 'unknown'
|
||||
})
|
||||
else:
|
||||
# No progress stored
|
||||
@@ -1295,7 +1351,8 @@ async def handle_get_server_creation_progress(client_id, data):
|
||||
'type': 'server_creation_progress',
|
||||
'phase': 'Unknown',
|
||||
'progress': 0,
|
||||
'message': 'No active server creation found'
|
||||
'message': 'No recent server creation status is available.',
|
||||
'state': 'unknown'
|
||||
})
|
||||
|
||||
async def handle_create_domain(client_id, data):
|
||||
|
||||
Reference in new issue
Block a user