splitclient-rb 7.3.0 → 7.3.1.pre.rc1
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/.rubocop.yml +2 -0
- data/lib/splitclient-rb.rb +2 -2
- data/lib/splitclient-rb/cache/fetchers/segment_fetcher.rb +7 -4
- data/lib/splitclient-rb/cache/fetchers/split_fetcher.rb +4 -4
- data/lib/splitclient-rb/engine/api/segments.rb +8 -4
- data/lib/splitclient-rb/engine/api/splits.rb +5 -3
- data/lib/splitclient-rb/engine/back_off.rb +26 -0
- data/lib/splitclient-rb/engine/push_manager.rb +1 -1
- data/lib/splitclient-rb/engine/synchronizer.rb +111 -6
- data/lib/splitclient-rb/split_config.rb +14 -0
- data/lib/splitclient-rb/sse/workers/segments_worker.rb +1 -5
- data/lib/splitclient-rb/sse/workers/splits_worker.rb +1 -8
- data/lib/splitclient-rb/version.rb +1 -1
- metadata +5 -5
- data/lib/splitclient-rb/sse/event_source/back_off.rb +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2de576884003cfed4ba2398d7f0909652f042eea9c2801fe4a835241f047a25d
|
4
|
+
data.tar.gz: 06eaa9744fea0d744eb71e36314e32c7750195585acf65ef736a30031483754e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 697eade28108c8aded43cf86e9114a64cf5614c577c776108014d8fcccccdb7e031d9c73305d91ce1ca0de3f78e3eeb5a7f477d28f5314f347457bb71b46bd5e
|
7
|
+
data.tar.gz: bb91fcc75c8c0f5e668e972f91264af9ab5e6684c2dea135ac9c24a9b39d9486ae0a899367d5a65bd996f812df285a1566f2914d58a6bf845516f697b9cecd24
|
data/.rubocop.yml
CHANGED
@@ -31,6 +31,7 @@ Metrics/LineLength:
|
|
31
31
|
- spec/engine/sync_manager_spec.rb
|
32
32
|
- spec/engine/auth_api_client_spec.rb
|
33
33
|
- spec/telemetry/synchronizer_spec.rb
|
34
|
+
- spec/splitclient/split_config_spec.rb
|
34
35
|
|
35
36
|
Style/BracesAroundHashParameters:
|
36
37
|
Exclude:
|
@@ -62,3 +63,4 @@ AllCops:
|
|
62
63
|
- lib/splitclient-rb/engine/models/**/*
|
63
64
|
- lib/splitclient-rb/engine/parser/**/*
|
64
65
|
- spec/telemetry/synchronizer_spec.rb
|
66
|
+
- lib/splitclient-rb/engine/synchronizer.rb
|
data/lib/splitclient-rb.rb
CHANGED
@@ -85,13 +85,13 @@ require 'splitclient-rb/engine/models/split'
|
|
85
85
|
require 'splitclient-rb/engine/models/label'
|
86
86
|
require 'splitclient-rb/engine/models/treatment'
|
87
87
|
require 'splitclient-rb/engine/auth_api_client'
|
88
|
+
require 'splitclient-rb/engine/back_off'
|
88
89
|
require 'splitclient-rb/engine/push_manager'
|
89
90
|
require 'splitclient-rb/engine/sync_manager'
|
90
91
|
require 'splitclient-rb/engine/synchronizer'
|
91
92
|
require 'splitclient-rb/utilitites'
|
92
93
|
|
93
|
-
# SSE
|
94
|
-
require 'splitclient-rb/sse/event_source/back_off'
|
94
|
+
# SSE
|
95
95
|
require 'splitclient-rb/sse/event_source/client'
|
96
96
|
require 'splitclient-rb/sse/event_source/event_parser'
|
97
97
|
require 'splitclient-rb/sse/event_source/event_types'
|
@@ -30,16 +30,19 @@ module SplitIoClient
|
|
30
30
|
def fetch_segments_if_not_exists(names, cache_control_headers = false)
|
31
31
|
names.each do |name|
|
32
32
|
change_number = @segments_repository.get_change_number(name)
|
33
|
-
|
34
|
-
|
33
|
+
|
34
|
+
if change_number == -1
|
35
|
+
fetch_options = { cache_control_headers: cache_control_headers, till: nil }
|
36
|
+
fetch_segment(name, fetch_options) if change_number == -1
|
37
|
+
end
|
35
38
|
end
|
36
39
|
rescue StandardError => error
|
37
40
|
@config.log_found_exception(__method__.to_s, error)
|
38
41
|
end
|
39
42
|
|
40
|
-
def fetch_segment(name,
|
43
|
+
def fetch_segment(name, fetch_options = { cache_control_headers: false, till: nil })
|
41
44
|
@semaphore.synchronize do
|
42
|
-
segments_api.fetch_segments_by_names([name],
|
45
|
+
segments_api.fetch_segments_by_names([name], fetch_options)
|
43
46
|
end
|
44
47
|
rescue StandardError => error
|
45
48
|
@config.log_found_exception(__method__.to_s, error)
|
@@ -27,9 +27,9 @@ module SplitIoClient
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
def fetch_splits(
|
30
|
+
def fetch_splits(fetch_options = { cache_control_headers: false, till: nil })
|
31
31
|
@semaphore.synchronize do
|
32
|
-
data = splits_since(@splits_repository.get_change_number,
|
32
|
+
data = splits_since(@splits_repository.get_change_number, fetch_options)
|
33
33
|
|
34
34
|
data[:splits] && data[:splits].each do |split|
|
35
35
|
add_split_unless_archived(split)
|
@@ -68,8 +68,8 @@ module SplitIoClient
|
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
71
|
-
def splits_since(since,
|
72
|
-
splits_api.since(since,
|
71
|
+
def splits_since(since, fetch_options = { cache_control_headers: false, till: nil })
|
72
|
+
splits_api.since(since, fetch_options)
|
73
73
|
end
|
74
74
|
|
75
75
|
def add_split_unless_archived(split)
|
@@ -11,13 +11,14 @@ module SplitIoClient
|
|
11
11
|
@telemetry_runtime_producer = telemetry_runtime_producer
|
12
12
|
end
|
13
13
|
|
14
|
-
def fetch_segments_by_names(names,
|
14
|
+
def fetch_segments_by_names(names, fetch_options = { cache_control_headers: false, till: nil })
|
15
15
|
return if names.nil? || names.empty?
|
16
16
|
|
17
17
|
names.each do |name|
|
18
18
|
since = @segments_repository.get_change_number(name)
|
19
|
+
|
19
20
|
loop do
|
20
|
-
segment = fetch_segment_changes(name, since,
|
21
|
+
segment = fetch_segment_changes(name, since, fetch_options)
|
21
22
|
@segments_repository.add_to_segment(segment)
|
22
23
|
|
23
24
|
@config.split_logger.log_if_debug("Segment #{name} fetched before: #{since}, \
|
@@ -32,9 +33,12 @@ module SplitIoClient
|
|
32
33
|
|
33
34
|
private
|
34
35
|
|
35
|
-
def fetch_segment_changes(name, since,
|
36
|
+
def fetch_segment_changes(name, since, fetch_options = { cache_control_headers: false, till: nil })
|
36
37
|
start = Time.now
|
37
|
-
|
38
|
+
|
39
|
+
params = { since: since }
|
40
|
+
params[:till] = fetch_options[:till] unless fetch_options[:till].nil?
|
41
|
+
response = get_api("#{@config.base_uri}/segmentChanges/#{name}", @api_key, params, fetch_options[:cache_control_headers])
|
38
42
|
|
39
43
|
if response.success?
|
40
44
|
segment = JSON.parse(response.body, symbolize_names: true)
|
@@ -10,10 +10,12 @@ module SplitIoClient
|
|
10
10
|
@telemetry_runtime_producer = telemetry_runtime_producer
|
11
11
|
end
|
12
12
|
|
13
|
-
def since(since,
|
13
|
+
def since(since, fetch_options = { cache_control_headers: false, till: nil })
|
14
14
|
start = Time.now
|
15
|
-
|
16
|
-
|
15
|
+
|
16
|
+
params = { since: since }
|
17
|
+
params[:till] = fetch_options[:till] unless fetch_options[:till].nil?
|
18
|
+
response = get_api("#{@config.base_uri}/splitChanges", @api_key, params, fetch_options[:cache_control_headers])
|
17
19
|
if response.success?
|
18
20
|
result = splits_with_segment_names(response.body)
|
19
21
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
module SplitIoClient
|
4
|
+
module Engine
|
5
|
+
BACKOFF_MAX_ALLOWED = 1.8
|
6
|
+
class BackOff
|
7
|
+
def initialize(back_off_base, attempt = 0, max_allowed = BACKOFF_MAX_ALLOWED)
|
8
|
+
@attempt = attempt
|
9
|
+
@back_off_base = back_off_base
|
10
|
+
@max_allowed = max_allowed
|
11
|
+
end
|
12
|
+
|
13
|
+
def interval
|
14
|
+
interval = 0
|
15
|
+
interval = (@back_off_base * (2**@attempt)) if @attempt.positive?
|
16
|
+
@attempt += 1
|
17
|
+
|
18
|
+
interval >= @max_allowed ? @max_allowed : interval
|
19
|
+
end
|
20
|
+
|
21
|
+
def reset
|
22
|
+
@attempt = 0
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -8,7 +8,7 @@ module SplitIoClient
|
|
8
8
|
@sse_handler = sse_handler
|
9
9
|
@auth_api_client = AuthApiClient.new(@config, telemetry_runtime_producer)
|
10
10
|
@api_key = api_key
|
11
|
-
@back_off =
|
11
|
+
@back_off = Engine::BackOff.new(@config.auth_retry_back_off_base, 1)
|
12
12
|
@telemetry_runtime_producer = telemetry_runtime_producer
|
13
13
|
end
|
14
14
|
|
@@ -6,7 +6,9 @@ module SplitIoClient
|
|
6
6
|
include SplitIoClient::Cache::Fetchers
|
7
7
|
include SplitIoClient::Cache::Senders
|
8
8
|
|
9
|
-
|
9
|
+
ON_DEMAND_FETCH_BACKOFF_BASE_SECONDS = 10
|
10
|
+
ON_DEMAND_FETCH_BACKOFF_MAX_WAIT_SECONDS = 60
|
11
|
+
ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES = 10
|
10
12
|
|
11
13
|
def initialize(
|
12
14
|
repositories,
|
@@ -54,17 +56,116 @@ module SplitIoClient
|
|
54
56
|
@segment_fetcher.stop_segments_thread
|
55
57
|
end
|
56
58
|
|
57
|
-
def fetch_splits
|
58
|
-
|
59
|
-
|
59
|
+
def fetch_splits(target_change_number)
|
60
|
+
return if target_change_number <= @splits_repository.get_change_number.to_i
|
61
|
+
|
62
|
+
fetch_options = { cache_control_headers: true, till: nil }
|
63
|
+
|
64
|
+
result = attempt_splits_sync(target_change_number,
|
65
|
+
fetch_options,
|
66
|
+
@config.on_demand_fetch_max_retries,
|
67
|
+
@config.on_demand_fetch_retry_delay_seconds,
|
68
|
+
false)
|
69
|
+
|
70
|
+
attempts = @config.on_demand_fetch_max_retries - result[:remaining_attempts]
|
71
|
+
if result[:success]
|
72
|
+
@segment_fetcher.fetch_segments_if_not_exists(result[:segment_names], true) unless result[:segment_names].empty?
|
73
|
+
@config.logger.debug("Refresh completed in #{attempts} attempts.") if @config.debug_enabled
|
74
|
+
|
75
|
+
return
|
76
|
+
end
|
77
|
+
|
78
|
+
fetch_options[:till] = target_change_number
|
79
|
+
result = attempt_splits_sync(target_change_number,
|
80
|
+
fetch_options,
|
81
|
+
ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES,
|
82
|
+
nil,
|
83
|
+
true)
|
84
|
+
|
85
|
+
attempts = @config.on_demand_fetch_max_retries - result[:remaining_attempts]
|
86
|
+
|
87
|
+
if result[:success]
|
88
|
+
@segment_fetcher.fetch_segments_if_not_exists(result[:segment_names], true) unless result[:segment_names].empty?
|
89
|
+
@config.logger.debug("Refresh completed bypassing the CDN in #{attempts} attempts.") if @config.debug_enabled
|
90
|
+
else
|
91
|
+
@config.logger.debug("No changes fetched after #{attempts} attempts with CDN bypassed.") if @config.debug_enabled
|
92
|
+
end
|
93
|
+
rescue StandardError => error
|
94
|
+
@config.log_found_exception(__method__.to_s, error)
|
60
95
|
end
|
61
96
|
|
62
|
-
def fetch_segment(name)
|
63
|
-
@
|
97
|
+
def fetch_segment(name, target_change_number)
|
98
|
+
return if target_change_number <= @segments_repository.get_change_number(name).to_i
|
99
|
+
|
100
|
+
fetch_options = { cache_control_headers: true, till: nil }
|
101
|
+
result = attempt_segment_sync(name,
|
102
|
+
target_change_number,
|
103
|
+
fetch_options,
|
104
|
+
@config.on_demand_fetch_max_retries,
|
105
|
+
@config.on_demand_fetch_retry_delay_seconds,
|
106
|
+
false)
|
107
|
+
|
108
|
+
attempts = @config.on_demand_fetch_max_retries - result[:remaining_attempts]
|
109
|
+
if result[:success]
|
110
|
+
@config.logger.debug("Segment #{name} refresh completed in #{attempts} attempts.") if @config.debug_enabled
|
111
|
+
|
112
|
+
return
|
113
|
+
end
|
114
|
+
|
115
|
+
fetch_options = { cache_control_headers: true, till: target_change_number }
|
116
|
+
result = attempt_segment_sync(name,
|
117
|
+
target_change_number,
|
118
|
+
fetch_options,
|
119
|
+
ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES,
|
120
|
+
nil,
|
121
|
+
true)
|
122
|
+
|
123
|
+
attempts = @config.on_demand_fetch_max_retries - result[:remaining_attempts]
|
124
|
+
if result[:success]
|
125
|
+
@config.logger.debug("Segment #{name} refresh completed bypassing the CDN in #{attempts} attempts.") if @config.debug_enabled
|
126
|
+
else
|
127
|
+
@config.logger.debug("No changes fetched for segment #{name} after #{attempts} attempts with CDN bypassed.") if @config.debug_enabled
|
128
|
+
end
|
129
|
+
rescue StandardError => error
|
130
|
+
@config.log_found_exception(__method__.to_s, error)
|
64
131
|
end
|
65
132
|
|
66
133
|
private
|
67
134
|
|
135
|
+
def attempt_segment_sync(name, target_cn, fetch_options, max_retries, retry_delay_seconds, with_backoff)
|
136
|
+
remaining_attempts = max_retries
|
137
|
+
backoff = Engine::BackOff.new(ON_DEMAND_FETCH_BACKOFF_BASE_SECONDS, 0, ON_DEMAND_FETCH_BACKOFF_MAX_WAIT_SECONDS) if with_backoff
|
138
|
+
|
139
|
+
loop do
|
140
|
+
remaining_attempts -= 1
|
141
|
+
|
142
|
+
@segment_fetcher.fetch_segment(name, fetch_options)
|
143
|
+
|
144
|
+
return sync_result(true, remaining_attempts) if target_cn <= @segments_repository.get_change_number(name).to_i
|
145
|
+
return sync_result(false, remaining_attempts) if remaining_attempts <= 0
|
146
|
+
|
147
|
+
delay = with_backoff ? backoff.interval : retry_delay_seconds
|
148
|
+
sleep(delay)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def attempt_splits_sync(target_cn, fetch_options, max_retries, retry_delay_seconds, with_backoff)
|
153
|
+
remaining_attempts = max_retries
|
154
|
+
backoff = Engine::BackOff.new(ON_DEMAND_FETCH_BACKOFF_BASE_SECONDS, 0, ON_DEMAND_FETCH_BACKOFF_MAX_WAIT_SECONDS) if with_backoff
|
155
|
+
|
156
|
+
loop do
|
157
|
+
remaining_attempts -= 1
|
158
|
+
|
159
|
+
segment_names = @split_fetcher.fetch_splits(fetch_options)
|
160
|
+
|
161
|
+
return sync_result(true, remaining_attempts, segment_names) if target_cn <= @splits_repository.get_change_number
|
162
|
+
return sync_result(false, remaining_attempts, segment_names) if remaining_attempts <= 0
|
163
|
+
|
164
|
+
delay = with_backoff ? backoff.interval : retry_delay_seconds
|
165
|
+
sleep(delay)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
68
169
|
def fetch_segments
|
69
170
|
@segment_fetcher.fetch_segments
|
70
171
|
end
|
@@ -87,6 +188,10 @@ module SplitIoClient
|
|
87
188
|
def start_telemetry_sync_task
|
88
189
|
Telemetry::SyncTask.new(@config, @telemetry_synchronizer).call
|
89
190
|
end
|
191
|
+
|
192
|
+
def sync_result(success, remaining_attempts, segment_names = nil)
|
193
|
+
{ success: success, remaining_attempts: remaining_attempts, segment_names: segment_names }
|
194
|
+
end
|
90
195
|
end
|
91
196
|
end
|
92
197
|
end
|
@@ -113,6 +113,9 @@ module SplitIoClient
|
|
113
113
|
|
114
114
|
@sdk_start_time = Time.now
|
115
115
|
|
116
|
+
@on_demand_fetch_retry_delay_seconds = SplitConfig.default_on_demand_fetch_retry_delay_seconds
|
117
|
+
@on_demand_fetch_max_retries = SplitConfig.default_on_demand_fetch_max_retries
|
118
|
+
|
116
119
|
startup_log
|
117
120
|
end
|
118
121
|
|
@@ -278,6 +281,17 @@ module SplitIoClient
|
|
278
281
|
|
279
282
|
attr_accessor :sdk_start_time
|
280
283
|
|
284
|
+
attr_accessor :on_demand_fetch_retry_delay_seconds
|
285
|
+
attr_accessor :on_demand_fetch_max_retries
|
286
|
+
|
287
|
+
def self.default_on_demand_fetch_retry_delay_seconds
|
288
|
+
0.05
|
289
|
+
end
|
290
|
+
|
291
|
+
def self.default_on_demand_fetch_max_retries
|
292
|
+
10
|
293
|
+
end
|
294
|
+
|
281
295
|
def self.default_impressions_mode
|
282
296
|
:optimized
|
283
297
|
end
|
@@ -51,11 +51,7 @@ module SplitIoClient
|
|
51
51
|
cn = item[:change_number]
|
52
52
|
@config.logger.debug("SegmentsWorker change_number dequeue #{segment_name}, #{cn}")
|
53
53
|
|
54
|
-
|
55
|
-
while cn > @segments_repository.get_change_number(segment_name).to_i && attempt <= Workers::MAX_RETRIES_ALLOWED
|
56
|
-
@synchronizer.fetch_segment(segment_name)
|
57
|
-
attempt += 1
|
58
|
-
end
|
54
|
+
@synchronizer.fetch_segment(segment_name, cn)
|
59
55
|
end
|
60
56
|
end
|
61
57
|
|
@@ -3,8 +3,6 @@
|
|
3
3
|
module SplitIoClient
|
4
4
|
module SSE
|
5
5
|
module Workers
|
6
|
-
MAX_RETRIES_ALLOWED = 10
|
7
|
-
|
8
6
|
class SplitsWorker
|
9
7
|
def initialize(synchronizer, config, splits_repository)
|
10
8
|
@synchronizer = synchronizer
|
@@ -62,12 +60,7 @@ module SplitIoClient
|
|
62
60
|
def perform
|
63
61
|
while (change_number = @queue.pop)
|
64
62
|
@config.logger.debug("SplitsWorker change_number dequeue #{change_number}")
|
65
|
-
|
66
|
-
attempt = 0
|
67
|
-
while change_number > @splits_repository.get_change_number.to_i && attempt <= Workers::MAX_RETRIES_ALLOWED
|
68
|
-
@synchronizer.fetch_splits
|
69
|
-
attempt += 1
|
70
|
-
end
|
63
|
+
@synchronizer.fetch_splits(change_number)
|
71
64
|
end
|
72
65
|
end
|
73
66
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: splitclient-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 7.3.
|
4
|
+
version: 7.3.1.pre.rc1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Split Software
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-07-
|
11
|
+
date: 2021-07-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: allocation_stats
|
@@ -374,6 +374,7 @@ files:
|
|
374
374
|
- lib/splitclient-rb/engine/api/splits.rb
|
375
375
|
- lib/splitclient-rb/engine/api/telemetry_api.rb
|
376
376
|
- lib/splitclient-rb/engine/auth_api_client.rb
|
377
|
+
- lib/splitclient-rb/engine/back_off.rb
|
377
378
|
- lib/splitclient-rb/engine/common/impressions_counter.rb
|
378
379
|
- lib/splitclient-rb/engine/common/impressions_manager.rb
|
379
380
|
- lib/splitclient-rb/engine/evaluator/splitter.rb
|
@@ -417,7 +418,6 @@ files:
|
|
417
418
|
- lib/splitclient-rb/split_factory_builder.rb
|
418
419
|
- lib/splitclient-rb/split_factory_registry.rb
|
419
420
|
- lib/splitclient-rb/split_logger.rb
|
420
|
-
- lib/splitclient-rb/sse/event_source/back_off.rb
|
421
421
|
- lib/splitclient-rb/sse/event_source/client.rb
|
422
422
|
- lib/splitclient-rb/sse/event_source/event_parser.rb
|
423
423
|
- lib/splitclient-rb/sse/event_source/event_types.rb
|
@@ -470,9 +470,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
470
470
|
version: '0'
|
471
471
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
472
472
|
requirements:
|
473
|
-
- - "
|
473
|
+
- - ">"
|
474
474
|
- !ruby/object:Gem::Version
|
475
|
-
version:
|
475
|
+
version: 1.3.1
|
476
476
|
requirements: []
|
477
477
|
rubygems_version: 3.0.6
|
478
478
|
signing_key:
|
@@ -1,25 +0,0 @@
|
|
1
|
-
# frozen_string_literal: false
|
2
|
-
|
3
|
-
module SplitIoClient
|
4
|
-
module SSE
|
5
|
-
module EventSource
|
6
|
-
class BackOff
|
7
|
-
def initialize(back_off_base, attempt = 0)
|
8
|
-
@attempt = attempt
|
9
|
-
@back_off_base = back_off_base
|
10
|
-
end
|
11
|
-
|
12
|
-
def interval
|
13
|
-
interval = (@back_off_base * (2**@attempt)) if @attempt.positive?
|
14
|
-
@attempt += 1
|
15
|
-
|
16
|
-
interval || 0
|
17
|
-
end
|
18
|
-
|
19
|
-
def reset
|
20
|
-
@attempt = 0
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|