union_station_hooks_core 1.0.0

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.
@@ -0,0 +1,62 @@
1
+ # Union Station - https://www.unionstationapp.com/
2
+ # Copyright (c) 2010-2015 Phusion Holding B.V.
3
+ #
4
+ # "Union Station" and "Passenger" are trademarks of Phusion Holding B.V.
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+
24
+
25
+ module UnionStationHooks
26
+ # A wrapper around a mutex, for use within Connection.
27
+ #
28
+ # @private
29
+ class Lock
30
+ def initialize(mutex)
31
+ @mutex = mutex
32
+ @locked = false
33
+ end
34
+
35
+ def reset(mutex, lock_now = true)
36
+ unlock if @locked
37
+ @mutex = mutex
38
+ lock if lock_now
39
+ end
40
+
41
+ def synchronize
42
+ lock if !@locked
43
+ begin
44
+ yield(self)
45
+ ensure
46
+ unlock if @locked
47
+ end
48
+ end
49
+
50
+ def lock
51
+ raise if @locked
52
+ @mutex.lock
53
+ @locked = true
54
+ end
55
+
56
+ def unlock
57
+ raise if !@locked
58
+ @mutex.unlock
59
+ @locked = false
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,66 @@
1
+ # Union Station - https://www.unionstationapp.com/
2
+ # Copyright (c) 2010-2015 Phusion Holding B.V.
3
+ #
4
+ # "Union Station" and "Passenger" are trademarks of Phusion Holding B.V.
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+
24
+
25
+ module UnionStationHooks
26
+ # Provides methods for `union_station_*` gems to log internal warnings and
27
+ # debugging messages. This module is *not* to be used by application
28
+ # developers for the purpose of logging information to Union Station.
29
+ #
30
+ # @private
31
+ module Log
32
+ @@debugging = false
33
+ @@warn_callback = nil
34
+ @@debug_callback = nil
35
+
36
+ def self.debugging=(value)
37
+ @@debugging = value
38
+ end
39
+
40
+ def self.warn(message)
41
+ if @@warn_callback
42
+ @@warn_callback.call(message)
43
+ else
44
+ STDERR.puts("[UnionStationHooks] #{message}")
45
+ end
46
+ end
47
+
48
+ def self.debug(message)
49
+ if @@debugging
50
+ if @@debug_callback
51
+ @@debug_callback.call(message)
52
+ else
53
+ STDERR.puts("[UnionStationHooks] #{message}")
54
+ end
55
+ end
56
+ end
57
+
58
+ def self.warn_callback=(cb)
59
+ @@warn_callback = cb
60
+ end
61
+
62
+ def self.debug_callback=(cb)
63
+ @@debug_callback = cb
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,157 @@
1
+ # encoding: binary
2
+ # Union Station - https://www.unionstationapp.com/
3
+ # Copyright (c) 2010-2015 Phusion Holding B.V.
4
+ #
5
+ # "Union Station" and "Passenger" are trademarks of Phusion Holding B.V.
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"), to deal
9
+ # in the Software without restriction, including without limitation the rights
10
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ # copies of the Software, and to permit persons to whom the Software is
12
+ # furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included in
15
+ # all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ # THE SOFTWARE.
24
+
25
+
26
+ module UnionStationHooks
27
+ # This class allows reading and writing structured messages over
28
+ # I/O channels. This is the Ruby implementation of Passenger's
29
+ # src/cxx_supportlib/Utils/MessageIO.h; see that file for more information.
30
+ #
31
+ # @private
32
+ class MessageChannel
33
+ HEADER_SIZE = 2
34
+ DELIMITER = "\0"
35
+ DELIMITER_NAME = 'null byte'
36
+ UINT16_PACK_FORMAT = 'n'
37
+ UINT32_PACK_FORMAT = 'N'
38
+
39
+ class InvalidHashError < StandardError
40
+ end
41
+
42
+ # The wrapped IO object.
43
+ attr_accessor :io
44
+
45
+ # Create a new MessageChannel by wrapping the given IO object.
46
+ def initialize(io = nil)
47
+ @io = io
48
+ # Make it binary just in case.
49
+ @io.binmode if @io
50
+ end
51
+
52
+ # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
53
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize
54
+
55
+ # Read an array message from the underlying file descriptor.
56
+ # Returns the array message as an array, or nil when end-of-stream has
57
+ # been reached.
58
+ #
59
+ # Might raise SystemCallError, IOError or SocketError when something
60
+ # goes wrong.
61
+ def read
62
+ buffer = new_buffer
63
+ if !@io.read(HEADER_SIZE, buffer)
64
+ return nil
65
+ end
66
+ while buffer.size < HEADER_SIZE
67
+ tmp = @io.read(HEADER_SIZE - buffer.size)
68
+ if tmp.empty?
69
+ return nil
70
+ else
71
+ buffer << tmp
72
+ end
73
+ end
74
+
75
+ chunk_size = buffer.unpack(UINT16_PACK_FORMAT)[0]
76
+ if !@io.read(chunk_size, buffer)
77
+ return nil
78
+ end
79
+ while buffer.size < chunk_size
80
+ tmp = @io.read(chunk_size - buffer.size)
81
+ if tmp.empty?
82
+ return nil
83
+ else
84
+ buffer << tmp
85
+ end
86
+ end
87
+
88
+ message = []
89
+ offset = 0
90
+ delimiter_pos = buffer.index(DELIMITER, offset)
91
+ while !delimiter_pos.nil?
92
+ if delimiter_pos == 0
93
+ message << ''
94
+ else
95
+ message << buffer[offset..delimiter_pos - 1]
96
+ end
97
+ offset = delimiter_pos + 1
98
+ delimiter_pos = buffer.index(DELIMITER, offset)
99
+ end
100
+ message
101
+ rescue Errno::ECONNRESET
102
+ nil
103
+ end
104
+
105
+ # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity
106
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/AbcSize
107
+
108
+ # Send an array message, which consists of the given elements, over the
109
+ # underlying file descriptor. _name_ is the first element in the message,
110
+ # and _args_ are the other elements. These arguments will internally be
111
+ # converted to strings by calling to_s().
112
+ #
113
+ # Might raise SystemCallError, IOError or SocketError when something
114
+ # goes wrong.
115
+ def write(name, *args)
116
+ check_argument(name)
117
+ args.each do |arg|
118
+ check_argument(arg)
119
+ end
120
+
121
+ message = "#{name}#{DELIMITER}"
122
+ args.each do |arg|
123
+ message << arg.to_s << DELIMITER
124
+ end
125
+ @io.write([message.size].pack('n') << message)
126
+ @io.flush
127
+ end
128
+
129
+ # Send a scalar message over the underlying IO object.
130
+ #
131
+ # Might raise SystemCallError, IOError or SocketError when something
132
+ # goes wrong.
133
+ def write_scalar(data)
134
+ @io.write([data.size].pack('N') << data)
135
+ @io.flush
136
+ end
137
+
138
+ private
139
+
140
+ def check_argument(arg)
141
+ if arg.to_s.index(DELIMITER)
142
+ raise ArgumentError,
143
+ "Message name and arguments may not contain #{DELIMITER_NAME}"
144
+ end
145
+ end
146
+
147
+ if defined?(ByteString)
148
+ def new_buffer
149
+ ByteString.new
150
+ end
151
+ else
152
+ def new_buffer
153
+ ''
154
+ end
155
+ end
156
+ end
157
+ end # module UnionStationHooks
@@ -0,0 +1,141 @@
1
+ # Union Station - https://www.unionstationapp.com/
2
+ # Copyright (c) 2010-2015 Phusion Holding B.V.
3
+ #
4
+ # "Union Station" and "Passenger" are trademarks of Phusion Holding B.V.
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+
24
+ UnionStationHooks.require_lib 'request_reporter/basics'
25
+ UnionStationHooks.require_lib 'request_reporter/controllers'
26
+ UnionStationHooks.require_lib 'request_reporter/view_rendering'
27
+ UnionStationHooks.require_lib 'request_reporter/misc'
28
+
29
+ module UnionStationHooks
30
+ # A RequestReporter object is used for logging request-specific information
31
+ # to Union Station. "Information" may include (and are not limited to):
32
+ #
33
+ # * Web framework controller and action name.
34
+ # * Exceptions raised during the request.
35
+ # * Cache hits and misses.
36
+ # * Database actions.
37
+ #
38
+ # A unique RequestReporter is created by Passenger at the beginning of every
39
+ # request (by calling {UnionStationHooks.begin_rack_request}). This object is
40
+ # closed at the end of the same request (after the Rack body object is
41
+ # closed).
42
+ #
43
+ # As an application developer, the RequestReporter is the main class
44
+ # that you will be interfacing with. See the {UnionStationHooks} module
45
+ # description for an example of how you can use RequestReporter.
46
+ #
47
+ # ## Obtaining a RequestReporter
48
+ #
49
+ # You are not supposed to create a RequestReporter object directly.
50
+ # You are supposed to obtain the RequestReporter object that Passenger creates
51
+ # for you. This is done through the `union_station_hooks` key in the Rack
52
+ # environment hash, as well as through the `:union_station_hooks` key in
53
+ # the current thread's object:
54
+ #
55
+ # env['union_station_hooks']
56
+ # # => RequestReporter object or nil
57
+ #
58
+ # Thread.current[:union_station_hooks]
59
+ # # => RequestReporter object or nil
60
+ #
61
+ # Note that Passenger may not have created such an object because of an
62
+ # error. At present, there are two error conditions that would cause a
63
+ # RequestReporter object not to be created. However, your code should take
64
+ # into account that in the future more error conditions may trigger this.
65
+ #
66
+ # 1. There is no transaction ID associated with the current request.
67
+ # When Union Station support is enabled in Passenger, Passenger always
68
+ # assigns a transaction ID. However, administrators can also
69
+ # {https://www.phusionpassenger.com/library/admin/nginx/request_individual_processes.html
70
+ # access Ruby processes directly} through process-private HTTP sockets,
71
+ # bypassing Passenger's load balancing mechanism. In that case, no
72
+ # transaction ID will be assigned.
73
+ # 2. An error occurred recently while sending data to the UstRouter, either
74
+ # because the UstRouter crashed or because of some other kind of
75
+ # communication error occurred. This error condition isn't cleared until
76
+ # certain a timeout has passed.
77
+ #
78
+ # The UstRouter is a Passenger process which runs locally and is
79
+ # responsible for aggregating Union Station log data from multiple
80
+ # processes, with the goal of sending the aggregate data over the network
81
+ # to the Union Station service.
82
+ #
83
+ # This kind of error is automatically recovered from after a certain
84
+ # period of time.
85
+ #
86
+ # ## Null mode
87
+ #
88
+ # The error condition 2 described above may also cause an existing
89
+ # RequestReporter object to enter the "null mode". When this mode is entered,
90
+ # any further actions on the RequestReporter object will become no-ops.
91
+ # You can check whether the null mode is active by calling {#null?}.
92
+ #
93
+ # Closing a RequestReporter also causes it to enter the null mode.
94
+ class RequestReporter
95
+ # Returns a new RequestReporter object. You should not call
96
+ # `RequestReporter.new` directly. See "Obtaining a RequestReporter"
97
+ # in the {RequestReporter class description}.
98
+ #
99
+ # @api private
100
+ def initialize(context, txn_id, app_group_name, key)
101
+ raise ArgumentError, 'Transaction ID must be given' if txn_id.nil?
102
+ raise ArgumentError, 'App group name must be given' if app_group_name.nil?
103
+ raise ArgumentError, 'Union Station key must be given' if key.nil?
104
+ @context = context
105
+ @txn_id = txn_id
106
+ @app_group_name = app_group_name
107
+ @key = key
108
+ @transaction = continue_transaction
109
+ end
110
+
111
+ # Indicates that no further information will be logged for this
112
+ # request.
113
+ #
114
+ # @api private
115
+ def close
116
+ @transaction.close
117
+ end
118
+
119
+ # Returns whether is this RequestReporter object is in null mode.
120
+ # See the {RequestReporter class description} for more information.
121
+ def null?
122
+ @transaction.null?
123
+ end
124
+
125
+ # Other methods are implemented in the files in the
126
+ # 'request_reporter/' subdirectory.
127
+
128
+ private
129
+
130
+ def continue_transaction
131
+ @context.continue_transaction(@txn_id, @app_group_name,
132
+ :requests, @key)
133
+ end
134
+
135
+ # Called when one of the methods return early upon detecting null
136
+ # mode. Used by tests to verify that methods return early.
137
+ def do_nothing_on_null(_source)
138
+ # Do nothing by default. Tests will stub this.
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,132 @@
1
+ # Union Station - https://www.unionstationapp.com/
2
+ # Copyright (c) 2010-2015 Phusion Holding B.V.
3
+ #
4
+ # "Union Station" and "Passenger" are trademarks of Phusion Holding B.V.
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+
24
+ require 'thread'
25
+
26
+ module UnionStationHooks
27
+ class RequestReporter
28
+ ###### Logging basic request information ######
29
+
30
+ # A mutex for synchronizing GC stats reporting. We do this because in
31
+ # multithreaded situations we don't want to interleave GC stats access with
32
+ # calls to `GC.clear_stats`. Not that GC stats are very helpful in
33
+ # multithreaded situations, but this is better than nothing.
34
+ #
35
+ # @private
36
+ GC_MUTEX = Mutex.new
37
+
38
+ # @private
39
+ OBJECT_SPACE_SUPPORTS_LIVE_OBJECTS = ObjectSpace.respond_to?(:live_objects)
40
+
41
+ # @private
42
+ OBJECT_SPACE_SUPPORTS_ALLOCATED_OBJECTS =
43
+ ObjectSpace.respond_to?(:allocated_objects)
44
+
45
+ # @private
46
+ OBJECT_SPACE_SUPPORTS_COUNT_OBJECTS =
47
+ ObjectSpace.respond_to?(:count_objects)
48
+
49
+ # @private
50
+ GC_SUPPORTS_TIME = GC.respond_to?(:time)
51
+
52
+ # @private
53
+ GC_SUPPORTS_CLEAR_STATS = GC.respond_to?(:clear_stats)
54
+
55
+ # Log the beginning of a Rack request. This is automatically called
56
+ # from {UnionStationHooks.begin_rack_request} (and thus automatically
57
+ # from Passenger).
58
+ #
59
+ # @private
60
+ def log_request_begin
61
+ return do_nothing_on_null(:log_request_begin) if null?
62
+ @transaction.log_activity_begin('app request handler processing')
63
+ end
64
+
65
+ # Log the end of a Rack request. This is automatically called
66
+ # from {UnionStationHooks.begin_rack_request} (and thus automatically
67
+ # from Passenger).
68
+ #
69
+ # @private
70
+ def log_request_end(uncaught_exception_raised_during_request = false)
71
+ return do_nothing_on_null(:log_request_end) if null?
72
+ @transaction.log_activity_end('app request handler processing',
73
+ UnionStationHooks.now, uncaught_exception_raised_during_request)
74
+ end
75
+
76
+ # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
77
+
78
+ # @private
79
+ def log_gc_stats_on_request_begin
80
+ return do_nothing_on_null(:log_gc_stats_on_request_begin) if null?
81
+
82
+ # See the docs for MUTEX on why we synchronize this.
83
+ GC_MUTEX.synchronize do
84
+ if OBJECT_SPACE_SUPPORTS_LIVE_OBJECTS
85
+ @transaction.message('Initial objects on heap: ' \
86
+ "#{ObjectSpace.live_objects}")
87
+ end
88
+ if OBJECT_SPACE_SUPPORTS_ALLOCATED_OBJECTS
89
+ @transaction.message('Initial objects allocated so far: ' \
90
+ "#{ObjectSpace.allocated_objects}")
91
+ elsif OBJECT_SPACE_SUPPORTS_COUNT_OBJECTS
92
+ count = ObjectSpace.count_objects
93
+ @transaction.message('Initial objects allocated so far: ' \
94
+ "#{count[:TOTAL] - count[:FREE]}")
95
+ end
96
+ if GC_SUPPORTS_TIME
97
+ @transaction.message("Initial GC time: #{GC.time}")
98
+ end
99
+ end
100
+ end
101
+
102
+ # @private
103
+ def log_gc_stats_on_request_end
104
+ return do_nothing_on_null(:log_gc_stats_on_request_end) if null?
105
+
106
+ # See the docs for MUTEX on why we synchronize this.
107
+ GC_MUTEX.synchronize do
108
+ if OBJECT_SPACE_SUPPORTS_LIVE_OBJECTS
109
+ @transaction.message('Final objects on heap: ' \
110
+ "#{ObjectSpace.live_objects}")
111
+ end
112
+ if OBJECT_SPACE_SUPPORTS_ALLOCATED_OBJECTS
113
+ @transaction.message('Final objects allocated so far: ' \
114
+ "#{ObjectSpace.allocated_objects}")
115
+ elsif OBJECT_SPACE_SUPPORTS_COUNT_OBJECTS
116
+ count = ObjectSpace.count_objects
117
+ @transaction.message('Final objects allocated so far: ' \
118
+ "#{count[:TOTAL] - count[:FREE]}")
119
+ end
120
+ if GC_SUPPORTS_TIME
121
+ @transaction.message("Final GC time: #{GC.time}")
122
+ end
123
+ if GC_SUPPORTS_CLEAR_STATS
124
+ # Clear statistics to void integer wraps.
125
+ GC.clear_stats
126
+ end
127
+ end
128
+ end
129
+
130
+ # rubocop:enable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
131
+ end
132
+ end