CrackedRuby logo

CrackedRuby

Sinatra Integration

Overview

Sinatra integration encompasses connecting Sinatra web applications with external systems, databases, authentication providers, monitoring services, and deployment platforms. Ruby provides multiple integration approaches through Rack middleware, database adapters, HTTP clients, and configuration management libraries.

The Sinatra::Base class serves as the foundation for integration patterns, offering hooks for middleware registration, configuration management, and request lifecycle customization. Integration typically occurs at three levels: application-level configuration, request-level middleware processing, and route-level service interaction.

require 'sinatra'
require 'sinatra/config_file'

class App < Sinatra::Base
  register Sinatra::ConfigFile
  config_file 'config.yml'
  
  configure :production do
    enable :logging
    set :database_url, ENV['DATABASE_URL']
  end
  
  get '/health' do
    { status: 'healthy', database: database_connected? }.to_json
  end
end

Core integration components include Rack middleware for cross-cutting concerns, database connection management through ActiveRecord or Sequel, authentication integration via Warden or OmniAuth, and monitoring integration through custom middleware or service-specific gems.

Ruby's metaprogramming capabilities enable dynamic integration patterns where services register themselves with the application, middleware chains compose automatically based on configuration, and route handlers adapt behavior based on runtime environment detection.

module DatabaseIntegration
  def self.registered(app)
    app.helpers DatabaseHelpers
    app.before { connect_database unless database_connected? }
    app.after { disconnect_database if settings.environment == :development }
  end
end

Sinatra::Base.register DatabaseIntegration

Basic Usage

Database integration typically uses ActiveRecord or Sequel for relational databases, with connection configuration managed through environment-specific settings. The integration establishes connections during application initialization and manages connection lifecycle through Sinatra's configuration system.

require 'sinatra'
require 'activerecord'
require 'pg'

class BlogApp < Sinatra::Base
  configure do
    ActiveRecord::Base.establish_connection(
      adapter: 'postgresql',
      host: ENV['DB_HOST'] || 'localhost',
      database: ENV['DB_NAME'] || 'blog_development',
      username: ENV['DB_USER'] || 'postgres',
      password: ENV['DB_PASSWORD']
    )
  end
  
  get '/posts' do
    @posts = Post.published.limit(10)
    erb :posts
  end
  
  post '/posts' do
    post = Post.create(params[:post])
    redirect "/posts/#{post.id}" if post.persisted?
    erb :new_post
  end
end

Authentication integration commonly uses Warden middleware for session management and strategy-based authentication. The integration requires middleware registration, strategy configuration, and helper method definition for route-level authentication checks.

require 'warden'

class App < Sinatra::Base
  use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
  
  use Warden::Manager do |config|
    config.serialize_into_session { |user| user.id }
    config.serialize_from_session { |id| User.find(id) }
    config.scope_defaults :default, strategies: [:password]
    config.failure_app = self
  end
  
  Warden::Strategies.add(:password) do
    def valid?
      params['username'] && params['password']
    end
    
    def authenticate!
      user = User.authenticate(params['username'], params['password'])
      user ? success!(user) : fail!('Invalid credentials')
    end
  end
  
  helpers do
    def current_user
      env['warden'].user
    end
    
    def authenticate!
      redirect '/login' unless current_user
    end
  end
end

External API integration uses HTTP client libraries like Faraday or HTTParty, with connection configuration, timeout management, and response parsing handled through dedicated service objects. Error handling becomes critical when integrating with unreliable external services.

require 'faraday'
require 'json'

class PaymentService
  def self.process_payment(amount, token)
    connection = Faraday.new(url: ENV['PAYMENT_API_URL']) do |conn|
      conn.request :json
      conn.response :json
      conn.adapter Faraday.default_adapter
      conn.options.timeout = 10
    end
    
    response = connection.post('/charges') do |req|
      req.headers['Authorization'] = "Bearer #{ENV['PAYMENT_API_KEY']}"
      req.body = { amount: amount, source: token }
    end
    
    response.body
  rescue Faraday::TimeoutError
    { error: 'Payment service timeout' }
  end
end

post '/checkout' do
  result = PaymentService.process_payment(params[:amount], params[:token])
  
  if result[:error]
    status 500
    { error: result[:error] }.to_json
  else
    { charge_id: result[:id], status: 'completed' }.to_json
  end
end

Configuration management integration uses gems like dotenv for environment variable loading or config for hierarchical configuration management. The integration occurs during application initialization, with environment-specific overrides and secret management.

require 'dotenv/load'
require 'yaml'

class App < Sinatra::Base
  configure do
    config_file = File.join(settings.root, 'config', "#{settings.environment}.yml")
    set :config, YAML.load_file(config_file) if File.exist?(config_file)
    
    set :redis_url, ENV['REDIS_URL'] || settings.config['redis']['url']
    set :smtp_settings, {
      address: ENV['SMTP_HOST'] || settings.config['smtp']['host'],
      port: ENV['SMTP_PORT'] || settings.config['smtp']['port'],
      authentication: :plain,
      user_name: ENV['SMTP_USER'],
      password: ENV['SMTP_PASSWORD']
    }
  end
end

Advanced Usage

Middleware composition enables complex integration patterns where multiple services coordinate through request processing pipelines. Custom middleware classes implement specific integration logic while maintaining separation of concerns and request flow control.

