Class: Boltless::Request
- Inherits:
-
Object
- Object
- Boltless::Request
- Defined in:
- lib/boltless/request.rb
Overview
A neo4j HTTP API request abstraction class, which consumes a single HTTP persistent connection for its whole runtime. This connection is strictly owned by a single request object. It is not safe to share it.
rubocop:disable Metrics/ClassLength because of the isolated
request abstraction
Class Method Summary collapse
-
.statement_payload(cypher, **args) ⇒ Hash{Symbol => Mixed}
Convert a single Cypher query string and
Hash
arguments into a HTTP API/Cypher transaction API compatible form. -
.statement_payloads(*statements) ⇒ Array<Hash{Symbol => Mixed}>
Convert a multiple Cypher queries and
Hash
arguments into multiple HTTP API/Cypher transaction API compatible hashes.
Instance Method Summary collapse
-
#begin_transaction ⇒ Integer
Start a new transaction within our dedicated HTTP connection object at the neo4j server.
-
#commit_transaction(tx_id, *statements) ⇒ Array<Hash{Symbol => Mixed}>
Commit an open transaction, by the given neo4j transaction identifier.
-
#generate_log_str(tx_id, duration, *statements) ⇒ String
Generate a logging string for the given details, without actually printing it.
-
#handle_response_body(res, tx_id: nil) ⇒ Array<Hash{Symbol => Mixed}>
Handle a neo4j HTTP API response body in a generic way.
-
#handle_transaction(tx_id: nil) ⇒ Array<Hash{Symbol => Mixed}>
Handle a generic transaction interaction.
-
#handle_transport_errors { ... } ⇒ Mixed
Handle all the low-level http.rb gem errors transparently.
-
#initialize(connection, access_mode: :write, database: Boltless.configuration.default_db, raw_results: false) ⇒ Request
constructor
Setup a new neo4j request instance with the given connection to use.
-
#log_query(tx_id, *statements) { ... } ⇒ Mixed
Log the query details for the given statements, while benchmarking the given user block (which should contain the full request preparation, request performing and response parsing).
-
#one_shot_transaction(*statements) ⇒ Array<Hash{Symbol => Mixed}>
Run one/multiple Cypher statements inside a one-shot transaction.
-
#rollback_transaction(tx_id) ⇒ Array<Hash{Symbol => Mixed}>
Rollback an open transaction, by the given neo4j transaction identifier.
-
#run_query(tx_id, *statements) ⇒ Array<Hash{Symbol => Mixed}>
Run one/multiple Cypher statements inside an open transaction.
-
#serialize_body(obj) ⇒ String
Serialize the given object to a JSON string.
Constructor Details
#initialize(connection, access_mode: :write, database: Boltless.configuration.default_db, raw_results: false) ⇒ Request
Setup a new neo4j request instance with the given connection to use.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/boltless/request.rb', line 54 def initialize(connection, access_mode: :write, database: Boltless.configuration.default_db, raw_results: false) # Check the given access mode @access_mode = mode = access_mode.to_s.upcase unless %(READ WRITE).include? mode raise ArgumentError, "Unknown access mode '#{access_mode}'. " \ "Use ':read' or ':write'." end @connection = connection @path_prefix = "/db/#{database}" @raw_results = raw_results @requests_done = 0 # Make sure the upstream server is ready to rumble Boltless.wait_for_server!(connection) end |
Class Method Details
.statement_payload(cypher, **args) ⇒ Hash{Symbol => Mixed}
Convert a single Cypher query string and Hash
arguments into a HTTP API/Cypher transaction API compatible form.
30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/boltless/request.rb', line 30 def statement_payload(cypher, **args) { statement: cypher }.tap do |payload| # Enable the statement statistics if requested payload[:includeStats] = true if args.delete(:with_stats) == true # Enable the graphing output if request payload[:resultDataContents] = %w[row graph] \ if args.delete(:result_as_graph) == true payload[:parameters] = args end end |
.statement_payloads(*statements) ⇒ Array<Hash{Symbol => Mixed}>
Convert a multiple Cypher queries and Hash
arguments into multiple HTTP API/Cypher transaction API compatible hashes.
18 19 20 21 22 |
# File 'lib/boltless/request.rb', line 18 def statement_payloads(*statements) statements.map do |(cypher, args)| statement_payload(cypher, **(args || {})) end end |
Instance Method Details
#begin_transaction ⇒ Integer
Start a new transaction within our dedicated HTTP connection object at the neo4j server. When everything is fine, we return the transaction identifier from neo4j for further usage.
rubocop:disable Metrics/MethodLength because of the error
handlings and transaction identifier parsing
rubocop:disable Metrics/AbcSize dito
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 |
# File 'lib/boltless/request.rb', line 108 def begin_transaction log_query(:begin, Request.statement_payload('BEGIN')) do handle_transport_errors do path = "#{@path_prefix}/tx" res = @connection.headers('Access-Mode' => @access_mode).post(path) # When neo4j sends a response code other than 2xx, # we stop further processing raise Errors::TransactionBeginError, res.to_s \ unless res.status.success? # Try to extract the transaction identifier location = res.headers['Location'] || '' location.split("#{path}/").last.to_i.tap do |tx_id| # Make sure we flush this request from the persistent connection, # in order to allow further requests res.flush # When we failed to parse the transaction identifier, # we stop further processing raise Errors::TransactionBeginError, res.to_s \ if tx_id.zero? end end end end |
#commit_transaction(tx_id, *statements) ⇒ Array<Hash{Symbol => Mixed}>
Commit an open transaction, by the given neo4j transaction identifier.
171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/boltless/request.rb', line 171 def commit_transaction(tx_id, *statements) log_query(tx_id, Request.statement_payload('COMMIT')) do handle_transaction(tx_id: tx_id) do |path| args = {} args[:body] = serialize_body(statements: statements) \ if statements.any? @connection.post("#{path}/commit", **args) end end end |
#generate_log_str(tx_id, duration, *statements) ⇒ String
Generate a logging string for the given details, without actually printing it.
rubocop:disable Metrics/MethodLength because of the complex
logging string assembling/formatting
rubocop:disable Metrics/AbcSize dito
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 |
# File 'lib/boltless/request.rb', line 363 def generate_log_str(tx_id, duration, *statements) dur = "(#{duration}ms)".colorize(color: :magenta, mode: :bold) \ if duration tag = [ '[', "tx:#{@access_mode.downcase}:#{tx_id || 'one-shot'}", tx_id ? " rq:#{@requests_done}" : '', ']' ].join.colorize(:white) prefix = ['Boltless'.colorize(:magenta), tag, dur].compact.join(' ') statements.map do |stmt| cypher = Boltless.resolve_cypher( stmt[:statement], **(stmt[:parameters] || {}) ).lines.map(&:strip).join(' ') cypher = cypher.colorize(color: Boltless.cypher_logging_color(cypher), mode: :bold) "#{prefix} #{cypher}" end.join("\n") end |
#handle_response_body(res, tx_id: nil) ⇒ Array<Hash{Symbol => Mixed}>
Handle a neo4j HTTP API response body in a generic way.
rubocop:disable Metrics/MethodLength because of the result
handling (error, raw result, restructured result)
rubocop:disable Metrics/AbcSize dito
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 |
# File 'lib/boltless/request.rb', line 240 def handle_response_body(res, tx_id: nil) # Parse the response body as a whole, which is returned by # the configured raw response handler body = FastJsonparser.parse( Boltless.configuration.raw_response_handler.call(res.to_s, res) ) # When we hit some response errors, we handle them and # re-raise in a wrapped exception if (errors = body.fetch(:errors, [])).any? list = errors.map do |error| Errors::ResponseError.new(error[:message], code: error[:code], response: res) end raise Errors::TransactionRollbackError.new( "Transaction (#{tx_id}) rolled back due to errors (#{list.count})", errors: list, response: res ) end # Otherwise return the results, either wrapped in a # lightweight struct or raw return body[:results] if @raw_results body.fetch(:results, []).map do |result| Boltless::Result.from(result) end rescue FastJsonparser::ParseError => e # When we got something we could not parse, we tell so raise Errors::InvalidJsonError.new(e., response: res) end |
#handle_transaction(tx_id: nil) ⇒ Array<Hash{Symbol => Mixed}>
Handle a generic transaction interaction.
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/boltless/request.rb', line 209 def handle_transaction(tx_id: nil) handle_transport_errors do # Run the user given block, and pass the transaction path to it res = yield("#{@path_prefix}/tx/#{tx_id}") # When the transaction was not found, we tell so raise Errors::TransactionNotFoundError.new(res.to_s, response: res) \ if res.code == 404 # When the response was simply not successful, we tell so, too raise Errors::TransactionRollbackError.new(res.to_s, response: res) \ unless res.status.success? # Handle the response body in a generic way handle_response_body(res, tx_id: tx_id) end end |
#handle_transport_errors { ... } ⇒ Mixed
Handle all the low-level http.rb gem errors transparently.
291 292 293 294 295 |
# File 'lib/boltless/request.rb', line 291 def handle_transport_errors yield rescue HTTP::Error => e raise Errors::RequestError, e. end |
#log_query(tx_id, *statements) { ... } ⇒ Mixed
Log the query details for the given statements, while benchmarking the given user block (which should contain the full request preparation, request performing and response parsing).
When the query_log_enabled
configuration flag is set to false
, we effectively do a no-op here, to keep things fast.
rubocop:disable Metrics/MethodLength because of the
configuration handling
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 |
# File 'lib/boltless/request.rb', line 311 def log_query(tx_id, *statements) # When no query logging is enabled, we won't do it enabled = Boltless.configuration.query_log_enabled return yield unless enabled # Add a new request to the counter @requests_done += 1 # When the +query_debug_log_enabled+ config flag is set, we prodce a # logging output before the actual request is sent, in order to help # while debugging slow/never-ending Cypher statements if enabled == :debug Boltless.logger.debug do generate_log_str(tx_id == :begin ? 'tbd' : tx_id, nil, *statements) end end # Otherwise measure the runtime of the user given block, # and log the related statements start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) res = yield stop = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) # As a fallback to the +query_log_enabled+ config flag, we just log to # the debug level with a block, so it won't be executed when the logger # is not configured to print debug level Boltless.logger.debug do generate_log_str(tx_id == :begin ? res : tx_id, (stop - start).truncate(1), *statements) end # Return the result of the user given block res end |
#one_shot_transaction(*statements) ⇒ Array<Hash{Symbol => Mixed}>
Run one/multiple Cypher statements inside a one-shot transaction. A new transaction is opened, the statements are run and the transaction is commited in a single HTTP request for efficiency.
85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/boltless/request.rb', line 85 def one_shot_transaction(*statements) # We do not allow to send a run-request without Cypher statements raise ArgumentError, 'No statements given' if statements.empty? log_query(nil, *statements) do handle_transaction(tx_id: 'commit') do |path| @connection.headers('Access-Mode' => @access_mode) .post(path, body: serialize_body(statements: statements)) end end end |
#rollback_transaction(tx_id) ⇒ Array<Hash{Symbol => Mixed}>
Rollback an open transaction, by the given neo4j transaction identifier.
192 193 194 195 196 197 198 |
# File 'lib/boltless/request.rb', line 192 def rollback_transaction(tx_id) log_query(tx_id, Request.statement_payload('ROLLBACK')) do handle_transaction(tx_id: tx_id) do |path| @connection.delete(path) end end end |
#run_query(tx_id, *statements) ⇒ Array<Hash{Symbol => Mixed}>
Run one/multiple Cypher statements inside an open transaction.
148 149 150 151 152 153 154 155 156 157 |
# File 'lib/boltless/request.rb', line 148 def run_query(tx_id, *statements) # We do not allow to send a run-request without Cypher statements raise ArgumentError, 'No statements given' if statements.empty? log_query(tx_id, *statements) do handle_transaction(tx_id: tx_id) do |path| @connection.post(path, body: serialize_body(statements: statements)) end end end |
#serialize_body(obj) ⇒ String
Serialize the given object to a JSON string.
280 281 282 283 |
# File 'lib/boltless/request.rb', line 280 def serialize_body(obj) obj = obj.deep_stringify_keys if obj.is_a? Hash Oj.dump(obj) end |