mongo 2.26.0 → 2.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 439e028e8b9ce062d109e1760508977f13a87dd6ee4cb9d5221a6a3108fec214
4
- data.tar.gz: bb346a893717256c3eac12af4a63c718bfd4f567b1ec17b95727e45f9f8e11d5
3
+ metadata.gz: 39d9792c95b149dbbcb1b065df222c345eea9c84912e03de9a8994d7aea32ab8
4
+ data.tar.gz: b491401e88ffde8324e802267b35f75f6f92997f5746b3634612e1a9d63f11e8
5
5
  SHA512:
6
- metadata.gz: 3d650edf8067fbe760d1c0526e62bef7cfda7c27284b725ffcc5a8e3c33c712011b3aa478e87db2abea876d36e332daf733d1180954ed6bd1e5003a258a05f49
7
- data.tar.gz: 6832d11c4cae68a7776c8e5f7ef8e5bd9fcbda45bedf8e8cb208fdc5103060582a315cb75434e003462500b4da05deb5e71618a6856159262ff3655fbe16888d
6
+ metadata.gz: aad7e612834dd20849a707f6fde3d5a8101765af93df37eee119ad0418dd518b738ec8834689f0aeac267ac117aab2f89f84362f0c588f8714b96c1c87c286ef
7
+ data.tar.gz: 6a1c05b23598ac63e37267393de256bd0f2d250f14e881e2fda7a436a2ee2775d60e61d076c4fbaaa77230e7135f89095c55a737edca13a3434f048fdf8aa5db
@@ -115,15 +115,15 @@ module Mongo
115
115
 
116
116
  def send_initial_query(server, context, operation: nil)
117
117
  if server.load_balancer?
118
- # Connection will be checked in when cursor is drained.
119
- connection = server.pool.check_out(context: context)
120
- initial_query_op(
121
- context.session,
122
- effective_read_preference(connection)
123
- ).execute_with_connection(
124
- connection,
125
- context: context
126
- )
118
+ server.pool.with_cursor_connection(context: context) do |connection|
119
+ initial_query_op(
120
+ context.session,
121
+ effective_read_preference(connection)
122
+ ).execute_with_connection(
123
+ connection,
124
+ context: context
125
+ )
126
+ end
127
127
  else
128
128
  server.with_connection do |connection|
129
129
  initial_query_op(
@@ -350,8 +350,7 @@ module Mongo
350
350
  if server.load_balancer?
351
351
  # In load balanced topology, manually check out a connection
352
352
  # so it remains checked out and pinned to the cursor.
353
- connection = server.pool.check_out(context: context)
354
- begin
353
+ server.pool.with_cursor_connection(context: context) do |connection|
355
354
  result = send_initial_query(connection, context)
356
355
 
357
356
  start_at_operation_time = if (doc = result.replies.first && result.replies.first.documents.first)
@@ -360,9 +359,6 @@ module Mongo
360
359
  nil
361
360
  end
362
361
  result
363
- rescue StandardError
364
- server.pool.check_in(connection)
365
- raise
366
362
  end
367
363
  else
368
364
  server.with_connection do |connection|
@@ -172,16 +172,9 @@ module Mongo
172
172
  def send_initial_query(server, context, operation: nil)
173
173
  operation ||= initial_query_op(context.session)
174
174
  if server.load_balancer?
175
- # Connection will be checked in when cursor is drained,
176
- # unless the connection is pinned to a transaction (in which
177
- # case it stays checked out for the transaction duration).
178
- if context.connection_global_id
179
- connection = server.pool.check_out_pinned_connection(
180
- context.connection_global_id
181
- )
175
+ server.pool.with_cursor_connection(context: context) do |connection|
176
+ operation.execute_with_connection(connection, context: context)
182
177
  end
183
- connection ||= server.pool.check_out(context: context)
184
- operation.execute_with_connection(connection, context: context)
185
178
  else
186
179
  operation.execute(server, context: context)
187
180
  end
@@ -73,10 +73,11 @@ module Mongo
73
73
  context = Operation::Context.new(client: client, session: session,
74
74
  operation_timeouts: view.operation_timeouts)
75
75
  if server.load_balancer?
76
- # Connection will be checked in when cursor is drained.
77
- connection = server.pool.check_out(context: context)
78
- result = send_initial_query_with_connection(connection, context.session, context: context)
79
- result = send_fetch_query_with_connection(connection, session) unless inline?
76
+ result = server.pool.with_cursor_connection(context: context) do |connection|
77
+ res = send_initial_query_with_connection(connection, context.session, context: context)
78
+ res = send_fetch_query_with_connection(connection, session) unless inline?
79
+ res
80
+ end
80
81
  else
81
82
  result = send_initial_query(server, context)
82
83
  result = send_fetch_query(server, session) unless inline?
@@ -739,9 +739,9 @@ module Mongo
739
739
  connection_global_id: result.connection_global_id
740
740
  )
741
741
  result = if server.load_balancer?
742
- # Connection will be checked in when cursor is drained.
743
- connection = server.pool.check_out(context: context)
744
- op.execute_with_connection(connection, context: context)
742
+ server.pool.with_cursor_connection(context: context) do |connection|
743
+ op.execute_with_connection(connection, context: context)
744
+ end
745
745
  else
746
746
  op.execute(server, context: context)
747
747
  end
data/lib/mongo/cursor.rb CHANGED
@@ -103,6 +103,19 @@ module Mongo
103
103
  self.class.finalize(kill_spec(@connection_global_id), cluster)
104
104
  )
105
105
  end