class DatabaseTransactionMiddleware
  def initialize(app)
    @app = app
  end
  
  def call(env)
    if transactional_request?(env)
      ActiveRecord::Base.transaction do
        response = @app.call(env)
        raise ActiveRecord::Rollback if error_response?(response)
        response
      end
    else
      @app.call(env)
    end
  end
  
  private
  
  def transactional_request?(env)
    %w[POST PUT PATCH DELETE].include?(env['REQUEST_METHOD']) &&
      !env['PATH_INFO'].start_with?('/api/webhooks')
  end
  
  def error_response?(response)
    status = response.first
    status >= 400 && status < 500
  end
end

class App < Sinatra::Base
  use DatabaseTransactionMiddleware
  
  post '/users' do
    user = User.create!(params[:user])
    AuditLog.create!(action: 'user_created', user_id: user.id)
    EmailService.send_welcome_email(user)
    
    status 201
    user.to_json
  end
end

Extension-based integration allows modular functionality through Sinatra's extension system. Extensions encapsulate integration logic, register helpers, and configure middleware while maintaining reusability across applications.

module Sinatra
  module CacheIntegration
    module Helpers
      def cache_key(*parts)
        ['app', settings.environment, *parts].join(':')
      end
      
      def cached(key, expires_in: 3600, &block)
        cache = settings.cache_store
        cached_value = cache.get(cache_key(key))
        
        return cached_value if cached_value
        
        value = yield
        cache.set(cache_key(key), value, expires_in)
        value
      end
    end
    
    def self.registered(app)
      app.helpers Helpers
      
      app.set :cache_store, Redis.new(url: ENV['REDIS_URL'])
      
      app.before do
        if request.get? && cacheable_path?
          @cache_key = request.path_info
          @cached_response = settings.cache_store.get(cache_key(@cache_key))
          
          if @cached_response
            content_type :json
            halt 200, @cached_response
          end
        end
      end
      
      app.after do
        if @cache_key && response.status == 200
          settings.cache_store.set(
            cache_key(@cache_key),
            response.body.join,
            300
          )
        end
      end
    end
    
    def cacheable_path?
      request.path_info.start_with?('/api/') && !request.path_info.include?('user')
    end
  end
  
  register CacheIntegration
end

Service object integration separates business logic from route handling while maintaining clean interfaces for external service communication. Service objects handle complex integration workflows, error recovery, and state management.

class OrderProcessingService
  def initialize(payment_service:, inventory_service:, notification_service:)
    @payment_service = payment_service
    @inventory_service = inventory_service
    @notification_service = notification_service
  end
  
  def process_order(order_params)
    order = Order.create!(order_params)
    
    inventory_result = @inventory_service.reserve_items(order.items)
    raise InventoryError, inventory_result[:error] unless inventory_result[:success]
    
    payment_result = @payment_service.charge_payment(
      order.total_amount,
      order_params[:payment_token]
    )
    
    if payment_result[:success]
      order.update!(
        status: 'paid',
        payment_id: payment_result[:payment_id]
      )
      
      @notification_service.send_confirmation(order)
      { success: true, order_id: order.id }
    else
      @inventory_service.release_reservation(inventory_result[:reservation_id])
      order.update!(status: 'failed', error: payment_result[:error])
      { success: false, error: payment_result[:error] }
    end
  rescue StandardError => e
    order&.update!(status: 'failed', error: e.message)
    @inventory_service.release_reservation(inventory_result[:reservation_id]) if inventory_result
    { success: false, error: e.message }
  end
end

class App < Sinatra::Base
  configure do
    set :order_service, OrderProcessingService.new(
      payment_service: PaymentService.new,
      inventory_service: InventoryService.new,
      notification_service: NotificationService.new
    )
  end
  
  post '/orders' do
    result = settings.order_service.process_order(params[:order])
    
    if result[:success]
      status 201
      { order_id: result[:order_id] }.to_json
    else
      status 422
      { error: result[:error] }.to_json
    end
  end
end

WebSocket integration enables real-time communication through Rack middleware or dedicated WebSocket servers. The integration requires connection management, message routing, and state synchronization between WebSocket handlers and HTTP routes.

require 'faye/websocket'
require 'thread'

class WebSocketManager
  def initialize
    @clients = {}
    @mutex = Mutex.new
  end
  
  def add_client(ws, user_id)
    @mutex.synchronize do
      @clients[user_id] ||= []
      @clients[user_id] << ws
    end
  end
  
  def remove_client(ws, user_id)
    @mutex.synchronize do
      @clients[user_id]&.delete(ws)
      @clients.delete(user_id) if @clients[user_id]&.empty?
    end
  end
  
  def broadcast_to_user(user_id, message)
    @mutex.synchronize do
      @clients[user_id]&.each do |ws|
        ws.send(message.to_json) if ws.ready_state == Faye::WebSocket::API::OPEN
      end
    end
  end
end

