aws_advanced_ruby_driver_wrapper 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE +175 -0
- data/NOTICE +1 -0
- data/README.md +168 -0
- data/THIRD-PARTY-LICENSES +473 -0
- data/aws_advanced_ruby_driver_wrapper.gemspec +73 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_mysql2_adapter.rb +73 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/custom_configuration.rb +58 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_mysql_dialect.rb +103 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_pg_dialect.rb +124 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/dialect_codes.rb +38 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_mysql_dialect.rb +91 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_pg_dialect.rb +92 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_mysql_dialect.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_pg_dialect.rb +86 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/mysql_dialect.rb +98 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/pg_dialect.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_mysql_dialect.rb +88 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_pg_dialect.rb +86 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/unknown_dialect.rb +72 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/utils/dialect_utils.rb +71 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +154 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +55 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +165 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +201 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +62 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +80 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/pg_error_handler.rb +126 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +59 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/connection_string_host_list_provider.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/global_aurora_host_list_provider.rb +65 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability.rb +24 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability_strategy.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_info.rb +137 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_role.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/random_host_selector.rb +40 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/rds_host_list_provider.rb +206 -0
- data/lib/aws_advanced_ruby_driver_wrapper/logging.rb +110 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +709 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/global_cluster_topology_monitor.rb +72 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor.rb +99 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_connection.rb +57 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_state.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +429 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +205 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +132 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/iam_host_tracker.rb +84 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interim_status.rb +92 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interval_rate.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase.rb +69 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_event_log.rb +85 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_time_info.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/role.rb +38 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +83 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/reject_connect_routing.rb +40 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +136 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +53 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +52 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +83 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status.rb +68 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +244 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_info.rb +30 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +564 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +414 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_state.rb +98 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_timer.rb +46 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +266 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +158 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/info.rb +111 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/member_list_type.rb +31 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/role.rb +45 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +108 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_mode.rb +43 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +467 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_mode.rb +68 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +403 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +159 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +485 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/audit_logger.rb +157 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_cipher.rb +159 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_encryption_config.rb +61 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/connection_source.rb +91 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/data_key_cache.rb +220 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_algorithm.rb +75 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_config.rb +146 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_service.rb +391 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/error_context.rb +198 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/errors.rb +259 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +435 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +378 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_metadata.rb +86 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +890 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_utility.rb +281 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/metadata_manager.rb +332 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sanitizer.rb +147 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_name.rb +70 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_validator.rb +211 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql_runner.rb +147 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/type_marker.rb +109 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +358 -0
- data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +659 -0
- data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +409 -0
- data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +122 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/connection_service.rb +143 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +267 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +199 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +186 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_call_context.rb +63 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +273 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +30 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +78 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/session_state_service.rb +56 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/accessible_regions.rb +52 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/ar_constants.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/aurora_topology_utils.rb +99 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/aws_credentials_utils.rb +62 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +91 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +368 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/conversion_utils.rb +51 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/events/batching_event_publisher.rb +119 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/events/data_access_event.rb +26 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/events/monitor_reset_event.rb +26 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +185 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/host_list_utils.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +112 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/multi_az_topology_utils.rb +117 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +99 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +641 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +502 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +63 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_type.rb +35 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint_parser.rb +50 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +139 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_url_type.rb +71 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_utils.rb +575 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +153 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_encoding.rb +56 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_method_analyzer.rb +195 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/cache_entry.rb +56 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/expiration_cache.rb +108 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/sliding_expiration_cache.rb +137 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +172 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +127 -0
- data/lib/aws_advanced_ruby_driver_wrapper/version.rb +19 -0
- data/lib/aws_advanced_ruby_driver_wrapper/wrapper_property.rb +64 -0
- data/lib/aws_advanced_ruby_driver_wrapper.rb +116 -0
- metadata +227 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
|
6
|
+
# You may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
require 'concurrent'
|
|
18
|
+
require_relative '../../logging'
|
|
19
|
+
require_relative '../../property_definition'
|
|
20
|
+
require_relative '../../utils/rds_utils'
|
|
21
|
+
require_relative 'iam_host_tracker'
|
|
22
|
+
require_relative 'interval_rate'
|
|
23
|
+
require_relative 'host_mapper'
|
|
24
|
+
require_relative 'phase'
|
|
25
|
+
require_relative 'phase_event_log'
|
|
26
|
+
require_relative 'phase_time_info'
|
|
27
|
+
require_relative 'role'
|
|
28
|
+
require_relative 'status'
|
|
29
|
+
require_relative 'status_builder'
|
|
30
|
+
require_relative 'status_monitor'
|
|
31
|
+
require_relative 'switchover_state'
|
|
32
|
+
require_relative 'switchover_timer'
|
|
33
|
+
require_relative '../../plugins/iam_auth_plugin'
|
|
34
|
+
|
|
35
|
+
module AwsAdvancedRubyDriverWrapper
|
|
36
|
+
module Plugins
|
|
37
|
+
module BlueGreen
|
|
38
|
+
class StatusProvider
|
|
39
|
+
include Logging
|
|
40
|
+
|
|
41
|
+
DEFAULT_CONNECT_TIMEOUT_SEC = 10
|
|
42
|
+
DEFAULT_SOCKET_TIMEOUT_SEC = 10
|
|
43
|
+
|
|
44
|
+
def initialize(service_container, wrapper_props, bgd_id, cluster_id)
|
|
45
|
+
@service_container = service_container
|
|
46
|
+
@storage_service = service_container.storage_service
|
|
47
|
+
@wrapper_props = wrapper_props
|
|
48
|
+
@bgd_id = bgd_id
|
|
49
|
+
@cluster_id = cluster_id
|
|
50
|
+
|
|
51
|
+
@monitoring_wrapper_props = build_monitoring_wrapper_props
|
|
52
|
+
@monitoring_driver_props = build_monitoring_driver_props
|
|
53
|
+
|
|
54
|
+
@monitors = { Role::SOURCE => nil, Role::TARGET => nil }
|
|
55
|
+
@interim_statuses = { Role::SOURCE => nil, Role::TARGET => nil }
|
|
56
|
+
@interim_status_hashes = { Role::SOURCE => 0, Role::TARGET => 0 }
|
|
57
|
+
@last_context_hash = 0
|
|
58
|
+
@summary_status = nil
|
|
59
|
+
@latest_status_phase = Phase::NOT_CREATED
|
|
60
|
+
@rollback = false
|
|
61
|
+
@process_status_lock = Mutex.new
|
|
62
|
+
@monitor_generation = 0
|
|
63
|
+
|
|
64
|
+
@status_check_interval_map = {
|
|
65
|
+
IntervalRate::BASELINE => PropertyDefinition::BG_INTERVAL_BASELINE_SEC.get_float(wrapper_props) * 1000,
|
|
66
|
+
IntervalRate::INCREASED => PropertyDefinition::BG_INTERVAL_INCREASED_SEC.get_float(wrapper_props) * 1000,
|
|
67
|
+
IntervalRate::HIGH => PropertyDefinition::BG_INTERVAL_HIGH_SEC.get_float(wrapper_props) * 1000
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@green_topology_recognized_logged = false
|
|
71
|
+
|
|
72
|
+
@timer = SwitchoverTimer.new(PropertyDefinition::BG_SWITCHOVER_TIMEOUT_SEC.get_float(wrapper_props) * 1_000_000_000)
|
|
73
|
+
@event_log = PhaseEventLog.new
|
|
74
|
+
@host_mapper = HostMapper.new
|
|
75
|
+
@iam_tracker = IamHostTracker.new(on_all_changed: method(:on_all_green_hosts_changed))
|
|
76
|
+
@switchover_state = SwitchoverState.new(
|
|
77
|
+
on_monitor_reset: method(:publish_monitor_reset),
|
|
78
|
+
on_event: method(:record_event)
|
|
79
|
+
)
|
|
80
|
+
@builder = StatusBuilder.new(@bgd_id, @host_mapper, @timer, @interim_statuses, @iam_tracker, @switchover_state)
|
|
81
|
+
|
|
82
|
+
dialect = service_container.dialect_service.db_dialect
|
|
83
|
+
if dialect.respond_to?(:blue_green_status_available?)
|
|
84
|
+
init_monitoring
|
|
85
|
+
else
|
|
86
|
+
logger.warn do
|
|
87
|
+
"[bgd_id: '#{@bgd_id}'] Blue/Green Deployments isn't supported by database dialect #{dialect.class.name.split('::').last}."
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def stop
|
|
93
|
+
@monitors.each_value { |m| m&.stop }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def log_switchover_final_summary
|
|
97
|
+
return unless switchover_finalized?
|
|
98
|
+
|
|
99
|
+
logger.info(@event_log.summary(@bgd_id, @rollback))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def log_current_context
|
|
103
|
+
phase_str = @summary_status&.current_phase&.to_s || '<null>'
|
|
104
|
+
logger.debug { "[bgd_id: '#{@bgd_id}'] BG status: #{phase_str}" }
|
|
105
|
+
logger.debug { "[bgd_id: '#{@bgd_id}'] Summary status:\n#{@summary_status || '<null>'}" }
|
|
106
|
+
logger.debug { "Corresponding hosts:\n#{@host_mapper.to_debug_s}" }
|
|
107
|
+
logger.debug { "Phase times:\n#{@event_log.to_debug_s}" }
|
|
108
|
+
logger.debug { "Green host certificate change times:\n#{@iam_tracker.to_debug_s}" }
|
|
109
|
+
logger.debug { "\n latest_status_phase: #{@latest_status_phase}\n#{@switchover_state.to_debug_s(@iam_tracker)}" }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def log_green_topology_recognized
|
|
113
|
+
return if @green_topology_recognized_logged
|
|
114
|
+
return unless green_topology_ready?
|
|
115
|
+
|
|
116
|
+
@green_topology_recognized_logged = true
|
|
117
|
+
|
|
118
|
+
source_status = @interim_statuses[Role::SOURCE]
|
|
119
|
+
target_status = @interim_statuses[Role::TARGET]
|
|
120
|
+
source_host_count = source_status.host_names.size
|
|
121
|
+
target_host_count = target_status.host_names.size
|
|
122
|
+
corresponding_host_count = @host_mapper.corresponding_hosts.count { |_, v| v[1] }
|
|
123
|
+
|
|
124
|
+
mapping_str = format_host_mapping
|
|
125
|
+
ip_str = format_ip_mapping
|
|
126
|
+
|
|
127
|
+
logger.info do
|
|
128
|
+
"[bgd_id: '#{@bgd_id}'] Blue/Green target topology recognized\n " \
|
|
129
|
+
"phase: #{@latest_status_phase}\n " \
|
|
130
|
+
"source_hosts: #{source_host_count}\n " \
|
|
131
|
+
"target_hosts: #{target_host_count}\n " \
|
|
132
|
+
"corresponding_hosts: #{corresponding_host_count}\n " \
|
|
133
|
+
"ready: true\n " \
|
|
134
|
+
"Blue -> Green Mapping:\n " \
|
|
135
|
+
"#{mapping_str}\n " \
|
|
136
|
+
"Host -> IP Mapping:\n " \
|
|
137
|
+
"#{ip_str}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def green_topology_ready?
|
|
142
|
+
source_status = @interim_statuses[Role::SOURCE]
|
|
143
|
+
target_status = @interim_statuses[Role::TARGET]
|
|
144
|
+
|
|
145
|
+
source_status &&
|
|
146
|
+
target_status &&
|
|
147
|
+
source_status.start_topology&.any? &&
|
|
148
|
+
target_status.start_topology&.any? &&
|
|
149
|
+
source_status.host_names&.any? &&
|
|
150
|
+
target_status.host_names&.any? &&
|
|
151
|
+
@host_mapper.corresponding_hosts.any?
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def format_host_mapping
|
|
155
|
+
rows = @host_mapper.corresponding_hosts.sort_by { |k, _| k }.map do |blue, pair|
|
|
156
|
+
"#{blue} -> #{pair[1]&.host_and_port || '<null>'}"
|
|
157
|
+
end
|
|
158
|
+
rows.any? ? rows.join("\n ") : '-'
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def format_ip_mapping
|
|
162
|
+
rows = @host_mapper.host_ip_addresses.sort_by { |k, _| k }.map do |host, ip|
|
|
163
|
+
"#{host} -> #{ip || '<null>'}"
|
|
164
|
+
end
|
|
165
|
+
rows.any? ? rows.join("\n ") : '-'
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def check_switchover_timer_expiry
|
|
169
|
+
return unless @timer.expired?
|
|
170
|
+
return unless [Phase::IN_PROGRESS, Phase::POST, Phase::PREPARATION].include?(@latest_status_phase)
|
|
171
|
+
|
|
172
|
+
logger.warn { 'Blue/Green switchover has timed out.' }
|
|
173
|
+
@summary_status = @rollback ? @builder.created : @builder.completed(@rollback)
|
|
174
|
+
update_monitors
|
|
175
|
+
update_status_cache
|
|
176
|
+
log_current_context
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def reset_context_when_completed
|
|
180
|
+
return unless switchover_finalized?
|
|
181
|
+
|
|
182
|
+
logger.debug { 'Resetting context.' }
|
|
183
|
+
|
|
184
|
+
old_monitors = @monitors
|
|
185
|
+
@monitor_generation += 1
|
|
186
|
+
@monitors = { Role::SOURCE => nil, Role::TARGET => nil }
|
|
187
|
+
old_monitors.each_value { |m| m&.stop }
|
|
188
|
+
|
|
189
|
+
Plugins::IamAuthPlugin.clear_cache(@storage_service) if iam_enabled?
|
|
190
|
+
|
|
191
|
+
@rollback = false
|
|
192
|
+
@green_topology_recognized_logged = false
|
|
193
|
+
@summary_status = nil
|
|
194
|
+
@latest_status_phase = Phase::NOT_CREATED
|
|
195
|
+
@interim_status_hashes = { Role::SOURCE => 0, Role::TARGET => 0 }
|
|
196
|
+
@last_context_hash = 0
|
|
197
|
+
@interim_statuses = { Role::SOURCE => nil, Role::TARGET => nil }
|
|
198
|
+
|
|
199
|
+
@timer.reset
|
|
200
|
+
@event_log.clear
|
|
201
|
+
@host_mapper.clear
|
|
202
|
+
@iam_tracker.clear
|
|
203
|
+
@switchover_state.reset
|
|
204
|
+
@builder = StatusBuilder.new(@bgd_id, @host_mapper, @timer, @interim_statuses, @iam_tracker, @switchover_state)
|
|
205
|
+
|
|
206
|
+
init_monitoring
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def update_summary_status(role, interim_status)
|
|
210
|
+
case @latest_status_phase
|
|
211
|
+
when Phase::NOT_CREATED then @summary_status = Status.new(@bgd_id, Phase::NOT_CREATED)
|
|
212
|
+
when Phase::CREATED
|
|
213
|
+
@switchover_state.update_dns_flags(@bgd_id, role, interim_status)
|
|
214
|
+
@summary_status = @builder.created
|
|
215
|
+
when Phase::PREPARATION
|
|
216
|
+
@timer.start
|
|
217
|
+
@switchover_state.update_dns_flags(@bgd_id, role, interim_status)
|
|
218
|
+
@summary_status = @builder.preparation(@rollback)
|
|
219
|
+
when Phase::IN_PROGRESS
|
|
220
|
+
@switchover_state.update_dns_flags(@bgd_id, role, interim_status)
|
|
221
|
+
@summary_status = @builder.in_progress(@rollback)
|
|
222
|
+
@switchover_state.trigger_in_progress_monitor_reset
|
|
223
|
+
when Phase::POST
|
|
224
|
+
@switchover_state.update_dns_flags(@bgd_id, role, interim_status)
|
|
225
|
+
@summary_status = @builder.post(@rollback)
|
|
226
|
+
when Phase::COMPLETED
|
|
227
|
+
@switchover_state.update_dns_flags(@bgd_id, role, interim_status)
|
|
228
|
+
@summary_status = @builder.completed(@rollback)
|
|
229
|
+
else
|
|
230
|
+
raise ArgumentError, "[bgd_id: '#{@bgd_id}'] Unknown BG phase '#{@latest_status_phase}'."
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
MONITOR_SETTINGS = {
|
|
235
|
+
Phase::NOT_CREATED => { interval: IntervalRate::BASELINE, collect_ips: false, collect_topo: false, use_ip: false }.freeze,
|
|
236
|
+
Phase::CREATED => { interval: IntervalRate::INCREASED, collect_ips: true, collect_topo: true, use_ip: false }.freeze,
|
|
237
|
+
Phase::PREPARATION => { interval: IntervalRate::HIGH, collect_ips: false, collect_topo: false, use_ip: true }.freeze,
|
|
238
|
+
Phase::IN_PROGRESS => { interval: IntervalRate::HIGH, collect_ips: false, collect_topo: false, use_ip: true }.freeze,
|
|
239
|
+
Phase::POST => { interval: IntervalRate::HIGH, collect_ips: false, collect_topo: false, use_ip: true }.freeze,
|
|
240
|
+
Phase::COMPLETED => { interval: IntervalRate::BASELINE, collect_ips: false, collect_topo: false, use_ip: false }.freeze
|
|
241
|
+
}.freeze
|
|
242
|
+
|
|
243
|
+
def update_monitors
|
|
244
|
+
settings = MONITOR_SETTINGS.fetch(@summary_status.current_phase) do
|
|
245
|
+
raise ArgumentError, "[bgd_id: '#{@bgd_id}'] Unknown BG phase '#{@summary_status.current_phase}'."
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
@monitors.values.compact.each do |monitor|
|
|
249
|
+
monitor.interval_rate = settings[:interval]
|
|
250
|
+
monitor.collect_ip_addresses = settings[:collect_ips]
|
|
251
|
+
monitor.collect_topology = settings[:collect_topo]
|
|
252
|
+
monitor.use_ip_address = settings[:use_ip]
|
|
253
|
+
monitor.reset_collected_data if (@rollback && @summary_status.current_phase == Phase::CREATED) ||
|
|
254
|
+
@summary_status.current_phase == Phase::COMPLETED
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def update_status_cache
|
|
259
|
+
latest_status = @storage_service.get(BlueGreenPlugin::BLUE_GREEN_NAME, @bgd_id)
|
|
260
|
+
@storage_service.set(BlueGreenPlugin::BLUE_GREEN_NAME, @bgd_id, @summary_status)
|
|
261
|
+
store_phase_time(@summary_status.current_phase)
|
|
262
|
+
latest_status&.notify
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def update_phase(role, interim_status)
|
|
266
|
+
new_phase = interim_status.blue_green_phase
|
|
267
|
+
latest_interim_phase = @interim_statuses[role]&.blue_green_phase || Phase::NOT_CREATED
|
|
268
|
+
|
|
269
|
+
if role == Role::TARGET &&
|
|
270
|
+
!new_phase.nil? &&
|
|
271
|
+
@latest_status_phase != Phase::COMPLETED &&
|
|
272
|
+
new_phase < latest_interim_phase
|
|
273
|
+
@rollback = true
|
|
274
|
+
logger.debug { "[bgd_id: '#{@bgd_id}'] Blue/Green deployment is in rollback mode." }
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
return if new_phase.nil?
|
|
278
|
+
|
|
279
|
+
if @rollback
|
|
280
|
+
@latest_status_phase = new_phase if new_phase < @latest_status_phase
|
|
281
|
+
elsif new_phase > @latest_status_phase
|
|
282
|
+
@latest_status_phase = new_phase
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def prepare_status(role, interim_status, generation = @monitor_generation)
|
|
287
|
+
@process_status_lock.synchronize do
|
|
288
|
+
return if generation != @monitor_generation
|
|
289
|
+
|
|
290
|
+
status_hash = interim_status.hash
|
|
291
|
+
ctx_hash = context_hash
|
|
292
|
+
|
|
293
|
+
if @interim_status_hashes[role] == status_hash && @last_context_hash == ctx_hash
|
|
294
|
+
check_switchover_timer_expiry
|
|
295
|
+
return
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
logger.debug { "[bgd_id: '#{@bgd_id}', role: #{role}] #{interim_status}" }
|
|
299
|
+
|
|
300
|
+
update_phase(role, interim_status)
|
|
301
|
+
|
|
302
|
+
@interim_statuses[role] = interim_status
|
|
303
|
+
@interim_status_hashes[role] = status_hash
|
|
304
|
+
@last_context_hash = ctx_hash
|
|
305
|
+
|
|
306
|
+
@host_mapper.merge_ips(interim_status.start_ip_addresses_by_host)
|
|
307
|
+
@host_mapper.register_role(interim_status.host_names, role)
|
|
308
|
+
@host_mapper.update(@interim_statuses[Role::SOURCE], @interim_statuses[Role::TARGET])
|
|
309
|
+
|
|
310
|
+
update_summary_status(role, interim_status)
|
|
311
|
+
check_switchover_timer_expiry
|
|
312
|
+
update_monitors
|
|
313
|
+
update_status_cache
|
|
314
|
+
log_current_context
|
|
315
|
+
log_green_topology_recognized
|
|
316
|
+
log_switchover_final_summary
|
|
317
|
+
reset_context_when_completed
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
private
|
|
322
|
+
|
|
323
|
+
def record_event(label, phase: nil)
|
|
324
|
+
@event_log.record(label, @rollback ? ' (rollback)' : '', phase: phase)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def store_phase_time(phase)
|
|
328
|
+
return if phase.nil?
|
|
329
|
+
|
|
330
|
+
record_event(phase.to_s, phase: phase)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def on_all_green_hosts_changed
|
|
334
|
+
record_event('Green host certificates changed')
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def publish_monitor_reset(_event_name)
|
|
338
|
+
blue_endpoints = @summary_status.role_by_host
|
|
339
|
+
.select { |_host, role| role == Role::SOURCE }
|
|
340
|
+
.keys
|
|
341
|
+
.to_set
|
|
342
|
+
|
|
343
|
+
@service_container.event_publisher.publish(
|
|
344
|
+
Utils::Events::MonitorResetEvent.new(cluster_id: @cluster_id, endpoints: blue_endpoints)
|
|
345
|
+
)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def switchover_finalized?
|
|
349
|
+
completed = (!@rollback && @summary_status.current_phase == Phase::COMPLETED) ||
|
|
350
|
+
(@rollback && @summary_status.current_phase == Phase::CREATED)
|
|
351
|
+
completed && @event_log.any? { |_, v| v.phase&.active_switchover_or_completed? }
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def iam_enabled?
|
|
355
|
+
@service_container.plugin_manager&.plugin_in_use?(Plugins::IamAuthPlugin) || false
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def context_hash
|
|
359
|
+
[@iam_tracker.all_changed?, @iam_tracker.size].hash
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def init_monitoring
|
|
363
|
+
generation = @monitor_generation
|
|
364
|
+
[Role::SOURCE, Role::TARGET].each do |role|
|
|
365
|
+
monitor = StatusMonitor.new(
|
|
366
|
+
role,
|
|
367
|
+
@bgd_id,
|
|
368
|
+
@service_container.connection_service.current_host_info,
|
|
369
|
+
@service_container,
|
|
370
|
+
monitoring_driver_props: @monitoring_driver_props,
|
|
371
|
+
status_check_interval_map: @status_check_interval_map,
|
|
372
|
+
on_change_func: ->(r, s) { prepare_status(r, s, generation) },
|
|
373
|
+
monitoring_wrapper_props: @monitoring_wrapper_props
|
|
374
|
+
)
|
|
375
|
+
@monitors[role] = monitor
|
|
376
|
+
monitor.start
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Wrapper overrides are merged onto base wrapper_props with defaults applied.
|
|
381
|
+
# @return Hash monitoring_wrapper_props
|
|
382
|
+
def build_monitoring_wrapper_props
|
|
383
|
+
prefixed_wrapper_config = @service_container.connection_service.prefixed_wrapper_config[PropertyDefinition::BG_MONITORING_PROPERTY_PREFIX] || {}
|
|
384
|
+
monitoring_wrapper_props = @wrapper_props.dup
|
|
385
|
+
|
|
386
|
+
prefixed_wrapper_config.each do |key, value|
|
|
387
|
+
monitoring_wrapper_props[key] = value
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Ensure BG monitoring connections bypass BG routing.
|
|
391
|
+
monitoring_wrapper_props[BlueGreenPlugin::BG_SKIP_ROUTING_KEY] = true
|
|
392
|
+
|
|
393
|
+
monitoring_wrapper_props
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Driver overrides are merged onto base driver_props with defaults applied.
|
|
397
|
+
# @return Hash monitoring_driver_props
|
|
398
|
+
def build_monitoring_driver_props
|
|
399
|
+
prefixed_driver_props = @service_container.connection_service.prefixed_driver_config[PropertyDefinition::BG_MONITORING_PROPERTY_PREFIX] || {}
|
|
400
|
+
monitoring_driver_props = @service_container.connection_service.driver_props.dup
|
|
401
|
+
|
|
402
|
+
prefixed_driver_props.each do |key, value|
|
|
403
|
+
monitoring_driver_props[key] = value
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
monitoring_driver_props[:connect_timeout] ||= DEFAULT_CONNECT_TIMEOUT_SEC
|
|
407
|
+
monitoring_driver_props[:read_timeout] ||= DEFAULT_SOCKET_TIMEOUT_SEC unless @service_container.connection_service.pg?
|
|
408
|
+
|
|
409
|
+
monitoring_driver_props
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
|
6
|
+
# You may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
require 'concurrent'
|
|
18
|
+
require_relative '../../logging'
|
|
19
|
+
require_relative 'role'
|
|
20
|
+
|
|
21
|
+
module AwsAdvancedRubyDriverWrapper
|
|
22
|
+
module Plugins
|
|
23
|
+
module BlueGreen
|
|
24
|
+
# Tracks DNS and topology change milestones during a switchover.
|
|
25
|
+
# Fires callbacks when each milestone is first reached so StatusProvider can
|
|
26
|
+
# record events and trigger monitor resets at the right moments.
|
|
27
|
+
class SwitchoverState
|
|
28
|
+
include Logging
|
|
29
|
+
|
|
30
|
+
attr_reader :blue_dns_update_completed, :green_dns_removed, :green_topology_changed
|
|
31
|
+
|
|
32
|
+
def initialize(on_monitor_reset:, on_event:)
|
|
33
|
+
@on_monitor_reset = on_monitor_reset
|
|
34
|
+
@on_event = on_event
|
|
35
|
+
|
|
36
|
+
@blue_dns_update_completed = false
|
|
37
|
+
@green_dns_removed = false
|
|
38
|
+
@green_topology_changed = false
|
|
39
|
+
|
|
40
|
+
@monitor_reset_on_in_progress_completed = Concurrent::AtomicBoolean.new(false)
|
|
41
|
+
@monitor_reset_on_topology_completed = Concurrent::AtomicBoolean.new(false)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def update_dns_flags(bgd_id, role, interim_status)
|
|
45
|
+
if role == Role::SOURCE && !@blue_dns_update_completed && interim_status.all_start_topology_ip_changed
|
|
46
|
+
logger.debug { "[bgd_id: '#{bgd_id}'] Blue DNS update completed." }
|
|
47
|
+
@blue_dns_update_completed = true
|
|
48
|
+
@on_event.call('Blue DNS updated')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
return unless role == Role::TARGET
|
|
52
|
+
|
|
53
|
+
if !@green_dns_removed && interim_status.all_start_topology_endpoints_removed
|
|
54
|
+
logger.debug { "[bgd_id: '#{bgd_id}'] Green DNS removed." }
|
|
55
|
+
@green_dns_removed = true
|
|
56
|
+
@on_event.call('Green DNS removed')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
return unless !@green_topology_changed && interim_status.all_topology_changed
|
|
60
|
+
|
|
61
|
+
logger.debug { "[bgd_id: '#{bgd_id}'] Green topology changed." }
|
|
62
|
+
@green_topology_changed = true
|
|
63
|
+
@on_event.call('Green topology changed')
|
|
64
|
+
trigger_monitor_reset(@monitor_reset_on_topology_completed, '- green topology')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def trigger_in_progress_monitor_reset
|
|
68
|
+
trigger_monitor_reset(@monitor_reset_on_in_progress_completed, '- start')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def reset
|
|
72
|
+
@blue_dns_update_completed = false
|
|
73
|
+
@green_dns_removed = false
|
|
74
|
+
@green_topology_changed = false
|
|
75
|
+
@monitor_reset_on_in_progress_completed.make_false
|
|
76
|
+
@monitor_reset_on_topology_completed.make_false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def to_debug_s(iam_tracker)
|
|
80
|
+
" blue_dns_update_completed: #{@blue_dns_update_completed}\n " \
|
|
81
|
+
"green_dns_removed: #{@green_dns_removed}\n " \
|
|
82
|
+
"green_host_changed_name: #{iam_tracker.all_changed?}\n " \
|
|
83
|
+
"green_topology_changed: #{@green_topology_changed}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def trigger_monitor_reset(flag, event_name)
|
|
89
|
+
return unless flag.false?
|
|
90
|
+
return unless flag.make_true
|
|
91
|
+
|
|
92
|
+
@on_monitor_reset.call(event_name)
|
|
93
|
+
@on_event.call("Monitors reset #{event_name}")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
|
6
|
+
# You may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module AwsAdvancedRubyDriverWrapper
|
|
18
|
+
module Plugins
|
|
19
|
+
module BlueGreen
|
|
20
|
+
class SwitchoverTimer
|
|
21
|
+
def initialize(timeout_nano)
|
|
22
|
+
@timeout_nano = timeout_nano
|
|
23
|
+
@end_time_nano = 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def start
|
|
27
|
+
@end_time_nano = nano_time + @timeout_nano if @end_time_nano.zero?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def expired?
|
|
31
|
+
@end_time_nano.positive? && @end_time_nano < nano_time
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def reset
|
|
35
|
+
@end_time_nano = 0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def nano_time
|
|
41
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|