106
+ rescue Exception # rubocop:disable Lint/RescueException
107
+ # In load-balanced topology the connection of the initial result is
108
+ # checked out of the pool until the cursor is drained. If the cursor
109
+ # cannot be constructed, nothing will ever check the connection back
110
+ # in, so release it here before the error propagates. Exception (not
111
+ # StandardError) is rescued so that an interrupt does not permanently
112
+ # leak the connection. In other topologies the connection is not owned
113
+ # by the cursor and must not be touched here.
114
+ if server&.load_balancer? && result.is_a?(Operation::Result) &&
115
+ (connection = result.connection)
116
+ connection.connection_pool.check_in_if_checked_out(connection)
117
+ end
118
+ raise
106
119
  end
107
120
 
108
121
  # @api private
@@ -273,12 +273,13 @@ module Mongo
273
273
  execution_opts = {}
274
274
  execution_opts[:deserialize_as_bson] = opts.delete(:deserialize_as_bson) if opts.key?(:deserialize_as_bson)
275
275
  if server.load_balancer?
276
- connection = server.pool.check_out(context: context)
277
- initial_query_op(session, opts).execute_with_connection(
278
- connection,
279
- context: context,
280
- options: execution_opts
281
- )
276
+ server.pool.with_cursor_connection(context: context) do |connection|
277
+ initial_query_op(session, opts).execute_with_connection(
278
+ connection,
279
+ context: context,
280
+ options: execution_opts
281
+ )
282
+ end
282
283
  else
283
284
  initial_query_op(session, opts).execute(
284
285
  server,
@@ -339,15 +339,12 @@ module Mongo
339
339
  server = selector.select_server(cluster, nil, session)
340
340
  if server.load_balancer?
341
341
  # The connection is checked in by the cursor when it is drained.
342
- connection = check_out_cursor_command_connection(server, context)
343
- begin
344
- op.execute_with_connection(connection, context: context, options: execution_opts)
345
- rescue StandardError
346
- # Release the connection before the error propagates so that
347
- # a retried attempt checks out a fresh one.
348
- connection.connection_pool.check_in(connection) unless connection.pinned?
349
- connection = nil
350
- raise
342
+ # If the command fails, the connection is released before the
343
+ # error propagates so that a retried attempt checks out a
344
+ # fresh one.
345
+ server.pool.with_cursor_connection(context: context) do |conn|
346
+ connection = conn
347
+ op.execute_with_connection(conn, context: context, options: execution_opts)
351
348
  end
352
349
  else
353
350
  op.execute(server, context: context, options: execution_opts)
@@ -366,7 +363,7 @@ module Mongo
366
363
  # If the cursor was created it owns the session and connection;
367
364
  # otherwise (error or no cursor in the response) release them here.
368
365
  unless cursor
369
- connection.connection_pool.check_in(connection) if connection && !connection.pinned?
366
+ connection.connection_pool.check_in_if_checked_out(connection) if connection
370
367
  session.end_session if session && session.implicit?
371
368
  end
372
369
  end
@@ -421,8 +421,9 @@ module Mongo
421
421
 
422
422
  def send_initial_query(op, server, _session, context)
423
423
  if server.load_balancer?
424
- connection = server.pool.check_out(context: context)
425
- op.execute_with_connection(connection, context: context)
424
+ server.pool.with_cursor_connection(context: context) do |connection|
425
+ op.execute_with_connection(connection, context: context)
426
+ end
426
427
  else
427
428
  op.execute(server, context: context)
428
429
  end
@@ -37,6 +37,11 @@ module Mongo
37
37
  copydb
38
38
  ].freeze
39
39
 
40
+ # The hello and legacy hello command names. A constant, because
41
+ # sensitive? runs on every command event and an array literal would
42
+ # allocate on each call.
43
+ HELLO_COMMANDS = %w[hello ismaster isMaster].freeze
44
+
40
45
  # Check whether the command is sensitive in terms of command monitoring
41
46
  # spec. A command is detected as sensitive if it is in the
42
47
  # list or if it is a hello/legacy hello command, and
@@ -53,7 +58,7 @@ module Mongo
53
58
  # when speculativeAuthenticate is present, their commands AND replies
54
59
  # MUST be redacted from the events.
55
60
  # See https://github.com/mongodb/specifications/blob/master/source/command-logging-and-monitoring/command-logging-and-monitoring.md#security
56
- %w[hello ismaster isMaster].include?(command_name.to_s) &&
61
+ HELLO_COMMANDS.include?(command_name.to_s) &&
57
62
  !!document['speculativeAuthenticate']
58
63
  end
59
64
 
@@ -80,7 +80,7 @@ module Mongo
80
80
  # @since 2.0.0
81
81
  # @api public
82
82
  def successful?
83
- !documents.nil?
83
+ !documents.nil? && parser.message.empty?
84
84
  end
85
85
 
86
86
  # Get the execution time of the map/reduce.
@@ -112,7 +112,11 @@ module Mongo
112
112
  # @since 2.0.0
113
113
  # @api private
114
114
  def validate!
115
- documents.nil? ? raise_operation_failure : self
115
+ if documents.nil? || !parser.message.empty?
116
+ raise_operation_failure
117
+ else
118
+ self
119
+ end
116
120
  end
117
121
 
118
122
  # Get the cursor id.
@@ -36,7 +36,7 @@ module Mongo
36
36
  return unless @replies
37
37
 
38
38
  @replies.reduce(nil) do |errors, reply|
