Module: Familia::Connection

Included in:
Familia
Defined in:
lib/familia/connection.rb

Overview

The Connection module provides Database connection management for Familia. It allows easy setup and access to Database clients across different URIs with robust connection pooling for thread safety.

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#connection_providerProc

Returns A callable that provides Database connections.

Returns:

  • (Proc)

    A callable that provides Database connections



31
32
33
# File 'lib/familia/connection.rb', line 31

def connection_provider
  @connection_provider
end

#connection_requiredBoolean

Returns Whether to require external connections (no fallback).

Returns:

  • (Boolean)

    Whether to require external connections (no fallback)



34
35
36
# File 'lib/familia/connection.rb', line 34

def connection_required
  @connection_required
end

#database_clientsHash (readonly)

Returns A hash of Database clients, keyed by server ID.

Returns:

  • (Hash)

    A hash of Database clients, keyed by server ID



22
23
24
# File 'lib/familia/connection.rb', line 22

def database_clients
  @database_clients
end

#enable_database_counterBoolean

Returns Whether Database command counter is enabled.

Returns:

  • (Boolean)

    Whether Database command counter is enabled



28
29
30
# File 'lib/familia/connection.rb', line 28

def enable_database_counter
  @enable_database_counter
end

#enable_database_loggingBoolean

Returns Whether Database command logging is enabled.

Returns:

  • (Boolean)

    Whether Database command logging is enabled



25
26
27
# File 'lib/familia/connection.rb', line 25

def enable_database_logging
  @enable_database_logging
end

#uriURI Also known as: url

Returns The default URI for Database connections.

Returns:

  • (URI)

    The default URI for Database connections



19
20
21
# File 'lib/familia/connection.rb', line 19

def uri
  @uri
end

Instance Method Details

#connect(uri = nil) ⇒ Redis

Establishes a connection to a Database server.

Examples:

Familia.connect('redis://localhost:6379')

Parameters:

  • uri (String, URI, nil) (defaults to: nil)

    The URI of the Database server to connect to. If nil, uses the default URI from @database_clients or Familia.uri.

Returns:

  • (Redis)

    The connected Database client.

Raises:

  • (ArgumentError)

    If no URI is specified.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/familia/connection.rb', line 58

def connect(uri = nil)
  parsed_uri = normalize_uri(uri)
  serverid = parsed_uri.serverid

  if Familia.enable_database_logging
    DatabaseLogger.logger = Familia.logger
    RedisClient.register(DatabaseLogger)
  end

  if Familia.enable_database_counter
    # NOTE: This middleware uses AtommicFixnum from concurrent-ruby which is
    # less contentious than Mutex-based counters. Safe for
    RedisClient.register(DatabaseCommandCounter)
  end

  dbclient = Redis.new(parsed_uri.conf)

  if @database_clients.key?(serverid)
    msg = "Overriding existing connection for #{serverid}"
    Familia.warn(msg)
  end

  @database_clients[serverid] = dbclient
end

#dbclient(uri = nil) ⇒ Redis

Retrieves a Database connection from the appropriate pool. Handles DB selection automatically based on the URI.

Examples:

Familia.dbclient('redis://localhost:6379/1')
Familia.dbclient(2)  # Use DB 2 with default server

Returns:

  • (Redis)

    The Database client for the specified URI

Raises:



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/familia/connection.rb', line 100

def dbclient(uri = nil)
  # First priority: Thread-local connection (middleware pattern)
  return Thread.current[:familia_connection] if Thread.current.key?(:familia_connection)

  # Second priority: Connection provider
  if connection_provider
    # Always pass normalized URI with database to provider
    # Provider MUST return connection already on the correct database
    parsed_uri = normalize_uri(uri)
    client = connection_provider.call(parsed_uri.to_s)

    # In debug mode, verify the provider honored the contract
    if Familia.debug? && client.respond_to?(:client)
      current_db = client.connection[:db]
      expected_db = parsed_uri.db || 0
      Familia.ld "Connection provider returned client on DB #{current_db}, expected #{expected_db}"
      if current_db != expected_db
        Familia.warn "Connection provider returned client on DB #{current_db}, expected #{expected_db}"
      end
    end

    return client
  end

  # Third priority: Fallback behavior or error
  raise Familia::NoConnectionAvailable, 'No connection available.' if connection_required

  # Legacy behavior: create connection
  parsed_uri = normalize_uri(uri)

  # Only cache when no specific URI/DB is requested to avoid DB conflicts
  if uri.nil?
    @dbclient ||= connect(parsed_uri)
    @dbclient.select(parsed_uri.db) if parsed_uri.db
    @dbclient
  else
    # When a specific DB is requested, create a new connection
    # to avoid conflicts with cached connections
    connection = connect(parsed_uri)
    connection.select(parsed_uri.db) if parsed_uri.db
    connection
  end
