After ctx.shutdown and deleting the room, dialler ...
# ask-ai
a
After ctx.shutdown and deleting the room, dialler server doesn't receive the disconnect signal from Livekit Server.. What should be the issue??
t
Thanks for reaching out, Hiren. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I hear you: after calling
ctx.shutdown
and deleting the room the dialler server still isn't getting the disconnect signal from LiveKit. Let’s walk through likely causes and some targeted checks to pinpoint the problem. What this usually means • Deleting the room or shutting down the server should terminate SIP participants, but if the SIP participant was removed only from LiveKit’s internal state (or removed with different logic) the downstream dialler might not get the expected SIP BYE/Disconnect. • In your codebase there are two relevant ways SIP callers are terminated: ◦ A cleanup path that looks for participants whose
identity
starts with
sip_
and calls
remove_participant
. ◦ Deleting the entire room with
delete_room
, which terminates all participants including SIP callers. Quick checklist — please try these and report results 1. Confirm how you are ending the call in practice: ◦ Are you calling
remove_participant
for the SIP identity (starts with
sip_
) or only calling
ctx.shutdown
+
delete_room
? ◦ If you rely solely on a participant-removal path inside an exception handler, it may not run consistently. 1. Test deleting the room explicitly: ◦ Call
delete_room
and verify whether the dialler receives the disconnect. If
delete_room
does cause the dialler to get the disconnect, that indicates the participant-specific cleanup path is not performing the SIP disconnect. 1. Check logs and timing: ◦ Inspect LiveKit server logs around the time of shutdown/deletion for messages about removing
sip_
participants or room deletion. ◦ Confirm the order: if
ctx.shutdown
happens before
remove_participant
or before
delete_room
, the cleanup code might exit early and not send the SIP disconnect. 1. Verify identity naming and matching: ◦ Ensure the SIP participant identity actually begins with
sip_
when cleanup runs. If the identity differs, the conditional that calls
remove_participant
won’t run. 1. Confirm dialler-side behavior: ◦ Verify the dialler server is listening for the expected SIP BYE/Disconnect and hasn’t timed out or dropped the session before LiveKit issues the disconnect. Next diagnostic steps I can help with • If you can share which method you currently use to end calls (participant removal vs delete_room), any relevant code snippet for the cleanup logic, and logs around the shutdown, I can point to the exact cause and suggest a fix. • If you want a quick, robust approach: use
delete_room
to ensure all SIP participants are disconnected. If you need targeted removal, ensure
remove_participant
is called for identities starting with
sip_
and that the cleanup code reliably runs before the process exits. Which of these checks can you run now, and can you paste the cleanup snippet or a relevant log excerpt? I’ll walk through the next steps with you.
a
I am only calling cx.shutdown ... and in that shutdown callback function, I'm calling deleteRoom request
t
Thanks — that helps. You’re calling
ctx.shutdown
, and inside that shutdown callback you send a
deleteRoom
request. Let’s narrow down why the dialler isn’t seeing the disconnect and what to check/change. What could be happening •
deleteRoom
should disconnect all participants, including SIP callers, but if the shutdown sequence or process exit happens before LiveKit finishes sending the SIP disconnect (BYE), the dialler may never receive it. • If the SIP participant was already removed earlier or its identity doesn’t match the cleanup logic (e.g., not starting with
sip_
), the code path that triggers the SIP teardown may not run as you expect. • Race conditions: shutting down the context/process too quickly after requesting
deleteRoom
can interrupt the outgoing SIP signaling. Steps to troubleshoot and fix 1. Confirm ordering and wait for completion ◦ After calling
deleteRoom
, ensure you wait for the delete request to complete (success response) before the process exits or other shutdown steps continue. Make the delete call synchronous or await its completion. ◦ Add a short delay only for debugging (e.g., a few hundred ms) after a successful
deleteRoom
to see if the dialler receives the BYE — if it does, that indicates a timing/race issue. 1. Check logs for evidence of SIP BYE ◦ Inspect LiveKit server logs for an attempt to disconnect the SIP participant when
deleteRoom
is processed. ◦ Look for errors or warnings that indicate the server failed to signal the dialler. 1. Verify SIP participant identity and presence ◦ Confirm the SIP participant’s
identity
actually starts with
sip_
at the time of deletion if your cleanup relies on that pattern. ◦ If identities are different or change before shutdown, the SIP-specific removal path might not be executed. 1. Try explicit participant removal as a test ◦ As a diagnostic, call
remove_participant
for the SIP identity (the
sip_
participant) before
deleteRoom
and see whether the dialler receives the disconnect. If it does, that confirms
deleteRoom
wasn’t finishing or wasn’t reaching the SIP teardown path before process shutdown. 1. Ensure dialler readiness ◦ Confirm the dialler is ready to receive and process the BYE (not blocked, crashed, or disconnected) at the time LiveKit sends it. What I can help with next • If you can paste the shutdown callback code (the snippet that calls
deleteRoom
), I’ll review it for ordering/race issues and recommend where to await or add checks. • If you can share the LiveKit logs around the delete call, I’ll look for evidence of the SIP disconnect attempt and any errors. If you want a quick robust approach: explicitly remove the SIP participant (
remove_participant
for the
sip_
identity) and wait for that operation to complete before finishing the shutdown. That guarantees the SIP disconnect path runs even if room deletion or shutdown sequencing is flaky.
a
async def hangup_call(): logger.info("DISCONNECTION REQUEST RECD") ctx = get_job_context() if ctx is None: return await ctx.api.room.delete_room(api.DeleteRoomRequest(room=ctx.room.name)) above function gets called from my closeCall() function which is registered as shutdown callback
t
Thanks — this is helpful. You're calling
delete_room
from your async
hangup_call()
shutdown callback, and the code snippet shows you await the delete call. Let’s focus on the most likely causes and a few precise checks you can run to confirm and fix the missing SIP disconnect. Quick summary of what I’m responding to • Your shutdown callback
hangup_call()
awaits
ctx.api.room.delete_room(...)
. • The dialler still doesn’t receive the SIP disconnect (BYE) after this runs. Likely causes • Race/timing: the process may continue shutting down (or the runtime may exit) before LiveKit finishes sending the SIP BYE, even though the delete request returned. • SIP teardown path may be skipped if the SIP participant was already removed earlier or if identity matching differs. • Dialler-side: the dialler might be unable to receive/process the BYE at that moment. Concrete checks and fixes to try 1. Confirm the delete actually completed successfully ◦ After
await ctx.api.room.delete_room(...)
, log the response or catch and log exceptions to ensure it returned success. ◦ Example: log response.status or the returned object so you know the request finished. 1. Add an explicit small delay after successful delete (diagnostic) ◦ Temporarily add
await asyncio.sleep(0.5)
(500ms) after delete to see if the dialler then receives the BYE. If it does, that shows a race between delete and process shutdown. 1. Ensure process doesn’t exit immediately ◦ Verify the shutdown sequence doesn’t exit the event loop or kill the process immediately after the callback finishes. Make sure the event loop runs long enough for LiveKit to complete any outbound SIP signaling. 1. Try explicit participant removal before deleting the room (diagnostic) ◦ Call
await ctx.api.room.remove_participant(...)
for the SIP identity (e.g.,
sip_...
) and await its completion, then call
delete_room
. If the dialler receives disconnect on
remove_participant
, use that as a reliable teardown step. 1. Log and verify identity matching ◦ At shutdown, enumerate
ctx.room.participants
(or the API equivalent) and log identities to ensure the SIP participant identity still starts with
sip_
and is present when you delete the room. 1. Inspect LiveKit server logs ◦ Look for entries that show the SIP BYE being sent (or errors during deletion). If you see the BYE attempt, the problem is likely on the dialler side. If you see errors, paste them here and I’ll help interpret. What I can help with next • Paste your shutdown registration code (how
closeCall()
registers the callback) so I can confirm the callback ordering and lifecycle. • If possible, share the delete_room response or relevant server logs around the delete time (timestamped lines showing deletion and any SIP activity). • If you want a quick robust change: explicitly call
remove_participant
for the SIP identity and await it; then call
delete_room
and await that as a final cleanup. This ensures the SIP-specific teardown runs reliably. If you want, paste the shutdown registration and any logs you have and I’ll review them with you.
a
is ctx becoming none when I call ctx.shutdown??
t
Thanks — good question. I’m responding to your concern about whether
ctx
can become
None
when you call
ctx.shutdown
, and how that might affect your shutdown callback. Short answer • Yes —
ctx
can become
None
after shutdown if the job/context lifecycle clears it as part of the shutdown sequence. If your shutdown callback reads
ctx
from a global getter (like
get_job_context()
), that getter can return
None
once the context is torn down. Why that matters • If
hangup_call()
calls
get_job_context()
inside the callback and the global/context store has already been cleared by the time the callback runs, you’ll get
None
and won’t be able to call
delete_room
or
remove_participant
. • That would explain why the dialler never sees the SIP BYE: the code that sends the disconnect never runs because
ctx
is missing. What to check now 1. Log the context value right before you call shutdown and at the start of the shutdown callback: ◦ At shutdown entry: log the value of
ctx
you pass to
ctx.shutdown
(if you pass it). ◦ Inside `hangup_call()`: immediately log the result of
get_job_context()
to confirm whether it’s
None
. 1. Confirm how the shutdown callback is registered and invoked: ◦ If you register a callback that runs after the context is cleared, it will not have access to
ctx
. Verify the callback runs while the context is still available. Safe patterns to ensure you have the data you need • Capture required data before calling `ctx.shutdown`: ◦ Read and stash the room name and SIP participant identity into local variables (or pass them into the shutdown callback) before invoking shutdown, so the callback doesn’t need to call
get_job_context()
. • Or: perform the delete/remove actions immediately before initiating the shutdown sequence: ◦ Call
remove_participant
or
delete_room
while
ctx
is guaranteed valid, then call
ctx.shutdown
afterward. • Make shutdown callback defensive: ◦ If you must use
get_job_context()
inside the callback, handle
None
by falling back to previously captured values or skipping non-critical cleanup. Example pattern (conceptual) 1. Capture room and SIP identity: ◦ room_name = ctx.room.name ◦ sip_id = the SIP participant identity 1. Call delete/remove while ctx is still valid: ◦ await ctx.api.room.remove_participant(room=room_name, participant=sip_id) ◦ await ctx.api.room.delete_room(room=room_name) 1. Then start
ctx.shutdown
. Next steps I can help with • Paste how you register
closeCall()
as the shutdown callback and the code that calls
ctx.shutdown
. I’ll point out where the context may be cleared and suggest the exact minimal code changes to ensure the SIP BYE is sent before
ctx
becomes
None
. • If you can share the logs showing the
DISCONNECTION REQUEST RECD
and any subsequent lines, I’ll check whether the callback ran and whether the delete call returned.
a
Copy code
async def close_call(_reason: str) -> None:
        try:
            inactivity_handler.stop()
            call_endtime = int(datetime.now(timezone).timestamp())
            <http://logger.info|logger.info>(f"######### C A L L - E N D E D ######### [{_reason}]")
            # session.input.set_audio_enabled(False)

            current_speech = session.current_speech
            if current_speech:
                await current_speech.wait_for_playout()

            if ctx and ctx.api:
                try:
                    # Check if room exists and api is available
                    await ctx.api.room.delete_room(api.DeleteRoomRequest(room=ctx.room.name))
                    <http://logger.info|logger.info>("...... room deleted and disconnected ......")
                except Exception as api_err:
                    logger.warning(f"Could not delete room via API: {api_err}")
            else:
                <http://logger.info|logger.info>("ctx.api is None, skipping room deletion.")    
            <http://logger.info|logger.info>("...... d i s c o n n e c t e d ......")

        except Exception as e:
            <http://logger.info|logger.info>(f"Error in function close_call :: {e}")
