union_station_hooks_core 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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