39
- next unless (write_errors = reply.documents.first['writeErrors'])
39
+ next errors unless (write_errors = reply.documents.first['writeErrors'])
40
40
 
41
41
  wes = write_errors.collect do |we|
42
42
  we.merge!('index' => count + we['index'])
@@ -59,11 +59,11 @@ module Mongo
59
59
  return unless @replies
60
60
 
61
61
  @replies.each_with_index.reduce(nil) do |errors, (reply, _)|
62
- next unless (write_concern_errors = reply.documents.first['writeConcernErrors'])
62
+ next errors unless (write_concern_errors = reply.documents.first['writeConcernErrors'])
63
63
 
64
64
  (errors || []) << write_concern_errors.reduce(nil) do |errs, wce|
65
65
  wce.merge!('index' => count + wce['index'])
66
- (errs || []) << write_concern_error
66
+ (errs || []) << wce
67
67
  end
68
68
  end
69
69
  end
@@ -319,16 +319,8 @@ module Mongo
319
319
  # @return [ Array<BSON::Document> ] Deserialized section.
320
320
  #
321
321
  # @since 2.5.0
322
- def self.deserialize(buffer)
322
+ def self.deserialize(buffer, _options = {})
323
323
  raise NotImplementedError
324
-
325
- start_size = buffer.length
326
- section_size = buffer.get_int32 # get the size
327
- end_size = start_size - section_size
328
- buffer.get_cstring # get the identifier
329
- documents = []
330
- documents << BSON::Document.from_bson(buffer) until buffer.length == end_size
331
- documents
332
324
  end
333
325
  end
334
326
  end
@@ -130,7 +130,6 @@ module Mongo
130
130
  doc[:driver] = driver_doc
131
131
  doc[:os] = os_doc
132
132
  doc[:platform] = platform_string
133
- doc[:backpressure] = '2'
134
133
  env_doc.tap { |env| doc[:env] = env if env }
135
134
  end
136
135
  end
@@ -157,7 +156,7 @@ module Mongo
157
156
  def document
158
157
  @document ||= begin
159
158
  client = Truncator.new(client_document).document
160
- BSON::Document.new(compression: @compressors, client: client).tap do |doc|
159
+ BSON::Document.new(compression: @compressors, client: client, backpressure: '2').tap do |doc|
161
160
  doc[:saslSupportedMechs] = @request_auth_mech if @request_auth_mech
162
161
  doc.update(Utils.transform_server_api(@server_api)) if @server_api
163
162
  end
@@ -124,6 +124,13 @@ module Mongo
124
124
  # across all connections.
125
125
  attr_reader :global_id
126
126
 
127
+ # @return [ Hash | nil ] OpenTelemetry span attributes describing this
128
+ # connection. Built once by the tracer and reused for every command
129
+ # on the connection.
130
+ #
131
+ # @api private
132
+ attr_accessor :tracing_attributes
133
+
127
134
  def_delegators :server, :tracer
128
135
 
129
136
  # The connection pool from which this connection was created.
@@ -432,6 +432,30 @@ module Mongo
432
432
  check_invariants
433
433
  end
434
434
 
435
+ # Check a connection back into the pool only if this pool still holds
436
+ # it as checked out and no other owner has claimed it.
437
+ #
438
+ # Unlike #check_in, this method is safe to call when the connection may
439
+ # have been checked in already (e.g. by Session#unpin while handling a
440
+ # transient transaction error) or may be pinned to a transaction or
441
+ # cursor that will check it in later; in both cases it does nothing.
442
+ #
443
+ # @param [ Mongo::Server::Connection ] connection The connection.
444
+ #
445
+ # @api private
446
+ def check_in_if_checked_out(connection)
447
+ check_invariants
448
+
449
+ @lock.synchronize do
450
+ return if connection.pinned?
451
+ return unless @checked_out_connections.include?(connection)
452
+
453
+ do_check_in(connection)
454
+ end
455
+ ensure
456
+ check_invariants
457
+ end
458
+
435
459
  # Executes the check in after having already acquired the lock.
436
460
  #
437
461
  # @param [ Mongo::Server::Connection ] connection The connection.
@@ -758,16 +782,43 @@ module Mongo
758
782
  rescue Error::SocketError, Error::SocketTimeoutError, Error::ConnectionPerished => e
759
783
  maybe_raise_pool_cleared!(connection, e)
760
784
  ensure
761
- if connection && !connection.pinned?
762
- # Do not check in if the connection is pinned (the session or cursor
763
- # owns it and will check it in later when unpinning). Also skip
764
- # check-in if the connection was already checked in during the block
765
- # (e.g. by Session#unpin after an error on the first operation).
766
- checked_out = @lock.synchronize do
767
- @checked_out_connections.include?(connection)
768
- end
769
- check_in(connection) if checked_out
785
+ # Do not check in if the connection is pinned (the session or cursor
786
+ # owns it and will check it in later when unpinning) or was already
787
+ # checked in during the block (e.g. by Session#unpin after an error
788
+ # on the first operation).
789
+ check_in_if_checked_out(connection) if connection
790
+ end
791
+
792
+ # Check out a connection for the initial command of a cursor-returning
793
+ # operation in load-balanced topology and yield it to the block.
794
+ #
795
+ # On success the connection remains checked out: the cursor assumes
796
+ # ownership and checks it in when drained or closed. If the block
797
+ # raises, no cursor exists to do that, so the connection is checked
798
+ # back in here before the error propagates, unless another owner
799
+ # already claimed it (it is pinned to a transaction, or Session#unpin
800
+ # checked it in while handling a transient transaction error).
801
+ #
802
+ # If the operation context is pinned to a connection (e.g. inside a
803
+ # transaction), the pinned connection is reused.
804
+ #
805
+ # @param [ Mongo::Operation::Context | nil ] :context Context of the
806
+ # operation the connection is requested for, if any.
807
+ #
808
+ # @return [ Object ] The result of the block.
809
+ #
810
+ # @api private
811
+ def with_cursor_connection(context:)
812
+ if context&.connection_global_id
813
+ connection = check_out_pinned_connection(context.connection_global_id)
770
814
  end
