Skip to content

[Bug]: Service with replica IP refs stuck in provisioning when instances are newly provisioned #4331

Open

Description

@r4victor

Steps to reproduce

  1. Create a fleet with nodes: min: 0 on a VM backend (reproduced on jarvislabs, india-noida-01), so that instances are provisioned for the run rather than reused.
  2. Apply a service whose group commands reference replica IPs, e.g. (reproduced with a 4-replica GPU service with the same refs; this is a minimal equivalent):
type: service
name: replica-ip-refs

groups:
  - name: workers
    replicas: 2
    commands:
      - echo "head=${{ groups[0].replicas[0].IP_ADDRESS }}"
      - python3 -m http.server 8000
    resources:
      cpu: 2

port: 8000

Actual behaviour

The run stays in provisioning forever. The instances come up and the instances rows get hostname and internal_ip set, but the jobs' job_provisioning_data keeps hostname=None, internal_ip=None, and every job logs waiting for referenced group IPs on each iteration.

For a PROVISIONING job, _prepare_startup_context runs before _process_provisioning_status and returns None when a referenced IP is missing:

if context.job_model.status == JobStatus.PROVISIONING:
startup_context = await _prepare_startup_context(context=context, result=result)
if startup_context is None:
return result
await _process_provisioning_status(
context=context, startup_context=startup_context, result=result

commands = context.job.job_spec.commands
try:
for c in commands:
validate_groups_refs(c)
if not _substitute_groups_ip_refs(commands, context):
logger.debug(
"%s: waiting for referenced group IPs",
fmt(context.job_model),
)
return None

But copying the instance's provisioning data (with hostname/internal_ip) into the job only happens in _process_provisioning_status, which is therefore never reached:

if job_provisioning_data.hostname is None:
_wait_for_instance_provisioning_data(context.job_model, result)

def _wait_for_instance_provisioning_data(
job_model: JobModel,
result: _ProcessResult,
) -> None:
if job_model.instance is None:
logger.error(
"%s: cannot update job_provisioning_data. job_model.instance is None.",
fmt(job_model),
)
return
if job_model.instance.job_provisioning_data is None:
logger.error(
"%s: cannot update job_provisioning_data. job_model.job_provisioning_data is None.",
fmt(job_model),
)
return
if job_model.instance.status == InstanceStatus.TERMINATED:
_terminate_job(
job_model=job_model,
job_update_map=result.job_update_map,
termination_reason=JobTerminationReason.WAITING_INSTANCE_LIMIT_EXCEEDED,
termination_reason_message="Instance is terminated",
)
return
result.job_update_map["job_provisioning_data"] = job_model.instance.job_provisioning_data

The IP view reads internal_ip from the jobs' own provisioning data, so the referenced IPs never appear and all jobs wait on each other:

def _build_replica_groups_ip_view(
jobs: list[Job], configuration: ServiceConfiguration
) -> list[list[str]]:
"""Fixed-length rows of replicas.min; slot j is the j-th live job in that group."""
view: list[list[str]] = []
for group_index, group in enumerate(configuration.replica_groups):
size = group.replicas.min or 0
row = [""] * size
group_name = group.name if group.name is not None else str(group_index)
group_jobs = [
job
for job in jobs
if job.job_spec.replica_group == group_name and not _job_is_finished(job)
]
group_jobs.sort(key=lambda job: job.job_spec.replica_num)
for slot, job in enumerate(group_jobs[:size]):
row[slot] = _job_internal_ip(job)
view.append(row)
return view
def _job_internal_ip(job: Job) -> str:
if job.job_submissions:
jpd = job.job_submissions[-1].job_provisioning_data
if jpd is not None:
return jpd.internal_ip or ""

It works only when jobs land on already-provisioned idle fleet instances, since their provisioning data is copied with IPs at assignment. Stopping the stuck run and re-applying within the idle window works around it. groups[i].nodes[j].IP_ADDRESS refs in multi-node tasks go through the same gate and are likely affected too (not verified).

Expected behaviour

Jobs pick up their instance's provisioning data while waiting, and the run starts once all referenced replicas have internal IPs.

dstack version

master (70c2616)

Activity

  1. r4victor commented on Oct 2, 2026

    @r4victor
    CollaboratorAuthor

    Another consequence: if an instance fails to provision while its job is waiting on the IP gate, the job is never failed. Seen on JarvisLabs: the instance went TERMINATED with Error while waiting for instance to become running, but the job stayed PROVISIONING indefinitely, so the run neither failed nor retried. The check that terminates the job when its instance is terminated lives in the same skipped _wait_for_instance_provisioning_data:

    if job_model.instance.status == InstanceStatus.TERMINATED:
    _terminate_job(
    job_model=job_model,
    job_update_map=result.job_update_map,
    termination_reason=JobTerminationReason.WAITING_INSTANCE_LIMIT_EXCEEDED,
    termination_reason_message="Instance is terminated",
    )
    return

  2. self-assigned this
    on Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions