Replace hybrid ESL model with single librevox inbound connection#18
Merged
Conversation
Switch from threads + concurrent-ruby primitives to the socketry async ecosystem (async, io-endpoint, io-stream) for simpler, cooperative fiber-based concurrency. Key changes: - Reader thread replaced by an Async task (no IO.select polling) - Concurrent::IVar/Queue command pipeline replaced by direct writes + Async::Condition signals - Concurrent::CountDownLatch replaced by Async::Variable for call state waits - All Mutex usage removed (cooperative scheduling eliminates races) - Concurrent::Map replaced by plain Hash - Scenario#run wraps in Sync for transparent async reactor
Add Dockerfile and docker-compose.yml for FreeSWITCH, passing SignalWire repo credentials via environment variables. Update CI to build from secrets and gitignore .env for local use.
Use custom Dockerfile with SignalWire repo credentials instead of the public image, while keeping existing volumes and healthcheck.
The vanilla config tries to load dozens of modules not installed in our image, causing CRIT errors. Mount a minimal modules.conf.xml that only loads what the integration tests need.
- Move call and scenario tests to test/scenarios/ with _scenario suffix - Add scenario_helper.rb for scenario test configuration - Add Sync wrapper to ESL connection/client integration tests to prevent fiber leaks between test runs - Add separate CI jobs for integration and scenario tests - Add rake scenarios task
Remove custom Dockerfile and modules.conf.xml in favour of the stable third-party image. Drop FS_REPO secrets from CI.
Replace the complex hybrid architecture (inbound ESL connection for bgapi + custom outbound TCP server for per-call control) with a single Librevox::Listener::Inbound connection. - Use bgapi originate with &park() instead of &socket() for call origination - Detect inbound calls via CHANNEL_PARK events for unknown UUIDs - Control calls via uuid-targeted API commands (uuid_kill, uuid_answer, uuid_broadcast) - Remove outbound server, custom protocol handling, and CaseInsensitiveHash - Flatten ESL namespace (Switest::ESL::Client → Switest::Client) - Simplify Docker setup (no outbound port, no network detection) - Update dependencies: concurrent-ruby/async → librevox ~> 1.0
- Normalize 6-space to 2-space indentation in Session, Client, Call - Add receive_call method to Agent, replacing instance_variable_set - Add missing Agent delegations (id, outbound?, inbound?, end_time, headers, bridged?) - Update scenarios to use Agent delegations instead of .call. access - Simplify Events to one-shot only (remove unused on/off methods) - Remove no-op CHANNEL_PARK handler from Call - Document Session class-level state design constraint
Matches the original ESL behavior. CHANNEL_HANGUP carries minimal data and was being silently discarded by the guard anyway since it fired before CHANNEL_HANGUP_COMPLETE.
The librevox rewrite used `api uuid_answer` and `api uuid_kill` which are global API calls that return before the channel finishes processing. The original ESL version used `sendmsg` which queues commands on the channel thread, ensuring answer/hangup settle internal state before returning. This fixes a race condition where immediate hangup after answer could cause ORIGINATOR_CANCEL instead of NORMAL_CLEARING. Also switches play_audio from uuid_broadcast to sendmsg execute playback with event-lock, matching the original behavior. Adds bridge dialplan extension and two-leg bridge scenario test.
CHANNEL_ANSWER fires while channel_call_state is still RINGING. The call isn't fully established until CHANNEL_CALLSTATE ACTIVE. This fixes the race condition where hangup after answer could cause ORIGINATOR_CANCEL because the channel wasn't settled. Also removes bridge tracking (bridged?, wait_for_bridge, assert_bridged) — CHANNEL_BRIDGE events fire on internal gateway channel UUIDs, not tracked call UUIDs, making them unreliable for real SIP scenarios. Other changes: - Bump FreeSWITCH session rate limit to 200/sec for tests - Fix scenario test to use inbound_test instead of echo for listen_for_call (echo doesn't park) - Simplify answered? to match active? (no post-hangup check)
No code outside of tests used these callbacks. Tests use wait_for_answer/wait_for_end and assertions instead.
- Use CALLSTATE_MAP to drive state transitions, guard against redundant updates - Initial call state is :new, inbound calls set to :ringing on CHANNEL_PARK - Drop CHANNEL_ANSWER handling entirely, rely on CHANNEL_CALLSTATE - Extract assertions into Switest::Assertions module with new SIP assertions - Remove integration tests (covered by scenarios) - Set Librevox log level to WARN for scenarios
Sleeps were unnecessary — async fiber scheduling interleaves correctly without artificial delays. Timeout-based tests reduced from 0.2-0.5s to 0.01s. Test suite drops from ~4.8s to ~0.07s.
Without sleeps, Async do blocks with no yield points run synchronously and immediately — they were just testing the early-return path of wait_for_* methods, not the promise-waiting path. Remove the wrappers and call handle_*/emit directly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Librevox::Listener::Inboundconnectionbgapi originatewith&park()instead of&socket()for call origination, detect inbound calls viaCHANNEL_PARKevents, control calls via uuid-targeted API commands (uuid_kill,uuid_answer,uuid_broadcast)CaseInsensitiveHash, and flatten theESLnamespace (Switest::ESL::Client→Switest::Client)concurrent-ruby/asynctolibrevox ~> 1.0