815
+ connection ||= check_out(context: context)
816
+ succeeded = false
817
+ result = yield(connection)
818
+ succeeded = true
819
+ result
820
+ ensure
821
+ check_in_if_checked_out(connection) if connection && !succeeded
771
822
  end
772
823
 
773
824
  # Close sockets that have been open for longer than the max idle time,
data/lib/mongo/session.rb CHANGED
@@ -297,6 +297,13 @@ module Mongo
297
297
  # @api private
298
298
  attr_accessor :recovery_token
299
299
 
300
+ # @return [ Array<Integer, String> | nil ] The transaction number and the
301
+ # OpenTelemetry tracer's key for that transaction. Built once by the
302
+ # tracer and reused for every operation in the transaction.
303
+ #
304
+ # @api private
305
+ attr_accessor :tracing_transaction_key
306
+
300
307
  # Error message indicating that the session was retrieved from a client with a different cluster than that of the
301
308
  # client through which it is currently being used.
302
309
  #
@@ -23,12 +23,8 @@ module Mongo
23
23
  class CommandTracer
24
24
  include Mongo::Monitoring::Event::Secure
25
25
 
26
- # Commands for which a span MUST NOT be created. The OpenTelemetry spec
27
- # requires drivers to skip command spans for sensitive commands listed in
28
- # the Command Logging and Monitoring spec. We additionally skip hello /
29
- # legacy hello in all forms — these are handshake/heartbeat traffic and
30
- # would only add noise to traces.
31
- HELLO_COMMANDS = %w[hello ismaster isMaster].freeze
26
+ # Upper bound of the lsid string cache (see #lsid).
27
+ LSID_CACHE_MAX = 128
32
28
 
33
29
  # Initializes a new CommandTracer.
34
30
  #
@@ -41,6 +37,8 @@ module Mongo
41
37
  @otel_tracer = otel_tracer
42
38
  @parent_tracer = parent_tracer
43
39
  @query_text_max_length = query_text_max_length
40
+ @lsid_cache = {}
41
+ @lsid_cache_mutex = Mutex.new
44
42
  end
45
43
 
46
44
  # Starts a span for a MongoDB command.
@@ -64,55 +62,84 @@ module Mongo
64
62
  # @yield the block representing the command to be traced.
65
63
  #
66
64
  # @return [ Object ] the result of the command.
67
- # rubocop:disable Lint/RescueException
68
- def trace_command(message, _operation_context, connection)
69
- return yield if skip_tracing?(message)
65
+ def trace_command(message, _operation_context, connection, &block)
66
+ # Read the command document and name once and pass them on: every
67
+ # read of message.documents allocates.
68
+ doc = message.documents.first
69
+ name = command_name(doc)
70
+ return yield if skip_tracing?(doc, name)
70
71
 
71
72
  # Commands should always be nested under their operation span, not directly under
72
73
  # the transaction span. Don't pass with_parent to use automatic parent resolution
73
74
  # from the currently active span (the operation span).
74
- span = create_command_span(message, connection)
75
- ::OpenTelemetry::Trace.with_span(span) do |s, c|
75
+ span = create_command_span(name, doc, connection)
76
+ # With no SDK and no valid parent, the API returns a span with an
77
+ # invalid context. It cannot be made current, recorded or
78
+ # propagated, so it gets no work at all, not even finishing it.
79
+ return yield unless span.context.valid?
80
+
81
+ trace_with_span(span, message, name, doc, &block)
82
+ end
83
+
84
+ private
85
+
86
+ # Runs the command with the span made current, and finishes the span.
87
+ # A span that is not recording is still made current, so that its
88
+ # context is propagated, but gets no further attributes and no
89
+ # outcome: the SDK would discard them.
90
+ #
91
+ # @param span [ OpenTelemetry::Trace::Span ] a span with a valid context.
92
+ # @param message [ Mongo::Protocol::Message ] the command message.
93
+ # @param name [ String ] the command name.
94
+ # @param doc [ Hash ] the command document.
95
+ #
96
+ # @return [ Object ] the result of the command.
97
+ #
98
+ # rubocop:disable Lint/RescueException
99
+ def trace_with_span(span, message, name, doc)
100
+ recording = span.recording?
101
+ apply_deferred_attributes(span, message, name, doc) if recording
102
+ ::OpenTelemetry::Trace.with_span(span) do |s, _c|
76
103
  yield.tap do |result|
77
- process_command_result(result, cursor_id(message), c, s)
104
+ process_command_result(result, s) if recording
78
105
  end
79
106
  end
80
107
  rescue Exception => e
81
- handle_command_exception(span, e)
108
+ handle_command_exception(span, e) if recording
82
109
  raise e
83
110
  ensure
84
- span&.finish
111
+ span.finish
85
112
  end
86
113
  # rubocop:enable Lint/RescueException
87
114
 
88
- private
89
-
90
115
  # Determines whether the command must not be traced. Sensitive auth
91
116
  # commands carry credentials in their payloads (SCRAM proofs, cleartext
