mongo 2.25.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mongo/bulk_write/combineable.rb +0 -7
  3. data/lib/mongo/bulk_write/validatable.rb +0 -4
  4. data/lib/mongo/bulk_write.rb +0 -34
  5. data/lib/mongo/client.rb +35 -5
  6. data/lib/mongo/client_encryption.rb +36 -11
  7. data/lib/mongo/cluster.rb +1 -2
  8. data/lib/mongo/collection/queryable_encryption.rb +7 -6
  9. data/lib/mongo/collection/view/aggregation.rb +9 -9
  10. data/lib/mongo/collection/view/change_stream.rb +1 -5
  11. data/lib/mongo/collection/view/iterable.rb +2 -9
  12. data/lib/mongo/collection/view/map_reduce.rb +5 -4
  13. data/lib/mongo/collection/view/readable.rb +3 -3
  14. data/lib/mongo/collection/view/writable.rb +0 -35
  15. data/lib/mongo/crypt/auto_encrypter.rb +5 -1
  16. data/lib/mongo/crypt/binding.rb +66 -1
  17. data/lib/mongo/crypt/explicit_encrypter.rb +27 -12
  18. data/lib/mongo/crypt/explicit_encryption_context.rb +51 -12
  19. data/lib/mongo/crypt/handle.rb +18 -0
  20. data/lib/mongo/cursor.rb +13 -0
  21. data/lib/mongo/database/view.rb +7 -6
  22. data/lib/mongo/database.rb +7 -10
  23. data/lib/mongo/error/unsupported_option.rb +0 -40
  24. data/lib/mongo/grid/fs_bucket.rb +2 -2
  25. data/lib/mongo/grid/stream/read.rb +2 -2
  26. data/lib/mongo/grid/stream/write.rb +1 -1
  27. data/lib/mongo/index/view.rb +3 -2
  28. data/lib/mongo/monitoring/event/secure.rb +6 -1
  29. data/lib/mongo/operation/map_reduce/result.rb +6 -2
  30. data/lib/mongo/operation/result.rb +9 -0
  31. data/lib/mongo/operation/shared/executable.rb +1 -6
  32. data/lib/mongo/operation/shared/result/aggregatable.rb +3 -3
  33. data/lib/mongo/protocol/message.rb +0 -4
  34. data/lib/mongo/protocol/msg.rb +1 -12
  35. data/lib/mongo/protocol/serializers.rb +1 -9
  36. data/lib/mongo/retryable/backpressure.rb +17 -2
  37. data/lib/mongo/retryable/read_worker.rb +1 -1
  38. data/lib/mongo/retryable/retry_policy.rb +2 -2
  39. data/lib/mongo/retryable/write_worker.rb +2 -2
  40. data/lib/mongo/retryable.rb +1 -1
  41. data/lib/mongo/server/app_metadata.rb +1 -2
  42. data/lib/mongo/server/connection.rb +7 -0
  43. data/lib/mongo/server/connection_pool.rb +60 -9
  44. data/lib/mongo/server/description/features.rb +2 -3
  45. data/lib/mongo/server/description.rb +0 -1
  46. data/lib/mongo/server/pending_connection.rb +0 -5
  47. data/lib/mongo/server.rb +1 -2
  48. data/lib/mongo/session.rb +10 -3
  49. data/lib/mongo/tracing/open_telemetry/command_tracer.rb +141 -102
  50. data/lib/mongo/tracing/open_telemetry/operation_tracer.rb +52 -17
  51. data/lib/mongo/tracing/open_telemetry/tracer.rb +10 -1
  52. data/lib/mongo/uri/options_mapper.rb +15 -4
  53. data/lib/mongo/version.rb +1 -1
  54. metadata +2 -2
@@ -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,
@@ -24,7 +24,6 @@ module Mongo
24
24
  # List of features and the wire protocol version they appear in.
25
25
  #
26
26
  # Wire protocol versions map to server releases as follows:
27
- # - 8 => 4.2
28
27
  # - 9 => 4.4
29
28
  # - 13 => 5.0
30
29
  # - 14 => 5.1
@@ -61,7 +60,7 @@ module Mongo
61
60
  # The wire protocol versions that this version of the driver supports.
