class ActiveRecordSpannerAdapter::Connection
Attributes
Public Class Methods
# File lib/activerecord_spanner_adapter/connection.rb, line 261 def self.database_path config "#{config[:emulator_host]}/#{config[:project]}/#{config[:instance]}/#{config[:database]}" end
# 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
# 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
# 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
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
# 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
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
# 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
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
# 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
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
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
# File lib/activerecord_spanner_adapter/connection.rb, line 66 def disconnect! session.release! true ensure @session = nil end
@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
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
# 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
# File lib/activerecord_spanner_adapter/connection.rb, line 73 def reset! disconnect! session true end
# 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
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
# File lib/activerecord_spanner_adapter/connection.rb, line 45 def session @last_used = Time.current @session ||= spanner.create_session instance_id, database_id end
# 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
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
# 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
# File lib/activerecord_spanner_adapter/connection.rb, line 253 def transaction_selector return current_transaction&.transaction_selector if current_transaction&.active? end
# File lib/activerecord_spanner_adapter/connection.rb, line 257 def truncate table_name session.delete table_name end
Private Instance Methods
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
# 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