forest_admin_datasource_rpc 1.7.1 → 1.8.0
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:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: ede19279c81c39b065be45f1741b08acc5babc63efdec6314a9c691631eb41a7
         | 
| 4 | 
            +
              data.tar.gz: 7e9701d233a9fa29ce2e5197ce8d5a41bba5daec470ed2e519cb1441ca7266b1
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 7668783a404d091882070ae683cac5db1d04447c74310e7ce572c060ab2891589d8eb6ac919b93357edaca6a282ff297f3204d8858a47c8ab932c27a4cdc123a
         | 
| 7 | 
            +
              data.tar.gz: e98bf680d0e374e2974470d209d52b61ffa28a977dcc332c9fb091c289a8458a1591cbc888e5a025ff0f0aac7e999c22e9bcfe9eae88e42d6dc5c441bc72878a
         | 
| @@ -6,17 +6,58 @@ require 'ld-eventsource' | |
| 6 6 | 
             
            module ForestAdminDatasourceRpc
         | 
| 7 7 | 
             
              module Utils
         | 
| 8 8 | 
             
                class SseClient
         | 
| 9 | 
            +
                  attr_reader :closed
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  MAX_BACKOFF_DELAY = 30 # seconds
         | 
| 12 | 
            +
                  INITIAL_BACKOFF_DELAY = 2 # seconds
         | 
| 13 | 
            +
             | 
| 9 14 | 
             
                  def initialize(uri, auth_secret, &on_rpc_stop)
         | 
| 10 15 | 
             
                    @uri = uri
         | 
| 11 16 | 
             
                    @auth_secret = auth_secret
         | 
| 12 17 | 
             
                    @on_rpc_stop = on_rpc_stop
         | 
| 13 18 | 
             
                    @client = nil
         | 
| 14 19 | 
             
                    @closed = false
         | 
| 20 | 
            +
                    @connection_attempts = 0
         | 
| 21 | 
            +
                    @reconnect_thread = nil
         | 
| 22 | 
            +
                    @connecting = false
         | 
| 15 23 | 
             
                  end
         | 
| 16 24 |  | 
| 17 25 | 
             
                  def start
         | 
| 18 26 | 
             
                    return if @closed
         | 
| 19 27 |  | 
| 28 | 
            +
                    attempt_connection
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def close
         | 
| 32 | 
            +
                    return if @closed
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                    @closed = true
         | 
| 35 | 
            +
                    ForestAdminRpcAgent::Facades::Container.logger&.log('Debug', '[SSE Client] Closing connection')
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                    # Stop reconnection thread if running
         | 
| 38 | 
            +
                    if @reconnect_thread&.alive?
         | 
| 39 | 
            +
                      @reconnect_thread.kill
         | 
| 40 | 
            +
                      @reconnect_thread = nil
         | 
| 41 | 
            +
                    end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    begin
         | 
| 44 | 
            +
                      @client&.close
         | 
| 45 | 
            +
                    rescue StandardError => e
         | 
| 46 | 
            +
                      ForestAdminRpcAgent::Facades::Container.logger&.log('Debug',
         | 
| 47 | 
            +
                                                                          "[SSE Client] Error during close: #{e.message}")
         | 
| 48 | 
            +
                    end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    ForestAdminRpcAgent::Facades::Container.logger&.log('Debug', '[SSE Client] Connection closed')
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  private
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  def attempt_connection
         | 
| 56 | 
            +
                    return if @closed
         | 
| 57 | 
            +
                    return if @connecting
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    @connecting = true
         | 
| 60 | 
            +
                    @connection_attempts += 1
         | 
| 20 61 | 
             
                    timestamp = Time.now.utc.iso8601(3)
         | 
| 21 62 | 
             
                    signature = generate_signature(timestamp)
         | 
| 22 63 |  | 
| @@ -26,29 +67,106 @@ module ForestAdminDatasourceRpc | |
| 26 67 | 
             
                      'X_SIGNATURE' => signature
         | 
| 27 68 | 
             
                    }
         | 
| 28 69 |  | 
| 29 | 
            -
                    ForestAdminRpcAgent::Facades::Container.logger | 
| 70 | 
            +
                    ForestAdminRpcAgent::Facades::Container.logger&.log(
         | 
| 71 | 
            +
                      'Debug',
         | 
| 72 | 
            +
                      "[SSE Client] Connecting to #{@uri} (attempt ##{@connection_attempts})"
         | 
| 73 | 
            +
                    )
         | 
