Skip to content

Replace hybrid ESL model with single librevox inbound connection#18

Merged
henrikbjorn merged 26 commits into
masterfrom
replace-esl-with-librevox
Mar 11, 2026
Merged

Replace hybrid ESL model with single librevox inbound connection#18
henrikbjorn merged 26 commits into
masterfrom
replace-esl-with-librevox

Conversation

@henrikbjorn

Copy link
Copy Markdown
Member

Summary

  • 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, control calls via uuid-targeted API commands (uuid_kill, uuid_answer, uuid_broadcast)
  • Remove outbound server, custom protocol handling, CaseInsensitiveHash, and flatten the ESL namespace (Switest::ESL::ClientSwitest::Client)
  • Simplify Docker setup (no outbound port, no network detection) and update dependencies from concurrent-ruby/async to librevox ~> 1.0
  • Net deletion of ~1000 lines of code

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
@henrikbjorn henrikbjorn self-assigned this Feb 25, 2026
@henrikbjorn henrikbjorn changed the base branch from master to replace-concurrent-ruby-with-async February 25, 2026 14:45
@henrikbjorn henrikbjorn requested a review from c960657 February 25, 2026 14:46

@c960657 c960657 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤯

- 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.
@henrikbjorn henrikbjorn changed the base branch from replace-concurrent-ruby-with-async to master February 27, 2026 12:22
- 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.
@henrikbjorn henrikbjorn merged commit cfd2103 into master Mar 11, 2026
2 checks passed
@henrikbjorn henrikbjorn deleted the replace-esl-with-librevox branch March 11, 2026 11:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants