Stop sidekiq worker when user logs out - ruby-on-rails

I only want to perform the background job when the user is logged in. When the user is logged in, I start a worker:
Warden::Manager.after_authentication do |user,auth,opts|
ImapWorker.perform_async(user.id)
end
All my ImapWorker does is use ruby's Net::Imap API to wait for new emails to come in from a remote email server. It uses Imap's idle functionality to wait for new events.
class ImapWorker
include Sidekiq::Worker
def perform(user_id)
user = User.find(user_id)
imap = Net::IMAP.new('imap.gmail.com',993,true)
imap.login(user.email, user.password)
imap.select('INBOX')
imap.add_response_handler do |resp|
if resp.kind_of?(Net::IMAP::UntaggedResponse) and resp.name == "EXISTS"
check_email
end
end
imap.idle
end
end
Now when my user logs out, I want to end the worker. I know in devise, I can hook into the log out functionality as such:
Warden::Manager.before_logout do |user,auth,opts|
...
end
But how exactly do I stop the sidekiq worker?

require 'sidekiq/api'; Sidekiq::ProcessSet.new.each(&:stop!)

Related

Rails Service never called

I've written a simple service to create the data I want to send over my ActionCable connection, but judging from the development log it looks like it is never executed and I can't figure out why.
#app\channels\quiz_data_channel.rb:
class QuizDataChannel < ApplicationCable::Channel
def subscribed
stream_from specific_channel
end
def send_data
logger.debug "[AC] send data method entered" #<-- this part is executed
QuizDataCreation.new(user: current_user).create #<-- calling the service HERE
logger.debug "[AC] service created new quiz data" #<-- not executed
#after I fix this issue I would broadcast the data here
end
private
def specific_channel
"quiz_data_#{params[:access_key]}"
end
end
#app\services\quiz_data_creation.rb:
module Services
class QuizDataCreation
def initialize(user)
self.user = user
logger.debug "[AC] service - initialize user: #{self.user.inspect}" #<-- not executed
end
def create
logger.debug "[AC] Service entered!" #<-- not executed
end
end
end
Calling the send_data method works, and the first text ("[AC] send data method entered") is printed into the development log, but that's it. I've been looking at several tutorials and tried placing the service in a subfolder of models at first, and calling Services::QuizDataCreation.new(user: current_user).create , but haven't gotten it to work yet. I'm sure there is a pretty obvious solution, but I just can't see it so I would be really thankful for any pointers.
Try defining the service class without module names Services as below:
#app\services\quiz_data_creation.rb:
class QuizDataCreation
def initialize(user)
self.user = user
logger.debug "[AC] service - initialize user: #{self.user.inspect}"
end
def create
logger.debug "[AC] Service entered!"
end
end
And then, you can access it normally as:
QuizDataCreation.new(current_user).create

Rails action cable specific consumer

