freeswitch-esl 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c0d4b60476e72dd268724e8d4d2e560505295de1e5d91cb78c53e065b539d8e
4
- data.tar.gz: d79c813eb54beaa5074f7b7e05659fb1fe272bb3ef914b2d50c0a1b35540de62
3
+ metadata.gz: a8cd34473eb85fec06aef418a2560d26a64975cb08245de7776aae2678dea90e
4
+ data.tar.gz: e79323aad3dd7655ea46c731818edbbf056dc6c24fb051fedc26a46327133231
5
5
  SHA512:
6
- metadata.gz: 6c815f9380e5de15a2e31d3fa9940b761ccdd4ac10b856cf82bb986c209a9f7d1b4b6236f626fd7ebf828a7379caaabc1469e0eb8eb3a62e3e9ed54b341dafde
7
- data.tar.gz: eeca1cbb4a3ca90cfa48fbb5e4589f1efac8b55c29529162bc82f4f6bbe06749cb6d190aba2a2b4e0d005b222a30c7e0cd3d4537fd208d274f243e7cc6c41a18
6
+ metadata.gz: 106d51156de70d8fcd97dc9c1d2f1f4c8d748b3c107f0967a23c40627dbcdd4d7a86d146c91b7a30e6e11913932b17c5e7470f5ef0d0651e48d49b332a802ac2
7
+ data.tar.gz: 4a4d954236148455f75d44518e9c08e8f17f430d786bba00332cb724920788af46e361b40c5d4421f9acfc95dc46e2e7b78a0341c08e813669542f09f9aa1348
data/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # freeswitch-esl
2
2
 