class App < Sinatra::Base
  set :websocket_manager, WebSocketManager.new
  
  get '/ws' do
    if Faye::WebSocket.websocket?(env)
      ws = Faye::WebSocket.new(env)
      user_id = authenticate_websocket_user(request.params['token'])
      
      ws.on :open do |event|
        settings.websocket_manager.add_client(ws, user_id)
      end
      
      ws.on :message do |event|
        data = JSON.parse(event.data)
        handle_websocket_message(user_id, data)
      end
      
      ws.on :close do |event|
        settings.websocket_manager.remove_client(ws, user_id)
      end
      
      ws.rack_response
    else
      status 400
      'WebSocket connection required'
    end
  end
  
  post '/notifications/:user_id' do
    settings.websocket_manager.broadcast_to_user(
      params[:user_id].to_i,
      { type: 'notification', data: params[:message] }
    )
    
    status 200
    'Notification sent'
  end
end

Error Handling & Debugging

Integration error handling requires comprehensive exception management across service boundaries, with specific strategies for network failures, timeout conditions, and external service degradation. Ruby's exception hierarchy supports granular error classification and recovery strategies.

class IntegrationError < StandardError; end
class ServiceUnavailableError < IntegrationError; end
class AuthenticationError < IntegrationError; end
class ValidationError < IntegrationError; end

class ExternalServiceClient
  def initialize(base_url, api_key, timeout: 10, retries: 3)
    @base_url = base_url
    @api_key = api_key
    @timeout = timeout
    @retries = retries
  end
  
  def make_request(endpoint, params = {})
    attempt = 0
    
    begin
      attempt += 1
      response = http_client.post(endpoint, params.to_json)
      
      case response.status
      when 200..299
        JSON.parse(response.body)
      when 401, 403
        raise AuthenticationError, "Authentication failed: #{response.body}"
      when 422
        raise ValidationError, "Validation failed: #{response.body}"
      when 500..599
        raise ServiceUnavailableError, "Service error: #{response.status}"
      else
        raise IntegrationError, "Unexpected response: #{response.status}"
      end
      
    rescue Net::TimeoutError, Net::OpenTimeout => e
      if attempt <= @retries
        sleep(2 ** attempt)
        retry
      end
      raise ServiceUnavailableError, "Service timeout after #{@retries} attempts"
      
    rescue JSON::ParserError => e
      raise IntegrationError, "Invalid response format: #{e.message}"
    end
  end
  
  private
  
  def http_client
    @http_client ||= Net::HTTP.new(URI(@base_url).host).tap do |http|
      http.read_timeout = @timeout
      http.open_timeout = @timeout
    end
  end
end

Application-level error handling provides consistent error responses and logging while maintaining service stability during integration failures. The error handling strategy differentiates between recoverable and fatal errors, implementing appropriate fallback behaviors.

class App < Sinatra::Base
  configure do
    set :show_exceptions, false
    set :raise_errors, false
  end
  
  error IntegrationError do
    error = env['sinatra.error']
    logger.error "Integration error: #{error.class} - #{error.message}"
    logger.error error.backtrace.join("\n") if settings.development?
    
    status 503
    {
      error: 'Service temporarily unavailable',
      type: 'integration_error',
      retry_after: calculate_retry_delay(error)
    }.to_json
  end
  
  error AuthenticationError do
    error = env['sinatra.error']
    logger.warn "Authentication error: #{error.message}"
    
    status 401
    {
      error: 'Authentication required',
      type: 'auth_error'
    }.to_json
  end
  
  error ValidationError do
    error = env['sinatra.error']
    logger.info "Validation error: #{error.message}"
    
    status 422
    {
      error: 'Invalid request data',
      type: 'validation_error',
      details: parse_validation_details(error.message)
    }.to_json
  end
  
  not_found do
    content_type :json
    { error: 'Resource not found' }.to_json
  end
  
  private
  
  def calculate_retry_delay(error)
    case error
    when ServiceUnavailableError
      60 # seconds
    else
      30
    end
  end
  
  def parse_validation_details(message)
    JSON.parse(message)
  rescue JSON::ParserError
    { message: message }
  end
end

Database integration error handling manages connection failures, transaction rollbacks, and data consistency issues. The error handling strategy includes connection pool management, deadlock detection, and automatic retry logic for transient database errors.

class DatabaseMiddleware
  def initialize(app)
    @app = app
    @max_retries = 3
    @deadlock_retry_delay = 0.1
  end
  
  def call(env)
    attempt = 0
    
    begin
      attempt += 1
      @app.call(env)
      
    rescue ActiveRecord::ConnectionNotEstablished => e
      logger.error "Database connection lost: #{e.message}"
      ActiveRecord::Base.establish_connection
      
      if attempt <= @max_retries
        retry
      else
        [503, {}, ['Database unavailable']]
      end
      
    rescue ActiveRecord::Deadlocked => e
      logger.warn "Database deadlock detected: #{e.message}"
      
      if attempt <= @max_retries
        sleep(@deadlock_retry_delay * attempt)
        retry
      else
        [409, {}, ['Resource conflict - please retry']]
      end
      
    rescue ActiveRecord::RecordInvalid => e
      logger.info "Validation failed: #{e.record.errors.full_messages}"
      
      [422, { 'Content-Type' => 'application/json' }, [{
        error: 'Validation failed',
        details: e.record.errors.as_json
      }.to_json]]
      
    rescue ActiveRecord::RecordNotFound => e
      [404, { 'Content-Type' => 'application/json' }, [{
        error: 'Resource not found'
      }.to_json]]
    end
  end
end