92
117
  # passwords, etc.) and the OpenTelemetry spec requires drivers to skip
93
- # command spans for them. Hello / legacy hello are also skipped to keep
94
- # handshake traffic out of traces.
118
+ # command spans for them. Hello / legacy hello in all forms
119
+ # (Secure::HELLO_COMMANDS) are also skipped: they are handshake and
120
+ # heartbeat traffic and would only add noise to traces.
95
121
  #
96
- # @param message [ Mongo::Protocol::Message ] the command message.
122
+ # @param doc [ Hash ] the command document.
123
+ # @param name [ String ] the command name.
97
124
  #
98
125
  # @return [ Boolean ] true when no command span should be created.
99
- def skip_tracing?(message)
100
- name = command_name(message)
126
+ def skip_tracing?(doc, name)
101
127
  return true if HELLO_COMMANDS.include?(name)
102
128
 
103
- sensitive?(command_name: name, document: message.documents.first)
129
+ sensitive?(command_name: name, document: doc)
104
130
  end
105
131
 
106
132
  # Creates a span for a command.
107
133
  #
108
- # @param message [ Mongo::Protocol::Message ] the command message.
134
+ # @param name [ String ] the command name.
135
+ # @param doc [ Hash ] the command document.
109
136
  # @param connection [ Mongo::Server::Connection ] the connection.
110
137
  #
111
138
  # @return [ OpenTelemetry::Trace::Span ] the created span.
112
- def create_command_span(message, connection)
139
+ def create_command_span(name, doc, connection)
113
140
  @otel_tracer.start_span(
114
- command_name(message),
115
- attributes: span_attributes(message, connection),
141
+ name,
142
+ attributes: span_attributes(doc, name, connection),
116
143
  kind: :client
117
144
  )
118
145
  end
@@ -120,21 +147,17 @@ module Mongo
120
147
  # Processes the command result and updates span attributes.
121
148
  #
122
149
  # @param result [ Object ] the command result.
123
- # @param cursor_id [ Integer | nil ] the cursor ID.
124
- # @param context [ OpenTelemetry::Context ] the context.
125
150
  # @param span [ OpenTelemetry::Trace::Span ] the current span.
126
- def process_command_result(result, cursor_id, context, span)
127
- process_cursor_context(result, cursor_id, context, span)
151
+ def process_command_result(result, span)
152
+ process_cursor_context(result, span)
128
153
  maybe_trace_error(result, span)
129
154
  end
130
155
 
131
156
  # Handles exceptions that occur during command execution.
132
157
  #
133
- # @param span [ OpenTelemetry::Trace::Span | nil ] the span.
158
+ # @param span [ OpenTelemetry::Trace::Span ] the span.
134
159
  # @param exception [ Exception ] the exception that occurred.
135
160
  def handle_command_exception(span, exception)
136
- return unless span
137
-
138
161
  if exception.is_a?(Mongo::Error::OperationFailure)
139
162
  span.set_attribute('db.response.status_code', exception.code.to_s)
140
163
  end
@@ -142,70 +165,68 @@ module Mongo
142
165
  span.status = ::OpenTelemetry::Trace::Status.error("Unhandled exception of type: #{exception.class}")
143
166
  end
144
167
 
145
- # Builds span attributes for the command.
168
+ # Builds the attributes passed at span creation: the cheap set the
169
+ # OpenTelemetry spec recommends. They are built for every span,
170
+ # recorded or not, and are the only ones the sampler sees. The rest
171
+ # are set by apply_deferred_attributes once the span is known to
172
+ # record.
146
173
  #
147
- # @param message [ Mongo::Protocol::Message ] the command message.
174
+ # @param doc [ Hash ] the command document.
175
+ # @param name [ String ] the command name.
148
176
  # @param connection [ Mongo::Server::Connection ] the connection.
149
177
  #
150
178
  # @return [ Hash ] OpenTelemetry span attributes following MongoDB semantic conventions.
151
- def span_attributes(message, connection)
152
- base_attributes(message)
153
- .merge(connection_attributes(connection))
154
- .merge(session_attributes(message))
155
- .compact
179
+ def span_attributes(doc, name, connection)
180
+ {
181
+ 'db.system.name' => 'mongodb',
182
+ 'db.namespace' => database(doc),
183
+ 'db.collection.name' => collection_name(name, doc),
184
+ 'db.command.name' => name
185
+ }.compact.merge(connection_attributes(connection))
156
186
  end
157
187
 
158
- # Returns base database and command attributes.
188
+ # Sets the remaining attributes on a span that is recording. They are
189
+ # not visible to the sampler; only custom samplers keying on them are
190
+ # affected.
159
191
  #
192
+ # @param span [ OpenTelemetry::Trace::Span ] the span.
160
193
  # @param message [ Mongo::Protocol::Message ] the command message.
161
- #
162
- # @return [ Hash ] base span attributes.
163
- def base_attributes(message)
194
+ # @param name [ String ] the command name.
195
+ # @param doc [ Hash ] the command document.
196
+ def apply_deferred_attributes(span, message, name, doc)
164
197
  {
165
- 'db.system.name' => 'mongodb',
166
- 'db.namespace' => database(message),
167
- 'db.collection.name' => collection_name(message),
168
- 'db.command.name' => command_name(message),
169
- 'db.query.summary' => query_summary(message),
170
- 'db.query.text' => query_text(message)
171
- }
198
+ 'db.query.summary' => query_summary(name, doc),
199
+ 'db.query.text' => query_text(message),
200
+ 'db.mongodb.cursor_id' => cursor_id(name, doc),
201
+ 'db.mongodb.lsid' => lsid(doc),
202
+ 'db.mongodb.txn_number' => txn_number(doc)
203
+ }.each do |key, value|
204
+ span.set_attribute(key, value) unless value.nil?
205
+ end
172
206
  end