I'm struggling with action cable. In my case I have couple of users (via Devise) who can share tasks with each other.
Now when user#1 share task (via Form) with user#2 all authenticated users receive notifications.
How and where should I identify my user#2 to broadcast only to him?
Here is my code so far:
connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
logger.add_tags 'ActionCable', current_user.id
end
protected
def find_verified_user # this checks whether a user is authenticated with devise
if verified_user = env['warden'].user
verified_user
else
reject_unauthorized_connection
end
end
end
end
cable.js
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);
todo_channel.rb
class TodoChannel < ApplicationCable::Channel
def subscribed
stream_from "todo_channel_#{current_user.id}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def notify
ActionCable.server.broadcast "todo_channel_#{current_user.id}", message: 'some message'(not implemented yet)
end
end
todo.coffee
App.todo = App.cable.subscriptions.create "TodoChannel",
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
console.log(data['message'])
notify: ->
#perform 'notify'
i've faced something similar before until i realized that you can actually call stream_from multiple times in the channel and that user will be subscribed to multiple different "rooms" within the same channel connection. Which means you can basically do this
class TodoChannel < ApplicationCable::Channel
def subscribed
stream_from "todo_channel_all"
stream_from "todo_channel_#{current_user.id}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def notify(data)
# depending on data given from the user, send it to only one specific user or everyone listening to the "todo_channel_all"
if data['message']['other_user_id']
ActionCable.server.broadcast "todo_channel_#{data['message']['other_user_id']}", message: 'some message'
else
ActionCable.server.broadcast "todo_channel_all", message: 'some message'
end
end
end
that code assuming that the user already knows the other user's id and sent it to the channel, you would probably have to wrap that with some security or something, i admit i'm not very well experienced with rails as i'm still learning.
Something else that might be beneficial to you in the future is the fact that you can also broadcast several messages/times in the same channel function. That means you can potentially support sending out your tasks to a single specific user, a list of specific users or everyone. Just iterate on the list/array/whatever of users and broadcast the task/message/notification/whatever to them each on their personal "todo_channel_#{user.id}"
I ended up with a different approach. I'll write it here in case someone will find it helpful.
Notification has an id of a user that has to be notified. So in model I have:
after_commit :broadcast_notification, on: :create
def broadcast_notification
ActionCable.server.broadcast "todo_channel_#{self.user_id}", message: 'some message'
end
As I placed broadcasting into the model my todo_channel.rb looks like this:
class TodoChannel < ApplicationCable::Channel
def subscribed
stream_from "todo_channel_#{current_user.id}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
Step#1: Let each user have unique session token. While subscription, each user will send session token in headers and header is accessible in connection class. Find user by using session token.
Step#2: Stream user on user id.
Step#3: While sharing task, take user id too in the request and broadcast on given user id.
This is called "private chat". If you really want to get current_user.id you could do it in 3 ways:
Some AJAX onload call from todo.coffee to server.
Render current_user.id as an attribute in Rails HTML view and then get it via jQuery inside todo.coffee (as in https://www.sitepoint.com/create-a-chat-app-with-rails-5-actioncable-and-devise/ )
Create a plain cookie while a user logging in and then check it inside todo.coffee
But you shouldn't use current_user.id because it's not secure. If you use it then someone might register in the same site and easily listen to other users' private chats by simply providing a random user_id.
Instead use the signed (e.g. Rails-encrypted) cookie as a user's unique broadcast identifier. That way if you register in the same site you would never know some other user's signed cookie - so you can't break into an alien private chat.
app/config/initializers/warden_hooks.rb
See https://rubytutorial.io/actioncable-devise-authentication/
# Be sure to restart your server when you modify this file.
Warden::Manager.after_set_user do |user,auth,opts|
scope = opts[:scope]
auth.cookies.signed["#{scope}.id"] = user.id
end
Warden::Manager.before_logout do |user, auth, opts|
scope = opts[:scope]
auth.cookies.delete("#{scope}.id")
end
todo_channel.rb
class TodoChannel < ApplicationCable::Channel
def subscribed
stream_from "todo_channel_#{params['user_signed_cookie']}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def notify(param_message)
ActionCable.server.broadcast "todo_channel_#{param_message['user_signed_cookie']}", message: 'some message'(not implemented yet)
end
end
todo.coffee
user_signed_cookie = document.cookie.replace(/(?:(?:^|.*;\s*)user.id\s*\=\s*([^;]*).*$)|^.*$/, "$1");
user_logged_in = if user_signed_cookie then true else false
if user_logged_in #avoid repetitive HTTP->WebSockets upgrade pointless browser attempts when no user logged in.
App.todo = App.cable.subscriptions.create {
channel:"TodoChannel"
user_signed_cookie: user_signed_cookie
},
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
console.log(data['message'])
notify: (name, content, _) -> #name, content - message fields
#perform 'notify', name: name, content: content, user_signed_cookie: user_signed_cookie

Sidekiq: perform_async and order-dependent operations

There's a controller action in my Rails app that contacts a user via text-message and email. For reasons I won't go into, the text-message needs to complete before the email can be sent successfully. I originally had something like this:
controller:
class MyController < ApplicationController
def contact_user
ContactUserWorker.perform_async(#user.id)
end
end
workers:
class ContactUserWorker
include Sidekiq::Worker
def perform(user_id)
SendUserTextWorker.perform_async(user_id)
SendUserEmailWorker.perform_async(user_id)
end
end
class SendUserTextWorker
include Sidekiq::Worker
def perform(user_id)
user = User.find(user_id)
user.send_text
end
end
class SendUserEmailWorker
include Sidekiq::Worker
def perform(user_id)
user = User.find(user_id)
user.send_email
end
end
This was unreliable; sometimes the email would fail, sometimes both would fail. I'm trying to determine whether perform_async was the cause of the problem. Was the async part allowing the email to fire off before the text had completed? I'm a little fuzzy on how exactly perform_async works, but that sounded like a reasonable guess.
At first, I refactored ContactUserWorker to:
class ContactUserWorker
include Sidekiq::Worker
def perform(user_id)
user = User.find(user_id)
User.send_text
SendUserEmailWorker.perform_async(user_id)
end
end
Eventually though, I just moved the call to send_text out of the workers altogether and into the controller:
class MyController < ApplicationController
def contact_user
#user.send_text
SendUserEmailWorker.perform_async(#user.id)
end
end
This is a simplified version of the real code, but that's the gist of it. It seems to be working fine now, though I still wonder whether the problem was Sidekiq-related or if something else was going on.
I'm curious whether my original structure would've worked if I'd used perform instead of perform_async for all the calls except the email call. Like this:
class MyController < ApplicationController
def contact_user
ContactUserWorker.perform(#user.id)
end
end
class ContactUserWorker
include Sidekiq::Worker
def perform(user_id)
SendUserTextWorker.perform(user_id)
SendUserEmailWorker.perform_async(user_id)
end
end
If the email can only be sent after the text message has been sent, then send the email after successful completion of sending the text.
class ContactUserWorker
include Sidekiq::Worker
def perform(user_id)
SendUserTextWorker.perform_async(user_id)
end
end
class SendUserTextWorker
include Sidekiq::Worker
def perform(user_id)
user = User.find(user_id)
text_sent = user.send_text
SendUserEmailWorker.perform_async(user_id) if text_sent
end
end
class SendUserEmailWorker
include Sidekiq::Worker
def perform(user_id)
user = User.find(user_id)
user.send_email
end
end
In user.send_text you need to handle the fact that neither the text or the email has been sent.
I'm curious whether my original structure would've worked if I'd used perform instead of perform_async for all the calls except the email call
It would have. But this is not what you actually intdending. What you really want is this:
class MyController < ApplicationController
def contact_user
ContactUserWorker.perform_async(#user.id)
end
end
class ContactUserWorker
include Sidekiq::Worker
attr_reader :user_id
def perform(user_id)
#user_id = user_id
user.send_text
user.send_email
end
def user
#user ||= User.find user_id
end
end
The problem was indeed the perform async part. It schedules both tasks to be executed in the background by a separate sidekiq daemon process. i guess your sidekiq is configured to execute the jobs concurrently. In the first version you've first scheduled the ContactUserWorker to perform it's job in a background outside of the current rails request. As this worker is startet later on, it kicks off two separate delayed workers in turn, which are then run in parallel and so there is no way to determine which of the both executes/finishes first.
I don't know what you mean exatly by sending text, but sending an email is an io blocking process and therefore it was a good idea to perform this in a background, because it would be blocking a complete rails process otherwise until the email is delivered (on a typical unicorn/passenger multi-process deployment). And as you actually want to execute both tasks sequentially and as an atomic operation, it's totally fine, performing them by a single sidekiq job/worker.
You also don't have to check if send_text succeeds. Sidekiq will retry the complete job if any part of it fails with an exception

Rails: Call sign_out(#user) from a background worker

I want to sign_out a given user from a background job? E.g. sign out a given user fom a Sidekiq background worker. Any idea how I can access the method sign_out from the worker?
class SessionWorker
include Sidekiq::Worker
sidekiq_options retry: false
def perform(user_id)
# user = User.find(user_id)
# sign_out(user) if user
end
end
end
Not 100% sure, but:
I don't think this is possible because when you run code from a background worker, that code is executed in a different context than your web application and thus has no access to the warden session where the login is 'registrated'.

Rails 3 + Devise: log out user after timeout

In my app I set Devise to timeout a session after 30 minutes. Which works fine... the user has to log in again after that time. My only problem is that devise apparently doesn't use the destroy action in the session controller after a session has timed out. So the attribute :signed_in for the user isn't set to 'false'.
So even after a session has timed out this user is still displayed as online.
Is there a way to destroy the session after the timeout or set the signed_in attribute to false after a certain time and on browser close?
My destroy action in session controller:
def destroy
current_user.try("signed_in=", false); current_user.save
signed_in = signed_in?(resource_name)
sign_out_and_redirect(resource_name)
set_flash_message :notice, :signed_out if signed_in
end
I'm not exactly an expert on Devise but I suspect this is because of the stateless nature of HTTP. Devise only knows a session is timed out when the user tries to access a page again after your timeout length and will likely only call the destroy method when a user actually logs out and the destroy method is called on the session controller.
In order to achieve what you are likely looking for, you would need to run a background process that sweeps for old sessions and then manually calls the destroy method (or does something similar).
With the new Devise version it works as follows to have an accurate online/offline status:
put this in your application_controller:
before_filter :last_request
def last_request
if user_signed_in?
if current_user.last_request_at < 1.minutes.ago
current_user.update_attribute(:last_request_at, Time.now)
end
if current_user.currently_signed_in = false
current_user.update_attribute(:currently_signed_in, true)
end
end
end
With every action the app checks if the last request was over 1min ago, if yes, he updates the user attribute.
put this in user.rb:
before_save :set_last_request
Warden::Manager.after_authentication do |user,auth,opts|
user.update_attribute(:currently_signed_in, true)
end
Warden::Manager.before_logout do |user,auth,opts|
user.update_attribute(:currently_signed_in, false)
end
def set_last_request
self.last_request_at = Time.now
end
def signed_in?
if self.currently_signed_in
if self.timedout?(self.last_request_at.localtime)
return false
else
return true
end
else
false
end
end
you can then use the signed_in? method to determine a users online status.

Resources