ddtrace 1.23.2 → 1.23.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 836f8f52564c86b8ee1a4681309d3d168b1689f3742dfed4d1a8171e23852ffa
4
- data.tar.gz: f0ad632313b36e6550e33f952da8e7ea89349fd8abc2122f41d649292a700028
3
+ metadata.gz: 9e52d825fdd7cb0391c1e529a979862f24f8ec0412cbd1952f0ae21f3129b83f
4
+ data.tar.gz: 27ab67ddd6bd0c21a0f7de60c022c143ae2367051b4b1a744f33f01453c1d8e5
5
5
  SHA512:
6
- metadata.gz: 1021b4efd906f5cf362e27be17249ce82fad3100b62b9a39ba2fd65d508c07ac520cb94971d6986a5171d1575a7fc8e219ed2dc4785166d0bd0ec774cd447d19
7
- data.tar.gz: 6ef6b7f391bb98e8e7f039cd72d6df56ee821df539803d3398ef7aaebe2c414eef87f4c2f0cf361ef3a066faf0490bb4b75831563a17729d11115c84243ab349
6
+ metadata.gz: b0c94e18051c3fe9522493daf7321ae77246d9e5cf6d5888fd3b9c565668c882e684d3156d58867104743c6769f07437c4bc68f69f4884d0609c92279d1ea3bb
7
+ data.tar.gz: 83bab49e211de5749555906b2f59b0ee8cb8a501c2363ea625a52e6f2c318714be26dc0226e51aabfd8461b96f3036b15a856eb372729ce69bd29df7baa06ef8
data/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.23.3] - 2024-07-01
6
+
7
+ ### Added
8
+
9
+ * Add post install message about 2.x upgrade ([#3723][])
10
+
11
+ ### Fixed
12
+
13
+ * Fix telemetry events blocking main thread ([#3740][])
14
+ * Fix deadlock from telemetry threads ([#3745][])
15
+
5
16
  ## [1.23.2] - 2024-06-13
6
17
 
7
18
  ### Fixed
@@ -2826,7 +2837,8 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1
2826
2837
  Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
2827
2838
 
2828
2839
 
2829
- [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v1.23.2...master
2840
+ [Unreleased]: https://github.com/DataDog/dd-trace-rb/compare/v1.23.3...1.x-stable
2841
+ [1.23.3]: https://github.com/DataDog/dd-trace-rb/compare/v1.23.2...v1.23.3
2830
2842
  [1.23.2]: https://github.com/DataDog/dd-trace-rb/compare/v1.23.1...v1.23.2
2831
2843
  [1.23.1]: https://github.com/DataDog/dd-trace-rb/compare/v1.23.0...v1.23.1
2832
2844
  [1.23.0]: https://github.com/DataDog/dd-trace-rb/compare/v1.22.0...v1.23.0
@@ -4142,6 +4154,7 @@ Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1
4142
4154
  [#3623]: https://github.com/DataDog/dd-trace-rb/issues/3623
4143
4155
  [#3650]: https://github.com/DataDog/dd-trace-rb/issues/3650
4144
4156
  [#3683]: https://github.com/DataDog/dd-trace-rb/issues/3683
4157
+ [#3745]: https://github.com/DataDog/dd-trace-rb/issues/3745
4145
4158
  [@AdrianLC]: https://github.com/AdrianLC
4146
4159
  [@Azure7111]: https://github.com/Azure7111
4147
4160
  [@BabyGroot]: https://github.com/BabyGroot
@@ -4,7 +4,7 @@ require_relative '../diagnostics/environment_logger'
4
4
  require_relative '../diagnostics/health'
5
5
  require_relative '../logger'
6
6
  require_relative '../runtime/metrics'
7
- require_relative '../telemetry/client'
7
+ require_relative '../telemetry/component'
8
8
  require_relative '../workers/runtime_metrics'
9
9
 
10
10
  require_relative '../remote/component'
@@ -60,7 +60,7 @@ module Datadog
60
60
  logger.debug { "Telemetry disabled. Agent network adapter not supported: #{agent_settings.adapter}" }
61
61
  end
62
62
 
63
- Telemetry::Client.new(
63
+ Telemetry::Component.new(
64
64
  enabled: enabled,
65
65
  heartbeat_interval_seconds: settings.telemetry.heartbeat_interval_seconds,
66
66
  dependency_collection: settings.telemetry.dependency_collection
@@ -165,8 +165,9 @@ module Datadog
165
165
  unused_statsd = (old_statsd - (old_statsd & new_statsd))
166
166
  unused_statsd.each(&:close)
167
167
 
168
- telemetry.stop!
168
+ # enqueue closing event before stopping telemetry so it will be send out on shutdown
169
169
  telemetry.emit_closing! unless replacement
170
+ telemetry.stop!
170
171
  end
171
172
  end
172
173
  end
@@ -81,23 +81,16 @@ module Datadog
81
81
  configuration = self.configuration
82
82
  yield(configuration)
83
83
 
84
- built_components = false
85
-
86
- components = safely_synchronize do |write_components|
84
+ safely_synchronize do |write_components|
87
85
  write_components.call(
88
86
  if components?
89
87
  replace_components!(configuration, @components)
90
88
  else
91
- components = build_components(configuration)
92
- built_components = true
93
- components
89
+ build_components(configuration)
94
90
  end
95
91
  )
96
92
  end
97
93
 
98
- # Should only be called the first time components are built
99
- components.telemetry.started! if built_components
100
-
101
94
  configuration
102
95
  end
103
96
 
@@ -197,20 +190,13 @@ module Datadog
197
190
  current_components = COMPONENTS_READ_LOCK.synchronize { defined?(@components) && @components }
198
191
  return current_components if current_components || !allow_initialization
199
192
 
200
- built_components = false
201
-
202
- components = safely_synchronize do |write_components|
193
+ safely_synchronize do |write_components|
203
194
  if defined?(@components) && @components
204
195
  @components
205
196
  else
206
- built_components = true
207
197
  write_components.call(build_components(configuration))
208
198
  end
209
199
  end
210
-
211
- # Should only be called the first time components are built
212
- components.telemetry.started! if built_components && components && components.telemetry
213
- components
214
200
  end
215
201
 
216
202
  private
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'emitter'
4
+ require_relative 'event'
5
+ require_relative 'worker'
6
+ require_relative '../utils/forking'
7
+
8
+ module Datadog
9
+ module Core
10
+ module Telemetry
11
+ # Telemetry entrypoint, coordinates sending telemetry events at various points in app lifecycle.
12
+ class Component
13
+ attr_reader :enabled
14
+
15
+ include Core::Utils::Forking
16
+
17
+ # @param enabled [Boolean] Determines whether telemetry events should be sent to the API
18
+ # @param heartbeat_interval_seconds [Float] How frequently heartbeats will be reported, in seconds.
19
+ # @param [Boolean] dependency_collection Whether to send the `app-dependencies-loaded` event
20
+ def initialize(heartbeat_interval_seconds:, dependency_collection:, enabled: true)
21
+ @enabled = enabled
22
+ @stopped = false
23
+
24
+ @worker = Telemetry::Worker.new(
25
+ enabled: @enabled,
26
+ heartbeat_interval_seconds: heartbeat_interval_seconds,
27
+ emitter: Emitter.new,
28
+ dependency_collection: dependency_collection
29
+ )
30
+ @worker.start
31
+ end
32
+
33
+ def disable!
34
+ @enabled = false
35
+ @worker.enabled = false
36
+ end
37
+
38
+ def stop!
39
+ return if @stopped
40
+
41
+ @worker.stop(true)
42
+ @stopped = true
43
+ end
44
+
45
+ def emit_closing!
46
+ return if !@enabled || forked?
47
+
48
+ @worker.enqueue(Event::AppClosing.new)
49
+ end
50
+
51
+ def integrations_change!
52
+ return if !@enabled || forked?
53
+
54
+ @worker.enqueue(Event::AppIntegrationsChange.new)
55
+ end
56
+
57
+ # Report configuration changes caused by Remote Configuration.
58
+ def client_configuration_change!(changes)
59
+ return if !@enabled || forked?
60
+
61
+ @worker.enqueue(Event::AppClientConfigurationChange.new(changes, 'remote_config'))
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -38,6 +38,7 @@ module Datadog
38
38
  private
39
39
 
40
40
  def products
41
+ # @type var products: Hash[Symbol, Hash[Symbol, Object]]
41
42
  products = {
42
43
  appsec: {
43
44
  enabled: Datadog::AppSec.enabled?,
@@ -13,7 +13,7 @@ module Datadog
13
13
  :timeout,
14
14
  :ssl
15
15
 
16
- DEFAULT_TIMEOUT = 30
16
+ DEFAULT_TIMEOUT = 2
17
17
 
18
18
  def initialize(hostname:, port: nil, timeout: DEFAULT_TIMEOUT, ssl: true)
19
19
  @hostname = hostname
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'event'
4
+
5
+ require_relative '../utils/only_once_successful'
6
+ require_relative '../workers/polling'
7
+ require_relative '../workers/queue'
8
+
9
+ module Datadog
10
+ module Core
11
+ module Telemetry
12
+ # Accumulates events and sends them to the API at a regular interval, including heartbeat event.
13
+ class Worker
14
+ include Core::Workers::Queue
15
+ include Core::Workers::Polling
16
+
17
+ DEFAULT_BUFFER_MAX_SIZE = 1000
18
+ APP_STARTED_EVENT_RETRIES = 10
19
+
20
+ TELEMETRY_STARTED_ONCE = Utils::OnlyOnceSuccessful.new(APP_STARTED_EVENT_RETRIES)
21
+
22
+ def initialize(
23
+ heartbeat_interval_seconds:,
24
+ emitter:,
25
+ dependency_collection:,
26
+ enabled: true,
27
+ shutdown_timeout: Workers::Polling::DEFAULT_SHUTDOWN_TIMEOUT,
28
+ buffer_size: DEFAULT_BUFFER_MAX_SIZE
29
+ )
30
+ @emitter = emitter
31
+ @dependency_collection = dependency_collection
32
+
33
+ # Workers::Polling settings
34
+ self.enabled = enabled
35
+ # Workers::IntervalLoop settings
36
+ self.loop_base_interval = heartbeat_interval_seconds
37
+ self.fork_policy = Core::Workers::Async::Thread::FORK_POLICY_STOP
38
+
39
+ @shutdown_timeout = shutdown_timeout
40
+ @buffer_size = buffer_size
41
+
42
+ self.buffer = buffer_klass.new(@buffer_size)
43
+ end
44
+
45
+ def start
46
+ return if !enabled? || forked?
47
+
48
+ # starts async worker
49
+ perform
50
+ end
51
+
52
+ def stop(force_stop = false, timeout = @shutdown_timeout)
53
+ buffer.close if running?
54
+
55
+ super
56
+ end
57
+
58
+ def enqueue(event)
59
+ return if !enabled? || forked?
60
+
61
+ buffer.push(event)
62
+ end
63
+
64
+ def sent_started_event?
65
+ TELEMETRY_STARTED_ONCE.success?
66
+ end
67
+
68
+ def failed_to_start?
69
+ TELEMETRY_STARTED_ONCE.failed?
70
+ end
71
+
72
+ private
73
+
74
+ def perform(*events)
75
+ return if !enabled? || forked?
76
+
77
+ started! unless sent_started_event?
78
+
79
+ heartbeat!
80
+
81
+ flush_events(events)
82
+ end
83
+
84
+ def flush_events(events)
85
+ return if events.nil?
86
+ return if !enabled? || !sent_started_event?
87
+
88
+ Datadog.logger.debug { "Sending #{events.count} telemetry events" }
89
+ events.each do |event|
90
+ send_event(event)
91
+ end
92
+ end
93
+
94
+ def heartbeat!
95
+ return if !enabled? || !sent_started_event?
96
+
97
+ send_event(Event::AppHeartbeat.new)
98
+ end
99
+
100
+ def started!
101
+ return unless enabled?
102
+
103
+ if failed_to_start?
104
+ Datadog.logger.debug('Telemetry app-started event exhausted retries, disabling telemetry worker')
105
+ self.enabled = false
106
+ return
107
+ end
108
+
109
+ TELEMETRY_STARTED_ONCE.run do
110
+ res = send_event(Event::AppStarted.new)
111
+
112
+ if res.ok?
113
+ Datadog.logger.debug('Telemetry app-started event is successfully sent')
114
+
115
+ send_event(Event::AppDependenciesLoaded.new) if @dependency_collection
116
+
117
+ true
118
+ else
119
+ Datadog.logger.debug('Error sending telemetry app-started event, retry after heartbeat interval...')
120
+ false
121
+ end
122
+ end
123
+ end
124
+
125
+ def send_event(event)
126
+ res = @emitter.request(event)
127
+
128
+ disable_on_not_found!(res)
129
+
130
+ res
131
+ end
132
+
133
+ def dequeue
134
+ buffer.pop
135
+ end
136
+
137
+ def work_pending?
138
+ run_loop? || !buffer.empty?
139
+ end
140
+
141
+ def buffer_klass
142
+ if Core::Environment::Ext::RUBY_ENGINE == 'ruby'
143
+ Core::Buffer::CRuby
144
+ else
145
+ Core::Buffer::ThreadSafe
146
+ end
147
+ end
148
+
149
+ def disable_on_not_found!(response)
150
+ return unless response.not_found?
151
+
152
+ Datadog.logger.debug('Agent does not support telemetry; disabling future telemetry events.')
153
+ self.enabled = false
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'only_once'
4
+
5
+ module Datadog
6
+ module Core
7
+ module Utils
8
+ # Helper class to execute something with only one success.
9
+ #
10
+ # This is useful for cases where we want to ensure that a block of code is only executed once, and only if it
11
+ # succeeds. One such example is sending app-started telemetry event.
12
+ #
13
+ # Successful execution is determined by the return value of the block: any truthy value is considered success.
14
+ #
15
+ # Thread-safe when used correctly (e.g. be careful of races when lazily initializing instances of this class).
16
+ #
17
+ # Note: In its current state, this class is not Ractor-safe.
18
+ # In https://github.com/DataDog/dd-trace-rb/pull/1398#issuecomment-797378810 we have a discussion of alternatives,
19
+ # including an alternative implementation that is Ractor-safe once spent.
20
+ class OnlyOnceSuccessful < OnlyOnce
21
+ def initialize(limit = 0)
22
+ super()
23
+
24
+ @limit = limit
25
+ @failed = false
26
+ @retries = 0
27
+ end
28
+
29
+ def run
30
+ @mutex.synchronize do
31
+ return if @ran_once
32
+
33
+ result = yield
34
+ @ran_once = !!result
35
+
36
+ if !@ran_once && limited?
37
+ @retries += 1
38
+ check_limit!
39
+ end
40
+
41
+ result
42
+ end
43
+ end
44
+
45
+ def success?
46
+ @mutex.synchronize { @ran_once && !@failed }
47
+ end
48
+
49
+ def failed?
50
+ @mutex.synchronize { @ran_once && @failed }
51
+ end
52
+
53
+ private
54
+
55
+ def check_limit!
56
+ if @retries >= @limit
57
+ @failed = true
58
+ @ran_once = true
59
+ end
60
+ end
61
+
62
+ def limited?
63
+ !@limit.nil? && @limit > 0
64
+ end
65
+
66
+ def reset_ran_once_state_for_tests
67
+ @mutex.synchronize do
68
+ @ran_once = false
69
+ @failed = false
70
+ @retries = 0
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -4,7 +4,7 @@ module DDTrace
4
4
  module VERSION
5
5
  MAJOR = 1
6
6
  MINOR = 23
7
- PATCH = 2
7
+ PATCH = 3
8
8
  PRE = nil
9
9
  BUILD = nil
10
10
  # PRE and BUILD above are modified for dev gems during gem build GHA workflow
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ddtrace
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.23.2
4
+ version: 1.23.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Datadog, Inc.
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-13 00:00:00.000000000 Z
11
+ date: 2024-07-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack
@@ -296,17 +296,17 @@ files:
296
296
  - lib/datadog/core/remote/worker.rb
297
297
  - lib/datadog/core/runtime/ext.rb
298
298
  - lib/datadog/core/runtime/metrics.rb
299
- - lib/datadog/core/telemetry/client.rb
299
+ - lib/datadog/core/telemetry/component.rb
300
300
  - lib/datadog/core/telemetry/emitter.rb
301
301
  - lib/datadog/core/telemetry/event.rb
302
302
  - lib/datadog/core/telemetry/ext.rb
303
- - lib/datadog/core/telemetry/heartbeat.rb
304
303
  - lib/datadog/core/telemetry/http/adapters/net.rb
305
304
  - lib/datadog/core/telemetry/http/env.rb
306
305
  - lib/datadog/core/telemetry/http/ext.rb
307
306
  - lib/datadog/core/telemetry/http/response.rb
308
307
  - lib/datadog/core/telemetry/http/transport.rb
309
308
  - lib/datadog/core/telemetry/request.rb
309
+ - lib/datadog/core/telemetry/worker.rb
310
310
  - lib/datadog/core/transport/ext.rb
311
311
  - lib/datadog/core/transport/http/adapters/net.rb
312
312
  - lib/datadog/core/transport/http/adapters/registry.rb
@@ -327,6 +327,7 @@ files:
327
327
  - lib/datadog/core/utils/hash.rb
328
328
  - lib/datadog/core/utils/network.rb
329
329
  - lib/datadog/core/utils/only_once.rb
330
+ - lib/datadog/core/utils/only_once_successful.rb
330
331
  - lib/datadog/core/utils/safe_dup.rb
331
332
  - lib/datadog/core/utils/sequence.rb
332
333
  - lib/datadog/core/utils/time.rb
@@ -880,9 +881,16 @@ licenses:
880
881
  - Apache-2.0
881
882
  metadata:
882
883
  allowed_push_host: https://rubygems.org
883
- changelog_uri: https://github.com/DataDog/dd-trace-rb/blob/v1.23.2/CHANGELOG.md
884
- source_code_uri: https://github.com/DataDog/dd-trace-rb/tree/v1.23.2
885
- post_install_message:
884
+ changelog_uri: https://github.com/DataDog/dd-trace-rb/blob/v1.23.3/CHANGELOG.md
885
+ source_code_uri: https://github.com/DataDog/dd-trace-rb/tree/v1.23.3
886
+ post_install_message: |2
887
+ Thank you for installing ddtrace. We have released our next major version!
888
+
889
+ As of version 2, `ddtrace` gem has been renamed to `datadog`.
890
+ The 1.x series will now only receive maintenance updates for security and critical bug fixes.
891
+
892
+ To upgrade, please replace gem `ddtrace` with gem `datadog`.
893
+ For detailed instructions on migration, see: https://dtdg.co/ruby-v2-upgrade
886
894
  rdoc_options: []
887
895
  require_paths:
888
896
  - lib
@@ -900,8 +908,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
900
908
  - !ruby/object:Gem::Version
901
909
  version: 2.0.0
902
910
  requirements: []
903
- rubygems_version: 3.4.10
904
- signing_key:
911
+ rubygems_version: 3.4.21
912
+ signing_key:
905
913
  specification_version: 4
906
914
  summary: Datadog tracing code for your Ruby applications
907
915
  test_files: []
@@ -1,95 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'emitter'
4
- require_relative 'event'
5
- require_relative 'heartbeat'
6
- require_relative '../utils/forking'
7
-
8
- module Datadog
9
- module Core
10
- module Telemetry
11
- # Telemetry entrypoint, coordinates sending telemetry events at various points in app lifecycle.
12
- class Client
13
- attr_reader \
14
- :enabled,
15
- :unsupported
16
-
17
- include Core::Utils::Forking
18
-
19
- # @param enabled [Boolean] Determines whether telemetry events should be sent to the API
20
- # @param heartbeat_interval_seconds [Float] How frequently heartbeats will be reported, in seconds.
21
- # @param [Boolean] dependency_collection Whether to send the `app-dependencies-loaded` event
22
- def initialize(heartbeat_interval_seconds:, dependency_collection:, enabled: true)
23
- @enabled = enabled
24
- @emitter = Emitter.new
25
- @stopped = false
26
- @unsupported = false
27
- @started = false
28
- @dependency_collection = dependency_collection
29
-
30
- @worker = Telemetry::Heartbeat.new(enabled: @enabled, heartbeat_interval_seconds: heartbeat_interval_seconds) do
31
- next unless @started # `started!` should be the first event, thus ensure that `heartbeat!` is not sent first.
32
-
33
- heartbeat!
34
- end
35
- end
36
-
37
- def disable!
38
- @enabled = false
39
- @worker.enabled = false
40
- end
41
-
42
- def started!
43
- return if !@enabled || forked?
44
-
45
- res = @emitter.request(Event::AppStarted.new)
46
-
47
- if res.not_found? # Telemetry is only supported by agent versions 7.34 and up
48
- Datadog.logger.debug('Agent does not support telemetry; disabling future telemetry events.')
49
- disable!
50
- @unsupported = true # Prevent telemetry from getting re-enabled
51
- return res
52
- end
53
-
54
- @emitter.request(Event::AppDependenciesLoaded.new) if @dependency_collection
55
-
56
- @started = true
57
- end
58
-
59
- def emit_closing!
60
- return if !@enabled || forked?
61
-
62
- @emitter.request(Event::AppClosing.new)
63
- end
64
-
65
- def stop!
66
- return if @stopped
67
-
68
- @worker.stop(true, 0)
69
- @stopped = true
70
- end
71
-
72
- def integrations_change!
73
- return if !@enabled || forked?
74
-
75
- @emitter.request(Event::AppIntegrationsChange.new)
76
- end
77
-
78
- # Report configuration changes caused by Remote Configuration.
79
- def client_configuration_change!(changes)
80
- return if !@enabled || forked?
81
-
82
- @emitter.request(Event::AppClientConfigurationChange.new(changes, 'remote_config'))
83
- end
84
-
85
- private
86
-
87
- def heartbeat!
88
- return if !@enabled || forked?
89
-
90
- @emitter.request(Event::AppHeartbeat.new)
91
- end
92
- end
93
- end
94
- end
95
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../worker'
4
- require_relative '../workers/polling'
5
-
6
- module Datadog
7
- module Core
8
- module Telemetry
9
- # Periodically (every DEFAULT_INTERVAL_SECONDS) sends a heartbeat event to the telemetry API.
10
- class Heartbeat < Core::Worker
11
- include Core::Workers::Polling
12
-
13
- def initialize(heartbeat_interval_seconds:, enabled: true, &block)
14
- # Workers::Polling settings
15
- self.enabled = enabled
16
- # Workers::IntervalLoop settings
17
- self.loop_base_interval = heartbeat_interval_seconds
18
- self.fork_policy = Core::Workers::Async::Thread::FORK_POLICY_STOP
19
- super(&block)
20
- start
21
- end
22
-
23
- def loop_wait_before_first_iteration?; end
24
-
25
- private
26
-
27
- def start
28
- perform
29
- end
30
- end
31
- end
32
- end
33
- end