| 30 74 |  | 
| 31 | 
            -
                     | 
| 32 | 
            -
                      client | 
| 33 | 
            -
             | 
| 75 | 
            +
                    begin
         | 
| 76 | 
            +
                      # Close existing client if any
         | 
| 77 | 
            +
                      begin
         | 
| 78 | 
            +
                        @client&.close
         | 
| 79 | 
            +
                      rescue StandardError
         | 
| 80 | 
            +
                        # Ignore close errors
         | 
| 34 81 | 
             
                      end
         | 
| 35 82 |  | 
| 36 | 
            -
                      client. | 
| 37 | 
            -
                         | 
| 38 | 
            -
             | 
| 83 | 
            +
                      @client = SSE::Client.new(@uri, headers: headers) do |client|
         | 
| 84 | 
            +
                        client.on_event do |event|
         | 
| 85 | 
            +
                          handle_event(event)
         | 
| 86 | 
            +
                        end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                        client.on_error do |err|
         | 
| 89 | 
            +
                          handle_error_with_reconnect(err)
         | 
| 90 | 
            +
                        end
         | 
| 39 91 | 
             
                      end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                      ForestAdminRpcAgent::Facades::Container.logger&.log('Debug', '[SSE Client] Connected successfully')
         | 
| 94 | 
            +
                    rescue StandardError => e
         | 
| 95 | 
            +
                      ForestAdminRpcAgent::Facades::Container.logger&.log(
         | 
| 96 | 
            +
                        'Error',
         | 
| 97 | 
            +
                        "[SSE Client] Failed to connect: #{e.class} - #{e.message}"
         | 
| 98 | 
            +
                      )
         | 
| 99 | 
            +
                      @connecting = false
         | 
| 100 | 
            +
                      schedule_reconnect
         | 
| 40 101 | 
             
                    end
         | 
| 41 102 | 
             
                  end
         | 
| 42 103 |  | 
| 43 | 
            -
                  def  | 
| 104 | 
            +
                  def handle_error_with_reconnect(err)
         | 
| 105 | 
            +
                    # Ignore errors when client is intentionally closed
         | 
| 44 106 | 
             
                    return if @closed
         | 
| 45 107 |  | 
| 46 | 
            -
                     | 
| 47 | 
            -
                     | 
| 48 | 
            -
             | 
| 108 | 
            +
                    is_auth_error = false
         | 
| 109 | 
            +
                    log_level = 'Warn'
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                    error_message = case err
         | 
| 112 | 
            +
                                    when SSE::Errors::HTTPStatusError
         | 
| 113 | 
            +
                                      # Extract more details from HTTP errors
         | 
| 114 | 
            +
                                      status = err.respond_to?(:status) ? err.status : 'unknown'
         | 
| 115 | 
            +
                                      body = err.respond_to?(:body) && !err.body.to_s.strip.empty? ? err.body : 'empty response'
         | 
| 116 | 
            +
                                      is_auth_error = status.to_s =~ /^(401|403)$/
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                                      # Auth errors during reconnection are expected (server shutdown or credentials expiring)
         | 
| 119 | 
            +
                                      log_level = 'Debug' if is_auth_error
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                                      "HTTP #{status} - #{body}"
         | 
| 122 | 
            +
                                    when EOFError, IOError
         | 
| 123 | 
            +
                                      # Connection lost is expected when server stops
         | 
| 124 | 
            +
                                      log_level = 'Debug'
         | 
| 125 | 
            +
                                      "Connection lost: #{err.class}"
         | 
| 126 | 
            +
                                    when StandardError
         | 
| 127 | 
            +
                                      "#{err.class} - #{err.message}"
         | 
| 128 | 
            +
                                    else
         | 
| 129 | 
            +
                                      err.to_s
         | 
| 130 | 
            +
                                    end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                    ForestAdminRpcAgent::Facades::Container.logger&.log(log_level, "[SSE Client] Error: #{error_message}")
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                    # Close client immediately to prevent ld-eventsource from reconnecting with stale credentials
         | 
| 135 | 
            +
                    begin
         | 
| 136 | 
            +
                      @client&.close
         | 
| 137 | 
            +
                    rescue StandardError
         | 
| 138 | 
            +
                      # Ignore close errors
         | 
| 139 | 
            +
                    end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                    # Reset connecting flag and schedule reconnection
         | 
| 142 | 
            +
                    @connecting = false
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                    # For auth errors, increase attempt count to get longer backoff
         | 