173
207
 
174
- # Returns connection-related attributes.
208
+ # Returns connection-related attributes, built once per connection,
209
+ # frozen and kept on the connection. Two threads may build them at
210
+ # the same time; they build the same hash, so either result is fine.
175
211
  #
176
212
  # @param connection [ Mongo::Server::Connection ] the connection.
177
213
  #
178
214
  # @return [ Hash ] connection span attributes.
179
215
  def connection_attributes(connection)
180
- {
216
+ connection.tracing_attributes ||= {
181
217
  'server.port' => connection.address.port,
182
218
  'server.address' => connection.address.host,
183
219
  'network.transport' => connection.transport.to_s,
184
220
  'db.mongodb.server_connection_id' => connection.server.description.server_connection_id,
185
221
  'db.mongodb.driver_connection_id' => connection.id
186
- }
187
- end
188
-
189
- # Returns session and transaction attributes.
190
- #
191
- # @param message [ Mongo::Protocol::Message ] the command message.
192
- #
193
- # @return [ Hash ] session span attributes.
194
- def session_attributes(message)
195
- {
196
- 'db.mongodb.cursor_id' => cursor_id(message),
197
- 'db.mongodb.lsid' => lsid(message),
198
- 'db.mongodb.txn_number' => txn_number(message)
199
- }
222
+ }.freeze
200
223
  end
201
224
 
202
225
  # Processes cursor context from the command result.
203
226
  #
204
227
  # @param result [ Object ] the command result.
205
- # @param _cursor_id [ Integer | nil ] the cursor ID (unused).
206
- # @param _context [ OpenTelemetry::Context ] the context (unused).
207
228
  # @param span [ OpenTelemetry::Trace::Span ] the current span.
208
- def process_cursor_context(result, _cursor_id, _context, span)
229
+ def process_cursor_context(result, span)
209
230
  cursor_id = normalize_cursor_id(result.cursor_id)
210
231
  return unless cursor_id.positive?
211
232
 
@@ -242,51 +263,56 @@ module Mongo
242
263
 
243
264
  # Generates a summary string for the query.
244
265
  #
245
- # @param message [ Mongo::Protocol::Message ] the command message.
266
+ # @param name [ String ] the command name.
267
+ # @param doc [ Hash ] the command document.
246
268
  #
247
269
  # @return [ String ] summary in format "command_name db.collection" or "command_name db".
248
- def query_summary(message)
249
- if (coll_name = collection_name(message))
250
- "#{command_name(message)} #{database(message)}.#{coll_name}"
270
+ def query_summary(name, doc)
271
+ if (coll_name = collection_name(name, doc))
272
+ "#{name} #{database(doc)}.#{coll_name}"
251
273
  else
252
- "#{command_name(message)} #{database(message)}"
274
+ "#{name} #{database(doc)}"
253
275
  end
254
276
  end
255
277
 
256
- # Extracts the collection name from the command message.
278
+ # Extracts the collection name from the command document.
257
279
  #
258
- # @param message [ Mongo::Protocol::Message ] the command message.
280
+ # @param name [ String ] the command name.
281
+ # @param doc [ Hash ] the command document.
259
282
  #
260
283
  # @return [ String | nil ] the collection name, or nil if not applicable.
261
- def collection_name(message)
262
- case command_name(message)
284
+ def collection_name(name, doc)
285
+ case name
263
286
  when 'getMore'
264
- message.documents.first['collection'].to_s
287
+ doc['collection'].to_s
265
288
  when 'listCollections', 'listDatabases', 'commitTransaction', 'abortTransaction'
266
289
  nil
267
290
  else
268
- value = message.documents.first.values.first
291
+ # The first value, read without building an array of all of them.
292
+ value = doc.each_value { |v| break v } # rubocop:disable Lint/UnreachableLoop -- first value only
269
293
  # Return nil if the value is not a string (e.g., for admin commands that have numeric values)
270
294
  value.is_a?(String) ? value : nil
271
295
  end
272
296
  end
273
297
 
274
- # Extracts the command name from the message.
298
+ # Extracts the command name, the first key of the command document,
299
+ # without building an array of all keys.
275
300
  #
276
- # @param message [ Mongo::Protocol::Message ] the command message.
301
+ # @param doc [ Hash ] the command document.
277
302
  #
278
303
  # @return [ String ] the command name.
279
- def command_name(message)
280
- message.documents.first.keys.first.to_s
304
+ def command_name(doc)
305
+ doc.each_key { |key| return key.to_s } # rubocop:disable Lint/UnreachableLoop -- first key only
306
+ ''
281
307
  end
282
308
 
283
- # Extracts the database name from the message.
309
+ # Extracts the database name from the command document.
284
310
  #
285
- # @param message [ Mongo::Protocol::Message ] the command message.
311
+ # @param doc [ Hash ] the command document.
286
312
  #
287
313
  # @return [ String ] the database name.
288
- def database(message)
289
- message.documents.first['$db'].to_s
314
+ def database(doc)
315
+ doc['$db'].to_s
290
316
  end
291
317
 
292
318
  # Checks if query text capture is enabled.
@@ -298,34 +324,47 @@ module Mongo
298
324
 
299
325
  # Extracts the cursor ID from getMore commands.
300
326
  #
