Module: Hijacker

Defined in:
lib/hijacker.rb,
lib/hijacker/alias.rb,
lib/hijacker/base_model.rb,
lib/hijacker/middleware.rb,
lib/hijacker/request_parser.rb

Defined Under Namespace

Modules: ControllerMethods Classes: Alias, BaseModel, Database, Host, InvalidDatabase, Middleware, RequestParser, UnparseableURL

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.configObject

Returns the value of attribute config.



18
19
20
# File 'lib/hijacker.rb', line 18

def config
  @config
end

.masterObject

Returns the value of attribute master.



18
19
20
# File 'lib/hijacker.rb', line 18

def master
  @master
end

.sisterObject

Returns the value of attribute sister.



18
19
20
# File 'lib/hijacker.rb', line 18

def sister
  @sister
end

.valid_routesObject



22
23
24
# File 'lib/hijacker.rb', line 22

def self.valid_routes
  @valid_routes ||= {}
end

Class Method Details

.check_connectionObject

just calling establish_connection doesn’t actually check to see if we’ve established a VALID connection. a call to connection will check this, and throw an error if the connection’s invalid. It is important to catch the error and reconnect to a known valid database or rails will get stuck. This is because once we establish a connection to an invalid database, the next request will do a courteousy touch to the invalid database before reaching establish_connection and throw an error, preventing us from retrying to establish a valid connection and effectively locking us out of the app.



195
196
197
# File 'lib/hijacker.rb', line 195

def self.check_connection
  ::ActiveRecord::Base.connection
end

.connect(target_name, sister_name = nil, options = {}) ⇒ Object

Manually establishes a new connection to the database.

Background: every time rails gets information from the database, it uses the last established connection. So, although we’ve already established a connection to a root db (“crystal”, in this case), if we establish a new connection, all subsequent database calls will use these settings instead (well, until it’s called again when it gets another request).

Note that you can manually call this from script/console (or wherever) to connect to the database you want, ex Hijacker.connect(“database”)



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
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/hijacker.rb', line 41

def self.connect(target_name, sister_name = nil, options = {})
  original_database = Hijacker::Database.current

  begin
    raise InvalidDatabase.new(nil, 'master cannot be nil') if target_name.nil?

    target_name = target_name.downcase
    sister_name = sister_name.downcase unless sister_name.nil?

    if already_connected?(target_name, sister_name)
      run_after_hijack_callback
      return "Already connected to #{target_name}"
    end

    database = determine_database(target_name, sister_name)

    establish_connection_to_database(database)

    check_connection

    if database.sister?
      self.master = database.master.name
      self.sister = database.name
    else
      self.master = database.name
      self.sister = nil
    end

    # don't cache sister site
    cache_database_route(target_name, database) unless sister_name

    # Do this even on a site without a master so we reconnect these models
    connect_sister_site_models(database.master || database)

    reenable_query_caching

    run_after_hijack_callback
  rescue
    if original_database.present?
      establish_connection_to_database(original_database)
    else
      self.establish_root_connection
    end
    raise
  end
end

.connect_sister_site_models(master_database) ⇒ Object

very small chance this will raise, but if it does, we will still handle it the same as Hijacker.connect so we don’t lock up the app.

Also note that sister site models share a connection via minor management of AR’s connection_pool stuff, and will use ActiveRecord::Base.connection_pool if we’re not in a sister-site situation



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
# File 'lib/hijacker.rb', line 94

def self.connect_sister_site_models(master_database)
  master_db_connection_pool = if processing_sister_site?
                                nil
                              else
                                ActiveRecord::Base.connection_pool
                              end
  master_config = connection_config(master_database)

  config[:sister_site_models].each do |model_name|
    klass = model_name.constantize

    klass.establish_connection(master_config)

    if !master_db_connection_pool
      begin
        klass.connection
      rescue
        klass.establish_connection(root_config)
        raise Hijacker::InvalidDatabase.new(database.name)
      end
      master_db_connection_pool = klass.connection_pool
    else
      ActiveRecord::Base.connection_handler.connection_pools[model_name] = master_db_connection_pool
    end
  end
end

.connect_to_master(db_name) ⇒ Object



26
27
28
# File 'lib/hijacker.rb', line 26

def self.connect_to_master(db_name)
  connect(*Hijacker::Database.find_master_and_sister_for(db_name))
end

.current_clientObject



177
178
179
# File 'lib/hijacker.rb', line 177

def self.current_client
  sister || master
end

.database_configurationsObject



159
160
161
# File 'lib/hijacker.rb', line 159

def self.database_configurations
  ActiveRecord::Base.configurations
end

.do_hijacking?Boolean

Returns:

  • (Boolean)


181
182
183
184
# File 'lib/hijacker.rb', line 181

def self.do_hijacking?
  (Hijacker.config[:hosted_environments] || %w[staging production]).
    include?(ENV['RAILS_ENV'] || Rails.env)
end

.establish_root_connectionObject

this should establish a connection to a database containing the bare minimum for loading the app, usually a sessions table if using sql-based sessions.



165
166
167
# File 'lib/hijacker.rb', line 165

def self.establish_root_connection
  ActiveRecord::Base.establish_connection('root')
end

.processing_sister_site?Boolean

Returns:

  • (Boolean)


169
170
171
# File 'lib/hijacker.rb', line 169

def self.processing_sister_site?
  !sister.nil?
end

.root_configObject



155
156
157
# File 'lib/hijacker.rb', line 155

def self.root_config
  database_configurations.fetch('root').with_indifferent_access
end

.root_connectionObject

The advantage of using this over just calling ActiveRecord::Base.establish_connection (without arguments) to reconnect to the root database is that reusing the same connection greatly reduces context switching overhead etc involved with establishing a connection to the database. It may seem trivial, but it actually seems to speed things up by ~ 1/3 for already fast requests (probably less noticeable on slower pages).

Note: does not hijack, just returns the root connection (i.e. AR::Base will maintain its connection)



144
145
146
147
148
149
150
151
152
153
# File 'lib/hijacker.rb', line 144

def self.root_connection
  unless $hijacker_root_connection
    current_config = ActiveRecord::Base.connection.config
    ActiveRecord::Base.establish_connection('root') # establish with defaults
    $hijacker_root_connection = ActiveRecord::Base.connection
    ActiveRecord::Base.establish_connection(current_config) # reconnect, we don't intend to hijack
  end

  $hijacker_root_connection
end

.temporary_sister_connect(db, &block) ⇒ Object

connects the sister_site_models to db while calling the block if db and self.master differ



123
124
125
126
127
128
129
130
131
132
# File 'lib/hijacker.rb', line 123

def self.temporary_sister_connect(db, &block)
  processing_sister_site = (db != master && db != sister)
  self.sister = db if processing_sister_site
  self.connect_sister_site_models(db) if processing_sister_site
  result = block.call
  self.connect_sister_site_models(self.master) if processing_sister_site
  self.sister = nil if processing_sister_site

  result
end