post '/users/:id' do
  user = User.find(params[:id])
  
  ActiveRecord::Base.transaction do
    user.update!(params[:user])
    AuditLog.create!(
      user: user,
      action: 'profile_updated',
      changes: user.previous_changes
    )
  end
  
  user.to_json
end

Circuit breaker pattern implementation prevents cascade failures when external services become unavailable. The circuit breaker monitors failure rates, implements automatic recovery detection, and provides fallback responses during service outages.

class CircuitBreaker
  STATES = [:closed, :open, :half_open].freeze
  
  def initialize(failure_threshold: 5, recovery_timeout: 60, success_threshold: 3)
    @failure_threshold = failure_threshold
    @recovery_timeout = recovery_timeout
    @success_threshold = success_threshold
    @failure_count = 0
    @success_count = 0
    @last_failure_time = nil
    @state = :closed
  end
  
  def call
    case @state
    when :closed
      execute_with_monitoring { yield }
    when :open
      check_recovery_timeout
      raise CircuitBreakerOpenError, 'Circuit breaker is open'
    when :half_open
      execute_recovery_attempt { yield }
    end
  end
  
  private
  
  def execute_with_monitoring
    result = yield
    reset_failure_count
    result
  rescue StandardError => e
    record_failure
    raise e
  end
  
  def execute_recovery_attempt
    result = yield
    record_success
    result
  rescue StandardError => e
    record_failure
    raise e
  end
  
  def record_failure
    @failure_count += 1
    @last_failure_time = Time.now
    
    if @failure_count >= @failure_threshold
      @state = :open
    end
  end
  
  def record_success
    @success_count += 1
    
    if @success_count >= @success_threshold
      @state = :closed
      reset_counters
    end
  end
  
  def check_recovery_timeout
    if Time.now - @last_failure_time >= @recovery_timeout
      @state = :half_open
      @success_count = 0
    end
  end
  
  def reset_failure_count
    @failure_count = 0
  end
  
  def reset_counters
    @failure_count = 0
    @success_count = 0
  end
end

class PaymentService
  def initialize
    @circuit_breaker = CircuitBreaker.new(
      failure_threshold: 3,
      recovery_timeout: 120,
      success_threshold: 2
    )
  end
  
  def process_payment(amount, token)
    @circuit_breaker.call do
      make_payment_request(amount, token)
    end
  rescue CircuitBreakerOpenError
    { error: 'Payment service temporarily unavailable', retry_after: 120 }
  end
end

Testing Strategies

Integration testing requires isolated test environments, service mocking, and database transaction management. Ruby's testing frameworks provide comprehensive tools for testing external service integration while maintaining test speed and reliability.

require 'minitest/autorun'
require 'rack/test'
require 'webmock/minitest'
require 'database_cleaner'

class IntegrationTest < Minitest::Test
  include Rack::Test::Methods
  
  def setup
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.start
    WebMock.disable_net_connect!(allow_localhost: true)
    
    # Setup test database state
    @user = User.create!(
      email: 'test@example.com',
      api_key: 'test-api-key-123'
    )
  end
  
  def teardown
    DatabaseCleaner.clean
    WebMock.reset!
  end
  
  def app
    TestApp
  end
  
  def test_successful_payment_integration
    # Mock external payment service
    stub_request(:post, 'https://api.payment-provider.com/charges')
      .with(
        body: { amount: 1000, source: 'tok_123' },
        headers: { 'Authorization' => 'Bearer test-key' }
      )
      .to_return(
        status: 200,
        body: { id: 'ch_123', status: 'succeeded' }.to_json,
        headers: { 'Content-Type' => 'application/json' }
      )
    
    post '/payments', {
      amount: 1000,
      token: 'tok_123'
    }, { 'HTTP_AUTHORIZATION' => "Bearer #{@user.api_key}" }
    
    assert_equal 201, last_response.status
    
    response_body = JSON.parse(last_response.body)
    assert_equal 'ch_123', response_body['charge_id']
    assert_equal 'succeeded', response_body['status']
    
    # Verify database state
    payment = Payment.find_by(charge_id: 'ch_123')
    assert_not_nil payment
    assert_equal @user.id, payment.user_id
    assert_equal 1000, payment.amount
  end
  
  def test_payment_service_timeout
    stub_request(:post, 'https://api.payment-provider.com/charges')
      .to_timeout
    
    post '/payments', {
      amount: 1000,
      token: 'tok_123'
    }, { 'HTTP_AUTHORIZATION' => "Bearer #{@user.api_key}" }
    
    assert_equal 503, last_response.status
    
    response_body = JSON.parse(last_response.body)
    assert_equal 'Service temporarily unavailable', response_body['error']
    assert response_body['retry_after'].is_a?(Integer)
  end
  
  def test_database_rollback_on_external_service_failure
    # Mock successful payment service
    stub_request(:post, 'https://api.payment-provider.com/charges')
      .to_return(
        status: 200,
        body: { id: 'ch_123', status: 'succeeded' }.to_json,
        headers: { 'Content-Type' => 'application/json' }
      )
    
    # Mock failing notification service
    stub_request(:post, 'https://api.notification-service.com/send')
      .to_return(status: 500, body: 'Internal server error')
    
    assert_no_difference 'Payment.count' do
      post '/payments', {
        amount: 1000,
        token: 'tok_123',
        send_notification: true
      }, { 'HTTP_AUTHORIZATION' => "Bearer #{@user.api_key}" }
    end
    
    assert_equal 503, last_response.status
  end