301
- # @param message [ Mongo::Protocol::Message ] the command message.
327
+ # @param name [ String ] the command name.
328
+ # @param doc [ Hash ] the command document.
302
329
  #
303
330
  # @return [ Integer | nil ] the cursor ID, or nil if not a getMore command.
304
- def cursor_id(message)
305
- return unless command_name(message) == 'getMore'
331
+ def cursor_id(name, doc)
332
+ return unless name == 'getMore'
306
333
 
307
- message.documents.first['getMore'].value
334
+ doc['getMore'].value
308
335
  end
309
336
 
310
- # Extracts the logical session ID from the command.
337
+ # Extracts the logical session ID from the command, as a UUID string.
338
+ # The string is formatted once per session id and kept in a small
339
+ # cache, cleared when it reaches LSID_CACHE_MAX entries.
311
340
  #
312
- # @param message [ Mongo::Protocol::Message ] the command message.
341
+ # @param doc [ Hash ] the command document.
313
342
  #
314
- # @return [ BSON::Binary | nil ] the session ID, or nil if not present.
315
- def lsid(message)
316
- lsid_doc = message.documents.first['lsid']
343
+ # @return [ String | nil ] the session ID, or nil if not present.
344
+ def lsid(doc)
345
+ lsid_doc = doc['lsid']
317
346
  return unless lsid_doc
318
347
 
319
- lsid_doc['id'].to_uuid
348
+ binary = lsid_doc['id']
349
+ key = binary.data
350
+ cached = @lsid_cache_mutex.synchronize { @lsid_cache[key] }
351
+ return cached if cached
352
+
353
+ uuid = binary.to_uuid
354
+ @lsid_cache_mutex.synchronize do
355
+ @lsid_cache.clear if @lsid_cache.size >= LSID_CACHE_MAX
356
+ @lsid_cache[key] = uuid
357
+ end
358
+ uuid
320
359
  end
321
360
 
322
361
  # Extracts the transaction number from the command.
323
362
  #
324
- # @param message [ Mongo::Protocol::Message ] the command message.
363
+ # @param doc [ Hash ] the command document.
325
364
  #
326
365
  # @return [ Integer | nil ] the transaction number, or nil if not present.
327
- def txn_number(message)
328
- txn_num = message.documents.first['txnNumber']
366
+ def txn_number(doc)
367
+ txn_num = doc['txnNumber']
329
368
  return unless txn_num
330
369
 
331
370
  txn_num.value
@@ -37,6 +37,8 @@ module Mongo
37
37
  def initialize(otel_tracer, parent_tracer)
38
38
  @otel_tracer = otel_tracer
39
39
  @parent_tracer = parent_tracer
40
+ @operation_names = {}
41
+ @operation_names_mutex = Mutex.new
40
42
  end
41
43
 
42
44
  # Trace a MongoDB operation.
@@ -53,21 +55,43 @@ module Mongo
53
55
  # @yield the block representing the operation to be traced.
54
56
  #
55
57
  # @return [ Object ] the result of the operation.
56
- #
57
- # rubocop:disable Lint/RescueException
58
58
  def trace_operation(operation, operation_context, op_name: nil, &block)
59
59
  span = create_operation_span(operation, operation_context, op_name)
60
- execute_with_span(span, operation, &block)
60
+ # With no SDK and no valid parent, the API returns a span with an
61
+ # invalid context. It cannot be made current, recorded or
62
+ # propagated, so it gets no work at all, not even finishing it.
63
+ return yield unless span.context.valid?
64
+
65
+ trace_with_span(span, operation, &block)
66
+ end
67
+
68
+ private
69
+
70
+ # Runs the operation with the span made current, and finishes the span.
71
+ # A span that is not recording is still made current, so that the
72
+ # command spans created within it nest under it, but gets no further
73
+ # attributes and no outcome: the SDK would discard them.
74
+ #
75
+ # @param span [ OpenTelemetry::Trace::Span ] a span with a valid context.
76
+ # @param operation [ Mongo::Operation ] the operation.
77
+ #
78
+ # @return [ Object ] the result of the operation.
79
+ #
80
+ # rubocop:disable Lint/RescueException
81
+ def trace_with_span(span, operation, &block)
82
+ recording = span.recording?
83
+ if recording && !operation.cursor_id.nil?
84
+ span.set_attribute('db.mongodb.cursor_id', operation.cursor_id)
85
+ end
86
+ execute_with_span(span, operation, recording, &block)
61
87
  rescue Exception => e
62
- handle_span_exception(span, e)
88
+ handle_span_exception(span, e) if recording
63
89
  raise e
64
90
  ensure
65
- span&.finish
91
+ span.finish
66
92
  end
67
93
  # rubocop:enable Lint/RescueException
68
94
 
69
- private
70
-
71
95
  # Creates an OpenTelemetry span for the operation.
72
96
  #
73
97
  # @param operation [ Mongo::Operation ] the operation.
@@ -89,14 +113,15 @@ module Mongo
89
113
  #
90
114
  # @param span [ OpenTelemetry::Trace::Span ] the span.
91
115
  # @param operation [ Mongo::Operation ] the operation.
116
+ # @param recording [ true | false ] whether the span is recording.
92
117
  #
93
118
  # @yield the block to execute.
94
119
  #
95
120
  # @return [ Object ] the result of the block.
96
- def execute_with_span(span, operation)
121
+ def execute_with_span(span, operation, recording)
97
122
  ::OpenTelemetry::Trace.with_span(span) do |s, c|
98
123
  yield.tap do |result|
