Puma sleeps an important thread on boot of rails application - ruby-on-rails

I am running Rails 3 with Ruby 2.3.3 on puma with postgresql. I have an initializer/twitter.rb file that starts a thread on boot with a streaming api for twitter. When I use rails server to start my application, the twitter streaming works and I can reach my website like normal. (If I do not put the streaming on a different thread, the streaming works but I can not view my application in the browser since the thread is blocked by the twitter stream). But when I use puma -C config/puma.rb to start my application, I get the following message that is telling me that my thread was found on startup and was put to sleep. How can I tell puma to let me run this thread in the background on startup?
initializer/twitter.rb
### START TWITTER THREAD ### if production
if Rails.env.production?
puts 'Starting Twitter Stream...'
Thread.start {
twitter_stream.user do |object|
case object
when Twitter::Tweet
handle_tweet(object)
when Twitter::DirectMessage
handle_direct_message(object)
when Twitter::Streaming::Event
puts "Received Event: #{object.to_yaml}"
when Twitter::Streaming::FriendList
puts "Received FriendList: #{object.to_yaml}"
when Twitter::Streaming::DeletedTweet
puts "Deleted Tweet: #{object.to_yaml}"
when Twitter::Streaming::StallWarning
puts "Stall Warning: #{object.to_yaml}"
else
puts "It's something else: #{object.to_yaml}"
end
end
}
end
config/puma.rb
workers Integer(ENV['WEB_CONCURRENCY'] || 2)
threads_count = Integer(ENV['RAILS_MAX_THREADS'] || 5)
threads threads_count, threads_count
preload_app!
rackup DefaultRackup
port ENV['PORT'] || 3000
environment ENV['RACK_ENV'] || 'development'
on_worker_boot do
# Valid on Rails up to 4.1 the initializer method of setting `pool` size
ActiveSupport.on_load(:active_record) do
config = ActiveRecord::Base.configurations[Rails.env] ||
Rails.application.config.database_configuration[Rails.env]
config['pool'] = ENV['RAILS_MAX_THREADS'] || 5
ActiveRecord::Base.establish_connection(config)
end
end
Message on startup
2017-04-19T23:52:47.076636+00:00 app[web.1]: Connecting to database specified by DATABASE_URL
2017-04-19T23:52:47.115595+00:00 app[web.1]: Starting Twitter Stream...
2017-04-19T23:52:47.229203+00:00 app[web.1]: Received FriendList: --- !ruby/array:Twitter::Streaming::FriendList []
2017-04-19T23:52:47.865735+00:00 app[web.1]: [4] * Listening on tcp://0.0.0.0:13734
2017-04-19T23:52:47.865830+00:00 app[web.1]: [4] ! WARNING: Detected 1 Thread(s) started in app boot:
2017-04-19T23:52:47.865870+00:00 app[web.1]: [4] ! #<Thread:0x007f4df8bf6240#/app/config/initializers/twitter.rb:135 sleep> - /app/vendor/ruby-2.3.3/lib/ruby/2.3.0/openssl/buffering.rb:125:in `sysread'
2017-04-19T23:52:47.875056+00:00 app[web.1]: [4] - Worker 0 (pid: 7) booted, phase: 0
2017-04-19T23:52:47.865919+00:00 app[web.1]: [4] Use Ctrl-C to stop
2017-04-19T23:52:47.882759+00:00 app[web.1]: [4] - Worker 1 (pid: 11) booted, phase: 0
2017-04-19T23:52:48.148831+00:00 heroku[web.1]: State changed from starting to up
Thanks in advance for the help. I have looked at several other posts mentioning WARNING: Detected 1 Thread(s) started in app boot but the answers say to ignore the warning if the thread is not important. In my case, the thread is very important and I need this thread to not sleep.