end

Service object testing isolates business logic from framework dependencies, enabling fast unit tests with comprehensive service interaction coverage. Test doubles provide controlled external service responses while maintaining test determinism.

require 'minitest/autorun'
require 'minitest/mock'

class OrderProcessingServiceTest < Minitest::Test
  def setup
    @payment_service = Minitest::Mock.new
    @inventory_service = Minitest::Mock.new
    @notification_service = Minitest::Mock.new
    
    @service = OrderProcessingService.new(
      payment_service: @payment_service,
      inventory_service: @inventory_service,
      notification_service: @notification_service
    )
    
    @order_params = {
      user_id: 1,
      items: [
        { product_id: 1, quantity: 2, price: 500 },
        { product_id: 2, quantity: 1, price: 1000 }
      ],
      payment_token: 'tok_test_123',
      total_amount: 2000
    }
  end
  
  def test_successful_order_processing
    order = Minitest::Mock.new
    order.expect :id, 123
    order.expect :items, @order_params[:items]
    order.expect :total_amount, @order_params[:total_amount]
    order.expect :update!, true, [Hash]
    
    Order.stub :create!, order do
      @inventory_service.expect :reserve_items, 
        { success: true, reservation_id: 'res_123' },
        [@order_params[:items]]
      
      @payment_service.expect :charge_payment,
        { success: true, payment_id: 'pay_123' },
        [2000, 'tok_test_123']
      
      @notification_service.expect :send_confirmation, true, [order]
      
      result = @service.process_order(@order_params)
      
      assert result[:success]
      assert_equal 123, result[:order_id]
    end
    
    assert_mock @payment_service
    assert_mock @inventory_service
    assert_mock @notification_service
  end
  
  def test_payment_failure_releases_inventory
    order = Minitest::Mock.new
    order.expect :items, @order_params[:items]
    order.expect :total_amount, @order_params[:total_amount]
    order.expect :update!, true, [Hash]
    
    Order.stub :create!, order do
      @inventory_service.expect :reserve_items,
        { success: true, reservation_id: 'res_123' },
        [@order_params[:items]]
      
      @payment_service.expect :charge_payment,
        { success: false, error: 'Insufficient funds' },
        [2000, 'tok_test_123']
      
      @inventory_service.expect :release_reservation, true, ['res_123']
      
      result = @service.process_order(@order_params)
      
      refute result[:success]
      assert_equal 'Insufficient funds', result[:error]
    end
    
    assert_mock @payment_service
    assert_mock @inventory_service
  end
  
  def test_inventory_failure_prevents_payment
    order = Minitest::Mock.new
    order.expect :items, @order_params[:items]
    
    Order.stub :create!, order do
      @inventory_service.expect :reserve_items,
        { success: false, error: 'Insufficient stock' },
        [@order_params[:items]]
      
      # Payment service should not be called
      
      assert_raises InventoryError do
        @service.process_order(@order_params)
      end
    end
    
    assert_mock @inventory_service
  end
end

WebSocket integration testing requires connection simulation, message exchange verification, and asynchronous event handling. Testing frameworks provide utilities for WebSocket connection mocking and real-time communication verification.

require 'minitest/autorun'
require 'eventmachine'
require 'faye/websocket'

class WebSocketIntegrationTest < Minitest::Test
  def setup
    @received_messages = []
    @connection_established = false
  end
  
  def test_websocket_authentication_and_messaging
    EM.run do
      # Connect to WebSocket endpoint with authentication token
      ws = Faye::WebSocket::Client.new(
        'ws://localhost:4567/ws?token=valid-user-token'
      )
      
      ws.on :open do |event|
        @connection_established = true
        
        # Send test message
        ws.send({
          type: 'chat_message',
          content: 'Hello, world!',
          channel: 'general'
        }.to_json)
      end
      
      ws.on :message do |event|
        message = JSON.parse(event.data)
        @received_messages << message
        
        # Verify message echo
        if message['type'] == 'message_confirmation'
          assert_equal 'Hello, world!', message['original_content']
          assert_equal 'general', message['channel']
          
          ws.close
        end
      end
      
      ws.on :close do |event|
        assert @connection_established
        assert_equal 1, @received_messages.length
        EM.stop
      end
      
      # Set timeout to prevent test hanging
      EM.add_timer(5) do
        flunk 'WebSocket test timed out'
        EM.stop
      end
    end
  end
  
  def test_websocket_broadcast_functionality
    user_connections = {}
    
    EM.run do
      # Simulate multiple user connections
      %w[user1 user2].each do |user|
        ws = Faye::WebSocket::Client.new(
          "ws://localhost:4567/ws?token=#{user}-token"
        )
        
        user_connections[user] = {
          websocket: ws,
          received_messages: []
        }
        
        ws.on :message do |event|
          message = JSON.parse(event.data)
          user_connections[user][:received_messages] << message
          
          # Check if both users received the broadcast
          if user_connections.values.all? { |conn| 
               conn[:received_messages].any? { |msg| msg['type'] == 'broadcast' }
             }
            
            # Verify broadcast content
            user_connections.each do |username, connection|
              broadcast_msg = connection[:received_messages].find { |msg| 
                msg['type'] == 'broadcast' 
              }
              
              assert_equal 'System announcement', broadcast_msg['content']
            end
            
            # Close all connections
            user_connections.each { |_, conn| conn[:websocket].close }
          end
        end
      end
      
      # Wait for connections to establish, then trigger broadcast
      EM.add_timer(0.1) do
        # Simulate server-side broadcast trigger
        Net::HTTP.post_form(
          URI('http://localhost:4567/admin/broadcast'),
          message: 'System announcement'
        )
      end
      
      # Cleanup timer
      EM.add_timer(5) { EM.stop }
    end
  end