| 145 | 
            +
                    @connection_attempts += 2 if is_auth_error
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                    schedule_reconnect
         | 
| 49 148 | 
             
                  end
         | 
| 50 149 |  | 
| 51 | 
            -
                   | 
| 150 | 
            +
                  def schedule_reconnect
         | 
| 151 | 
            +
                    return if @closed
         | 
| 152 | 
            +
                    return if @reconnect_thread&.alive?
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                    @reconnect_thread = Thread.new do
         | 
| 155 | 
            +
                      delay = calculate_backoff_delay
         | 
| 156 | 
            +
                      ForestAdminRpcAgent::Facades::Container.logger&.log(
         | 
| 157 | 
            +
                        'Debug',
         | 
| 158 | 
            +
                        "[SSE Client] Reconnecting in #{delay} seconds..."
         | 
| 159 | 
            +
                      )
         | 
| 160 | 
            +
                      sleep(delay)
         | 
| 161 | 
            +
                      attempt_connection unless @closed
         | 
| 162 | 
            +
                    end
         | 
| 163 | 
            +
                  end
         | 
| 164 | 
            +
             | 
| 165 | 
            +
                  def calculate_backoff_delay
         | 
| 166 | 
            +
                    # Exponential backoff: 1, 2, 4, 8, 16, 30, 30, ...
         | 
| 167 | 
            +
                    delay = INITIAL_BACKOFF_DELAY * (2**[@connection_attempts - 1, 0].max)
         | 
| 168 | 
            +
                    [delay, MAX_BACKOFF_DELAY].min
         | 
| 169 | 
            +
                  end
         | 
| 52 170 |  | 
| 53 171 | 
             
                  def handle_event(event)
         | 
| 54 172 | 
             
                    type = event.type.to_s.strip
         | 
| @@ -56,14 +174,34 @@ module ForestAdminDatasourceRpc | |
| 56 174 |  | 
| 57 175 | 
             
                    case type
         | 
| 58 176 | 
             
                    when 'heartbeat'
         | 
| 59 | 
            -
                       | 
| 177 | 
            +
                      if @connecting
         | 
| 178 | 
            +
                        @connecting = false
         | 
| 179 | 
            +
                        @connection_attempts = 0
         | 
| 180 | 
            +
                        ForestAdminRpcAgent::Facades::Container.logger&.log('Debug', '[SSE Client] Connection stable')
         | 
| 181 | 
            +
                      end
         | 
| 60 182 | 
             
                    when 'RpcServerStop'
         | 
| 61 | 
            -
                       | 
| 62 | 
            -
                       | 
| 183 | 
            +
                      ForestAdminRpcAgent::Facades::Container.logger&.log('Debug', '[SSE Client] RpcServerStop received')
         | 
| 184 | 
            +
                      handle_rpc_stop
         | 
| 63 185 | 
             
                    else
         | 
| 64 | 
            -
                      ForestAdminRpcAgent::Facades::Container.logger | 
| 65 | 
            -
             | 
| 186 | 
            +
                      ForestAdminRpcAgent::Facades::Container.logger&.log(
         | 
| 187 | 
            +
                        'Debug',
         | 
| 188 | 
            +
                        "[SSE Client] Unknown event: #{type} with payload: #{data}"
         | 
| 189 | 
            +
                      )
         | 
| 66 190 | 
             
                    end
         | 
| 191 | 
            +
                  rescue StandardError => e
         | 
| 192 | 
            +
                    ForestAdminRpcAgent::Facades::Container.logger&.log(
         | 
| 193 | 
            +
                      'Error',
         | 
| 194 | 
            +
                      "[SSE Client] Error handling event: #{e.class} - #{e.message}"
         | 
| 195 | 
            +
                    )
         | 
| 196 | 
            +
                  end
         | 
| 197 | 
            +
             | 
| 198 | 
            +
                  def handle_rpc_stop
         | 
| 199 | 
            +
                    @on_rpc_stop&.call
         | 
| 200 | 
            +
                  rescue StandardError => e
         | 
| 201 | 
            +
                    ForestAdminRpcAgent::Facades::Container.logger&.log(
         | 
| 202 | 
            +
                      'Error',
         | 
| 203 | 
            +
                      "[SSE Client] Error in RPC stop callback: #{e.class} - #{e.message}"
         | 
| 204 | 
            +
                    )
         | 
| 67 205 | 
             
                  end
         | 
| 68 206 |  | 
| 69 207 | 
             
                  def generate_signature(timestamp)
         |