From your code I think you have a bigger issue on your hands than a sleeping thread... which I guess might be caused by the fact that some things are misnamed and others are just not often considered when relying on a web framework.
In the world of servers, "workers" refer to forked processes that perform server related tasks, often accepting new connections and handling web requests.
BUT - fork doesn't duplicate threads! - the new process (the worker) starts with only one single thread, which is a copy of the thread that called fork.
This is because processes don't share memory (normally). Whatever global data you have in a process is private to that process (i.e., if you save connected websocket clients in an array, that array is different for each "worker").
This can't be helped, it's part of how the OS and fork are designed.
So, the warning is not something you can circumvent - it's an indication of a design flaw in the app(!).
For example, in your current design (assuming the thread wasn't sleeping), the handle_tweet method will only be called for the original server process and it won't be called for any worker process.
If you're using pub/sub, you only need one twitter_stream connection for the whole app (no matter how many servers or workers you application has) - perhaps a twitter_stream process (or background app) will be better than a thread.
But if you're implementing handle_tweet in a process specific way - i.e., by sending a message to every connected clients saved in an array - you need to make sure every "worker" initiates a twitter_stream thread(!).
When I wrote Iodine (a different server than Puma), I handled these use cases using the Iodine.run method, which defers tasks for later. The "saved" task should be performed only after the workers are initialized and the event loop starts running, so it's performed in each process (allowing you to start a new tread in each proccess).
i.e.
Iodine.run do
Thread.start do
twitter_stream.user do |object|
# ...
end
end
end
I assume Puma has a similar solution. From what I understand of the Puma Clustered-Mode Documentation, Adding the following block to your config/puma.rb might help:
# config/puma.rb
on_worker_boot do
Thread.start do
twitter_stream.user do |object|
# ...
end
end
end
Good luck!
EDIT: relating to the comment about twitter_stream using ActiveRecord
From the comments I gather that the twitter_stream callbacks store data in the DataBase as well as handle "push" events or notices.
Although these two concerns are connected, they are very different from each other.
For example, twitter_stream callbacks should only store data in the DataBase once. Even if your application grows to a billion users, you will only need to save the data in the database once.
This means that the twitter_stream callbacks should have their own dedicated process that runs only once, possibly separate from the main application.
At first, and as long as you limit your application to a single (only one server/application running), you might use fork together with the initializer/twitter.rb script... i.e.:
### START TWITTER PROCESS ### if production
if Rails.env.production?
puts 'Starting Twitter Stream...'
Process.fork do
twitter_stream.user do |object|
# ...
end
end
end
On the other hand, notifications should be addressed to a specific user on a specific connection owned by a specific process.
Hence, notifications should be a separate concern from the twitter_stream DataBase update and they should be running in the background of every process, using the on_worker_boot (or Iodine.run) described above.
To achieve this, you might have on_worker_boot start a background thread that will listen to a pub/sub service such as Redis, while the twitter_stream callbacks "publish" updates to the pub/sub service.
This would allow each process to review the update and check if any of the connections it "owns" belongs to a client that should be notified of the update.

The way I'm reading your question, this doesn't look like an issue. A sleeping thread is different from a dead thread. Sleep just means that the thread is waiting idle, not consuming any cpu. If all else is hooked up properly, then as soon as the twitter api detects an event, it should wake the the thread up, run whatever handler you've defined, and then go right back to sleep. Sleeping isn't "running in the background," but it is "waiting for something to happen (e.g. someone tweets #me.) so I can run in the background."
A quick example to demonstrate this:
2.4.0 :001 > t = Thread.new { TCPServer.new(1234).accept ; puts "Got a connection! Dying..." }
=> #<Thread:0x007fa3941fed90#(irb):1 sleep>
2.4.0 :002 > t
=> #<Thread:0x007fa3941fed90#(irb):1 sleep>
2.4.0 :003 > t
=> #<Thread:0x007fa3941fed90#(irb):1 sleep>
2.4.0 :004 > TCPSocket.new 'localhost', 1234
=> #<TCPSocket:fd 35>
2.4.0 :005 > Got a connection! Dying...
t
=> #<Thread:0x007fa3941fed90#(irb):1 dead>
Sleeping just means "waiting for action."
Puma is a thread-based server, and is very particular about spinning threads up in its boot process, hence the warning about a thread started at app boot.
For what it's worth though, it's kind of weird to have a thread listening for updates from an api like that in a webserver. Maybe you should look into having a worker handle twitter events using something like Resque? Or maybe ActionCable is relevant to your use case?

Related

How do I get the Puma worker index within a Rails request?

When a Rails server starts, it lists the Puma workers by their index and PID.
[17042] - Worker 0 (PID: 17069) booted in 0.01s, phase: 0
[17042] - Worker 1 (PID: 17070) booted in 0.01s, phase: 0
I can get the PID of the worker from within the request with Process.pid.
> Process.pid
=> 17069
Is there a way to get the index of the Puma worker (0, 1, etc.) from within the request processing?
I see that the Puma::Cluster::Worker class has an index method that should get me that value, but I can't figure out how to get the instance of that class making the current request.
I believe I can get it through ObjectSpace, but I have to imagine I'm missing a better way.
> ObjectSpace.each_object(Puma::Cluster::Worker) { |x| p x.index }
0
=> 1
I don't think the API supports it, but the following workaround can work
The index is passed to some callbacks, e.g.
https://github.com/puma/puma/blob/34c706be2cd8d48d1e3fa80b2b0b33bcc2b8d8eb/lib/puma/cluster/worker.rb#L55
You can then do something with it (save it somewhere you can reference later, e.g)
https://github.com/puma/puma/blob/15b079439222e489915380021b9dca7b9fa5ff65/test/config/state_file_testing_config.rb#L3
On a side note, I find it a bit confusing that defining on_worker_boot in the DSL will actually call before_worker_boot hook
https://github.com/puma/puma/blob/e9f09ba1fe6b168bed7fff59d0bdbfd65351cf9d/lib/puma/dsl.rb#L567

Threading error when using `ActiveRecord with_connection do` & ActionController::Live

Major edit: Since originally finding this issue I have whittled it down to the below. I think this is now a marginally more precise description of the problem. Comments on the OP may therefore not correlate entirely.
Edit lightly modified version posted in rails/puma projects: https://github.com/rails/rails/issues/21209, https://github.com/puma/puma/issues/758
Edit Now reproduced with OS X and Rainbows
Summary: When using Puma and running long-running connections I am consistently receiving errors related to ActiveRecord connections crossing threads. This manifests itself in message like message type 0x## arrived from server while idle and a locked (crashed) server.
The set up:
Ubuntu 15 / OSX Yosemite
PostgreSQL (9.4) / MySQL (mysqld 5.6.25-0ubuntu0.15.04.1)
Ruby - MRI 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux] / Rubinius rbx-2.5.8
Rails (4.2.3, 4.2.1)
Puma (2.12.2, 2.11)
pg (pg-0.18.2) / mysql2
Note, not all combinations of the above versions have been tried. First listed version is what I'm currently testing against.
rails new issue-test
Add a route get 'events' => 'streaming#events'
Add a controller streaming_controller.rb
Set up database stuff (pool: 2, but seen with different pool sizes)
Code:
class StreamingController < ApplicationController
include ActionController::Live
def events
begin
response.headers["Content-Type"] = "text/event-stream"
sse = SSE.new(response.stream)
sse.write( {:data => 'starting'} , {:event => :version_heartbeat})
ActiveRecord::Base.connection_pool.release_connection
while true do
ActiveRecord::Base.connection_pool.with_connection do |conn|
ActiveRecord::Base.connection.query_cache.clear
logger.info 'START'
conn.execute 'SELECT pg_sleep(3)'
logger.info 'FINISH'
sse.write( {:data => 'continuing'}, {:event => :version_heartbeat})
sleep 0.5
end
end
rescue IOError
rescue ClientDisconnected
ensure
logger.info 'Ensuring event stream is closed'
sse.close
end
render nothing: true
end
end
Puma configuration:
workers 1
threads 2, 2
#...
bind "tcp://0.0.0.0:9292"
#...
activate_control_app
on_worker_boot do
require "active_record"
ActiveRecord::Base.connection.disconnect! rescue ActiveRecord::ConnectionNotEstablished
ActiveRecord::Base.establish_connection(YAML.load_file("#{app_dir}/config/database.yml")[rails_env])
end
Run the server puma -e production -C path/to/puma/config/production.rb
Test script:
#!/bin/bash
timeout 30 curl -vS http://0.0.0.0/events &
timeout 5 curl -vS http://0.0.0.0/events &
timeout 30 curl -vS http://0.0.0.0/events
This reasonably consistently results in a complete lock of the application server (in PostgreSQL, see notes). The scary message comes from libpq:
message type 0x44 arrived from server while idle
message type 0x43 arrived from server while idle
message type 0x5a arrived from server while idle
message type 0x54 arrived from server while idle
In the 'real-world' I have quite a few extra elements and the issue presents itself at random. My research indicates that this message comes from libpq and is subtext for 'communication problem, possibly using connection in different threads'. Finally, while writing this up, I had the server lock up without a single message in any log.
So, the question(s):
Is the pattern I'm following not legal in some way? What have I mis[sed|understood]?
What is the 'standard' for working with database connections here that should avoid these problems?
Can you see a way to reliably reproduce this?
or
What is the underlying issue here and how can I solve it?
MySQL
If running MySQL, the message is a bit different, and the application recovers (though I'm not sure if it is then in some undefined state):
F, [2015-07-30T14:12:07.078215 #15606] FATAL -- :
ActiveRecord::StatementInvalid (Mysql2::Error: This connection is in use by: #<Thread:0x007f563b2faa88#/home/dev/.rbenv/versions/2.2.2/lib/ruby/gems/2.2.0/gems/actionpack-4.2.3/lib/action_controller/metal/live.rb:269 sleep>: SELECT `tasks`.* FROM `tasks` ORDER BY `tasks`.`id` ASC LIMIT 1):
Warning: read 'answer' as 'seems to make a difference'
I don't see the issue happen if I change the controller block to look like:
begin
#...
while true do
t = Thread.new do #<<<<<<<<<<<<<<<<<
ActiveRecord::Base.connection_pool.with_connection do |conn|
#...
end
end
t.join #<<<<<<<<<<<<<<<<<
end
#...
rescue IOError
#...
But I don't know whether this has actually solved the problem or just made it extremely unlikely. Nor can I really fathom why this would make a difference.
Posting this as a solution in case it helps, but still digging on the issue.

Rails threads testing db lock

Let's say we want to test that the database is being locked..
$transaction = Thread.new {
Rails.logger.debug 'transaction process start'
Inventory.transaction do
inventory.lock!
Thread.stop
inventory.units_available=99
inventory.save
end
}
$race_condition = Thread.new {
Rails.logger.debug 'race_condition process start'
config = ActiveRecord::Base.configurations[Rails.env].symbolize_keys
config[:flags] = 65536 | 131072 | Mysql2::Client::FOUND_ROWS
begin
connection = Mysql2::Client.new(config)
$transaction.run
$transaction.join
rescue NoMethodError
ensure
connection.close if connection
end
}
Rails.logger.debug 'main process start'
$transaction.join
Rails.logger.debug 'main process after transaction.join'
sleep 0.1 while $transaction.status!='sleep'
Rails.logger.debug 'main process after sleep'
$race_condition.join
Rails.logger.debug 'main process after race_condition.join'
In theory, I'd think it would do the transaction thread, then wait( Thread.stop ), then the main process would see that it's sleeping, and start the race condition thread(which will be trying to alter data in the locked table when it actually works). Then the race condition would continue the transaction thread after it was done.
what's weird is the trace
main process start
transaction process start
race_condition process start
Coming from nodejs, it seems like threads aren't exactly as user friendly.. though, there has to be a way to get this done.
Is there an easier way to lock the database, then try to change it with a different thread?
Thread.new automatically starts the Thread.
But that does not mean that it is executing.
That depends on Operations system, ruby or jruby, how many cores, etc.
In your example the main thread runs until
$transaction.join,
and only then your transaction thread starts, just by chance.
It runs still Thread.stop, then your '$race_condition' Thread starts, because both other are blocked (it might have started before)
So that explains your log.
You have two $transaction.join
they wait until the thread exits, but a thread can only exit once...
I don't know what is happen then, maybe the second call waits forever.
For your test, you need some sort of explicit synchronization, so that our race_thread writes exactly when the transaction_thread is in the middle of the transaction. You can do this with Mutex, but better would be some sort of message passing. The following blog post may help:
http://www.engineyard.com/blog/2011/a-modern-guide-to-threads/
For any resource to make it a "Mutually Exclusive", you need to use Mutex class and use a synchronize method to make the resources locked while one thread is using them. You have to do something like this:
semaphore = Mutex.new
and use it inside the Thread instance.
$transaction = Thread.new {
semaphore.synchronize{
# Do whatever you want with the *your shared resource*
}
}
This way you can prevent any deadlocks.
Hope this helps.

Dynamic Ruby Daemon Management

I have a Ruby process that listens on a given device. I would like to spin up/down instances of it for different devices with a rails app. Everything I can find for Ruby daemons seems to be based around a set number of daemons running or background processing with message queues.
Should I just be doing this with Kernel.spawn and storing the PIDs in the database? It seems a bit hacky but if there isn't an existing framework that allows me to bring up/down daemons it seems I may not have much choice.
Instead of spawning another script and keeping the PIDs in the database, you can do it all within the same script, using fork, and keeping PIDs in memory. Here's a sample script - you add and delete "worker instances" by typing commands "add" and "del" in console, exiting with "quit":
#pids = []
#counter = 0
def add_process
#pids.push(Process.fork {
loop do
puts "Hello from worker ##{#counter}"
sleep 1
end
})
#counter += 1
end
def del_process
return false if #pids.empty?
pid = #pids.pop
Process.kill('SIGTERM', pid)
true
end
def kill_all
while del_process
end
end
while cmd = gets.chomp
case cmd.downcase
when 'quit'
kill_all
exit
when 'add'
add_process
when 'del'
del_process
end
end
Of course, this is just an example, and for sending comands and/or monitoring instances you can replace this simple gets loop with a small Sinatra app, or socket interface, or named pipes etc.

workling doesnt work when the database is down in my rails app

i want to schedule my worker using a cron job to check for database connection periodically (say like 5 mins) and update a memcache key accordingly. so in my app if i find the memcache variable to be set. i render my pages differently then, when the database is up.
But the problem is, the worker doent start when the database is down. when the database is up. it correctly finds out that the database connection is present and update the memcache variable and everything works fine.
I dont know, why worker doesnt start when the database is down.
Am running out on a deadline. any help very much appreciated !
Update:
This is the error i get when the workling doesnt start
/apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/activerecord-2.1.1/lib/active_record/connection_adapters/mysql_adapter.rb:527:in real_connect': Can't connect to MySQL server on '10.223.2.50' (111) (Mysql::Error)
from /apps/Symantec/shasta/website/vendor/plugins/workling/script/../lib/workling/starling/poller.rb:35:injoin'
from /apps/Symantec/shasta/website/vendor/plugins/workling/script/../lib/workling/starling/poller.rb:35:in listen'
from /apps/Symantec/shasta/website/vendor/plugins/workling/script/../lib/workling/starling/poller.rb:35:ineach'
from /apps/Symantec/shasta/website/vendor/plugins/workling/script/../lib/workling/starling/poller.rb:35:in listen'
from /apps/Symantec/shasta/website/vendor/plugins/workling/script/listen.rb:19
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/application.rb:203:inload'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/application.rb:203:in start_load'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/application.rb:296:instart'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/monitor.rb:51:in watch'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/monitor.rb:51:infork'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/monitor.rb:51:in watch'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/monitor.rb:45:ineach'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/monitor.rb:45:in watch'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/monitor.rb:44:inloop'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/monitor.rb:44:in watch'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/monitor.rb:84:instart_with_pidfile'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/monitor.rb:64:in fork'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/monitor.rb:64:instart_with_pidfile'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/monitor.rb:111:in start'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/application_group.rb:149:increate_monitor'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/application.rb:283:in start'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/controller.rb:70:inrun'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons.rb:143:in run'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/cmdline.rb:112:incall'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons/cmdline.rb:112:in catch_exceptions'
from /apps/Symantec/shasta/website/install/local/ruby-1.8.7-p299/lib/ruby/gems/1.8/gems/daemons-1.1.0/lib/daemons.rb:142:inrun'
from script/workling_starling_client:17
Maybe the worker tries to connect to the database when starting (always) and throws an exception? Have you any errors logged by the worker?
Did you write your worker in Rails? Maybe write a shell script, which will assume the database is down when the worker cannot start?
UPDATE: In your stack trace there is the starting point: script/workling_starling_client:17. What is there, in the line 17?
As the first line (the exception message itself) says that "real_connect': Can't connect to MySQL server on '10.223.2.50' (111) (Mysql::Error)" then it will be enough if you wrap the line 17 (possibly with a few more) in a "rescue" block, and check the error message whether it has the answer you are looking for:
(Of course, don't stop here. Continue your checks, as the lack of exception does not mean that the connection IS established)
begin
line_17_is_here
rescue => e
if e.message =~ /Can't connect to MySQL/
handle_your_no_connection_state
else
raise e
end
end
The question is: can you handle the no-connection state without ActiveRecord?

Resources