end

Production Patterns

Production deployment patterns for Sinatra applications require careful consideration of process management, load balancing, and monitoring integration. Ruby provides deployment tools like Puma, Unicorn, and Passenger that offer different concurrency models and operational characteristics.

# config/puma.rb
workers ENV.fetch('WEB_CONCURRENCY', 2).to_i
threads_count = ENV.fetch('RAILS_MAX_THREADS', 5).to_i
threads threads_count, threads_count

preload_app!

port ENV.fetch('PORT', 3000)
environment ENV.fetch('RACK_ENV', 'development')

before_fork do
  # Disconnect from database and Redis before forking
  ActiveRecord::Base.connection_pool.disconnect!
  Redis.current.disconnect! if defined?(Redis)
end

on_worker_boot do
  # Reconnect to external services after forking
  ActiveRecord::Base.establish_connection
  Redis.current = Redis.new(url: ENV['REDIS_URL']) if ENV['REDIS_URL']
end

# Graceful shutdown handling
on_worker_shutdown do
  ActiveRecord::Base.connection_pool.disconnect!
end

Health check endpoints provide monitoring systems with application status information, including database connectivity, external service availability, and resource utilization metrics. The implementation includes both shallow and deep health checks for different monitoring scenarios.

class HealthCheck
  def initialize(app)
    @app = app
  end
  
  def call(env)
    if env['PATH_INFO'] == '/health'
      perform_health_check
    elsif env['PATH_INFO'] == '/health/deep'
      perform_deep_health_check
    else
      @app.call(env)
    end
  end
  
  private
  
  def perform_health_check
    checks = {
      status: 'healthy',
      timestamp: Time.now.utc.iso8601,
      version: ENV['APP_VERSION'] || 'unknown',
      uptime: Process.clock_gettime(Process::CLOCK_MONOTONIC)
    }
    
    [200, { 'Content-Type' => 'application/json' }, [checks.to_json]]
  end
  
  def perform_deep_health_check
    checks = {
      status: 'healthy',
      timestamp: Time.now.utc.iso8601,
      checks: {}
    }
    
    # Database connectivity check
    checks[:checks][:database] = check_database_connection
    
    # Redis connectivity check
    checks[:checks][:redis] = check_redis_connection
    
    # External service checks
    checks[:checks][:payment_service] = check_external_service(
      'https://api.payment-provider.com/health'
    )
    
    # Memory usage check
    checks[:checks][:memory] = check_memory_usage
    
    overall_status = checks[:checks].values.all? { |check| check[:status] == 'healthy' }
    checks[:status] = overall_status ? 'healthy' : 'unhealthy'
    
    status_code = overall_status ? 200 : 503
    [status_code, { 'Content-Type' => 'application/json' }, [checks.to_json]]
  end
  
  def check_database_connection
    ActiveRecord::Base.connection.execute('SELECT 1')
    { status: 'healthy', response_time: measure_response_time { 
        ActiveRecord::Base.connection.execute('SELECT 1') 
      } 
    }
  rescue StandardError => e
    { status: 'unhealthy', error: e.message }
  end
  
  def check_redis_connection
    Redis.current.ping
    { status: 'healthy', response_time: measure_response_time { Redis.current.ping } }
  rescue StandardError => e
    { status: 'unhealthy', error: e.message }
  end
  
  def check_external_service(url)
    response = Net::HTTP.get_response(URI(url))
    { 
      status: response.code.to_i < 400 ? 'healthy' : 'unhealthy',
      response_code: response.code.to_i,
      response_time: measure_response_time { Net::HTTP.get_response(URI(url)) }
    }
  rescue StandardError => e
    { status: 'unhealthy', error: e.message }
  end
  
  def check_memory_usage
    # Ruby memory usage in MB
    memory_mb = `ps -o rss= -p #{Process.pid}`.to_i / 1024
    
    {
      status: memory_mb < 1024 ? 'healthy' : 'warning',
      memory_mb: memory_mb,
      threshold_mb: 1024
    }
  end
  
  def measure_response_time
    start_time = Time.now
    yield
    ((Time.now - start_time) * 1000).round(2) # milliseconds
  end
end

Logging integration provides structured logging for production monitoring, with correlation ID tracking, performance metrics, and integration event logging. The logging strategy includes request tracing, error context capture, and audit trail generation.

require 'logger'
require 'json'
require 'securerandom'

