pg_eventstore 1.4.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/lib/pg_eventstore/extensions/callback_handlers_extension.rb +21 -0
- data/lib/pg_eventstore/subscriptions/callback_handlers/commands_handler_handlers.rb +38 -0
- data/lib/pg_eventstore/subscriptions/callback_handlers/events_processor_handlers.rb +45 -0
- data/lib/pg_eventstore/subscriptions/callback_handlers/subscription_feeder_handlers.rb +103 -0
- data/lib/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rb +75 -0
- data/lib/pg_eventstore/subscriptions/commands_handler.rb +13 -29
- data/lib/pg_eventstore/subscriptions/events_processor.rb +17 -41
- data/lib/pg_eventstore/subscriptions/subscription_feeder.rb +77 -125
- data/lib/pg_eventstore/subscriptions/subscription_runner.rb +30 -55
- data/lib/pg_eventstore/subscriptions/subscriptions_lifecycle.rb +70 -0
- data/lib/pg_eventstore/subscriptions/subscriptions_manager.rb +8 -2
- data/lib/pg_eventstore/subscriptions/subscriptions_set_lifecycle.rb +39 -0
- data/lib/pg_eventstore/utils.rb +8 -0
- data/lib/pg_eventstore/version.rb +1 -1
- data/lib/pg_eventstore/web/application.rb +9 -1
- data/lib/pg_eventstore/web/paginator/events_collection.rb +1 -3
- data/lib/pg_eventstore/web/paginator/helpers.rb +4 -1
- data/lib/pg_eventstore/web/public/javascripts/pg_eventstore.js +10 -1
- data/lib/pg_eventstore/web/subscriptions/helpers.rb +3 -4
- data/lib/pg_eventstore/web/views/home/dashboard.erb +11 -0
- data/lib/pg_eventstore/web/views/home/partials/events.erb +6 -1
- data/lib/pg_eventstore/web/views/subscriptions/index.erb +2 -2
- data/lib/pg_eventstore.rb +1 -0
- data/sig/interfaces/_raw_event_handler.rbs +3 -0
- data/sig/pg_eventstore/extensions/callback_handlers_extension.rbs +11 -0
- data/sig/pg_eventstore/subscriptions/callback_handlers/commands_handler_handlers.rbs +10 -0
- data/sig/pg_eventstore/subscriptions/callback_handlers/events_processor_handlers.rbs +11 -0
- data/sig/pg_eventstore/subscriptions/callback_handlers/subscription_feeder_handlers.rbs +31 -0
- data/sig/pg_eventstore/subscriptions/callback_handlers/subscription_runner_handlers.rbs +19 -0
- data/sig/pg_eventstore/subscriptions/events_processor.rbs +1 -1
- data/sig/pg_eventstore/subscriptions/subscription_feeder.rbs +2 -30
- data/sig/pg_eventstore/subscriptions/subscriptions_lifecycle.rbs +27 -0
- data/sig/pg_eventstore/subscriptions/subscriptions_manager.rbs +1 -1
- data/sig/pg_eventstore/subscriptions/subscriptions_set_lifecycle.rbs +24 -0
- data/sig/pg_eventstore/utils.rbs +2 -0
- data/sig/pg_eventstore/web/subscriptions/helpers.rbs +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec41b398e1f38d66e73967447ea4f12896ed2cf57eb0dbad8b71acf4257f9cd4
|
4
|
+
data.tar.gz: ce571ef411adeeb18b7bae463cac339a053d96f1b36e3de0b1023b8551b03ac5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9273a4efb9855d219b1a92b5d9081e3d0d62f9de7f21c48e634949d218ed6a8278d81367f794ee8b70efa64541d7a4442ccf09e7b4cc3bcbd39398f5c7980ec2
|
7
|
+
data.tar.gz: ffb85f8c9063aa6e11b8f900e128b4b873e89db117b39e703de6603fc73c014811b63950ff0a0340b0316da11ed2562418f56f1faf42a04491fb0105d913fced
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.5.0]
|
4
|
+
- Add ability to toggle link events in the admin UI
|
5
|
+
- Mark linked events in the admin UI with "link" icon
|
6
|
+
|
3
7
|
## [1.4.0]
|
4
8
|
- Add an ability to configure subscription graceful shutdown timeout globally and per subscription. Default value is 15 seconds. Previously it was hardcoded to 5 seconds. Examples:
|
5
9
|
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
module Extensions
|
5
|
+
module CallbackHandlersExtension
|
6
|
+
def self.included(klass)
|
7
|
+
klass.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# @param name [Symbol] a name of the handler
|
12
|
+
# @return [Proc]
|
13
|
+
def setup_handler(name, *args)
|
14
|
+
proc do |*rest|
|
15
|
+
public_send(name, *args, *rest)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
class CommandsHandlerHandlers
|
5
|
+
include Extensions::CallbackHandlersExtension
|
6
|
+
|
7
|
+
class << self
|
8
|
+
# @param config_name [Symbol]
|
9
|
+
# @param subscription_feeder [PgEventstore::SubscriptionFeeder]
|
10
|
+
# @return [void]
|
11
|
+
def process_feeder_commands(config_name, subscription_feeder)
|
12
|
+
CommandHandlers::SubscriptionFeederCommands.new(config_name, subscription_feeder).process
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param config_name [Symbol]
|
16
|
+
# @param runners [Array<PgEventstore::SubscriptionRunner>]
|
17
|
+
# @param subscription_feeder [PgEventstore::SubscriptionFeeder]
|
18
|
+
# @return [void]
|
19
|
+
def process_runners_commands(config_name, runners, subscription_feeder)
|
20
|
+
CommandHandlers::SubscriptionRunnersCommands.new(config_name, runners, subscription_feeder.id).process
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param basic_runner [PgEventstore::BasicRunner]
|
24
|
+
# @param restart_delay [Integer]
|
25
|
+
# @param error [StandardError]
|
26
|
+
# @return [void]
|
27
|
+
def restore_runner(basic_runner, restart_delay, error)
|
28
|
+
PgEventstore.logger&.error "PgEventstore::CommandsHandler: Error occurred: #{error.message}"
|
29
|
+
PgEventstore.logger&.error "PgEventstore::CommandsHandler: Backtrace: #{error.backtrace&.join("\n")}"
|
30
|
+
PgEventstore.logger&.error "PgEventstore::CommandsHandler: Trying to auto-repair in #{restart_delay} seconds..."
|
31
|
+
Thread.new do
|
32
|
+
sleep restart_delay
|
33
|
+
basic_runner.restore
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
class EventsProcessorHandlers
|
5
|
+
include Extensions::CallbackHandlersExtension
|
6
|
+
|
7
|
+
class << self
|
8
|
+
# @param callbacks [PgEventstore::Callbacks]
|
9
|
+
# @param handler [#call]
|
10
|
+
# @param raw_events [Array<Hash>]
|
11
|
+
# @return [void]
|
12
|
+
def process_event(callbacks, handler, raw_events)
|
13
|
+
raw_event = raw_events.shift
|
14
|
+
return sleep 0.5 if raw_event.nil?
|
15
|
+
|
16
|
+
callbacks.run_callbacks(:process, Utils.original_global_position(raw_event)) do
|
17
|
+
handler.call(raw_event)
|
18
|
+
end
|
19
|
+
rescue
|
20
|
+
raw_events.unshift(raw_event)
|
21
|
+
raise
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param callbacks [PgEventstore::Callbacks]
|
25
|
+
# @param error [StandardError]
|
26
|
+
# @return [void]
|
27
|
+
def after_runner_died(callbacks, error)
|
28
|
+
callbacks.run_callbacks(:error, error)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param callbacks [PgEventstore::Callbacks]
|
32
|
+
# @return [void]
|
33
|
+
def before_runner_restored(callbacks)
|
34
|
+
callbacks.run_callbacks(:restart)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @param callbacks [PgEventstore::Callbacks]
|
38
|
+
# @param state [String]
|
39
|
+
# @return [void]
|
40
|
+
def change_state(callbacks, state)
|
41
|
+
callbacks.run_callbacks(:change_state, state)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
class SubscriptionFeederHandlers
|
5
|
+
include Extensions::CallbackHandlersExtension
|
6
|
+
|
7
|
+
class << self
|
8
|
+
# @param subscriptions_set_lifecycle [PgEventstore::SubscriptionsSetLifecycle]
|
9
|
+
# @param state [String]
|
10
|
+
# @return [void]
|
11
|
+
def update_subscriptions_set_state(subscriptions_set_lifecycle, state)
|
12
|
+
subscriptions_set_lifecycle.persisted_subscriptions_set.update(state: state)
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param subscriptions_lifecycle [PgEventstore::SubscriptionsLifecycle]
|
16
|
+
# @return [void]
|
17
|
+
def lock_subscriptions(subscriptions_lifecycle)
|
18
|
+
subscriptions_lifecycle.lock_all
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param subscriptions_lifecycle [PgEventstore::SubscriptionsLifecycle]
|
22
|
+
# @return [void]
|
23
|
+
def start_runners(subscriptions_lifecycle)
|
24
|
+
subscriptions_lifecycle.runners.each(&:start)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param cmds_handler [PgEventstore::CommandsHandler]
|
28
|
+
# @return [void]
|
29
|
+
def start_cmds_handler(cmds_handler)
|
30
|
+
cmds_handler.start
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param subscriptions_set_lifecycle [PgEventstore::SubscriptionsSetLifecycle]
|
34
|
+
# @param error [StandardError]
|
35
|
+
# @return [void]
|
36
|
+
def persist_error_info(subscriptions_set_lifecycle, error)
|
37
|
+
subscriptions_set_lifecycle.persisted_subscriptions_set.update(
|
38
|
+
last_error: Utils.error_info(error), last_error_occurred_at: Time.now.utc
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param subscriptions_set_lifecycle [PgEventstore::SubscriptionsSetLifecycle]
|
43
|
+
# @param basic_runner [PgEventstore::BasicRunner]
|
44
|
+
# @param _error [StandardError]
|
45
|
+
# @return [void]
|
46
|
+
def restart_runner(subscriptions_set_lifecycle, basic_runner, _error)
|
47
|
+
subscriptions_set = subscriptions_set_lifecycle.persisted_subscriptions_set
|
48
|
+
return if subscriptions_set.restart_count >= subscriptions_set.max_restarts_number
|
49
|
+
|
50
|
+
Thread.new do
|
51
|
+
sleep subscriptions_set.time_between_restarts
|
52
|
+
basic_runner.restore
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# @param subscriptions_set_lifecycle [PgEventstore::SubscriptionsSetLifecycle]
|
57
|
+
# @return [void]
|
58
|
+
def ping_subscriptions_set(subscriptions_set_lifecycle)
|
59
|
+
subscriptions_set_lifecycle.ping_subscriptions_set
|
60
|
+
end
|
61
|
+
|
62
|
+
# @param subscriptions_lifecycle [PgEventstore::SubscriptionsLifecycle]
|
63
|
+
# @param config_name [Symbol]
|
64
|
+
# @return [void]
|
65
|
+
def feed_runners(subscriptions_lifecycle, config_name)
|
66
|
+
SubscriptionRunnersFeeder.new(config_name).feed(subscriptions_lifecycle.runners)
|
67
|
+
end
|
68
|
+
|
69
|
+
# @param subscriptions_lifecycle [PgEventstore::SubscriptionsLifecycle]
|
70
|
+
# @return [void]
|
71
|
+
def ping_subscriptions(subscriptions_lifecycle)
|
72
|
+
subscriptions_lifecycle.ping_subscriptions
|
73
|
+
end
|
74
|
+
|
75
|
+
# @param subscriptions_lifecycle [PgEventstore::SubscriptionsLifecycle]
|
76
|
+
# @return [void]
|
77
|
+
def stop_runners(subscriptions_lifecycle)
|
78
|
+
subscriptions_lifecycle.runners.each(&:stop_async).each(&:wait_for_finish)
|
79
|
+
end
|
80
|
+
|
81
|
+
# @param subscriptions_set_lifecycle [PgEventstore::SubscriptionsSetLifecycle]
|
82
|
+
# @return [void]
|
83
|
+
def reset_subscriptions_set(subscriptions_set_lifecycle)
|
84
|
+
subscriptions_set_lifecycle.reset_subscriptions_set
|
85
|
+
end
|
86
|
+
|
87
|
+
# @param cmds_handler [PgEventstore::CommandsHandler]
|
88
|
+
# @return [void]
|
89
|
+
def stop_commands_handler(cmds_handler)
|
90
|
+
cmds_handler.stop
|
91
|
+
end
|
92
|
+
|
93
|
+
# @param subscriptions_set_lifecycle [PgEventstore::SubscriptionsSetLifecycle]
|
94
|
+
# @return [void]
|
95
|
+
def update_subscriptions_set_restarts(subscriptions_set_lifecycle)
|
96
|
+
subscriptions_set = subscriptions_set_lifecycle.persisted_subscriptions_set
|
97
|
+
subscriptions_set.update(
|
98
|
+
last_restarted_at: Time.now.utc, restart_count: subscriptions_set.restart_count + 1
|
99
|
+
)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgEventstore
|
4
|
+
class SubscriptionRunnerHandlers
|
5
|
+
include Extensions::CallbackHandlersExtension
|
6
|
+
|
7
|
+
class << self
|
8
|
+
# @param stats [PgEventstore::SubscriptionHandlerPerformance]
|
9
|
+
# @param action [Proc]
|
10
|
+
# @param _current_position [Integer]
|
11
|
+
# @return [void]
|
12
|
+
def track_exec_time(stats, action, _current_position)
|
13
|
+
stats.track_exec_time { action.call }
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param subscription [PgEventstore::Subscription]
|
17
|
+
# @param stats [PgEventstore::SubscriptionHandlerPerformance]
|
18
|
+
# @param current_position [Integer]
|
19
|
+
# @return [void]
|
20
|
+
def update_subscription_stats(subscription, stats, current_position)
|
21
|
+
subscription.update(
|
22
|
+
average_event_processing_time: stats.average_event_processing_time,
|
23
|
+
current_position: current_position,
|
24
|
+
total_processed_events: subscription.total_processed_events + 1
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param subscription [PgEventstore::Subscription]
|
29
|
+
# @param error [StandardError]
|
30
|
+
# @return [void]
|
31
|
+
def update_subscription_error(subscription, error)
|
32
|
+
subscription.update(last_error: Utils.error_info(error), last_error_occurred_at: Time.now.utc)
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param subscription [PgEventstore::Subscription]
|
36
|
+
# @param restart_terminator [#call, nil]
|
37
|
+
# @param failed_subscription_notifier [#call, nil]
|
38
|
+
# @param events_processor [PgEventstore::EventsProcessor]
|
39
|
+
# @param error [StandardError]
|
40
|
+
# @return [void]
|
41
|
+
def restart_events_processor(subscription, restart_terminator, failed_subscription_notifier, events_processor,
|
42
|
+
error)
|
43
|
+
return if restart_terminator&.call(subscription.dup)
|
44
|
+
if subscription.restart_count >= subscription.max_restarts_number
|
45
|
+
return failed_subscription_notifier&.call(subscription.dup, error)
|
46
|
+
end
|
47
|
+
|
48
|
+
Thread.new do
|
49
|
+
sleep subscription.time_between_restarts
|
50
|
+
events_processor.restore
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# @param subscription [PgEventstore::Subscription]
|
55
|
+
# @param global_position [Integer]
|
56
|
+
# @return [void]
|
57
|
+
def update_subscription_chunk_stats(subscription, global_position)
|
58
|
+
subscription.update(last_chunk_fed_at: Time.now.utc, last_chunk_greatest_position: global_position)
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param subscription [PgEventstore::Subscription]
|
62
|
+
# @return [void]
|
63
|
+
def update_subscription_restarts(subscription)
|
64
|
+
subscription.update(last_restarted_at: Time.now.utc, restart_count: subscription.restart_count + 1)
|
65
|
+
end
|
66
|
+
|
67
|
+
# @param subscription [PgEventstore::Subscription]
|
68
|
+
# @param state [String]
|
69
|
+
# @return [void]
|
70
|
+
def update_subscription_state(subscription, state)
|
71
|
+
subscription.update(state: state)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -31,35 +31,19 @@ module PgEventstore
|
|
31
31
|
private
|
32
32
|
|
33
33
|
def attach_runner_callbacks
|
34
|
-
@basic_runner.define_callback(
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
PgEventstore.logger&.error "#{self.class.name}: Backtrace: #{error.backtrace&.join("\n")}"
|
48
|
-
PgEventstore.logger&.error "#{self.class.name}: Trying to auto-repair in #{RESTART_DELAY} seconds..."
|
49
|
-
Thread.new do
|
50
|
-
sleep RESTART_DELAY
|
51
|
-
@basic_runner.restore
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
# @return [PgEventstore::CommandHandlers::SubscriptionFeederCommands]
|
56
|
-
def subscription_feeder_commands
|
57
|
-
CommandHandlers::SubscriptionFeederCommands.new(@config_name, @subscription_feeder)
|
58
|
-
end
|
59
|
-
|
60
|
-
# @return [PgEventstore::CommandHandlers::SubscriptionRunnersCommands]
|
61
|
-
def subscription_runners_commands
|
62
|
-
CommandHandlers::SubscriptionRunnersCommands.new(@config_name, @runners, @subscription_feeder.id)
|
34
|
+
@basic_runner.define_callback(
|
35
|
+
:process_async, :before,
|
36
|
+
CommandsHandlerHandlers.setup_handler(:process_feeder_commands, @config_name, @subscription_feeder)
|
37
|
+
)
|
38
|
+
@basic_runner.define_callback(
|
39
|
+
:process_async, :before,
|
40
|
+
CommandsHandlerHandlers.setup_handler(:process_runners_commands, @config_name, @runners, @subscription_feeder)
|
41
|
+
)
|
42
|
+
|
43
|
+
@basic_runner.define_callback(
|
44
|
+
:after_runner_died, :before,
|
45
|
+
CommandsHandlerHandlers.setup_handler(:restore_runner, @basic_runner, RESTART_DELAY)
|
46
|
+
)
|
63
47
|
end
|
64
48
|
end
|
65
49
|
end
|
@@ -26,7 +26,7 @@ module PgEventstore
|
|
26
26
|
raise EmptyChunkFedError.new("Empty chunk was fed!") if raw_events.empty?
|
27
27
|
|
28
28
|
within_state(:running) do
|
29
|
-
callbacks.run_callbacks(:feed,
|
29
|
+
callbacks.run_callbacks(:feed, Utils.original_global_position(raw_events.last))
|
30
30
|
@raw_events.push(*raw_events)
|
31
31
|
end
|
32
32
|
end
|
@@ -44,47 +44,23 @@ module PgEventstore
|
|
44
44
|
|
45
45
|
private
|
46
46
|
|
47
|
-
# @param raw_event [Hash]
|
48
|
-
# @return [void]
|
49
|
-
def process_event(raw_event)
|
50
|
-
callbacks.run_callbacks(:process, global_position(raw_event)) do
|
51
|
-
@handler.call(raw_event)
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
47
|
def attach_runner_callbacks
|
56
|
-
@basic_runner.define_callback(
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
def after_runner_died(...)
|
73
|
-
callbacks.run_callbacks(:error, ...)
|
74
|
-
end
|
75
|
-
|
76
|
-
def before_runner_restored
|
77
|
-
callbacks.run_callbacks(:restart)
|
78
|
-
end
|
79
|
-
|
80
|
-
def change_state(...)
|
81
|
-
callbacks.run_callbacks(:change_state, ...)
|
82
|
-
end
|
83
|
-
|
84
|
-
# @param raw_event [Hash]
|
85
|
-
# @return [Integer]
|
86
|
-
def global_position(raw_event)
|
87
|
-
raw_event['link'] ? raw_event['link']['global_position'] : raw_event['global_position']
|
48
|
+
@basic_runner.define_callback(
|
49
|
+
:process_async, :before,
|
50
|
+
EventsProcessorHandlers.setup_handler(:process_event, @callbacks, @handler, @raw_events)
|
51
|
+
)
|
52
|
+
|
53
|
+
@basic_runner.define_callback(
|
54
|
+
:after_runner_died, :before, EventsProcessorHandlers.setup_handler(:after_runner_died, callbacks)
|
55
|
+
)
|
56
|
+
|
57
|
+
@basic_runner.define_callback(
|
58
|
+
:before_runner_restored, :before, EventsProcessorHandlers.setup_handler(:before_runner_restored, callbacks)
|
59
|
+
)
|
60
|
+
|
61
|
+
@basic_runner.define_callback(
|
62
|
+
:change_state, :before, EventsProcessorHandlers.setup_handler(:change_state, callbacks)
|
63
|
+
)
|
88
64
|
end
|
89
65
|
end
|
90
66
|
end
|