class ActiveRecordSpannerAdapter::Connection

Attributes

current_transaction[RW]
database_id[R]
instance_id[R]
spanner[R]

Public Class Methods

database_path(config) click to toggle source
# File lib/activerecord_spanner_adapter/connection.rb, line 261
def self.database_path config
  "#{config[:emulator_host]}/#{config[:project]}/#{config[:instance]}/#{config[:database]}"
end
information_schema(config) click to toggle source
# File lib/activerecord_spanner_adapter/connection.rb, line 39
def self.information_schema config
  @information_schemas ||= {}
  @information_schemas[database_path(config)] ||= \
    ActiveRecordSpannerAdapter::InformationSchema.new new(config)
end
new(config) click to toggle source
# File lib/activerecord_spanner_adapter/connection.rb, line 16
def initialize config
  @instance_id = config[:instance]
  @database_id = config[:database]
  @spanner = self.class.spanners config
end
spanners(config) click to toggle source
# File lib/activerecord_spanner_adapter/connection.rb, line 22
def self.spanners config
  config = config.symbolize_keys
  @spanners ||= {}
  @mutex ||= Mutex.new
  @mutex.synchronize do
    @spanners[database_path(config)] ||= Google::Cloud::Spanner.new(
      project_id: config[:project],
      credentials: config[:credentials],
      emulator_host: config[:emulator_host],
      scope: config[:scope],
      timeout: config[:timeout],
      lib_name: "spanner-activerecord-adapter",
      lib_version: ActiveRecordSpannerAdapter::VERSION
    )
  end
end

Public Instance Methods

abort_batch() click to toggle source

Aborts the current batch on this connection. This is a no-op if there is no batch on this connection.

@see start_batch_ddl

# File lib/activerecord_spanner_adapter/connection.rb, line 174
def abort_batch
  @ddl_batch = nil
end
active?() click to toggle source
# File lib/activerecord_spanner_adapter/connection.rb, line 51
def active?
  # This method should not initialize a session.
  unless @session
    return false
  end
  # Assume that it is still active if it has been used in the past 50 minutes.
  if ((Time.current - @last_used) / 60).round < 50
    return true
  end
  session.execute_query "SELECT 1"
  true
rescue StandardError
  false
end
begin_transaction(isolation = nil) click to toggle source

Transactions

# File lib/activerecord_spanner_adapter/connection.rb, line 236
def begin_transaction isolation = nil
  raise "Nested transactions are not allowed" if current_transaction&.active?
  self.current_transaction = Transaction.new self, isolation
  current_transaction.begin
  current_transaction
end
commit_transaction() click to toggle source
# File lib/activerecord_spanner_adapter/connection.rb, line 243
def commit_transaction
  raise "This connection does not have a transaction" unless current_transaction
  current_transaction.commit
end
connect!()
Alias for: session
create_database() click to toggle source

Database Operations

# File lib/activerecord_spanner_adapter/connection.rb, line 81
def create_database
  job = spanner.create_database instance_id, database_id
  job.wait_until_done!
  raise Google::Cloud::Error.from_error job.error if job.error?
  job.database
end
database() click to toggle source
# File lib/activerecord_spanner_adapter/connection.rb, line 88
def database
  @database ||= begin
    database = spanner.database instance_id, database_id
    unless database
      raise ActiveRecord::NoDatabaseError(
        "#{spanner.project}/#{instance_id}/#{database_id}"
      )
    end
    database
  end
end
ddl_batch() { || ... } click to toggle source

Executes a set of DDL statements as one batch. This method raises an error if no block is given.

@example

connection.ddl_batch do
  connection.execute_ddl "CREATE TABLE `Users` (Id INT64, Name STRING(MAX)) PRIMARY KEY (Id)"
  connection.execute_ddl "CREATE INDEX Idx_Users_Name ON `Users` (Name)"
