2020)
2121from dstack ._internal .core .models .configurations import (
2222 DevEnvironmentConfiguration ,
23- ServiceConfiguration ,
2423)
2524from dstack ._internal .core .models .files import FileArchiveMapping
2625from dstack ._internal .core .models .instances import InstanceStatus , SSHConnectionParams
3433 Job ,
3534 JobProvisioningData ,
3635 JobRuntimeData ,
37- JobSpec ,
3836 JobStatus ,
3937 JobSubmission ,
4038 JobTerminationReason ,
@@ -366,6 +364,7 @@ class _JobUpdateMap(ItemUpdateMap, total=False):
366364 disconnected_at : Optional [datetime ]
367365 inactivity_secs : Optional [int ]
368366 exit_status : Optional [int ]
367+ ready : bool
369368 registered : bool
370369 image_pull_progress : Optional [str ]
371370 skip_min_processing_interval : bool
@@ -1083,6 +1082,12 @@ def _emit_result_events(
10831082 job_model .disconnected_at ,
10841083 ),
10851084 )
1085+ _emit_readiness_change_event (
1086+ session = session ,
1087+ job_model = job_model ,
1088+ old_ready = job_model .ready ,
1089+ new_ready = result .job_update_map .get ("ready" , job_model .ready ),
1090+ )
10861091 if result .replica_registration is not None :
10871092 targets = [events .Target .from_model (job_model )]
10881093 if result .replica_registration .gateway_target is not None :
@@ -1175,13 +1180,32 @@ async def _maybe_register_replica(
11751180) -> None :
11761181 if (
11771182 context .run .run_spec .configuration .type != "service"
1178- or _get_result_registered (context .job_model , result )
11791183 or context .job_model .job_num != 0
11801184 or result .new_probe_models
1181- or not is_job_ready (context .job_model .probes , context .job .job_spec .probes )
11821185 ):
11831186 return
11841187
1188+ is_ready = is_job_ready (context .job_model .probes , context .job .job_spec .probes )
1189+ if is_ready and not context .job_model .ready :
1190+ result .job_update_map ["ready" ] = True
1191+
1192+ router_group = next (
1193+ (g for g in context .run .run_spec .configuration .replica_groups if g .router is not None ),
1194+ None ,
1195+ )
1196+ is_router_replica = (
1197+ router_group is not None and context .job .job_spec .replica_group == router_group .name
1198+ )
1199+ # non-router replicas aren't registered if the service has a router
1200+ if router_group is not None and not is_router_replica :
1201+ if context .job_model .registered :
1202+ # migration edge case: a pre-0.21.1 server replica incorrectly set registered=True
1203+ result .job_update_map ["registered" ] = False
1204+ return
1205+
1206+ if not is_ready or _get_result_registered (context .job_model , result ):
1207+ return
1208+
11851209 ssh_head_proxy : Optional [SSHConnectionParams ] = None
11861210 ssh_head_proxy_private_key : Optional [str ] = None
11871211 instance = get_or_error (context .job_model .instance )
@@ -1220,23 +1244,6 @@ async def _register_service_replica(
12201244) -> Optional [events .Target ]:
12211245 if context .run_model .gateway_id is None :
12221246 return None
1223-
1224- job_spec = validate_json_extra_ignore (JobSpec , context .job_model .job_spec_data )
1225-
1226- # For router-based services (e.g. PD disaggregation), only router replicas should be
1227- # registered with the gateway. Worker replicas are discovered by the router-worker
1228- # sync pipeline and should not be routed to directly by the gateway.
1229- config = context .run .run_spec .configuration
1230- assert isinstance (config , ServiceConfiguration )
1231- router_group = next ((g for g in config .replica_groups if g .router is not None ), None )
1232- is_router_replica = router_group is not None and job_spec .replica_group == router_group .name
1233- if router_group is not None and not is_router_replica :
1234- logger .debug (
1235- "%s: skipping gateway replica registration (non-router replica)" ,
1236- fmt (context .job_model ),
1237- )
1238- return None
1239-
12401247 async with get_session_ctx () as session :
12411248 gateway_model , connections = await get_or_add_gateway_connections (
12421249 session , context .run_model .gateway_id
@@ -1261,7 +1268,7 @@ async def _register_service_replica(
12611268 async with conn .client () as gateway_client :
12621269 await gateway_client .register_replica (
12631270 run = context .run ,
1264- job_spec = job_spec ,
1271+ job_spec = context . job . job_spec ,
12651272 job_submission = job_submission ,
12661273 instance_project_ssh_private_key = instance_project_ssh_private_key ,
12671274 ssh_head_proxy = ssh_head_proxy ,
@@ -1877,6 +1884,23 @@ def _emit_reachability_change_event(
18771884 )
18781885
18791886
1887+ def _emit_readiness_change_event (
1888+ session : AsyncSession ,
1889+ job_model : JobModel ,
1890+ old_ready : bool ,
1891+ new_ready : bool ,
1892+ ) -> None :
1893+ # ready: False -> True
1894+ if not old_ready and new_ready :
1895+ events .emit (
1896+ session ,
1897+ "Service replica ready to receive requests" ,
1898+ actor = events .SystemActor (),
1899+ targets = [events .Target .from_model (job_model )],
1900+ )
1901+ # ready: True -> False is not possible as of this writing
1902+
1903+
18801904def _terminate_job (
18811905 job_model : JobModel ,
18821906 job_update_map : _JobUpdateMap ,
0 commit comments