Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Metaprogramming from Oban::Worker #1

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 128 additions & 8 deletions lib/oban/worker.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,138 @@
# frozen_string_literal: true

module Oban
# Public: The Worker abstract class
class Worker
Job = Oban::Job
# Public: The Worker module
module Worker
# Options class to include some metaprogramming methods
module Options
def self.included(base)
base.extend(ClassMethods)
base.oban_class_attribute :oban_worker_options
base.oban_class_attribute :job
base.oban_class_attribute :args
end

def initialize(job)
@job = job
# Options metaprogramming methods
module ClassMethods
ACCESSOR_MUTEX = Mutex.new

def worker_options(opts = {})
opts = opts.transform_keys(&:to_sym)
self.oban_worker_options = options.merge(opts)
end

def options
oban_worker_options || { queue: 'default' }
end

def oban_class_attribute(*attrs) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
instance_reader = true
instance_writer = true

attrs.each do |name| # rubocop:disable Metrics/BlockLength
synchronized_getter = "__synchronized_#{name}"

singleton_class.instance_eval do
undef_method(name) if method_defined?(name) || private_method_defined?(name)
end

define_singleton_method(synchronized_getter) { nil }

singleton_class.class_eval do
private(synchronized_getter)
end

define_singleton_method(name) { ACCESSOR_MUTEX.synchronize { send synchronized_getter } }

ivar = "@#{name}"

singleton_class.instance_eval do
m = "#{name}="
undef_method(m) if method_defined?(m) || private_method_defined?(m)
end

define_singleton_method("#{name}=") do |val|
singleton_class.class_eval do
ACCESSOR_MUTEX.synchronize do
if method_defined?(synchronized_getter) || private_method_defined?(synchronized_getter)
undef_method(synchronized_getter)
end
define_method(synchronized_getter) { val }
end
end

if singleton_class?
class_eval do
undef_method(name) if method_defined?(name) || private_method_defined?(name)

define_method(name) do
if instance_variable_defined? ivar
instance_variable_get ivar
else
singleton_class.send name
end
end
end
end

val
end

if instance_reader
undef_method(name) if method_defined?(name) || private_method_defined?(name)
define_method(name) do
if instance_variable_defined?(ivar)
instance_variable_get ivar
else
self.class.public_send name
end
end
end

next unless instance_writer

m = "#{name}="
undef_method(m) if method_defined?(m) || private_method_defined?(m)
attr_writer name
end
end
end
end

def self.included(base)
message = "Oban::Worker cannot be included in an ActiveJob: #{base.name}"
base_active_job = base.ancestors.any? { |c| c.name == 'ActiveJob::Base' }

raise ArgumentError, message if base_active_job

base.include(Options)
base.extend(ClassMethods)
end

def self.perform_job(job)
worker = job.worker
klass = job.worker.constantize.new

raise ArgumentError, "#{worker} doesn't implements the method perform" unless klass.methods.include?(:perform)

klass = job.worker.constantize.new
klass.job = job
klass.args = job.args

klass.perform
end

private
# Public: Export functions to custom worker class
module ClassMethods
def perform_async(args, options = {})
params = {
args: args,
worker: name,
state: 'available'
}.merge(options)

def params
@job.args
Job.create(params)
end
end
end
end
9 changes: 5 additions & 4 deletions spec/fixtures/worker_without_options.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# frozen_string_literal: true

class WorkerWithoutOptions < Oban::Worker
def perform(_job)
puts 'Worker without options'
params
class WorkerWithoutOptions
include Oban::Worker

def perform
args
end
end
30 changes: 18 additions & 12 deletions spec/oban/worker_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,29 @@
require 'spec_helper'

RSpec.describe Oban::Worker, type: :worker do
let(:args) { { id: 123 } }

context 'without options' do
it 'instantiates a custom worker' do
job = Job.new({ args: { id: 123 } })
worker = WorkerWithoutOptions.new(job)
let(:worker) { WorkerWithoutOptions }

expect(worker.is_a?(described_class)).to be true
end
context 'from custom worker' do
it 'inserts job with correctly fields' do
job = worker.perform_async(args)
expected_args = args.stringify_keys!

expect(job.args).to eq(expected_args)
expect(job.worker).to eq('WorkerWithoutOptions')
expect(job.state).to eq('available')
end

it 'performs the worker with created job' do
args = { id: 123 }
job = Oban::Job.create({ args: args })
worker = WorkerWithoutOptions.new(job)
it 'performs the worker with created job' do
job = worker.perform_async(args)

result = worker.perform(job)
expected_result = args.stringify_keys!
result = described_class.perform_job(job)
expected_result = args.stringify_keys!

expect(result).to eq(expected_result)
expect(result).to eq(expected_result)
end
end
end
end