end
# File lib/activerecord_spanner_adapter/connection.rb, line 129
def ddl_batch
  raise Google::Cloud::FailedPreconditionError, "No block given for the DDL batch" unless block_given?
  begin
    start_batch_ddl
    yield
    run_batch
  rescue StandardError
    abort_batch
    raise
  ensure
    @ddl_batch = nil
  end
end
ddl_batch?() click to toggle source

Returns true if this connection is currently executing a DDL batch, and otherwise false.

# File lib/activerecord_spanner_adapter/connection.rb, line 145
def ddl_batch?
  return true if @ddl_batch
  false
end
disconnect!() click to toggle source
# File lib/activerecord_spanner_adapter/connection.rb, line 66
def disconnect!
  session.release!
  true
ensure
  @session = nil
end
execute_ddl(statements, operation_id: nil, wait_until_done: true) click to toggle source

@params [Array<String>, String] sql Single or list of statements

# File lib/activerecord_spanner_adapter/connection.rb, line 103
def execute_ddl statements, operation_id: nil, wait_until_done: true
  raise "DDL cannot be executed during a transaction" if current_transaction&.active?
  self.current_transaction = nil

  statements = Array statements
  return unless statements.any?

  # If a DDL batch is active we only buffer the statements on the connection until the batch is run.
  if @ddl_batch
    @ddl_batch.push(*statements)
    return true
  end

  execute_ddl_statements statements, operation_id, wait_until_done
end
execute_query(sql, params: nil, types: nil, single_use_selector: nil) click to toggle source

DQL, DML Statements

# File lib/activerecord_spanner_adapter/connection.rb, line 198
def execute_query sql, params: nil, types: nil, single_use_selector: nil
  if params
    converted_params, types = \
      Google::Cloud::Spanner::Convert.to_input_params_and_types(
        params, types
      )
  end

  # Clear the transaction from the previous statement.
  unless current_transaction&.active?
    self.current_transaction = nil
  end

  begin
    session.execute_query \
      sql,
      params: converted_params,
      types: types,
      transaction: transaction_selector || single_use_selector,
      seqno: (current_transaction&.next_sequence_number)
  rescue Google::Cloud::AbortedError
    # Mark the current transaction as aborted to prevent any unnecessary further requests on the transaction.
    current_transaction&.mark_aborted
    raise
  rescue Google::Cloud::NotFoundError => e
    if session_not_found?(e) || transaction_not_found?(e)
      reset!
      # Force a retry of the entire transaction if this statement was executed as part of a transaction.
      # Otherwise, just retry the statement itself.
      raise_aborted_err if current_transaction&.active?
      retry
    end
    raise
  end
end
raise_aborted_err() click to toggle source
# File lib/activerecord_spanner_adapter/connection.rb, line 283
def raise_aborted_err
  retry_info = Google::Rpc::RetryInfo.new retry_delay: Google::Protobuf::Duration.new(seconds: 0, nanos: 1)
  begin
    raise GRPC::BadStatus.new(
      GRPC::Core::StatusCodes::ABORTED,
      "Transaction aborted",
      "google.rpc.retryinfo-bin": Google::Rpc::RetryInfo.encode(retry_info)
    )
  rescue GRPC::BadStatus
    raise Google::Cloud::AbortedError
  end
end
reset!() click to toggle source
# File lib/activerecord_spanner_adapter/connection.rb, line 73
def reset!
  disconnect!
  session
  true
end
rollback_transaction() click to toggle source
# File lib/activerecord_spanner_adapter/connection.rb, line 248
def rollback_transaction
  raise "This connection does not have a transaction" unless current_transaction
  current_transaction.rollback
end
run_batch() click to toggle source

Runs the current batch on this connection. This will raise a FailedPreconditionError if there is no active batch on this connection.

@see start_batch_ddl

