Class variable reset with rails / spring - ruby-on-rails

I've a class defined as such:
class PublicationJob < ActiveJob::Base
def self.jobs
#jobs ||= Hash{|h, k| h[k] = []}
end
def self.register(format, job_class)
jobs[format] << job_class
end
# [...]
end
To register different job classes, I put in an initializer:
PublicationJob.register(:tex, SaveJob)
PublicationJob.register(:saved_tex, TexJob)
#...
The in the rails console I try:
PublicationJob.jobs
#> {:tex => [SaveJob], :saved_tex => [TexJob]}
But if I exit the console (Ctrl-D) then restart it, at some point the hash will be empty!
Why is the class variable reset in this case?
I use rails 4.2.1 and spring, and I know that if I kill/stop spring it works again for some time. Is it related to spring?

Okay, so this was entirely Spring related, and I fixed it by removing spring.
Thanks to #NekoNova who pointed me to the right part of the documentation, I found that:
This saves off the first version of the User class, which will not be the same object as User after the code has been reloaded:
[...]
So to avoid this problem, don't save off references to application constants in your initialization code.
In other words, I can't initialize my classes using initializers, because althought it'll work in production, it won't work in development.

I know this is rather old, but I have encountered this issue a couple of times, and feel that you don't have to abandon spring if you set class level variables at initialization.
All you need to do is re-assign them in a spring ".after_fork" block. So for the above issue, place in a "config/spring.rb" file, the following:
if ("Spring".constantize rescue nil)
Spring.after_fork do
PublicationJob.register(:tex, SaveJob)
PublicationJob.register(:saved_tex, TexJob)
end
end
This will reset those variables after spring completes the fork and reloading code. I wrap it in a check to ensure that Spring is available, which it likely won't be in production.

Related

How to use cattr_accessor to define config in a Rails initializer

