Module: SqlCmd::AlwaysOn
- Defined in:
- lib/sql_cmd/always_on.rb
Class Method Summary collapse
-
.add_database_to_replica_using_backups(primary_connection_string, replica_connection_string, server_info, database_info, replica_database_info, values: {}, idempotent: false) ⇒ Object
TODO: idempotent log backups and restores are not stable and need improvement.
-
.add_to_availability_group(connection_string, database_name, options = {}, seeding_mode: nil, full_backup_method: nil, async: false, idempotent: false, start_time: Time.now) ⇒ Object
Optionally provide your own lambda for creating a full non-copyonly backup using the full_backup_method parameter Seeding modes: AUTOMATIC (No backup/restore needed to synchronize), MANUAL (traditional backup and restore process) async option is only observed when using automatic seeding.
- .add_to_availability_group_automatically(primary_connection_string, replica_connection_string, database_name, server_info, values: {}, async: false) ⇒ Object
- .database_synchronized?(primary_connection_string, database_name, replica_connection_string: nil) ⇒ Boolean
- .enabled?(connection_string) ⇒ Boolean
- .monitor_automatic_seeding(connection_string, database_name, retries: 3, retry_delay: 15) ⇒ Object
- .remove_from_availability_group(connection_string, database_name) ⇒ Object
Class Method Details
.add_database_to_replica_using_backups(primary_connection_string, replica_connection_string, server_info, database_info, replica_database_info, values: {}, idempotent: false) ⇒ Object
TODO: idempotent log backups and restores are not stable and need improvement. Use ‘idempotent: false’ for now
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/sql_cmd/always_on.rb', line 77 def add_database_to_replica_using_backups(primary_connection_string, replica_connection_string, server_info, database_info, replica_database_info, values: {}, idempotent: false) database_name = database_info['DatabaseName'] configure_primary_for_manual_seeding_script = ::File.read("#{SqlCmd.sql_script_dir}/AlwaysOn/ConfigurePrimaryForManualSeeding.sql") add_to_primary_availability_group_script = ::File.read("#{SqlCmd.sql_script_dir}/AlwaysOn/AddDatabaseToPrimaryAvailabilityGroup.sql") add_to_availability_group_on_secondary_script = ::File.read("#{SqlCmd.sql_script_dir}/AlwaysOn/AddDatabaseToAvailabilityGroupOnSecondary.sql") backup_url = SqlCmd.config['sql_cmd']['always_on_backup_url'] SqlCmd.execute_query(primary_connection_string, configure_primary_for_manual_seeding_script, values: values, retries: 3) if ::Gem::Version.new(server_info['SQLVersion']) >= ::Gem::Version.new('13.0') EasyIO.logger.info "Adding [#{database_name}] to availability group..." SqlCmd.execute_query(primary_connection_string, add_to_primary_availability_group_script, values: values, retries: 3) # Backup database on primary replica EasyIO.logger.header 'AlwaysOn CopyOnly Full Backup' copy_only_full_backup_exists = ::File.exist?("#{values['backupdir']}\\#{database_name}.bak") copy_only_backup_start_time = Time.now if copy_only_full_backup_exists && idempotent copy_only_full_backup_header = SqlCmd.get_sql_backup_headers(primary_connection_string, ["#{values['backupdir']}\\#{database_name}.bak"], values).first copy_only_backup_current = !SqlCmd.gap_in_log_backups?(server_info, copy_only_full_backup_header, replica_database_info['LastRestoreLSN'], values['backupdir'], database_name, values) copy_only_backup_current &&= (database_info['LastRestoreDate'] || Time.at(0)) < copy_only_full_backup_header['BackupFinishDate'] # Make sure the database hasn't been restored again since the last backup EasyIO.logger.debug "LSN of CopyOnly backup: #{copy_only_full_backup_header['DatabaseBackupLSN']}" EasyIO.logger.debug "Last restore LSN: #{replica_database_info['LastRestoreLSN']}" EasyIO.logger.debug "Copy only backup current?: #{copy_only_backup_current}" end if copy_only_full_backup_exists && copy_only_backup_current && idempotent EasyIO.logger.info 'CopyOnly Database backup current. Skipping CopyOnly backup...' else EasyIO.logger.info 'Backing up database on primary server (CopyOnly)...' = { 'formatbackup' => true, 'init' => true, 'rewind' => true, 'nounload' => true, 'compressbackup' => true, 'splitfiles' => false } SqlCmd::Database.backup(copy_only_backup_start_time, primary_connection_string, database_name, backup_folder: values['backupdir'], backup_url: backup_url, backup_basename: database_name, options: ) end # Restore database to secondary replica if it was restored before the copy only backup was created or if it doesn't exist EasyIO.logger.header 'AlwaysOn Restore of CopyOnly Backup to Replica' if !idempotent || !copy_only_full_backup_exists || !copy_only_backup_current || copy_only_full_backup_header.nil? || replica_database_info['LastRestoreDate'].nil? || replica_database_info['LastRestoreDate'] < database_info['LastCopyOnlyFullBackupDate'] || replica_database_info['LastRestoreLSN'] < database_info['LastCopyOnlyLSN'] EasyIO.logger.info 'Restoring database to secondary replica...' = { 'recovery' => false, 'unload' => false, 'secondaryreplica' => true, 'datafilelogicalname' => '', 'logfilelogicalname' => '' } SqlCmd::Database.restore(copy_only_backup_start_time, replica_connection_string, database_name, backup_folder: values['backupdir'], backup_url: backup_url, backup_basename: database_name, options: ) else EasyIO.logger.info 'Restore of CopyOnly database backup current. Skipping restore of CopyOnly database backup...' end sleep(15) # Give time for database to be in correct state # Backup log on primary replica EasyIO.logger.header 'AlwaysOn Log Backup' log_only_backup_start_time = Time.now = { 'logonly' => true, 'skip' => false, 'rewind' => true, 'compressbackup' => true, 'splitfiles' => false } EasyIO.logger.info 'Backing up log file on primary replica...' SqlCmd::Database.backup(log_only_backup_start_time, primary_connection_string, database_name, backup_folder: values['backupdir'], backup_url: backup_url, backup_basename: database_name, options: ) # Restore log on secondary replica EasyIO.logger.header 'AlwaysOn Log Restore to Replica' = { 'recovery' => false, 'unload' => false, 'logonly' => true, 'secondaryreplica' => true } SqlCmd::Database.restore(log_only_backup_start_time, replica_connection_string, database_name, backup_folder: values['backupdir'], backup_url: backup_url, backup_basename: database_name, options: ) # Add to availability group on secondary replica EasyIO.logger.header 'AlwaysOn Database Sync' EasyIO.logger.info 'Waiting for communication and adding to availability group on secondary replica...' SqlCmd.execute_query(replica_connection_string, add_to_availability_group_on_secondary_script, values: values, retries: 3) raise 'Replica does not appear to be in the correct state!' if SqlCmd::Database.info(replica_connection_string, database_name)['AvailabilityGroup'].nil? EasyIO.logger.info "[#{database_name}] added to AlwaysOn availability group successfully..." end |
.add_to_availability_group(connection_string, database_name, options = {}, seeding_mode: nil, full_backup_method: nil, async: false, idempotent: false, start_time: Time.now) ⇒ Object
Optionally provide your own lambda for creating a full non-copyonly backup using the full_backup_method parameter Seeding modes: AUTOMATIC (No backup/restore needed to synchronize), MANUAL (traditional backup and restore process) async option is only observed when using automatic seeding
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/sql_cmd/always_on.rb', line 8 def add_to_availability_group(connection_string, database_name, = {}, seeding_mode: nil, full_backup_method: nil, async: false, idempotent: false, start_time: Time.now) EasyIO.logger.header 'AlwaysOn Availability' start_time = SqlCmd.unify_start_time(start_time) EasyIO.logger.info "Checking if server [#{SqlCmd.connection_string_part(connection_string, :server)}] is configured for AlwaysOn High Availability..." server_info = SqlCmd.get_sql_server_settings(connection_string) if server_info['AvailabilityGroup'].nil? || server_info['SecondaryReplica'].nil? EasyIO.logger.info "Server [#{SqlCmd.connection_string_part(connection_string, :server)}] not configured for AlwaysOn High Availability..." return end seeding_mode ||= server_info['SeedingMode'] || 'MANUAL' database_info = SqlCmd::Database.info(connection_string, database_name) primary_connection_string = SqlCmd.replace_connection_string_part(connection_string, :database, 'master') replica_connection_string = server_info['secondary_replica_connection_string'] replica_database_info = SqlCmd::Database.info(replica_connection_string, database_name) validate_logins_script = SqlCmd.validate_logins_script(connection_string, database_name) logins_migrated = SqlCmd.execute_query(replica_connection_string, validate_logins_script, retries: 3) unless database_info['AvailabilityGroup'].nil? || replica_database_info['AvailabilityGroup'].nil? || !logins_migrated EasyIO.logger.info 'Database already configured for AlwaysOn and synchronized. Skipping...' return end EasyIO.logger.info "Preparing to add [#{database_name}] to AlwaysOn availability group..." always_on_backup_time = EasyTime.yyyymmdd_hhmmss(start_time) full_recovery_script = ::File.read("#{SqlCmd.sql_script_dir}/Database/SetFullRecovery.sql") values = { 'databasename' => database_name, 'datetime' => always_on_backup_time, 'availabilitygroupname' => server_info['AvailabilityGroup'], 'secondaryreplica' => server_info['SecondaryReplica'], 'primarysqlserver' => server_info['ServerName'], 'seedingmode' => seeding_mode.upcase, 'backupdir' => SqlCmd.config['sql_cmd']['backups']['always_on_backup_temp_dir'], }.merge() raise "Could not determine secondary replica for #{server_info['ServerName']}" if server_info['SecondaryReplica'].nil? if database_info['AvailabilityGroup'].nil? || replica_database_info['AvailabilityGroup'].nil? EasyIO.logger.header 'AlwaysOn Full Backup' EasyIO.logger.info "Ensuring database [#{database_name}] is set to full recovery..." recovery_model_updated = SqlCmd.execute_query(connection_string, full_recovery_script, values: values, return_type: :scalar, retries: 3) force_backup = recovery_model_updated || !idempotent SqlCmd::Database.ensure_full_backup_has_occurred(connection_string, database_name, force_backup: force_backup, database_info: database_info, full_backup_method: full_backup_method) if seeding_mode.upcase == 'AUTOMATIC' add_to_availability_group_automatically(primary_connection_string, replica_connection_string, database_name, server_info, values: values, async: async) else add_database_to_replica_using_backups(primary_connection_string, replica_connection_string, server_info, database_info, replica_database_info, values: values, idempotent: idempotent) end else EasyIO.logger.info 'Database already in AlwaysOn Availability group.' end EasyIO.logger.header 'AlwaysOn Permissions Migration to Replica' EasyIO.logger.debug 'Migrating logins to replica...' import_script_filename = SqlCmd.export_logins(start_time, primary_connection_string, database_name) unless import_script_filename.nil? EasyIO.logger.info "Importing logins on [#{SqlCmd.connection_string_part(replica_connection_string, :server)}]..." SqlCmd.execute_script_file(replica_connection_string, import_script_filename) end EasyIO.logger.debug 'Running database_status script...' database_info = SqlCmd::Database.info(connection_string, database_name) replica_database_info = SqlCmd::Database.info(replica_connection_string, database_name) raise 'Failed to add database to AlwaysOn Availability Group' if database_info['AvailabilityGroup'].nil? || replica_database_info['AvailabilityGroup'].nil? EasyIO.logger.info "AlwaysOn availability for [#{database_name}] is configured and active..." end |
.add_to_availability_group_automatically(primary_connection_string, replica_connection_string, database_name, server_info, values: {}, async: false) ⇒ Object
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/sql_cmd/always_on.rb', line 142 def add_to_availability_group_automatically(primary_connection_string, replica_connection_string, database_name, server_info, values: {}, async: false) EasyIO.logger.header 'AlwaysOn Automatic Seeding' configure_primary_for_automatic_seeding_script = ::File.read("#{SqlCmd.sql_script_dir}/AlwaysOn/ConfigurePrimaryForAutomaticSeeding.sql") add_to_primary_availability_group_script = ::File.read("#{SqlCmd.sql_script_dir}/AlwaysOn/AddDatabaseToPrimaryAvailabilityGroup.sql") configure_secondary_for_automatic_seeding_script = ::File.read("#{SqlCmd.sql_script_dir}/AlwaysOn/ConfigureSecondaryForAutomaticSeeding.sql") SqlCmd.execute_query(primary_connection_string, configure_primary_for_automatic_seeding_script, values: values, retries: 3) EasyIO.logger.info "Adding [#{database_name}] to availability group..." SqlCmd.execute_query(primary_connection_string, add_to_primary_availability_group_script, values: values, retries: 3) SqlCmd.execute_query(replica_connection_string, configure_secondary_for_automatic_seeding_script, values: values, retries: 3) if async EasyIO.logger.info "#{database_name} added to availability group #{server_info['AvailabilityGroup']}. AlwaysOn will continue synchronizing in the background." return end monitor_automatic_seeding(primary_connection_string, database_name) raise 'Failed to add to AlwaysOn availability group!' unless database_synchronized?(primary_connection_string, database_name, replica_connection_string: replica_connection_string) EasyIO.logger.info "[#{database_name}] added to AlwaysOn availability group successfully..." end |
.database_synchronized?(primary_connection_string, database_name, replica_connection_string: nil) ⇒ Boolean
216 217 218 219 220 221 222 223 224 225 |
# File 'lib/sql_cmd/always_on.rb', line 216 def database_synchronized?(primary_connection_string, database_name, replica_connection_string: nil) database_info = SqlCmd::Database.info(primary_connection_string, database_name) # TODO: check if AlwaysOn is even enabled on this server if replica_connection_string.nil? || replica_connection_string.empty? server_info = SqlCmd.get_sql_server_settings(primary_connection_string) replica_connection_string = server_info['secondary_replica_connection_string'] end replica_database_info = replica_connection_string.nil? ? {} : SqlCmd::Database.info(replica_connection_string, database_name) !database_info['AvailabilityGroup'].nil? && !replica_database_info['AvailabilityGroup'].nil? end |
.enabled?(connection_string) ⇒ Boolean
269 270 271 272 |
# File 'lib/sql_cmd/always_on.rb', line 269 def enabled?(connection_string) server_info = SqlCmd.get_sql_server_settings(connection_string) !server_info['AvailabilityGroup'].nil? && !server_info['SecondaryReplica'].nil? end |
.monitor_automatic_seeding(connection_string, database_name, retries: 3, retry_delay: 15) ⇒ Object
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/sql_cmd/always_on.rb', line 163 def monitor_automatic_seeding(connection_string, database_name, retries: 3, retry_delay: 15) EasyIO.logger.header 'Monitoring AlwaysOn Seeding' progress_script = ::File.read("#{SqlCmd.sql_script_dir}/AlwaysOn/AutomaticSeedingProgress.sql") values = { 'databasename' => database_name } seeding_status = SqlCmd.execute_query(connection_string, progress_script, values: values, return_type: :first_row, retries: 3) if seeding_status['current_state'] == 'COMPLETED' EasyIO.logger.info 'AlwaysOn seeding complete.' return end start_time = Time.now seeding_start_timeout = 300 # Allow 5 minutes to start the seeding process before failing timeout = 86400 # 24 hours progress_interval = 4 # seconds begin while Time.now < start_time + timeout seeding_status = SqlCmd.execute_query(connection_string, progress_script, values: values, return_type: :first_row, retries: 3) if seeding_status['current_state'] == 'COMPLETED' EasyIO.logger.info 'AlwaysOn seeding complete.' return end if seeding_status['transferred_size_percent_complete'].nil? || seeding_status['current_state'] == 'FAILED' unless Time.now > start_time + seeding_start_timeout EasyIO.logger.info 'Waiting for automatic synchronization process...' sleep(progress_interval) next end raise "Failed to seed #{database_name} automatically! Check the synchronization status." if seeding_status.empty? = "Failed to seed #{database_name} automatically in availability group [#{seeding_status['ag_name']}]! Check the synchronization status on #{seeding_status['replica_server_name']}.\n" += seeding_status['failure_state_desc'] unless seeding_status['failure_state_desc'].nil? raise end percent_complete = seeding_status['transferred_size_percent_complete'] percent_complete = 0.1 if percent_complete == 0 # Avoid divide by zero elapsed_min = (Time.now - start_time) / 60 eta_min = (elapsed_min / (percent_complete / 100.0)) - elapsed_min eta_time = Time.now + (eta_min * 60).round EasyIO.logger.info "Percent complete: #{percent_complete.round(1)} / Elapsed min: #{elapsed_min.round(2)} / Min remaining: #{eta_min.round(2)} / ETA: #{eta_time}" sleep(progress_interval) end rescue raise if (retries -= 1) < 0 EasyIO.logger.warn "Failure monitoring AlwaysOn seeding. Retrying in #{retry_delay} seconds..." sleep(retry_delay) retry end raise "Automatic seeding timed out after #{timeout / 60 / 60} hours!" # only gets here if status never reached 'COMPLETED' end |
.remove_from_availability_group(connection_string, database_name) ⇒ Object
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 |
# File 'lib/sql_cmd/always_on.rb', line 227 def remove_from_availability_group(connection_string, database_name) EasyIO.logger.header 'AlwaysOn Availability Removal' EasyIO.logger.info "Checking if server [#{SqlCmd.connection_string_part(connection_string, :server)}] is configured for AlwaysOn High Availability..." server_info = SqlCmd.get_sql_server_settings(connection_string) if server_info['AvailabilityGroup'].nil? || server_info['SecondaryReplica'].nil? EasyIO.logger.info "Server [#{SqlCmd.connection_string_part(connection_string, :server)}] not configured for AlwaysOn High Availability..." return end primary_connection_string = SqlCmd.replace_connection_string_part(server_info['direct_connection_string'], :database, 'master') replica_connection_string = server_info['secondary_replica_connection_string'] primary_server_name = SqlCmd.connection_string_part(primary_connection_string, :server) database_info = SqlCmd::Database.info(primary_connection_string, database_name) replica_database_info = SqlCmd::Database.info(replica_connection_string, database_name) if database_info['DatabaseNotFound'] EasyIO.logger.info "Skipping removal from availability group: database [#{database_name}] does not exist on [#{primary_server_name}]..." return end if database_info['AvailabilityGroup'].nil? && replica_database_info['DatabaseNotFound'] EasyIO.logger.info "Skipping removal from availability group: database [#{database_name}] does not belong to an AvailabilityGroup on [#{primary_server_name}]..." return end remove_database_from_group_script = ::File.read("#{SqlCmd.sql_script_dir}/AlwaysOn/RemoveDatabaseFromGroup.sql") drop_database_from_secondary_script = ::File.read("#{SqlCmd.sql_script_dir}/AlwaysOn/DropSecondary.sql") values = { 'databasename' => database_name, 'availabilitygroupname' => server_info['AvailabilityGroup'] } EasyIO.logger.header 'AlwaysOn Remove From Group' EasyIO.logger.info "Removing [#{database_name}] from availability group..." SqlCmd.execute_query(primary_connection_string, remove_database_from_group_script, values: values, retries: 3) unless database_info['AvailabilityGroup'].nil? EasyIO.logger.header 'AlwaysOn Drop Database on Replica' drop_result = SqlCmd.execute_query(replica_connection_string, drop_database_from_secondary_script, return_type: :scalar, values: values, retries: 3) EasyIO.logger.debug "Drop secondary replica database result: #{drop_result}" raise 'Failed to drop database from secondary replica!' unless drop_result database_info = SqlCmd::Database.info(primary_connection_string, database_name) replica_database_info = SqlCmd::Database.info(replica_connection_string, database_name) raise 'Failed to remove database from AlwaysOn Availability Group' unless database_info['AvailabilityGroup'].nil? && replica_database_info['AvailabilityGroup'].nil? EasyIO.logger.info "[#{database_name}] removed from AlwaysOn availability group successfully..." end |