dalli 5.0.0 → 5.0.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 +4 -4
- data/CHANGELOG.md +54 -0
- data/Gemfile +1 -0
- data/README.md +1 -1
- data/lib/dalli/instrumentation.rb +2 -0
- data/lib/dalli/pid_cache.rb +1 -1
- data/lib/dalli/pipelined_getter.rb +9 -14
- data/lib/dalli/protocol/base.rb +16 -5
- data/lib/dalli/protocol/connection_manager.rb +2 -2
- data/lib/dalli/protocol/response_buffer.rb +27 -12
- data/lib/dalli/protocol/response_processor.rb +10 -22
- data/lib/dalli/socket.rb +4 -0
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +2 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 02d0fa949b7a065f86fb3ac7b511a3feacc50b700e82dcb385e0e79c56583a9d
|
|
4
|
+
data.tar.gz: d72b9e4b014ae1ae0b0d5a1ebe09ea48cdf9fbf0a2cda000efc19f17d5d34368
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf26484aa345df2d43a78da570027b68a7fd0dd70d7a62275ebe95a5e29ed36c11a8b1385ff0c71818f55184245e085cd75962510f9f8559aad10b0d284f8d71
|
|
7
|
+
data.tar.gz: 0b3913a33f61873d6e3da0d62ec643dbf49f679740d3469b7cb826083f02f3b32d37e2a20548634d09cecdc4389da7c5bdc43af530bb48956cb4084f91f10d9e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
Dalli Changelog
|
|
2
2
|
=====================
|
|
3
3
|
|
|
4
|
+
5.0.1
|
|
5
|
+
==========
|
|
6
|
+
|
|
7
|
+
Performance:
|
|
8
|
+
|
|
9
|
+
- Reduce object allocations in pipelined get response processing (#1072, #1078)
|
|
10
|
+
- Offset-based `ResponseBuffer`: track a read offset instead of slicing a new string after every parsed response; compact only when the consumed portion exceeds 4KB and more than half the buffer
|
|
11
|
+
- Inline response processor parsing: avoid intermediate array allocations from `split`-based header parsing
|
|
12
|
+
- Block-based `pipeline_next_responses`: yield `(key, value, cas)` directly when a block is given, avoiding per-call Hash allocation
|
|
13
|
+
- `PipelinedGetter`: replace Hash-based socket-to-server mapping with linear scan (faster for typical 1-5 server counts); use `Process.clock_gettime(CLOCK_MONOTONIC)` instead of `Time.now`
|
|
14
|
+
- Add cross-version benchmark script (`bin/compare_versions`) for reproducible performance comparisons across Dalli versions
|
|
15
|
+
|
|
16
|
+
Bug Fixes:
|
|
17
|
+
|
|
18
|
+
- Rescue `IOError` in connection manager `write`/`flush` methods (#1075)
|
|
19
|
+
- Prevents unhandled exceptions when a connection is closed mid-operation
|
|
20
|
+
- Thanks to Graham Cooper (Shopify) for this fix
|
|
21
|
+
|
|
22
|
+
Development:
|
|
23
|
+
|
|
24
|
+
- Add `rubocop-thread_safety` for detecting thread-safety issues (#1076)
|
|
25
|
+
- Add CONTRIBUTING.md with AI contribution policy (#1074)
|
|
26
|
+
|
|
4
27
|
5.0.0
|
|
5
28
|
==========
|
|
6
29
|
|
|
@@ -44,6 +67,37 @@ Internal:
|
|
|
44
67
|
- Removed deprecated binary protocol files and SASL authentication code
|
|
45
68
|
- Removed `require 'set'` (autoloaded in Ruby 3.3+)
|
|
46
69
|
|
|
70
|
+
4.3.3
|
|
71
|
+
==========
|
|
72
|
+
|
|
73
|
+
Performance:
|
|
74
|
+
|
|
75
|
+
- Reduce object allocations in pipelined get response processing (#1072)
|
|
76
|
+
- Offset-based `ResponseBuffer`: track a read offset instead of slicing a new string after every parsed response; compact only when the consumed portion exceeds 4KB and more than half the buffer
|
|
77
|
+
- Inline response processor parsing: avoid intermediate array allocations from `split`-based header parsing in both binary and meta protocols
|
|
78
|
+
- Block-based `pipeline_next_responses`: yield `(key, value, cas)` directly when a block is given, avoiding per-call Hash allocation
|
|
79
|
+
- `PipelinedGetter`: replace Hash-based socket-to-server mapping with linear scan (faster for typical 1-5 server counts); use `Process.clock_gettime(CLOCK_MONOTONIC)` instead of `Time.now`
|
|
80
|
+
- Add cross-version benchmark script (`bin/compare_versions`) for reproducible performance comparisons across Dalli versions
|
|
81
|
+
|
|
82
|
+
Bug Fixes:
|
|
83
|
+
|
|
84
|
+
- Skip OTel integration tests when meta protocol is unavailable (#1072)
|
|
85
|
+
|
|
86
|
+
4.3.2
|
|
87
|
+
==========
|
|
88
|
+
|
|
89
|
+
OpenTelemetry:
|
|
90
|
+
|
|
91
|
+
- Migrate to stable OTel semantic conventions
|
|
92
|
+
- `db.system` renamed to `db.system.name`
|
|
93
|
+
- `db.operation` renamed to `db.operation.name`
|
|
94
|
+
- `server.address` now contains hostname only; `server.port` is a separate integer attribute
|
|
95
|
+
- `get_with_metadata` and `fetch_with_lock` now include `server.address`/`server.port`
|
|
96
|
+
- Add `db.query.text` span attribute with configurable modes
|
|
97
|
+
- `:otel_db_statement` option: `:include`, `:obfuscate`, or `nil` (default: omitted)
|
|
98
|
+
- Add `peer.service` span attribute
|
|
99
|
+
- `:otel_peer_service` option for logical service naming
|
|
100
|
+
|
|
47
101
|
4.3.1
|
|
48
102
|
==========
|
|
49
103
|
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -113,7 +113,7 @@ To install this gem onto your local machine, run `bundle exec rake install`.
|
|
|
113
113
|
|
|
114
114
|
## Contributing
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to contribute, including our policy on AI-authored contributions.
|
|
117
117
|
|
|
118
118
|
## Appreciation
|
|
119
119
|
|
|
@@ -61,11 +61,13 @@ module Dalli
|
|
|
61
61
|
# Uses the library name 'dalli' and current Dalli::VERSION.
|
|
62
62
|
#
|
|
63
63
|
# @return [OpenTelemetry::Trace::Tracer, nil] the tracer or nil if OTel unavailable
|
|
64
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
64
65
|
def tracer
|
|
65
66
|
return @tracer if defined?(@tracer)
|
|
66
67
|
|
|
67
68
|
@tracer = (OpenTelemetry.tracer_provider.tracer('dalli', Dalli::VERSION) if defined?(OpenTelemetry))
|
|
68
69
|
end
|
|
70
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
69
71
|
|
|
70
72
|
# Returns true if instrumentation is enabled (OpenTelemetry SDK is available).
|
|
71
73
|
#
|
data/lib/dalli/pid_cache.rb
CHANGED
|
@@ -27,7 +27,7 @@ module Dalli
|
|
|
27
27
|
# Stores partial results collected during interleaved send phase
|
|
28
28
|
@partial_results = {}
|
|
29
29
|
servers = setup_requests(keys)
|
|
30
|
-
start_time =
|
|
30
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
31
31
|
|
|
32
32
|
# First yield any partial results collected during interleaved send
|
|
33
33
|
yield_partial_results(&block)
|
|
@@ -147,7 +147,7 @@ module Dalli
|
|
|
147
147
|
end
|
|
148
148
|
|
|
149
149
|
def remaining_time(start, timeout)
|
|
150
|
-
elapsed =
|
|
150
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
151
151
|
return 0 if elapsed > timeout
|
|
152
152
|
|
|
153
153
|
timeout - elapsed
|
|
@@ -166,8 +166,8 @@ module Dalli
|
|
|
166
166
|
# Processes responses from a server. Returns true if there are no
|
|
167
167
|
# additional responses from this server.
|
|
168
168
|
def process_server(server)
|
|
169
|
-
server.pipeline_next_responses
|
|
170
|
-
yield @key_manager.key_without_namespace(key),
|
|
169
|
+
server.pipeline_next_responses do |key, value, cas|
|
|
170
|
+
yield @key_manager.key_without_namespace(key), [value, cas]
|
|
171
171
|
end
|
|
172
172
|
|
|
173
173
|
server.pipeline_complete?
|
|
@@ -176,18 +176,13 @@ module Dalli
|
|
|
176
176
|
def servers_with_response(servers, timeout)
|
|
177
177
|
return [] if servers.empty?
|
|
178
178
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
# My suspicion is that we may want to try and push this down into the
|
|
182
|
-
# individual servers, but I'm not sure. For now, we keep the
|
|
183
|
-
# mapping between the alerted object (the socket) and the
|
|
184
|
-
# corrresponding server here.
|
|
185
|
-
server_map = servers.each_with_object({}) { |s, h| h[s.sock] = s }
|
|
186
|
-
|
|
187
|
-
readable, = IO.select(server_map.keys, nil, nil, timeout)
|
|
179
|
+
sockets = servers.map(&:sock)
|
|
180
|
+
readable, = IO.select(sockets, nil, nil, timeout)
|
|
188
181
|
return [] if readable.nil?
|
|
189
182
|
|
|
190
|
-
|
|
183
|
+
# For typical server counts (1-5), linear scan is faster than
|
|
184
|
+
# building and looking up a hash map
|
|
185
|
+
readable.filter_map { |sock| servers.find { |s| s.sock == sock } }
|
|
191
186
|
end
|
|
192
187
|
|
|
193
188
|
def groups_for_keys(*keys)
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -92,10 +92,13 @@ module Dalli
|
|
|
92
92
|
# repeatedly whenever this server's socket is readable until
|
|
93
93
|
# #pipeline_complete?.
|
|
94
94
|
#
|
|
95
|
-
#
|
|
96
|
-
|
|
95
|
+
# When a block is given, yields (key, value, cas) for each response,
|
|
96
|
+
# avoiding intermediate Hash allocation. Returns nil.
|
|
97
|
+
# Without a block, returns a Hash of { key => [value, cas] }.
|
|
98
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
99
|
+
def pipeline_next_responses(&block)
|
|
97
100
|
reconnect_on_pipeline_complete!
|
|
98
|
-
values =
|
|
101
|
+
values = nil
|
|
99
102
|
|
|
100
103
|
response_buffer.read
|
|
101
104
|
|
|
@@ -109,16 +112,24 @@ module Dalli
|
|
|
109
112
|
|
|
110
113
|
# If the status is ok and the key is not nil, then this is a
|
|
111
114
|
# getkq response with a value that we want to set in the response hash
|
|
112
|
-
|
|
115
|
+
unless key.nil?
|
|
116
|
+
if block
|
|
117
|
+
yield key, value, cas
|
|
118
|
+
else
|
|
119
|
+
values ||= {}
|
|
120
|
+
values[key] = [value, cas]
|
|
121
|
+
end
|
|
122
|
+
end
|
|
113
123
|
|
|
114
124
|
# Get the next response from the buffer
|
|
115
125
|
status, cas, key, value = response_buffer.process_single_getk_response
|
|
116
126
|
end
|
|
117
127
|
|
|
118
|
-
values
|
|
128
|
+
values || {}
|
|
119
129
|
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
120
130
|
@connection_manager.error_on_request!(e)
|
|
121
131
|
end
|
|
132
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
122
133
|
|
|
123
134
|
# Abort current pipelined get. Generally used to signal an external
|
|
124
135
|
# timeout during pipelined get. The underlying socket is
|
|
@@ -169,13 +169,13 @@ module Dalli
|
|
|
169
169
|
|
|
170
170
|
def write(bytes)
|
|
171
171
|
@sock.write(bytes)
|
|
172
|
-
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS => e
|
|
172
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, IOError => e
|
|
173
173
|
error_on_request!(e)
|
|
174
174
|
end
|
|
175
175
|
|
|
176
176
|
def flush
|
|
177
177
|
@sock.flush
|
|
178
|
-
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS => e
|
|
178
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, IOError => e
|
|
179
179
|
error_on_request!(e)
|
|
180
180
|
end
|
|
181
181
|
|
|
@@ -7,38 +7,39 @@ module Dalli
|
|
|
7
7
|
module Protocol
|
|
8
8
|
##
|
|
9
9
|
# Manages the buffer for responses from memcached.
|
|
10
|
+
# Uses an offset-based approach to avoid string allocations
|
|
11
|
+
# when advancing through parsed responses.
|
|
10
12
|
##
|
|
11
13
|
class ResponseBuffer
|
|
14
|
+
# Compact the buffer when the consumed portion exceeds this
|
|
15
|
+
# threshold and represents more than half the buffer
|
|
16
|
+
COMPACT_THRESHOLD = 4096
|
|
17
|
+
|
|
12
18
|
def initialize(io_source, response_processor)
|
|
13
19
|
@io_source = io_source
|
|
14
20
|
@response_processor = response_processor
|
|
15
21
|
@buffer = nil
|
|
22
|
+
@offset = 0
|
|
16
23
|
end
|
|
17
24
|
|
|
18
25
|
def read
|
|
19
26
|
@buffer << @io_source.read_nonblock
|
|
20
27
|
end
|
|
21
28
|
|
|
22
|
-
# Attempts to process a single response from the buffer
|
|
23
|
-
#
|
|
29
|
+
# Attempts to process a single response from the buffer,
|
|
30
|
+
# advancing the offset past the consumed bytes.
|
|
24
31
|
def process_single_getk_response
|
|
25
|
-
bytes, status, cas, key, value = @response_processor.getk_response_from_buffer(@buffer)
|
|
26
|
-
|
|
32
|
+
bytes, status, cas, key, value = @response_processor.getk_response_from_buffer(@buffer, @offset)
|
|
33
|
+
@offset += bytes
|
|
34
|
+
compact_if_needed
|
|
27
35
|
[status, cas, key, value]
|
|
28
36
|
end
|
|
29
37
|
|
|
30
|
-
# Advances the internal response buffer by bytes_to_advance
|
|
31
|
-
# bytes. The
|
|
32
|
-
def advance(bytes_to_advance)
|
|
33
|
-
return unless bytes_to_advance.positive?
|
|
34
|
-
|
|
35
|
-
@buffer = @buffer.byteslice(bytes_to_advance..-1)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
38
|
# Resets the internal buffer to an empty state,
|
|
39
39
|
# so that we're ready to read pipelined responses
|
|
40
40
|
def reset
|
|
41
41
|
@buffer = ''.b
|
|
42
|
+
@offset = 0
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
# Ensures the buffer is initialized for reading without discarding
|
|
@@ -48,16 +49,30 @@ module Dalli
|
|
|
48
49
|
return if in_progress?
|
|
49
50
|
|
|
50
51
|
@buffer = ''.b
|
|
52
|
+
@offset = 0
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
# Clear the internal response buffer
|
|
54
56
|
def clear
|
|
55
57
|
@buffer = nil
|
|
58
|
+
@offset = 0
|
|
56
59
|
end
|
|
57
60
|
|
|
58
61
|
def in_progress?
|
|
59
62
|
!@buffer.nil?
|
|
60
63
|
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Only compact when we've consumed a significant portion of the buffer.
|
|
68
|
+
# This avoids per-response string allocation while preventing unbounded
|
|
69
|
+
# memory growth for large pipelines.
|
|
70
|
+
def compact_if_needed
|
|
71
|
+
return unless @offset > COMPACT_THRESHOLD && @offset > @buffer.bytesize / 2
|
|
72
|
+
|
|
73
|
+
@buffer = @buffer.byteslice(@offset..)
|
|
74
|
+
@offset = 0
|
|
75
|
+
end
|
|
61
76
|
end
|
|
62
77
|
end
|
|
63
78
|
end
|
|
@@ -147,14 +147,6 @@ module Dalli
|
|
|
147
147
|
true
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
-
def tokens_from_header_buffer(buf)
|
|
151
|
-
header = header_from_buffer(buf)
|
|
152
|
-
tokens = header.split
|
|
153
|
-
header_len = header.bytesize + TERMINATOR.length
|
|
154
|
-
body_len = body_len_from_tokens(tokens)
|
|
155
|
-
[tokens, header_len, body_len]
|
|
156
|
-
end
|
|
157
|
-
|
|
158
150
|
def full_response_from_buffer(tokens, body, resp_size)
|
|
159
151
|
value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
|
|
160
152
|
[resp_size, tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value]
|
|
@@ -170,11 +162,15 @@ module Dalli
|
|
|
170
162
|
# The remaining three values in the array are the ResponseHeader,
|
|
171
163
|
# key, and value.
|
|
172
164
|
##
|
|
173
|
-
def getk_response_from_buffer(buf)
|
|
174
|
-
#
|
|
175
|
-
|
|
165
|
+
def getk_response_from_buffer(buf, offset = 0)
|
|
166
|
+
# Find the header terminator starting from offset
|
|
167
|
+
term_idx = buf.index(TERMINATOR, offset)
|
|
168
|
+
return [0, nil, nil, nil, nil] unless term_idx
|
|
176
169
|
|
|
177
|
-
|
|
170
|
+
header = buf.byteslice(offset, term_idx - offset)
|
|
171
|
+
tokens = header.split
|
|
172
|
+
header_len = header.bytesize + TERMINATOR.length
|
|
173
|
+
body_len = body_len_from_tokens(tokens)
|
|
178
174
|
|
|
179
175
|
# We have a complete response that has no body.
|
|
180
176
|
# This is either the response to the terminating
|
|
@@ -185,22 +181,14 @@ module Dalli
|
|
|
185
181
|
resp_size = header_len + body_len + TERMINATOR.length
|
|
186
182
|
# The header is in the buffer, but the body is not. As we don't have
|
|
187
183
|
# a complete response, don't advance the buffer
|
|
188
|
-
return [0, nil, nil, nil, nil] unless buf.bytesize >= resp_size
|
|
184
|
+
return [0, nil, nil, nil, nil] unless buf.bytesize >= offset + resp_size
|
|
189
185
|
|
|
190
186
|
# The full response is in our buffer, so parse it and return
|
|
191
187
|
# the values
|
|
192
|
-
body = buf.
|
|
188
|
+
body = buf.byteslice(offset + header_len, body_len)
|
|
193
189
|
full_response_from_buffer(tokens, body, resp_size)
|
|
194
190
|
end
|
|
195
191
|
|
|
196
|
-
def contains_header?(buf)
|
|
197
|
-
buf.include?(TERMINATOR)
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
def header_from_buffer(buf)
|
|
201
|
-
buf.split(TERMINATOR, 2).first
|
|
202
|
-
end
|
|
203
|
-
|
|
204
192
|
def error_on_unexpected!(expected_codes)
|
|
205
193
|
tokens = next_line_to_tokens
|
|
206
194
|
|
data/lib/dalli/socket.rb
CHANGED
|
@@ -108,12 +108,14 @@ module Dalli
|
|
|
108
108
|
# Detect and cache whether TCPSocket supports the connect_timeout: keyword argument.
|
|
109
109
|
# Returns false if TCPSocket#initialize has been monkey-patched by gems like
|
|
110
110
|
# socksify or resolv-replace, which don't support keyword arguments.
|
|
111
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
111
112
|
def self.supports_connect_timeout?
|
|
112
113
|
return @supports_connect_timeout if defined?(@supports_connect_timeout)
|
|
113
114
|
|
|
114
115
|
@supports_connect_timeout = RUBY_VERSION >= '3.0' &&
|
|
115
116
|
::TCPSocket.instance_method(:initialize).parameters == TCPSOCKET_NATIVE_PARAMETERS
|
|
116
117
|
end
|
|
118
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
117
119
|
|
|
118
120
|
def self.create_socket_with_timeout(host, port, options)
|
|
119
121
|
if supports_connect_timeout?
|
|
@@ -170,12 +172,14 @@ module Dalli
|
|
|
170
172
|
|
|
171
173
|
# Detect and cache the correct pack format for struct timeval on this platform.
|
|
172
174
|
# Different architectures have different sizes for time_t and suseconds_t.
|
|
175
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
173
176
|
def self.timeval_pack_format(sock)
|
|
174
177
|
@timeval_pack_format ||= begin
|
|
175
178
|
expected_size = sock.getsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO).data.bytesize
|
|
176
179
|
TIMEVAL_PACK_FORMATS.find { |fmt| TIMEVAL_TEST_VALUES.pack(fmt).bytesize == expected_size } || 'll'
|
|
177
180
|
end
|
|
178
181
|
end
|
|
182
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
179
183
|
|
|
180
184
|
def self.pack_timeval(sock, seconds, microseconds)
|
|
181
185
|
[seconds, microseconds].pack(timeval_pack_format(sock))
|
data/lib/dalli/version.rb
CHANGED
data/lib/dalli.rb
CHANGED
|
@@ -38,7 +38,7 @@ module Dalli
|
|
|
38
38
|
QUIET = :dalli_multi
|
|
39
39
|
|
|
40
40
|
def self.logger
|
|
41
|
-
@logger ||= rails_logger || default_logger
|
|
41
|
+
@logger ||= rails_logger || default_logger # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def self.rails_logger
|
|
@@ -54,7 +54,7 @@ module Dalli
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def self.logger=(logger)
|
|
57
|
-
@logger = logger
|
|
57
|
+
@logger = logger # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
60
|
|