[Heroku Tips] Problemas iniciais com Rails 4 no Heroku

Se ainda não leu, dê uma olhada sobre o que já postei como dicas de Heroku e minha opinião sobre o serviço.

Recentemente tentei subir um projeto Rails 4 bem simples no Heroku e encontrei problemas logo na primeira tentativa de deploy. O problema é o seguinte: a forma mais aceita de configurar uma aplicação é usar variáveis de ambiente (veja projetos como o dotenv-rails). No primeiro deploy essas variáveis não estão disponíveis, em particular o DATABASE_URL. Na task assets:precompile não deveria haver nada na inicialização que dependesse de conexão ao banco, mas algumas gems ainda não estão corrigidas dessa forma, em particular duas com esse bug já conhecido são o active_admin e o acts-as-taggable-on.

No final, a forma mais simples para resolver isso por enquanto é fazer o seguinte antes do primeiro deploy:

heroku labs:enable user-env-compile
heroku config:add DATABASE_URL=$(heroku config | awk '/HEROKU_POSTGRESQL.*:/ {print $2}')

Leia a documentação dessa funcionalidade user-env-compile entendendo que ela não é a forma mais correta, é apenas um facilitador enquanto todas as gems não estão da forma correta.

Rails 12 Factor

Rapidamente para não esquecer, no caso de apps Rails 4 não deixe de acrescentar o seguinte na sua Gemfile:

gem 'rails_12factor', group: :production

Em particular é importante para logging correto e servir assets estáticos, veja no Github deles para mais informações.

Migração de MySQL para PostgreSQL

Outro assunto que deve ser constante quando se considera mudar pra Heroku é ter que usar o Heroku Postgres (que é uma ótima opção). Mas muitos projetos, principalmente mais antigos, devem ter começado em MySQL.

A primeira coisa a fazer é verificar se você tem muitos SQL exclusivos de MySQL, funções e coisas do tipo. Se você usar ActiveRecord Relations padrão, não deveria ter nenhum problema.

O segundo problema é migrar os dados de um banco para o outro. Eu procurei várias opções mas a maioria é antiga e não funciona direito, a melhorzinha que achei foi uma task Rake. Ela tinha alguns probleminhas de usar API deprecada mas resolvi neste aqui:

#
# Convert/transfer data from production => development. This facilitates
# a conversion one database adapter type to another (say postgres -> mysql )
#
# WARNING 1: this script deletes all development data and replaces it with
# production data
#
# WARNING 2: This script assumes it is the only user updating either database.
# Database integrity could be corrupted if other users where
# writing to the databases.
#
# Usage: rake db:convert:prod2dev
#
# It assumes the development database has a schema identical to the production
# database, but will delete any data before importing the production data
#
# A couple of the outer loops evolved from
# http://snippets.dzone.com/posts/show/3393
#
# For further instructions see
# http://myutil.com/2008/8/31/rake-task-transfer-rails-database-mysql-to-postgres
#
# The master repository for this script is at github:
# http://github.com/face/rails_db_convert_using_adapters/tree/master
#
# Author: Rama McIntosh
# Matson Systems, Inc.
# http://www.matsonsystems.com
#
# This rake task is released under this BSD license:
#
# Copyright (c) 2008, Matson Systems, Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of Matson Systems, Inc. nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# PAGE_SIZE is the number of rows updated in a single transaction.
# This facilitates tables where the number of rows exceeds the systems
# memory
PAGE_SIZE=10000
namespace :db do
namespace :convert do
desc 'Convert/import production data to development. DANGER Deletes all data in the development database. Assumes both schemas are already migrated.'
task :prod2dev => :environment do
# We need unique classes so ActiveRecord can hash different connections
# We do not want to use the real Model classes because any business
# rules will likely get in the way of a database transfer
class ::ProductionModelClass < ActiveRecord::Base
end
class ::DevelopmentModelClass < ActiveRecord::Base
end
skip_tables = ["schema_info", "schema_migrations"]
ActiveRecord::Base.establish_connection(:production)
(ActiveRecord::Base.connection.tables - skip_tables).each do |table_name|
ProductionModelClass.table_name = table_name
DevelopmentModelClass.table_name = table_name
DevelopmentModelClass.establish_connection(:development)
DevelopmentModelClass.reset_column_information
ProductionModelClass.reset_column_information
DevelopmentModelClass.record_timestamps = false
# Page through the data in case the table is too large to fit in RAM
offset = count = 0;
print "Converting #{table_name}..."; STDOUT.flush
# First, delete any old dev data
DevelopmentModelClass.delete_all
while ((models = ProductionModelClass.find(:all,
:offset=>offset, :limit=>PAGE_SIZE)).size > 0)
count += models.size
offset += PAGE_SIZE
# Now, write out the prod data to the dev db
DevelopmentModelClass.transaction do
models.each do |model|
new_model = DevelopmentModelClass.new(model.attributes)
new_model.id = model.id
new_model.save(validate: false)
end
end
end
print "#{count} records converted\n"
end
end
end
end
view raw convert.rake hosted with ❤ by GitHub

Basta alterar seu config/database.yml para ter o seguinte:

development:
  adapter: postgresql
  database: legaltorrents_development
  username: fred
  password: password
  host: localhost

production:
  adapter: mysql
  database: legaltorrents_production
  username: fred
  password: password

Coloca o script como lib/tasks/convert.rake e executa rake db:convert:prod2dev.

Depois disso ainda precisa atualizar as sequences de primary key do PostgreSQL desta forma:

ALTER SEQUENCE users_id_seq restart with (select max(id)+1 from users) 

Isso deve ser feito para cada tabela que você tem. Se precisar atualizar em produção no Heroku, execute heroku run rails console e execute assim:

ActiveRecord::Base.connection.execute("ALTER SEQUENCE users_id_seq restart with (select max(id)+1 from users) ")

Não esqueça que você pode fazer dumps do banco de dados de produção, colocar num banco de dados local para testar e tudo mais e se quiser pode gerar um dump local e restaurar de novo no Heroku. Leia a documentação deles sobre PG Backups.

Gerar um dump local é simples:

pg_dump -Fc --no-acl --no-owner -h localhost -U vagrant my_db > mydb.dump

E restaurar um dump do Heroku no seu banco local também:

pg_restore --verbose --clean --no-acl --no-owner -h localhost -U vagrant -d my_db b078.dump

Isso deve resolver a maioria dos problemas.