t
Thanks — this is helpful. I’ve reviewed your
close_call
function. I’m answering your question about why the dialler might not get the SIP disconnect and whether
ctx
can be None during shutdown. Short summary of what I see • Your
close_call
awaits
ctx.api.room.delete_room(...)
only if
ctx
and
ctx.api
are truthy. • If
ctx
becomes None (or
ctx.api
is cleared) before
close_call
runs, your code will skip deletion and no SIP BYE will be sent. • Even when you do call
delete_room
, a race can still occur if the process shuts down before LiveKit finishes outbound SIP signaling. Concrete recommendations (small, practical changes) 1) Capture required values before shutdown • Before calling
ctx.shutdown
(or registering the shutdown), read and stash the room name and SIP identity so the cleanup callback does not rely on
get_job_context()
or a global
ctx
that might be cleared. • Example conceptual flow: ◦ room_name = ctx.room.name ◦ sip_identity = ... (if you need it) ◦ register/trigger shutdown passing room_name and sip_identity into the callback 2) Ensure delete/remove runs while ctx is valid • Prefer performing
remove_participant
(for the SIP identity) and/or
delete_room
immediately while
ctx
is still available rather than relying on a later callback that may run after context cleanup. • If you must use a shutdown callback, pass the captured room name into it rather than calling
get_job_context()
inside the callback. 3) Make delete synchronous and wait for completion • Log the delete response and any exceptions: ◦ After the await, log success (or full exception). This confirms the API call returned. • Add a short await for debugging to detect race issues: ◦ e.g.,
await asyncio.sleep(0.5)
after a successful delete; if the dialler then receives the BYE, it confirms a timing/race issue. 4) Try explicit participant removal as a robust teardown • Call
await ctx.api.room.remove_participant(...)
for the SIP participant (identity starting with
sip_
) then await it, then call
delete_room
. Removing the participant explicitly forces the SIP disconnect path and is a reliable way to send BYE. 5) Improve logging to verify behavior • At the top of
close_call
, log the value of
ctx
,
ctx.api
, and
ctx.room.name
(or the captured equivalents). • Log the exact response from
delete_room
or any exception message so we can see whether the delete completed. Minimal example pattern (conceptual) 1. Before shutdown: ◦ room_name = ctx.room.name ◦ sip_id = ... (if relevant) 1. In shutdown callback: ◦ try: await api.room.remove_participant(api.RemoveParticipantRequest(room=room_name, participant=sip_id)) await api.room.delete_room(api.DeleteRoomRequest(room=room_name)) log success except Exception as e: log exception • await asyncio.sleep(0.5) # temporary diagnostic Next steps I can help with • If you paste how you register/invoke
close_call
(the shutdown registration and where you call
ctx.shutdown
), I’ll point out exactly where
ctx
may be cleared and give the minimal code edits to pass the room name into the callback. • If you can share the logs around the
DISCONNECTION REQUEST RECD
and any subsequent lines (especially any "ctx.api is None" or delete exceptions), I’ll interpret them and recommend the exact fix. Which would you like to do first — share the shutdown registration or add the pre-shutdown capture and I’ll show the exact small code change?