Given the following class (this is the actual class there is no other code)
class PageRepo
cattr_accessor :root_path
end
and initializer file
PageRepo.root_path = Rails.root.join("content")
When I run
rails console
PageRepo.root_path
=> PageRepo.root_path
=> #<Pathname:/Users/blah/my_rails_app/content/pages>
However when I try to access this in my rails controller the root_path value is nil. (I've inspected this with web_console.)
No other class subclasses or is a parent of the PageRepo class and I'm not setting the root_path to nil anywhere at class level or instance level after the initializer stage. I have restarted spring and my server multiple times to no avail.
Is there something I'm not aware of when it comes to either Rails initializers or cattr_accessor?
Update
I'd like to set the path like this because throughout my code I will be initialising a PageRepo instance and the path will not change. However, it may change across different environments. I.e. in development it the path will be different than that of the test and production environments. Whilst I could just do
def initialize
#root_path = ENV['ROOT_PATH']
end
I'd prefer to not force the programmer to use ENV VARS to do this and hence my attempt above.
My solution would be just using a class method
class PageRepo
self.root_path
Rails.root.join("content")
end
end
PageRepo.root_path would return #<Pathname:/Users/blah/my_rails_app/content/pages>.
You want to have #<Pathname:/Users/blah/my_rails_app/content/pages>? or this depends on the action?
I believe your initializer is not working
an interesting post, what you are trying to achieve is writing something like this in the class initializer or constructor
class PageRepo
self.initialize
self.root_path = Rails.root.join("content")
end
end
but I never saw self.initialize being used with rails. so I believe the first approach is better.

Where is a good place to initialize an API?

I wanted to use this api: https://github.com/coinbase/coinbase-ruby and the first step is to initialize the API, like this:
coinbase = Coinbase::Client.new(ENV['COINBASE_API_KEY'], ENV['COINBASE_API_SECRET'])
I was wondering what the best place to put this code is, and how would I access it if I put it "there"? I want this variable (coinbase) to be accessible ANYWHERE in the application.
Thanks!
The answer to this question really depends on your use case and your approach. My geral recommendation, however, is to create a Service Object (in the DDD sense) (see the section named "Domain Objects Should Not Know Anything About Infrastructure Underneath" in that link), that handles all communication with the Coinbase API. And then, within this service object, you can simply initialize the Coinbase::Client object once for however many times you call into it. Here's an example:
# app/services/coinbase_service.rb
class CoinbaseService
cattr_reader :coinbase_client, instance_accessor: false do
Coinbase::Client.new(ENV['COINBASE_API_KEY'], ENV['COINBASE_API_SECRET'])
end
def self.do_something
coinbase_client.do_something_in_their_api
end
def self.do_something_else
coinbase_client.do_something_else_in_their_api
end
end
So then you might do, e.g.:
# From MyController#action_1
if CoinbaseService.do_something
# ...
else
# ...
end
Or:
# From MyModel
def do_something
CoinbaseService.do_something_else
end
To get the service object working, you may need to add app/services to your autoload paths in application.rb file. I normally just add this:
# config/application.rb
config.autoload_paths += %W(#{config.root}/app)
I find this Service Object approach to be very beneficial organizationally, more efficient (only 1 invocation of the new Coinbase client needed), easier to test (easy to mock-out calls to Coinbase::Client), and simply joyful :).
One way to go about having a global variable can be done as similar as initializing redis in a Rails application by creating an initializer in config/initializers/coinbase.rb with:
$coinbase = Coinbase::Client.new(ENV['COINBASE_API_KEY'], ENV['COINBASE_API_SECRET'])
Now, you can access $coinbase anywhere in the application!
In the file config/initializers/coinbase.rb
Rails.application.config.after_initialize do
CoinbaseClient = Coinbase::Client.new(
Rails.application.credentials.coinbase[:api_key],
Rails.application.credentials.coinbase[:api_secret])
end
In place of the encrypted credentials, you could also use environment variables: ENV['COINBASE_API_KEY'], ENV['COINBASE_API_SECRET']
The above will make the constant CoinbaseClient available everywhere in your app. It will also ensure all your gems are loaded before the client is initialized.
Note: I am using Rails 6.1.4.4, and Ruby 2.7.5

Rails application config access slow - why?

We always used to put our application config into the environment files. That's no good for production management, so now we load it through an initializer:
# myinitializer.rb
ApplicationConfig = YAML.load_file("#{Rails.root}/config/application/default.yml").symbolize_keys()
As soon as we started accessing the configuration through ApplicationConfig, application test performance got much worse. One rspec suite dropped from 4 seconds to 30.
In our application controller, we need to take some action using a before_filter, which works as follows:
before_filter :extra_control
def extra_control
if ApplicationConfig.some_flag
...
end
end
Declaring a variable pointing to ApplicationConfig fully restores performance:
config = ApplicationConfig
def extra_control
if config.some_flag
...
end
end
Why? Why does accessing this through the global variable destroy performance? We do this throughout the code base in views and other controllers. Do we need to do this differently, e.g. by injecting an instance variable into all controllers?
Edit: we did verify that the code loading the configuration from YAML gets called exactly once either way, so repeated loading does not seem to be the root cause.
Edit: it turns out that this was a bug caused by a setting variable that was being loaded as a string rather than a boolean, causing the application to go into a test sleep pattern :( Sorry, thanks for trying. 3 days of my life I'll never get back!
I can't speak to why your approach is slow, but maybe a different way of handling settings would be worth looking into. I prefer the idea of having a singleton class that contains config information:
class SiteSettings
def initialize
#settings = {}
end
##instance = SiteSettings.new
def set_setting(key, value)
#settings ||= {}
#settings[key] = value
end
def self.setting(key, value)
##instance.set_setting(key, value)
end
def settings
#settings
end
def self.method_missing(meth, *args, &block)
if ##instance.settings.has_key?(meth)
##instance.settings[meth]
else
super
end
end
setting :test_setting, 'test'
setting :another_test_setting '.....'
end
puts SiteSettings.test_setting
puts SiteSettings.another_test_setting
The Discourse app takes a similar approach.
site_setting.rb
site_setting_extension.rb
Try use const:
APPLICATIONCONFIG = YAML.load(File.read(File.expand_path("#{RAILS_ROOT}/config/application/default.yml", __FILE__)))
or
APPLICATIONCONFIG = YAML::load(File.open("#{RAILS_ROOT}/config/application/default.yml"))
I think its make your file slow because load file but not open/read.
I think it will help to figure out if the YAML file load\read is slow, or YAML parsing is slow. My guess is that ruby is parsing the YAML file every time you call your ApplicationConfig variable. Can you try renaming it to APPLICATION_CONFIG like so:
APPLICATION_CONFIG = YAML.load(File.read(Rails.root.join('config', 'application', 'my_config.yml')))
Please do not inject an instance variable in all your controllers :)
The other answers here did not make a difference - this was caused by an unrelated problem. For the record
Initializers are called exactly once by rspec
It does not make a difference whether ApplicationConfig or APPLICATION_CONFIG is used
Assigning to a variable did not make a speed difference either

Modifying a ruby class doesn't work as expected when running Spork

I have a plain Ruby class in my Rails app that I'm reopening in a test environment. It basically looks like
class A
def get_dependency
B
end
... some other methods ...
end
And in my test environment in cucumber (in a file loaded from features/env.rb) (and a similar place for rspec) I do
class A
def get_dependency
MockedB
end
end
This works fine in normal runs, but when I have Spork running, it fails strangely. Class A's get_dependency method is overwritten properly, but all its other public methods are now missing. Any ideas?
I'm assuming this is related to load order somehow, but I didn't get any changes when I moved the require for my file out of the preload section of Spork.
This isn't a great answer, but it's a workaround. Instead of reopening the class I just modified a singleton instance. The code is basically the same, except I added an instance method on A:
class A
def instance
##instance ||= A.new
end
end
Then in my test code I modified the instance
instance = A.instance
def instance.get_dependency
MockedB
end
And I just had to ensure that my actual code was always calling A.instance instead of A.new.
One possible scenario is that A is set to get autoloaded, but when you define the override for it in your cucumber environment, you do so before it has been autoloaded; since A now exists, it will never get autoloaded.
A possible solution, which invokes the autoloader before overriding the method is this:
A.class_exec do
def get_dependency
MockedB
end
end
It will raise a ConstMissing if A cannot be autoloaded at that point (perhaps the autoloaders have not yet been set up).

Why do includes in Rails Engine initializers malfunction when cache_classes = false?

I have an Engine which is extending another Engine's classes in its initializers like so:
module MyApp
class Engine < ::Rails::Engine
initializer 'extend Product' do
AnotherApp::Product.send :include, MyApp::ProductExtender
end
end
end
The ProductExtendermodule calls some methods on the AnotherApp::Product when it is included, e.g.
module ProductExtender
def self.included( model )
model.send :include, MethodsToCall
end
module MethodsToCall
def self.included( m )
m.has_many :variations
end
end
end
This works in test and production environments, but when config.cache_classes = false, it throws a NoMethodError at me when I try to call something defined by the ProductExtender, like #product.variations.
Needless to say, it is chilling to see all my tests pass and then get slammed with an error in development. It doesn't happen when I set cache_classes = true, but it makes me wonder if I'm doing something I shouldn't be.
My question is twofold: Why is this happening, and is there a better way I should be achieving this functionality of extending/calling methods on another application's object?
Thanks all!
I managed to solve this problem using a to_prepare block instead of the initializer. The to_prepare block executes once in production and before each request in development, so seems to meet our needs.
It wasn't obvious when I was researching Rails::Engine since it is inherited from Rails::Railtie::Configuration.
So instead of the code in the question, I would have:
module MyApp
class Engine < ::Rails::Engine
config.to_prepare do
AnotherApp::Product.send :include, MyApp::ProductExtender
end
end
end
cache_classes has actually a misleading name: There is no caching involved. If you set this option to false, rails explicitly unloads your application code and reloads it when needed. This enables for changes you make in development to have an effect without having to restart the (server) process.
In your case, AnotherApp::Product is reloaded, as is ProductExtender, but the initializer is not fired again, after the reload, so AnotherApp::Product is not 'extended'.
I know this problem very well and ended up running my development environment with cache_classes = true and occasionally restart my server. I had not so much development to do on engines/plugins, so this was the easiest way.

Resources