class StructuredLogger
  def initialize(app)
    @app = app
    @logger = Logger.new(STDOUT)
    @logger.formatter = method(:json_formatter)
  end
  
  def call(env)
    request_id = SecureRandom.uuid
    env['HTTP_X_REQUEST_ID'] = request_id
    
    start_time = Time.now
    
    log_request_start(env, request_id)
    
    status, headers, response = @app.call(env)
    
    end_time = Time.now
    duration = ((end_time - start_time) * 1000).round(2)
    
    log_request_end(env, status, duration, request_id)
    
    [status, headers, response]
  rescue StandardError => e
    end_time = Time.now
    duration = ((end_time - start_time) * 1000).round(2)
    
    log_request_error(env, e, duration, request_id)
    
    raise e
  end
  
  private
  
  def log_request_start(env, request_id)
    @logger.info({
      event: 'request_start',
      request_id: request_id,
      method: env['REQUEST_METHOD'],
      path: env['PATH_INFO'],
      query_string: env['QUERY_STRING'],
      user_agent: env['HTTP_USER_AGENT'],
      remote_ip: env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR']
    })
  end
  
  def log_request_end(env, status, duration, request_id)
    @logger.info({
      event: 'request_end',
      request_id: request_id,
      method: env['REQUEST_METHOD'],
      path: env['PATH_INFO'],
      status: status,
      duration_ms: duration
    })
  end
  
  def log_request_error(env, error, duration, request_id)
    @logger.error({
      event: 'request_error',
      request_id: request_id,
      method: env['REQUEST_METHOD'],
      path: env['PATH_INFO'],
      error_class: error.class.name,
      error_message: error.message,
      backtrace: error.backtrace&.first(10),
      duration_ms: duration
    })
  end
  
  def json_formatter(severity, datetime, progname, msg)
    log_entry = {
      timestamp: datetime.utc.iso8601,
      level: severity,
      hostname: ENV['HOSTNAME'] || 'unknown',
      service: ENV['SERVICE_NAME'] || 'sinatra-app'
    }
    
    case msg
    when Hash
      log_entry.merge!(msg)
    else
      log_entry[:message] = msg
    end
    
    "#{log_entry.to_json}\n"
  end
end

# Application-level logging helpers
module ApplicationLogging
  def log_integration_event(service:, action:, status:, duration: nil, **metadata)
    logger.info({
      event: 'integration_event',
      service: service,
      action: action,
      status: status,
      duration_ms: duration,
      request_id: env['HTTP_X_REQUEST_ID'],
      **metadata
    })
  end
  
  def log_business_event(event_type:, entity_type:, entity_id:, **metadata)
    logger.info({
      event: 'business_event',
      event_type: event_type,
      entity_type: entity_type,
      entity_id: entity_id,
      request_id: env['HTTP_X_REQUEST_ID'],
      user_id: current_user&.id,
      **metadata
    })
  end
end

class App < Sinatra::Base
  helpers ApplicationLogging
  
  post '/orders' do
    start_time = Time.now
    
    begin
      result = OrderService.create_order(params[:order])
      duration = ((Time.now - start_time) * 1000).round(2)
      
      log_integration_event(
        service: 'order_service',
        action: 'create_order',
        status: 'success',
        duration: duration,
        order_id: result[:order_id]
      )
      
      log_business_event(
        event_type: 'order_created',
        entity_type: 'order',
        entity_id: result[:order_id],
        amount: params[:order][:total_amount]
      )
      
      status 201
      result.to_json
      
    rescue StandardError => e
      duration = ((Time.now - start_time) * 1000).round(2)
      
      log_integration_event(
        service: 'order_service',
        action: 'create_order',
        status: 'error',
        duration: duration,
        error: e.message
      )
      
      raise e
    end
  end
end

Configuration management for production environments requires secure secret handling, environment-specific settings, and dynamic configuration updates. Ruby provides libraries for hierarchical configuration management with environment variable override capabilities.

require 'yaml'
require 'erb'

class ConfigurationManager
  def initialize(env = ENV['RACK_ENV'] || 'development')
    @env = env
    @config = load_configuration
    @secrets = load_secrets
  end
  
  def get(key_path)
    keys = key_path.split('.')
    value = keys.reduce(@config) { |config, key| config&.dig(key) }
    
    # Check for environment variable override
    env_key = key_path.upcase.gsub('.', '_')
    ENV[env_key] || value
  end
  
  def secret(key)
    @secrets[key] || ENV[key.upcase]
  end
  
  def database_config
    {
      adapter: get('database.adapter'),
      host: get('database.host'),
      port: get('database.port'),
      database: get('database.name'),
      username: secret('database_user'),
      password: secret('database_password'),
      pool: get('database.pool_size')&.to_i || 5,
      checkout_timeout: get('database.checkout_timeout')&.to_i || 5
    }
  end
  
  def redis_config
    {
      url: secret('redis_url') || build_redis_url,
      pool_size: get('redis.pool_size')&.to_i || 5,
      timeout: get('redis.timeout')&.to_f || 1.0
    }
  end
  
  def external_service_config(service_name)
    base_config = get("external_services.#{service_name}") || {}
    
    {
      base_url: base_config['base_url'],
      api_key: secret("#{service_name}_api_key"),
      timeout: base_config['timeout']&.to_i || 10,
      retries: base_config['retries']&.to_i || 3,
      circuit_breaker: {
        failure_threshold: base_config.dig('circuit_breaker', 'failure_threshold')&.to_i || 5,
        recovery_timeout: base_config.dig('circuit_breaker', 'recovery_timeout')&.to_i || 60
      }
    }
  end
  
  private
  
  def load_configuration
    config_file = File.join(settings_root, 'config', "#{@env}.yml")
    
    if File.exist?(config_file)
      erb_template = ERB.new(File.read(config_file))
      YAML.safe_load(erb_template.result, aliases: true)
    else
      default_configuration
    end
  end
  
  def load_secrets
    secrets_file = File.join(settings_root, 'config', 'secrets.yml')
    
    if File.exist?(secrets_file)
      all_secrets = YAML.load_file(secrets_file)
      all_secrets[@env] || {}
    else
      {}
    end
  end
  
  def build_redis_url
    host = get('redis.host') || 'localhost'
    port = get('redis.port') || 6379
    database = get('redis.database') || 0
    
    "redis://#{host}:#{port}/#{database}"
  end
  
  def settings_root
    ENV['APP_ROOT'] || Dir.pwd
  end
  
  def default_configuration
    {
      'database' => {
        'adapter' => 'postgresql',
        'host' => 'localhost',
        'port' => 5432,
        'pool_size' => 5
      },
      'redis' => {
        'host' => 'localhost',
        'port' => 6379,
        'database' => 0,
        'pool_size' => 5
      }
    }
  end