62
61
  #
63
62
  # @since 2.0.0
64
- DRIVER_WIRE_VERSIONS = 8..29
63
+ DRIVER_WIRE_VERSIONS = 9..29
65
64
 
66
65
  # The wire protocol versions that are deprecated in this version of the
67
66
  # driver. Support for these versions will be removed in the future.
@@ -74,7 +73,7 @@ module Mongo
74
73
  #
75
74
  # If there are no currently-deprecated wire versions, this should be
76
75
  # set to an empty range (e.g. the EMPTY_RANGE constant).
77
- DEPRECATED_WIRE_VERSIONS = 8..8
76
+ DEPRECATED_WIRE_VERSIONS = EMPTY_RANGE
78
77
 
79
78
  # make sure the deprecated versions are valid
80
79
  if DEPRECATED_WIRE_VERSIONS.min && (DRIVER_WIRE_VERSIONS.min > DEPRECATED_WIRE_VERSIONS.max)
@@ -905,7 +905,6 @@ module Mongo
905
905
  '5.1' => 14,
906
906
  '5.0' => 12,
907
907
  '4.4' => 9,
908
- '4.2' => 8,
909
908
  }.freeze
910
909
 
911
910
  # @api private
@@ -52,11 +52,6 @@ module Mongo
52
52
  # SCRAM-SHA-1. The use of SCRAM-SHA-256 by default is thus
53
53
  # sensible, and it is also mandated by the speculative auth spec.
54
54
  #
55
- # If no mechanism was specified and we are talking to a 4.2
56
- # server, we'll send speculative auth document, the server will
57
- # ignore it and we'll perform authentication using explicit
58
- # command.
59
- #
60
55
  # If no mechanism was specified and we are talking to a 4.4+
61
56
  # server and the user account doesn't allow SCRAM-SHA-256, we will
62
57
  # authenticate in a separate command with SCRAM-SHA-1 after
data/lib/mongo/server.rb CHANGED
@@ -567,8 +567,7 @@ module Mongo
567
567
  # @option options [ true | false ] :keep_connection_pool Usually when the
568
568
  # new server description is unknown, the connection pool on the
569
569
  # respective server is cleared. Set this option to true to keep the
570
- # existing connection pool (required when handling not master errors
571
- # on 4.2+ servers).
570
+ # existing connection pool (required when handling not master errors).
572
571
  # @option options [ TopologyVersion ] :topology_version Topology version
573
572
  # of the error response that is causing the server to be marked unknown.
574
573
  # @option options [ true | false ] :stop_push_monitor Set when the server
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
  #
@@ -471,7 +478,7 @@ module Mongo
471
478
  loop do
472
479
  if transaction_attempt > 0
473
480
  if overload_encountered
474
- delay = @client.retry_policy.backoff_delay(overload_error_count)
481
+ delay = @client.retry_policy.backoff_delay(overload_error_count, err: last_error)
475
482
  if backoff_would_exceed_deadline?(deadline, delay)
476
483
  make_timeout_error_from(last_error, 'CSOT timeout expired waiting to retry withTransaction')
477
484
  end
@@ -562,7 +569,7 @@ module Mongo
562
569
  end
563
570
 
564
571
  if overload_encountered
565
- delay = @client.retry_policy.backoff_delay(overload_error_count)
572
+ delay = @client.retry_policy.backoff_delay(overload_error_count, err: e)
566
573
  if backoff_would_exceed_deadline?(deadline, delay)
567
574
  transaction_in_progress = false
568
575
  make_timeout_error_from(e, 'CSOT timeout expired during withTransaction commit')
@@ -1429,7 +1436,7 @@ module Mongo
1429
1436
  private_constant :BACKOFF_INITIAL, :BACKOFF_MAX
1430
1437
 
1431
1438
  def backoff_seconds_for_retry(transaction_attempt)
1432
- exponential = BACKOFF_INITIAL * (1.5**(transaction_attempt - 1))
1439
+ exponential = BACKOFF_INITIAL * (1.5**transaction_attempt)
1433
1440
  Random.rand * [ exponential, BACKOFF_MAX ].min
1434
1441
  end
1435
1442
 
@@ -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