require 'tidy_reset/configuration' require 'tidy_reset/version' require 'open3' module TidyReset class RakeExecuteException < StandardError; end class RakeStatusException < StandardError; end class RakeCompletedException < StandardError; end class << self attr_accessor :configuration def configuration @configuration ||= Configuration.new end def configure yield(configuration) end def process(line) puts line @execute_error = RakeExecuteException if line =~ /rake aborted/ @rake_completed = true if line =~ /Tidy task done./ end def execute(command) Bundler.with_clean_env do begin tries ||= 3 @execute_error = false @rake_completed = false puts "running: #{command}" Open3.popen3(command) do |stdin, stdout, stderr, wait_thr| while line = stdout.gets process(line) end while line = stderr.gets process(line) end raise RakeStatusException, "Non-zero Exit Code: #{wait_thr.value}" unless wait_thr.value.success? raise RakeCompletedException if (@rake_completed == false && command =~ /heroku run rake tidy:db:/ ) raise @execute_error if @execute_error end rescue => e if (tries -= 1) > 0 puts "Failed running command. Waiting 10s before next retry. Error: #{e}" sleep(10) retry else raise RuntimeError, "Error running command. Retried 3 times. Error #{e}" end end end end def app_name(stage) "#{configuration.app}-#{stage}" end def dyno_config YAML.load_file("#{Rails.root}/config/tidy/dynos.yml") end def app_stop(stage) execute("heroku maintenance:on --app #{app_name(stage)}") scale_command = dyno_config[stage].inject("") { |command, config| command += "#{config.first}='0' " } execute("heroku ps:scale #{scale_command} --app #{app_name(stage)}") end def app_start(stage) execute("heroku maintenance:off --app #{app_name(stage)}") scale_command = dyno_config[stage].inject("") { |command, config| command += "#{config.first}='#{config.last}' " } execute("heroku ps:scale #{scale_command} --app #{app_name(stage)}") end def master_connection_from_db_config(db_config) # Get connection to master/maintenance database 'postgres' pool = ActiveRecord::Base.send(establish_connection_method, db_config.merge( 'database' => 'postgres', 'schema_search_path' => 'public' )) if pool.is_a?(Array) raise RuntimeError, "Pool an array size #{pool.size}. First: #{pool.first}" end pool.connection end def establish_connection_method # Choose proper connection method when ActiveRecord uses activerecord-import if ActiveRecord::Base.respond_to?(:establish_connection_without_activerecord_import) :establish_connection_without_activerecord_import else :establish_connection end end def database_purge(db_config) encoding = configuration.database_encoding ActiveRecord::Base.remove_connection begin master_connection = master_connection_from_db_config(db_config) # Test connection master_connection.select_all("SELECT 1;") # Disallow connections from thinknear user. master_connection.select_all("ALTER DATABASE #{db_config['database']} CONNECTION LIMIT 0;") # Terminate all connections to the database master_connection.select_all("SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE datname='#{db_config['database']}' AND pid <> pg_backend_pid();") # Drop the database master_connection.drop_database(db_config['database']) # Create the database master_connection.create_database(db_config['database'], db_config.merge('encoding' => encoding)) # Enable connections from thinknear user master_connection.select_all("ALTER DATABASE #{db_config['database']} CONNECTION LIMIT -1;") # Connect to created database to set extensions ActiveRecord::Base.remove_connection ActiveRecord::Base.send(establish_connection_method, db_config).connection.execute("CREATE EXTENSION IF NOT EXISTS postgis") rescue ActiveRecord::StatementInvalid => error if /database .* already exists/ === error.message raise DatabaseAlreadyExists elsif /database .* is being accessed by other users/ === error.message result = ActiveRecord::Base.send(establish_connection_method, db_config).connection.select_all("SELECT * FROM pg_stat_activity;") puts result.to_hash raise else raise end ensure master_connection = master_connection_from_db_config(db_config) master_connection.select_all("ALTER DATABASE #{db_config['database']} CONNECTION LIMIT -1;") end end end end