end

class App < Sinatra::Base
  configure do
    set :config_manager, ConfigurationManager.new
    
    # Database configuration
    ActiveRecord::Base.establish_connection(
      settings.config_manager.database_config
    )
    
    # Redis configuration
    redis_config = settings.config_manager.redis_config
    set :redis, Redis.new(redis_config)
    
    # External service configuration
    set :payment_service_config, 
        settings.config_manager.external_service_config('payment_service')
  end
end

Reference

Core Integration Classes

Class Purpose Key Methods
Sinatra::Base Base application class configure, use, helpers, before, after
Rack::Builder Middleware composition use, map, run
ActiveRecord::Base Database ORM integration establish_connection, connection_pool
Warden::Manager Authentication middleware authenticate!, user, logout
Faraday::Connection HTTP client integration get, post, put, delete

Configuration Methods

Method Parameters Returns Description
configure(env = nil, &block) env (Symbol), block nil Environment-specific configuration
set(setting, value = nil) setting (Symbol), value (Any) value Set application setting
enable(setting) setting (Symbol) true Enable boolean setting
disable(setting) setting (Symbol) false Disable boolean setting
use(middleware, *args, &block) middleware (Class), args, block nil Register Rack middleware

Database Integration Methods

Method Parameters Returns Description
establish_connection(config) config (Hash) Connection Create database connection
connection_pool.disconnect! None nil Disconnect all connections
transaction(&block) block Block result Execute in database transaction
with_connection(&block) block Block result Execute with database connection

HTTP Client Configuration

Option Type Default Description
:timeout Integer 10 Request timeout in seconds
:open_timeout Integer 10 Connection timeout in seconds
:retries Integer 3 Number of retry attempts
:retry_delay Float 1.0 Delay between retries in seconds
:headers Hash {} Default request headers

Authentication Configuration

Setting Type Description
:session_secret String Secret key for session encryption
:failure_app Class Application to handle auth failures
:default_strategies Array Default authentication strategies
:scope_defaults Hash Per-scope authentication configuration

Error Handling Classes

Exception Parent Usage
IntegrationError StandardError Base class for integration errors
ServiceUnavailableError IntegrationError External service unavailable
AuthenticationError IntegrationError Authentication failures
ValidationError IntegrationError Request validation errors
CircuitBreakerOpenError IntegrationError Circuit breaker is open

Production Configuration Settings

Setting Type Default Description
:environment Symbol :development Application environment
:logging Boolean false Enable request logging
:dump_errors Boolean true Show error details
:show_exceptions Boolean true Show exception pages
:raise_errors Boolean false Raise unhandled exceptions

Middleware Registration Patterns

# Basic middleware registration
use Rack::Logger
use Rack::CommonLogger

# Middleware with configuration
use Rack::Session::Cookie, {
  key: 'session_id',
  secret: ENV['SESSION_SECRET'],
  expire_after: 86400
}

# Conditional middleware registration
use Rack::SSL if production?

# Custom middleware with initialization
use CustomMiddleware.new(option: 'value')

Environment Detection Methods

Method Returns Description
development? Boolean True if development environment
production? Boolean True if production environment
test? Boolean True if test environment
settings.environment Symbol Current environment symbol

Request Lifecycle Hooks

Hook Execution Parameters
before Before route handling Filter pattern (optional)
after After route handling Filter pattern (optional)
error On exception Exception class
not_found On 404 responses None

Common Integration Patterns

# Database connection with connection pooling
ActiveRecord::Base.establish_connection(
  adapter: 'postgresql',
  pool: 25,
  checkout_timeout: 5,
  reaping_frequency: 10
)

# Redis connection with connection pooling
Redis.current = ConnectionPool::Wrapper.new(size: 25, timeout: 5) do
  Redis.new(url: ENV['REDIS_URL'])
end

# HTTP client with retry logic
Faraday.new do |conn|
  conn.request :retry, max: 3, interval: 0.5
  conn.request :timeout, timeout: 10
  conn.response :json
  conn.adapter Faraday.default_adapter
end

# Circuit breaker integration
circuit_breaker = CircuitBreaker.new(
  failure_threshold: 5,
  recovery_timeout: 60,
  success_threshold: 3
)