1
0
مراية لـ 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.
هذا الالتزام موجود في:
Adam Cooke
2024-02-14 13:46:04 +00:00
ملتزم من قبل Adam Cooke
الأصل 044058d0f1
التزام dc8e895bfe
69 ملفات معدلة مع 1675 إضافات و1186 حذوفات

عرض الملف

@@ -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

عرض الملف

@@ -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

عرض الملف

@@ -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

عرض الملف

@@ -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

عرض الملف

@@ -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

عرض الملف

@@ -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

عرض الملف

@@ -0,0 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
factory :worker_role do
role { "test" }
end
end

عرض الملف

@@ -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

عرض الملف

@@ -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")