end

#pipeline {|Redis| ... } ⇒ Array

Executes Database commands in a pipeline for improved performance.

Pipelines send multiple commands without waiting for individual responses, reducing network round-trips. Commands execute independently and can succeed or fail without affecting other commands in the pipeline.

Examples:

Basic pipeline usage

Familia.pipeline do |pipe|
  pipe.set("key1", "value1")
  pipe.incr("counter")
  pipe.lpush("list", "item")
end
# Returns: ["OK", 2, 1] - results of all commands

Error handling - commands succeed/fail independently

results = Familia.pipeline do |conn|
  conn.set("valid_key", "value")     # This will succeed
  conn.incr("string_key")            # This will fail (wrong type)
  conn.set("another_key", "value2")  # This will still succeed
end
# Returns: ["OK", Redis::CommandError, "OK"]
# Notice how the error doesn't prevent other commands from executing

Contrast with transaction behavior

results = Familia.transaction do |conn|
  conn.set("inventory:item1", 100)
  conn.incr("invalid_key")        # Fails, rolls back everything
  conn.set("inventory:item2", 200) # Won't be applied
end
# Result: neither item1 nor item2 are set due to the error

Yields:

  • (Redis)

    The Database pipeline connection

Returns:

  • (Array)

    Results of all commands executed in the pipeline



218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/familia/connection.rb', line 218

def pipeline(&)
  block_result = nil
  result = dbclient.pipelined do |conn|
    Fiber[:familia_pipeline] = conn
    begin
      block_result = yield(conn) # rubocop:disable Lint/UselessAssignment
    ensure
      Fiber[:familia_pipeline] = nil # cleanup reference
    end
  end
  # Return the pipeline result which contains the command results
  result
end

#reconnect(uri = nil) ⇒ Object



83
84
85
86
87
88
89
90
91
# File 'lib/familia/connection.rb', line 83

def reconnect(uri = nil)
  parsed_uri = normalize_uri(uri)
  serverid = parsed_uri.serverid

  # Close the existing connection if it exists
  @database_clients[serverid].close if @database_clients.key?(serverid)

  connect(parsed_uri)
end

#transaction {|Redis| ... } ⇒ Array Also known as: multi

Note:

Comparison of Database batch operations:

Feature Multi/Exec Pipeline
Atomicity Yes No
Performance Good Better
Error handling All-or-nothing Per-command
Use case Data consistency Bulk operations

Executes Database commands atomically within a transaction (MULTI/EXEC).

Database transactions queue commands and execute them atomically as a single unit. All commands succeed together or all fail together, ensuring data consistency.

Examples:

Basic transaction usage

Familia.transaction do |trans|
  trans.set("key1", "value1")
  trans.incr("counter")
  trans.lpush("list", "item")
end
# Returns: ["OK", 2, 1] - results of all commands

Yields:

  • (Redis)

    The Database transaction connection

Returns:

  • (Array)

    Results of all commands executed in the transaction



169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/familia/connection.rb', line 169

def transaction(&)
  block_result = nil
  result = dbclient.multi do |conn|
    Fiber[:familia_transaction] = conn
    begin
      block_result = yield(conn) # rubocop:disable Lint/UselessAssignment
    ensure
      Fiber[:familia_transaction] = nil # cleanup reference
    end
  end
  # Return the multi result which contains the transaction results
  result
end

#with_connection {|Redis| ... } ⇒ Object

Provides explicit access to a Database connection.

This method is useful when you need direct access to a connection for operations not covered by other methods. The connection is properly managed and returned to the pool (if using connection_provider).

Examples:

Using with_connection for custom operations

Familia.with_connection do |conn|
  conn.set("custom_key", "value")
  conn.expire("custom_key", 3600)
end

Yields:

  • (Redis)

    A Database connection

Returns:

  • The result of the block



247
248
249
# File 'lib/familia/connection.rb', line 247

def with_connection(&block)
  yield dbclient
end