3
+ ![Coverage](https://gitlab.opinioni.net/demetra-developers/rubygems/freeswitch-esl/badges/main/coverage.svg)
4
+ [![Gem Version](https://badge.fury.io/rb/freeswitch-esl.svg)](https://badge.fury.io/rb/freeswitch-esl)
5
+
6
+
3
7
  `freeswitch-esl` is a Ruby gem for interacting with FreeSWITCH through ESL (Event Socket Library).
4
8
  This version is intentionally focused on one direction only: your Ruby process connects inbound to `mod_event_socket` and manages one or more `Freeswitch::ESL::Client` instances.
5
9
 
@@ -179,6 +183,16 @@ You can:
179
183
  - inspect `command.status`, `command.ok?`, `command.failed?`, `command.timeout?`
180
184
  - attach `command.on_complete { |cmd| ... }` for async completion handling
181
185
 
186
+ As of 0.2.2, `client.exec(..., timeout: seconds)` shares one monotonic timeout
187
+ budget across client readiness, the initial `bgapi` acknowledgement and the
188
+ `BACKGROUND_JOB` result. `exec` waits for that acknowledgement; `wait`/`response`
189
+ wait for the result. `timeout: nil` disables the deadline. Submission timeout
190
+ raises `Freeswitch::ESL::TimeoutError` unless `raise_error: false`, in which case
191
+ the command records `status: :timeout`. Late replies are consumed in FIFO order
192
+ without reviving the command or closing the connection. A timeout after sending
193
+ does **not** prove that FreeSWITCH did not execute the command; reconcile its
194
+ remote outcome before deciding whether a retry is safe.
195
+
182
196
  If you prefer lower-level primitives, `Freeswitch::ESL::Client` also exposes the inherited connection API:
183
197
 
184
198
  - `send_command`
@@ -215,6 +229,9 @@ client.subscribe('CHANNEL_HANGUP')
215
229
 
216
230
  `client.close` stops reconnect attempts immediately. The same client instance can be
217
231
  connected again later with `client.connect`.
232
+ Call `close` for cleanup even after a remote disconnect: it releases the event
233
+ dispatcher too. Local shutdown fails pending command replies with
234
+ `Freeswitch::ESL::DisconnectedError` and does not trigger a reconnect.
218
235
 
219
236
  ## Docker Compose Test Lab
220
237
 
@@ -22,9 +22,10 @@ module Freeswitch
22
22
  #
23
23
  # === Timeout
24
24
  #
25
- # The default timeout is {DEFAULT_TIMEOUT} seconds. If the bgapi result event
26
- # does not arrive in time the command transitions to :timeout and any
27
- # subsequent result is silently ignored.
25
+ # The default timeout is {DEFAULT_TIMEOUT} seconds across client readiness,
26
+ # the bgapi acknowledgement and the result event. A timeout after submission
27
+ # does not establish whether FreeSWITCH executed the command. Late replies
28
+ # remain in the connection FIFO until consumed; late results are ignored.
28
29
  class Command
29
30
  DEFAULT_TIMEOUT = 60
30
31
 
@@ -61,17 +62,13 @@ module Freeswitch
61
62
  def execute!
62
63
  configure_timeout!
63
64
  return unless wait_until_client_ready
65
+ return unless begin_submission?
64
66
 
65
- @sent_at = now
66
- @status = :sent
67
- @job_uuid = @client.bgapi(@command, *@args) do |event|
67
+ @job_uuid = @client.bgapi(@command, *@args, timeout: remaining_timeout) do |event|
68
68
  event.error? ? fail!(event) : complete!(event)
69
69
  end
70
70
  rescue StandardError => e
71
- @status = :failed
72
- @error = e
73
- @finished_at = now
74
- raise e if @raise_error
71
+ handle_submission_error(e)
75
72
  end
76
73
 
77
74
  # Register a callback to be called when the command completes (success or
@@ -82,7 +79,7 @@ module Freeswitch
82
79
  raise ArgumentError, "Block is required" unless block_given?
83
80
 
84
81
  @callbacks << block
85
- block.call(self) if completed?
82
+ yield(self) if completed?
86
83
 
87
84
  self
88
85
  end
@@ -104,10 +101,14 @@ module Freeswitch
104
101
  @mutex.synchronize do
105
102
  return self if completed?
106
103
 
107
- wait_time = @timeout ? (@timeout - execution_time.to_f) : nil
108
- @cond.wait(@mutex, wait_time)
104
+ while pending?
105
+ break if deadline_expired?
106
+
107
+ @cond.wait(@mutex, remaining_timeout)
108
+ end
109
109
  end
110
110
 
111
+ timeout!("Command timed out after #{@timeout}s waiting for completion") if pending?
111
112
  raise @error if @error && @raise_error
112
113
 
113
114
  self
@@ -157,16 +158,48 @@ module Freeswitch
157
158
  def configure_timeout!
158
159
  return unless @timeout
159
160
 
161
+ @deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @timeout
160
162
  Ztimer.after(@timeout * 1000) do
161
- timeout!("Command timed out after #{@timeout}s waiting for BACKGROUND_JOB result")
163
+ timeout!("Command timed out after #{@timeout}s waiting for completion")
164
+ end
165
+ end
166
+
167
+ def remaining_timeout
168
+ [@deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max if @deadline
169
+ end
170
+
171
+ def deadline_expired?
172
+ @deadline && remaining_timeout.zero?
173
+ end
174
+
175
+ # The watchdog may already have settled the command while bgapi was
176
+ # waiting. Never overwrite that result or invoke completion twice.
177
+ def handle_submission_error(error)
178
+ settle(error.is_a?(TimeoutError) ? :timeout : :failed) { @error = error }
179
+ raise @error if @raise_error && @error
180
+ end
181
+
182
+ def begin_submission?
183
+ @mutex.synchronize do
184
+ return false unless pending?
185
+
186
+ if !@deadline || remaining_timeout.positive?
187
+ @sent_at = now
188
+ @status = :sent
189
+ @cond.broadcast
190
+ return true
191
+ end
162
192
  end
193
+
194
+ timeout!("Command timed out after #{@timeout}s waiting for client readiness")
195
+ false
163
196
  end
164
197
 
165
198
  # Wait until the client is ready to send commands. If readiness does not
166
199
  # happen within the configured timeout, the command fails before any bgapi
167
200
  # request is sent to FreeSWITCH.
168
201
  def wait_until_client_ready
169
- is_ready = @client.wait_until_ready(timeout: @timeout, raise_error: false).ready?
202
+ is_ready = @client.wait_until_ready(timeout: remaining_timeout, raise_error: false).ready?
170
203
  return is_ready if is_ready
171
204
 
172
205
  settle(:failed) do
@@ -100,6 +100,8 @@ module Freeswitch
100
100
 
101
101
  logger.debug "Stopping command dispatcher and closing socket"
102
102
  @closed = true
103
+ @disconnect_error ||= DisconnectedError.new("Connection is closed")
104
+ disconnect_pending_commands
103
105
  @healthcheck_timer&.cancel!
104
106
 
105
107
  begin
@@ -140,19 +142,25 @@ module Freeswitch
140
142
  # pending command with the same error and then invoke the disconnect
141
143
  # callback once teardown has completed.
142
144
  def disconnect!(error)
145
+ # Closing the socket in stop wakes a blocked reader with EOF/IOError.
146
+ # That is local teardown, not a new remote disconnect/reconnect signal.
147
+ return if closed?
148
+
143
149
  @disconnect_error = error
144
150
  logger.debug "Disconnected: #{error.message}"
145
151
 
146
- # Deliver DisconnectedError to all pending in-flight command executions
152
+ stop
153
+ @on_disconnect&.call(error)
154
+ end
155
+
156
+ # Local teardown must wake pending commands too, without triggering reconnect.
157
+ def disconnect_pending_commands
147
158
  loop do
148
159
  cmd = pop_command(non_block: true)
149
160
  break unless cmd
150
161
 
151
- cmd.disconnected!(error)
162
+ cmd.disconnected!(@disconnect_error)
152
163
  end
153
-
154
- stop
155
- @on_disconnect&.call(error)
156
164
  end
157
165
 
158
166
  # Enqueue and write the command while the caller holds +@write_mutex+ so
@@ -99,14 +99,14 @@ module Freeswitch
99
99
  # removed together with the dispatcher instances, so a reconnect path must
100
100
  # rebuild them through {#initialize_socket}.
101
101
  def close
102
- return if @closed
103
-
102
+ # A remote disconnect sets @closed before explicit cleanup. Dispatchers
103
+ # still need to be stopped even when the transport is already closed.
104
104
  @closed = true
105
105
  @command_dispatcher&.stop
106
106
  @event_dispatcher&.stop
107
107
 
108
- send(:remove_instance_variable, :@command_dispatcher)
109
- send(:remove_instance_variable, :@event_dispatcher)
108
+ @command_dispatcher = nil
109
+ @event_dispatcher = nil
110
110
  end
111
111
 
112
112
  protected
@@ -114,12 +114,15 @@ module Freeswitch
114
114
  # (Re-)initialise all per-connection state for the given socket.
115
115
  # Called by subclasses on initial connect and on reconnect.
116
116
  def initialize_socket(socket, debug: false)
117
+ # Failed authentication/subscription may leave the previous reader alive.
118
+ # Retire it before publishing a replacement dispatcher.
119
+ @command_dispatcher&.stop
117
120
  @socket = socket
118
121
  @closed = false
119
122
 
120
123
  # Keep dispatcher data across reconnects so existing handlers still work
121
124
  # after a new socket is created.
122
- unless instance_variable_defined?(:@event_dispatcher)
125
+ unless @event_dispatcher
123
126
  @event_dispatcher = EventDispatcher.new
124
127
  @event_dispatcher.start
125
128
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Freeswitch
4
4
  module ESL
5
- VERSION = "0.2.0"
5
+ VERSION = "0.2.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: freeswitch-esl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Demetra Opinioni.net Srl
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-06-26 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: configatron
@@ -151,9 +150,9 @@ dependencies:
151
150
  - !ruby/object:Gem::Version
152
151
  version: '0.22'
153
152
  description: |
154
- A thread-safe Ruby client for the FreeSWITCH Event Socket Library.
155
- Supports inbound (client→FreeSWITCH) and outbound (FreeSWITCH→client) socket modes,
156
- API commands, background API, event subscriptions, call control and DTMF handling.
153
+ A thread-safe Ruby client for FreeSWITCH Event Socket Library connections.
154
+ Focused on inbound mod_event_socket integration with command execution,
155
+ bgapi jobs, JSON event handling, and reconnect support.
157
156
  email:
158
157
  - developers@opinioni.net
159
158
  executables: []
@@ -175,12 +174,11 @@ files:
175
174
  - lib/freeswitch/esl/protocol/event.rb
176
175
  - lib/freeswitch/esl/protocol/message.rb
177
176
  - lib/freeswitch/esl/version.rb
178
- homepage: https://gitlab.opinioni.net/demetra-opinioni/rubygems/freeswitch-esl
177
+ homepage: https://gitlab.opinioni.net/demetra-developers/rubygems/freeswitch-esl
179
178
  licenses:
180
179
  - MIT
181
180
  metadata:
182
181
  rubygems_mfa_required: 'true'
183
- post_install_message:
184
182
  rdoc_options: []
185
183
  require_paths:
186
184
  - lib
@@ -195,8 +193,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
195
193
  - !ruby/object:Gem::Version
196
194
  version: '0'
197
195
  requirements: []
198
- rubygems_version: 3.5.16
199
- signing_key:
196
+ rubygems_version: 4.0.16
200
197
  specification_version: 4
201
- summary: Ruby client for FreeSWITCH Event Socket Library (ESL)
198
+ summary: Inbound Ruby client for FreeSWITCH ESL
202
199
  test_files: []