مراية لـ
https://github.com/postalserver/postal.git
تم المزامنة 2025-11-30 21:32:30 +00:00
feat: new background work process
This removes all previous dependencies on RabbitMQ and the need to run separate cron and requeueing processes.
هذا الالتزام موجود في:
58
spec/app/models/worker_role_spec.rb
Normal file
58
spec/app/models/worker_role_spec.rb
Normal file
@@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe WorkerRole do
|
||||
let(:locker_name) { "test" }
|
||||
|
||||
before do
|
||||
allow(Postal).to receive(:locker_name).and_return(locker_name)
|
||||
end
|
||||
|
||||
describe ".acquire" do
|
||||
context "when there are no existing roles" do
|
||||
it "returns :created" do
|
||||
expect(WorkerRole.acquire("test")).to eq(:created)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the current process holds a lock for a role" do
|
||||
it "returns :renewed" do
|
||||
create(:worker_role, role: "test", worker: "test", acquired_at: 1.minute.ago)
|
||||
expect(WorkerRole.acquire("test")).to eq(:renewed)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the role has become stale" do
|
||||
it "returns :stolen" do
|
||||
create(:worker_role, role: "test", worker: "another", acquired_at: 10.minute.ago)
|
||||
expect(WorkerRole.acquire("test")).to eq(:stolen)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the role is already locked by another worker" do
|
||||
it "returns false" do
|
||||
create(:worker_role, role: "test", worker: "another", acquired_at: 1.minute.ago)
|
||||
expect(WorkerRole.acquire("test")).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe ".release" do
|
||||
context "when the role is locked by the current worker" do
|
||||
it "deletes the role and returns true" do
|
||||
role = create(:worker_role, role: "test", worker: "test")
|
||||
expect(WorkerRole.release("test")).to eq(true)
|
||||
expect(WorkerRole.find_by(id: role.id)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "when the role is locked by another worker" do
|
||||
it "does not delete the role and returns false" do
|
||||
role = create(:worker_role, role: "test", worker: "another")
|
||||
expect(WorkerRole.release("test")).to eq(false)
|
||||
expect(WorkerRole.find_by(id: role.id)).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
23
spec/factories/ip_address_factory.rb
Normal file
23
spec/factories/ip_address_factory.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: ip_addresses
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# hostname :string(255)
|
||||
# ipv4 :string(255)
|
||||
# ipv6 :string(255)
|
||||
# priority :integer
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# ip_pool_id :integer
|
||||
#
|
||||
FactoryBot.define do
|
||||
factory :ip_address do
|
||||
ip_pool
|
||||
ipv4 { "10.0.0.1" }
|
||||
ipv6 { "2001:0db8:85a3:0000:0000:8a2e:0370:7334" }
|
||||
hostname { "ip.example.com" }
|
||||
end
|
||||
end
|
||||
23
spec/factories/ip_pool_factory.rb
Normal file
23
spec/factories/ip_pool_factory.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: ip_pools
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# default :boolean default(FALSE)
|
||||
# name :string(255)
|
||||
# uuid :string(255)
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_ip_pools_on_uuid (uuid)
|
||||
#
|
||||
FactoryBot.define do
|
||||
factory :ip_pool do
|
||||
name { "Default Pool" }
|
||||
default { true }
|
||||
end
|
||||
end
|
||||
40
spec/factories/queued_message_factory.rb
Normal file
40
spec/factories/queued_message_factory.rb
Normal file
@@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: queued_messages
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# attempts :integer default(0)
|
||||
# batch_key :string(255)
|
||||
# domain :string(255)
|
||||
# locked_at :datetime
|
||||
# locked_by :string(255)
|
||||
# manual :boolean default(FALSE)
|
||||
# retry_after :datetime
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# ip_address_id :integer
|
||||
# message_id :integer
|
||||
# route_id :integer
|
||||
# server_id :integer
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_queued_messages_on_domain (domain)
|
||||
# index_queued_messages_on_message_id (message_id)
|
||||
# index_queued_messages_on_server_id (server_id)
|
||||
#
|
||||
FactoryBot.define do
|
||||
factory :queued_message do
|
||||
server
|
||||
message_id { 1234 }
|
||||
domain { "example.com" }
|
||||
batch_key { nil }
|
||||
|
||||
trait :locked do
|
||||
locked_by { "worker1" }
|
||||
locked_at { 5.minutes.ago }
|
||||
end
|
||||
end
|
||||
end
|
||||
10
spec/factories/webhook_factory.rb
Normal file
10
spec/factories/webhook_factory.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :webhook do
|
||||
server
|
||||
name { "Example Webhook" }
|
||||
url { "https://example.com" }
|
||||
all_events { true }
|
||||
end
|
||||
end
|
||||
41
spec/factories/webhook_request_factory.rb
Normal file
41
spec/factories/webhook_request_factory.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: webhook_requests
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# attempts :integer default(0)
|
||||
# error :text(65535)
|
||||
# event :string(255)
|
||||
# locked_at :datetime
|
||||
# locked_by :string(255)
|
||||
# payload :text(65535)
|
||||
# retry_after :datetime
|
||||
# url :string(255)
|
||||
# uuid :string(255)
|
||||
# created_at :datetime
|
||||
# server_id :integer
|
||||
# webhook_id :integer
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_webhook_requests_on_locked_by (locked_by)
|
||||
#
|
||||
FactoryBot.define do
|
||||
factory :webhook_request do
|
||||
webhook
|
||||
url { "https://example.com" }
|
||||
event { "ExampleEvent" }
|
||||
payload { { "hello" => "world" } }
|
||||
|
||||
before(:create) do |webhook_request|
|
||||
webhook_request.server = webhook_request.webhook&.server
|
||||
end
|
||||
|
||||
trait :locked do
|
||||
locked_by { "test" }
|
||||
locked_at { 5.minutes.ago }
|
||||
end
|
||||
end
|
||||
end
|
||||
7
spec/factories/worker_role_factory.rb
Normal file
7
spec/factories/worker_role_factory.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :worker_role do
|
||||
role { "test" }
|
||||
end
|
||||
end
|
||||
114
spec/lib/worker/jobs/process_queued_messages_job.rb
Normal file
114
spec/lib/worker/jobs/process_queued_messages_job.rb
Normal file
@@ -0,0 +1,114 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
module Worker
|
||||
module Jobs
|
||||
|
||||
RSpec.describe ProcessQueuedMessagesJob do
|
||||
subject(:job) { described_class.new(logger: Postal.logger) }
|
||||
let(:mocked_service) { instance_double(UnqueueMessageService) }
|
||||
|
||||
before do
|
||||
allow(UnqueueMessageService).to receive(:new).and_return(mocked_service)
|
||||
allow(mocked_service).to receive(:call).with(any_args)
|
||||
end
|
||||
|
||||
describe "#call" do
|
||||
context "when there are no queued messages" do
|
||||
it "does nothing" do
|
||||
job.call
|
||||
expect(UnqueueMessageService).to_not have_received(:new)
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is an unlocked queued message for an IP address that is not ours" do
|
||||
it "does nothing" do
|
||||
ip_address = create(:ip_address)
|
||||
queued_message = create(:queued_message, ip_address: ip_address)
|
||||
job.call
|
||||
expect(UnqueueMessageService).to_not have_received(:new)
|
||||
expect(queued_message.reload.locked?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is an unlocked queued message without an IP address without a retry time" do
|
||||
it "locks the message and calls the service" do
|
||||
queued_message = create(:queued_message, ip_address: nil, retry_after: nil)
|
||||
job.call
|
||||
expect(UnqueueMessageService).to have_received(:new).with(logger: kind_of(Klogger::Logger), queued_message: queued_message)
|
||||
expect(mocked_service).to have_received(:call)
|
||||
expect(queued_message.reload.locked?).to be true
|
||||
expect(queued_message.locked_by).to eq Postal.locker_name
|
||||
expect(queued_message.locked_at).to be_within(1.second).of(Time.current)
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is an unlocked queued message without an IP address without a retry time in the past" do
|
||||
it "locks the message and calls the service" do
|
||||
queued_message = create(:queued_message, ip_address: nil, retry_after: 10.minutes.ago)
|
||||
job.call
|
||||
expect(UnqueueMessageService).to have_received(:new).with(logger: kind_of(Klogger::Logger), queued_message: queued_message)
|
||||
expect(mocked_service).to have_received(:call)
|
||||
expect(queued_message.reload.locked?).to be true
|
||||
expect(queued_message.locked_by).to eq Postal.locker_name
|
||||
expect(queued_message.locked_at).to be_within(1.second).of(Time.current)
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is an unlocked queued message without an IP address without a retry time in the future" do
|
||||
it "does nothing" do
|
||||
queued_message = create(:queued_message, ip_address: nil, retry_after: 10.minutes.from_now)
|
||||
job.call
|
||||
expect(UnqueueMessageService).to_not have_received(:new)
|
||||
expect(queued_message.reload.locked?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is a locked queued message without an IP address without a retry time" do
|
||||
it "does nothing" do
|
||||
queued_message = create(:queued_message, :locked, ip_address: nil, retry_after: nil)
|
||||
job.call
|
||||
expect(UnqueueMessageService).to_not have_received(:new)
|
||||
expect(queued_message.reload.locked?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is a locked queued message without an IP address with a retry time in the past" do
|
||||
it "does nothing" do
|
||||
queued_message = create(:queued_message, :locked, ip_address: nil, retry_after: 1.month.ago)
|
||||
job.call
|
||||
expect(UnqueueMessageService).to_not have_received(:new)
|
||||
expect(queued_message.reload.locked?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is an unlocked queued message with an IP address that is ours without a retry time" do
|
||||
it "locks the message and calls the service" do
|
||||
ip_address = create(:ip_address, ipv4: "10.20.30.40")
|
||||
allow(Socket).to receive(:ip_address_list).and_return([Addrinfo.new(["AF_INET", 1, "localhost.localdomain", "10.20.30.40"])])
|
||||
queued_message = create(:queued_message, ip_address: ip_address)
|
||||
job.call
|
||||
expect(UnqueueMessageService).to have_received(:new).with(logger: kind_of(Klogger::Logger), queued_message: queued_message)
|
||||
expect(mocked_service).to have_received(:call)
|
||||
expect(queued_message.reload.locked?).to be true
|
||||
expect(queued_message.locked_by).to eq Postal.locker_name
|
||||
expect(queued_message.locked_at).to be_within(1.second).of(Time.current)
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is an unlocked queued message with an IP address that is ours without a retry time in the future" do
|
||||
it "does nothing" do
|
||||
ip_address = create(:ip_address, ipv4: "10.20.30.40")
|
||||
allow(Socket).to receive(:ip_address_list).and_return([Addrinfo.new(["AF_INET", 1, "localhost.localdomain", "10.20.30.40"])])
|
||||
queued_message = create(:queued_message, ip_address: ip_address, retry_after: 1.month.from_now)
|
||||
job.call
|
||||
expect(UnqueueMessageService).to_not have_received(:new)
|
||||
expect(queued_message.reload.locked?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
56
spec/lib/worker/jobs/process_webhook_requests_job.rb
Normal file
56
spec/lib/worker/jobs/process_webhook_requests_job.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
module Worker
|
||||
module Jobs
|
||||
|
||||
RSpec.describe ProcessWebhookRequestsJob do
|
||||
subject(:job) { described_class.new(logger: Postal.logger) }
|
||||
|
||||
before do
|
||||
allow_any_instance_of(WebhookRequest).to receive(:deliver)
|
||||
end
|
||||
|
||||
context "when there are no requests to process" do
|
||||
it "does nothing" do
|
||||
job.call
|
||||
expect(job.work_completed?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is a unlocked request with no retry time" do
|
||||
it "delivers the request" do
|
||||
create(:webhook_request)
|
||||
job.call
|
||||
expect(job.work_completed?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is an unlocked request with a retry time in the past" do
|
||||
it "delivers the request" do
|
||||
create(:webhook_request, retry_after: 1.minute.ago)
|
||||
job.call
|
||||
expect(job.work_completed?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is an unlocked request with a retry time in the future" do
|
||||
it "does nothing" do
|
||||
create(:webhook_request, retry_after: 1.minute.from_now)
|
||||
job.call
|
||||
expect(job.work_completed?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "when there is a locked requested without a retry time" do
|
||||
it "does nothing" do
|
||||
create(:webhook_request, :locked)
|
||||
job.call
|
||||
expect(job.work_completed?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -8,6 +8,7 @@ require "spec_helper"
|
||||
require "factory_bot"
|
||||
require "timecop"
|
||||
require "database_cleaner"
|
||||
require "webmock/rspec"
|
||||
|
||||
DatabaseCleaner.allow_remote_database_url = true
|
||||
ActiveRecord::Base.logger = Logger.new("/dev/null")
|
||||
|
||||
المرجع في مشكلة جديدة
حظر مستخدم