99
- process_cursor_context(result, operation.cursor_id, c, s)
124
+ process_cursor_context(result, operation.cursor_id, c, s, recording)
100
125
  end
101
126
  end
102
127
  end
@@ -106,8 +131,6 @@ module Mongo
106
131
  # @param span [ OpenTelemetry::Trace::Span ] the span.
107
132
  # @param exception [ Exception ] the exception.
108
133
  def handle_span_exception(span, exception)
109
- return unless span
110
-
111
134
  span.record_exception(exception)
112
135
  span.status = ::OpenTelemetry::Trace::Status.error(
113
136
  "Unhandled exception of type: #{exception.class}"
@@ -115,16 +138,28 @@ module Mongo
115
138
  end
116
139
 
117
140
  # Returns the operation name from the provided name or operation class.
141
+ # The name derived from the class is cached per class. There are few
142
+ # operation classes, so the cache needs no bound. It is guarded by a
143
+ # mutex because not every Ruby runtime makes Hash writes thread-safe.
118
144
  #
119
145
  # @param operation [ Mongo::Operation ] the operation.
120
146
  # @param op_name [ String | nil ] optional operation name.
121
147
  #
122
148
  # @return [ String ] the operation name in lowercase.
123
149
  def operation_name(operation, op_name = nil)
124
- op_name || operation.class.name.split('::').last.downcase
150
+ return op_name if op_name
151
+
152
+ klass = operation.class
153
+ @operation_names_mutex.synchronize do
154
+ @operation_names[klass] ||= klass.name.split('::').last.downcase
155
+ end
125
156
  end
126
157
 
127
- # Builds span attributes for the operation.
158
+ # Builds the attributes passed at span creation. The cursor id is set
159
+ # by trace_with_span once the span is known to record.
160
+ # db.operation.summary is outside the spec's recommended creation set,
161
+ # but it is the span name, which is built anyway, and an attribute
162
+ # passed at creation is cheaper than one set later.
128
163
  #
129
164
  # @param operation [ Mongo::Operation ] the operation.
130
165
  # @param op_name [ String | nil ] optional operation name.
@@ -136,8 +171,7 @@ module Mongo
136
171
  'db.namespace' => operation.db_name.to_s,
137
172
  'db.collection.name' => collection_name(operation),
138
173
  'db.operation.name' => operation_name(operation, op_name),
139
- 'db.operation.summary' => operation_span_name(operation, op_name),
140
- 'db.mongodb.cursor_id' => operation.cursor_id,
174
+ 'db.operation.summary' => operation_span_name(operation, op_name)
141
175
  }.compact
142
176
  end
143
177
 
@@ -150,7 +184,8 @@ module Mongo
150
184
  # @param cursor_id [ Integer | nil ] the cursor ID before the operation.
151
185
  # @param context [ OpenTelemetry::Context ] the OpenTelemetry context.
152
186
  # @param span [ OpenTelemetry::Trace::Span ] the current span.
153
- def process_cursor_context(result, cursor_id, context, span)
187
+ # @param recording [ true | false ] whether the span is recording.
188
+ def process_cursor_context(result, cursor_id, context, span, recording)
154
189
  return unless result.is_a?(Cursor)
155
190
 
156
191
  if result.id.zero?
@@ -159,7 +194,7 @@ module Mongo
159
194
  elsif result.id && cursor_id.nil?
160
195
  # New cursor created, store its context.
161
196
  cursor_context_map[result.id] = context
162
- span.set_attribute('db.mongodb.cursor_id', result.id)
197
+ span.set_attribute('db.mongodb.cursor_id', result.id) if recording
163
198
  end
164
199
  end
165
200
 
@@ -200,6 +200,9 @@ module Mongo
200
200
  # Generates a unique key for transaction tracking.
201
201
  #
202
202
  # Returns nil for implicit sessions or sessions not in a transaction.
203
+ # Every operation in a transaction needs the key, so it is cached on
204
+ # the session together with the transaction number it was built for.
205
+ # Two threads may build it at the same time; they build the same key.
203
206
  #
204
207
  # @param session [ Mongo::Session ] the session.
205
208
  #
@@ -207,7 +210,13 @@ module Mongo
207
210
  def transaction_map_key(session)
208
211
  return if session.nil? || session.implicit? || !session.in_transaction?
209
212
 
210
- "#{session.session_id['id'].to_uuid}-#{session.txn_num}"
213
+ txn_num = session.txn_num
214
+ cached = session.tracing_transaction_key
215
+ return cached[1] if cached && cached[0] == txn_num
216
+
217
+ key = "#{session.session_id['id'].to_uuid}-#{txn_num}"
218
+ session.tracing_transaction_key = [ txn_num, key ].freeze
219
+ key
211
220
  end
212
221
 
213
222
  private
data/lib/mongo/version.rb CHANGED
@@ -5,5 +5,5 @@ module Mongo
5
5
  #
6
6
  # Note that this file is automatically updated via `rake candidate:create`.
7
7
  # Manual changes to this file will be overwritten by that rake task.
8
- VERSION = '2.26.0'
8
+ VERSION = '2.26.1'
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongo
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.26.0
4
+ version: 2.26.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - The MongoDB Ruby Team
@@ -561,7 +561,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
561
561
  - !ruby/object:Gem::Version
562
562
  version: '0'
563
563
  requirements: []
564
- rubygems_version: 4.0.20
564
+ rubygems_version: 4.0.23
565
565
  specification_version: 4
566
566
  summary: Ruby driver for MongoDB
567
567
  test_files: []