# File lib/activerecord_spanner_adapter/connection.rb, line 183
def run_batch
  unless @ddl_batch
    raise Google::Cloud::FailedPreconditionError, "There is no batch active on this connection"
  end
  # Just return if the batch is empty.
  return true if @ddl_batch.empty?
  begin
    execute_ddl_statements @ddl_batch, nil, true
  ensure
    @ddl_batch = nil
  end
end
session() click to toggle source
# File lib/activerecord_spanner_adapter/connection.rb, line 45
def session
  @last_used = Time.current
  @session ||= spanner.create_session instance_id, database_id
end
Also aliased as: connect!
session_not_found?(err) click to toggle source
# File lib/activerecord_spanner_adapter/connection.rb, line 265
def session_not_found? err
  if err.respond_to?(:metadata) && err.metadata["google.rpc.resourceinfo-bin"]
    resource_info = Google::Rpc::ResourceInfo.decode err.metadata["google.rpc.resourceinfo-bin"]
    type = resource_info["resource_type"]
    return "type.googleapis.com/google.spanner.v1.Session".eql? type
  end
  false
end
start_batch_ddl() click to toggle source

Starts a manual DDL batch. The batch must be ended by calling either run_batch or abort_batch.

@example

begin
  connection.start_batch_ddl
  connection.execute_ddl "CREATE TABLE `Users` (Id INT64, Name STRING(MAX)) PRIMARY KEY (Id)"
  connection.execute_ddl "CREATE INDEX Idx_Users_Name ON `Users` (Name)"
  connection.run_batch
rescue StandardError
  connection.abort_batch
  raise
end
# File lib/activerecord_spanner_adapter/connection.rb, line 163
def start_batch_ddl
  if @ddl_batch
    raise Google::Cloud::FailedPreconditionError, "A DDL batch is already active on this connection"
  end
  @ddl_batch = []
end
transaction_not_found?(err) click to toggle source
# File lib/activerecord_spanner_adapter/connection.rb, line 274
def transaction_not_found? err
  if err.respond_to?(:metadata) && err.metadata["google.rpc.resourceinfo-bin"]
    resource_info = Google::Rpc::ResourceInfo.decode err.metadata["google.rpc.resourceinfo-bin"]
    type = resource_info["resource_type"]
    return "type.googleapis.com/google.spanner.v1.Transaction".eql? type
  end
  false
end
transaction_selector() click to toggle source
# File lib/activerecord_spanner_adapter/connection.rb, line 253
def transaction_selector
  return current_transaction&.transaction_selector if current_transaction&.active?
end
truncate(table_name) click to toggle source
# File lib/activerecord_spanner_adapter/connection.rb, line 257
def truncate table_name
  session.delete table_name
end

Private Instance Methods

delay_from_aborted(err) click to toggle source

Retrieves the delay value from Google::Cloud::AbortedError or GRPC::Aborted

# File lib/activerecord_spanner_adapter/connection.rb, line 308
def delay_from_aborted err
  return nil if err.nil?
  if err.respond_to?(:metadata) && err.metadata["google.rpc.retryinfo-bin"]
    retry_info = Google::Rpc::RetryInfo.decode err.metadata["google.rpc.retryinfo-bin"]
    seconds = retry_info["retry_delay"].seconds
    nanos = retry_info["retry_delay"].nanos
    return seconds if nanos.zero?
    return seconds + (nanos / 1_000_000_000.0)
  end
  # No metadata? Try the inner error
  delay_from_aborted err.cause
rescue StandardError
  # Any error indicates the backoff should be handled elsewhere
  nil
end
execute_ddl_statements(statements, operation_id, wait_until_done) click to toggle source
# File lib/activerecord_spanner_adapter/connection.rb, line 298
def execute_ddl_statements statements, operation_id, wait_until_done
  job = database.update statements: statements, operation_id: operation_id
  job.wait_until_done! if wait_until_done
  raise Google::Cloud::Error.from_error job.error if job.error?
  job.done?
end