esse-async_indexing 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +35 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +168 -0
- data/LICENSE +21 -0
- data/README.md +262 -0
- data/Rakefile +4 -0
- data/docker-compose.yml +11 -0
- data/lib/esse/async_indexing/actions/batch_delete.rb +15 -0
- data/lib/esse/async_indexing/actions/batch_import.rb +16 -0
- data/lib/esse/async_indexing/actions/batch_import_all.rb +11 -0
- data/lib/esse/async_indexing/actions/batch_update.rb +32 -0
- data/lib/esse/async_indexing/actions/bulk_update_lazy_document_attribute.rb +20 -0
- data/lib/esse/async_indexing/actions/coerce_index_repository.rb +28 -0
- data/lib/esse/async_indexing/actions/delete_document.rb +16 -0
- data/lib/esse/async_indexing/actions/import_batch_id.rb +21 -0
- data/lib/esse/async_indexing/actions/index_document.rb +20 -0
- data/lib/esse/async_indexing/actions/update_document.rb +20 -0
- data/lib/esse/async_indexing/actions/update_lazy_document_attribute.rb +14 -0
- data/lib/esse/async_indexing/actions/upsert_document.rb +25 -0
- data/lib/esse/async_indexing/actions.rb +21 -0
- data/lib/esse/async_indexing/active_record.rb +102 -0
- data/lib/esse/async_indexing/active_record_callbacks/callback.rb +27 -0
- data/lib/esse/async_indexing/active_record_callbacks/lazy_update_attribute.rb +26 -0
- data/lib/esse/async_indexing/active_record_callbacks/on_create.rb +15 -0
- data/lib/esse/async_indexing/active_record_callbacks/on_destroy.rb +15 -0
- data/lib/esse/async_indexing/active_record_callbacks/on_update.rb +27 -0
- data/lib/esse/async_indexing/adapters/adapter.rb +29 -0
- data/lib/esse/async_indexing/adapters/faktory.rb +114 -0
- data/lib/esse/async_indexing/adapters/sidekiq.rb +94 -0
- data/lib/esse/async_indexing/adapters.rb +12 -0
- data/lib/esse/async_indexing/cli/async_import.rb +58 -0
- data/lib/esse/async_indexing/cli.rb +32 -0
- data/lib/esse/async_indexing/config.rb +27 -0
- data/lib/esse/async_indexing/configuration/base.rb +65 -0
- data/lib/esse/async_indexing/configuration/faktory.rb +6 -0
- data/lib/esse/async_indexing/configuration/sidekiq.rb +12 -0
- data/lib/esse/async_indexing/configuration.rb +45 -0
- data/lib/esse/async_indexing/errors.rb +12 -0
- data/lib/esse/async_indexing/jobs/bulk_update_lazy_document_attribute_job.rb +7 -0
- data/lib/esse/async_indexing/jobs/document_delete_by_id_job.rb +7 -0
- data/lib/esse/async_indexing/jobs/document_index_by_id_job.rb +7 -0
- data/lib/esse/async_indexing/jobs/document_update_by_id_job.rb +7 -0
- data/lib/esse/async_indexing/jobs/document_upsert_by_id_job.rb +7 -0
- data/lib/esse/async_indexing/jobs/import_all_job.rb +7 -0
- data/lib/esse/async_indexing/jobs/import_batch_id_job.rb +34 -0
- data/lib/esse/async_indexing/jobs/update_lazy_document_attribute_job.rb +7 -0
- data/lib/esse/async_indexing/testing.rb +79 -0
- data/lib/esse/async_indexing/version.rb +7 -0
- data/lib/esse/async_indexing/worker.rb +85 -0
- data/lib/esse/async_indexing/workers/faktory.rb +28 -0
- data/lib/esse/async_indexing/workers/shared_class_methods.rb +26 -0
- data/lib/esse/async_indexing/workers/sidekiq.rb +28 -0
- data/lib/esse/async_indexing/workers.rb +48 -0
- data/lib/esse/async_indexing.rb +72 -0
- data/lib/esse/plugins/async_indexing.rb +106 -0
- data/lib/esse-async-indexing.rb +3 -0
- data/lib/esse-async_indexing.rb +3 -0
- metadata +244 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "esse/cli"
|
|
4
|
+
|
|
5
|
+
module Esse::AsyncIndexing
|
|
6
|
+
module CLI
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
Esse::CLI::Index.class_eval do
|
|
11
|
+
desc "async_import *INDEX_CLASSES", "Async Import documents from the given classes into the index"
|
|
12
|
+
option :repo, type: :string, default: nil, alias: "-r", desc: "Repository to use for import"
|
|
13
|
+
option :suffix, type: :string, default: nil, aliases: "-s", desc: "Suffix to append to index name"
|
|
14
|
+
option :context, type: :hash, default: {}, required: true, desc: "List of options to pass to the index class"
|
|
15
|
+
option :service, type: :string, default: nil, alias: "-s", desc: "Service to use for async import: sidekiq, faktory"
|
|
16
|
+
option :eager_include_document_attributes, type: :string, default: nil, desc: "Comma separated list of lazy document attributes to include to the bulk index request. Or pass `true` to include all lazy attributes"
|
|
17
|
+
option :lazy_update_document_attributes, type: :string, default: nil, desc: "Comma separated list of lazy document attributes to bulk update after the bulk index request Or pass `true` to include all lazy attributes"
|
|
18
|
+
def async_import(*index_classes)
|
|
19
|
+
opts = Esse::HashUtils.deep_transform_keys(options.to_h, &:to_sym)
|
|
20
|
+
opts[:service] ||= Esse.config.async_indexing.services.first
|
|
21
|
+
opts.delete(:lazy_update_document_attributes) if opts[:lazy_update_document_attributes] == "false"
|
|
22
|
+
opts.delete(:eager_include_document_attributes) if opts[:eager_include_document_attributes] == "false"
|
|
23
|
+
if (val = opts[:eager_include_document_attributes])
|
|
24
|
+
opts[:eager_include_document_attributes] = (val == "true") ? true : val.split(",")
|
|
25
|
+
end
|
|
26
|
+
if (val = opts[:lazy_update_document_attributes])
|
|
27
|
+
opts[:lazy_update_document_attributes] = (val == "true") ? true : val.split(",")
|
|
28
|
+
end
|
|
29
|
+
require "esse/async_indexing/cli/async_import"
|
|
30
|
+
Esse::AsyncIndexing::CLI::AsyncImport.new(indices: index_classes, **opts).run
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "configuration"
|
|
4
|
+
require_relative "configuration/base"
|
|
5
|
+
require_relative "configuration/faktory"
|
|
6
|
+
require_relative "configuration/sidekiq"
|
|
7
|
+
|
|
8
|
+
module Esse
|
|
9
|
+
module AsyncIndexing
|
|
10
|
+
module Config
|
|
11
|
+
def self.included(base)
|
|
12
|
+
base.__send__(:include, InstanceMethods)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module InstanceMethods
|
|
16
|
+
def async_indexing
|
|
17
|
+
@async_indexing ||= AsyncIndexing::Configuration.new
|
|
18
|
+
if block_given?
|
|
19
|
+
yield @async_indexing
|
|
20
|
+
else
|
|
21
|
+
@async_indexing
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Esse::AsyncIndexing::Configuration::Base
|
|
4
|
+
class << self
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def attribute_accessor(field, validator: nil, normalizer: nil, default: nil)
|
|
8
|
+
normalizer ||= :"normalize_#{field}"
|
|
9
|
+
validator ||= :"validate_#{field}"
|
|
10
|
+
|
|
11
|
+
define_method(field) do
|
|
12
|
+
unless instance_variable_defined?(:"@#{field}")
|
|
13
|
+
return if default.nil?
|
|
14
|
+
|
|
15
|
+
send(:"#{field}=", default.respond_to?(:call) ? default.call : default)
|
|
16
|
+
end
|
|
17
|
+
instance_variable_get(:"@#{field}")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
define_method(:"#{field}=") do |value|
|
|
21
|
+
value = send(normalizer, field, value) if respond_to?(normalizer, true)
|
|
22
|
+
send(validator, field, value) if respond_to?(validator, true)
|
|
23
|
+
|
|
24
|
+
instance_variable_set(:"@#{field}", value)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# A Hash with all workers definitions. The worker class name must be the main hash key
|
|
30
|
+
# Example:
|
|
31
|
+
# "FaktoryIndexWorker":
|
|
32
|
+
# retry: false
|
|
33
|
+
# queue: "indexing"
|
|
34
|
+
# adapter: "faktory"
|
|
35
|
+
# "FaktoryBatchIndexWorker":
|
|
36
|
+
# retry: 5
|
|
37
|
+
# queue: "batch_index"
|
|
38
|
+
# adapter: "faktory"
|
|
39
|
+
attribute_accessor :workers, default: {}
|
|
40
|
+
|
|
41
|
+
def worker_options(class_name)
|
|
42
|
+
class_name = class_name.to_s
|
|
43
|
+
if strict? && !workers.key?(class_name)
|
|
44
|
+
raise Esse::AsyncIndexing::NotDefinedWorkerError.new(class_name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
workers.fetch(class_name, {})
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def strict?
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
protected
|
|
55
|
+
|
|
56
|
+
def normalize_workers(_, value)
|
|
57
|
+
return unless value.is_a?(Hash)
|
|
58
|
+
|
|
59
|
+
hash = Esse::AsyncIndexing::Workers::DEFAULT.values.map { |v| [v, {}] }.to_h
|
|
60
|
+
value.each do |class_name, opts|
|
|
61
|
+
hash[class_name.to_s] = Esse::HashUtils.deep_transform_keys(opts.to_h, &:to_sym)
|
|
62
|
+
end
|
|
63
|
+
hash
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse::AsyncIndexing
|
|
4
|
+
class Configuration::Sidekiq < Configuration::Base
|
|
5
|
+
attribute_accessor :redis
|
|
6
|
+
attribute_accessor :namespace, default: "sidekiq"
|
|
7
|
+
|
|
8
|
+
def redis_pool
|
|
9
|
+
@redis_pool ||= Esse::RedisStorage::Pool.new(redis)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse
|
|
4
|
+
module AsyncIndexing
|
|
5
|
+
class ConfigService < Set
|
|
6
|
+
def sidekiq?
|
|
7
|
+
include?(:sidekiq)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def faktory?
|
|
11
|
+
include?(:faktory)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Configuration
|
|
16
|
+
def services
|
|
17
|
+
@services ||= ConfigService.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def faktory
|
|
21
|
+
@faktory ||= begin
|
|
22
|
+
services.add(:faktory)
|
|
23
|
+
Configuration::Faktory.new
|
|
24
|
+
end
|
|
25
|
+
if block_given?
|
|
26
|
+
yield @faktory
|
|
27
|
+
else
|
|
28
|
+
@faktory
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def sidekiq
|
|
33
|
+
@sidekiq ||= begin
|
|
34
|
+
services.add(:sidekiq)
|
|
35
|
+
Configuration::Sidekiq.new
|
|
36
|
+
end
|
|
37
|
+
if block_given?
|
|
38
|
+
yield @sidekiq
|
|
39
|
+
else
|
|
40
|
+
@sidekiq
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Esse::AsyncIndexing::Jobs::BulkUpdateLazyDocumentAttributeJob
|
|
4
|
+
def perform(index_class_name, repo_name, attribute_name, batch_id, options = {})
|
|
5
|
+
Esse::AsyncIndexing::Actions::BulkUpdateLazyDocumentAttribute.call(index_class_name, repo_name, attribute_name, batch_id, options)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Esse::AsyncIndexing::Jobs::DocumentDeleteByIdJob
|
|
4
|
+
def perform(index_class_name, repo_name, document_id, options = {})
|
|
5
|
+
Esse::AsyncIndexing::Actions::DeleteDocument.call(index_class_name, repo_name, document_id, options)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Esse::AsyncIndexing::Jobs::DocumentIndexByIdJob
|
|
4
|
+
def perform(index_class_name, repo_name, document_id, options = {})
|
|
5
|
+
Esse::AsyncIndexing::Actions::IndexDocument.call(index_class_name, repo_name, document_id, options)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Esse::AsyncIndexing::Jobs::DocumentUpdateByIdJob
|
|
4
|
+
def perform(index_class_name, repo_name, document_id, options = {})
|
|
5
|
+
Esse::AsyncIndexing::Actions::UpdateDocument.call(index_class_name, repo_name, document_id, options)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Esse::AsyncIndexing::Jobs::DocumentUpsertByIdJob
|
|
4
|
+
def perform(index_class_name, repo_name, document_id, operation = "index", options = {})
|
|
5
|
+
Esse::AsyncIndexing::Actions::UpsertDocument.call(index_class_name, repo_name, document_id, operation, options)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Esse::AsyncIndexing::Jobs::ImportBatchIdJob
|
|
4
|
+
LAZY_ATTR_WORKER = "Esse::AsyncIndexing::Jobs::BulkUpdateLazyDocumentAttributeJob"
|
|
5
|
+
|
|
6
|
+
def perform(index_class_name, repo_name, batch_id, options = {})
|
|
7
|
+
total, ids = Esse::AsyncIndexing::Actions::ImportBatchId.call(index_class_name, repo_name, batch_id, options)
|
|
8
|
+
|
|
9
|
+
options = Esse::HashUtils.deep_transform_keys(options, &:to_s)
|
|
10
|
+
return total if total.zero?
|
|
11
|
+
return total if lazy_already_imported?(options)
|
|
12
|
+
return total unless self.class.respond_to?(:bg_worker_options)
|
|
13
|
+
|
|
14
|
+
_index_class, repo_class = Esse::AsyncIndexing::Actions::CoerceIndexRepository.call(index_class_name, repo_name)
|
|
15
|
+
|
|
16
|
+
repo_class.lazy_document_attributes.each_key do |attr_name|
|
|
17
|
+
queue = Esse::RedisStorage::Queue.for(repo: repo_class, attribute_name: attr_name)
|
|
18
|
+
queue.enqueue(id: batch_id, values: ids)
|
|
19
|
+
Esse::AsyncIndexing.worker(LAZY_ATTR_WORKER, service: self.class.bg_worker_options[:service])
|
|
20
|
+
.with_args(index_class_name, repo_name, attr_name.to_s, batch_id, options)
|
|
21
|
+
.push
|
|
22
|
+
end
|
|
23
|
+
total
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
protected
|
|
27
|
+
|
|
28
|
+
# The `import` action already eager or lazy load the document attributes when some of these options are set.
|
|
29
|
+
def lazy_already_imported?(options)
|
|
30
|
+
eager = options.delete("eager_include_document_attributes") || false
|
|
31
|
+
lazy = options.delete("lazy_update_document_attributes") || false
|
|
32
|
+
eager || lazy
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Esse::AsyncIndexing::Jobs::UpdateLazyDocumentAttributeJob
|
|
4
|
+
def perform(index_class_name, repo_name, attribute_name, ids, options = {})
|
|
5
|
+
Esse::AsyncIndexing::Actions::UpdateLazyDocumentAttribute.call(index_class_name, repo_name, attribute_name, ids, options)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse
|
|
4
|
+
module AsyncIndexing
|
|
5
|
+
class Testing
|
|
6
|
+
class << self
|
|
7
|
+
def enable!
|
|
8
|
+
Thread.current[:esse_async_indexing_testing] = true
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def disable!
|
|
12
|
+
Thread.current[:esse_async_indexing_testing] = false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def enabled?
|
|
16
|
+
Thread.current[:esse_async_indexing_testing] == true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def disabled?
|
|
20
|
+
!enabled?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
Esse::AsyncIndexing::Testing.disable!
|
|
28
|
+
|
|
29
|
+
module Esse::AsyncIndexing::Jobs
|
|
30
|
+
class << self
|
|
31
|
+
def jobs
|
|
32
|
+
@jobs ||= []
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def push(job)
|
|
36
|
+
jobs.push(job)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def clear
|
|
40
|
+
jobs.clear
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def size
|
|
44
|
+
jobs.size
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def jobs_for(service: nil, class_name: nil)
|
|
48
|
+
filtered = jobs
|
|
49
|
+
if service
|
|
50
|
+
filtered = filtered.select { |job| job["service"] == service.to_s }
|
|
51
|
+
end
|
|
52
|
+
if class_name
|
|
53
|
+
filtered = filtered.select { |job| job["__class_name__"] == class_name.to_s }
|
|
54
|
+
end
|
|
55
|
+
filtered
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
module Esse::AsyncIndexing::JobsInterceptorAdapter
|
|
61
|
+
def push
|
|
62
|
+
return super unless Esse::AsyncIndexing::Testing.enabled?
|
|
63
|
+
|
|
64
|
+
normalize_before_push
|
|
65
|
+
test_payload = @payload.dup
|
|
66
|
+
if @payload["jobtype"]
|
|
67
|
+
test_payload["service"] = "faktory"
|
|
68
|
+
test_payload["__class_name__"] = @payload["jobtype"]
|
|
69
|
+
else
|
|
70
|
+
test_payload["service"] = "sidekiq"
|
|
71
|
+
test_payload["__class_name__"] = @payload["class"]
|
|
72
|
+
end
|
|
73
|
+
Esse::AsyncIndexing::Jobs.push(test_payload)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
Esse::AsyncIndexing::SERVICES.each_value do |adapter|
|
|
78
|
+
adapter.prepend(Esse::AsyncIndexing::JobsInterceptorAdapter)
|
|
79
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse::AsyncIndexing
|
|
4
|
+
class Worker
|
|
5
|
+
attr_reader :options, :payload, :worker_class, :service
|
|
6
|
+
|
|
7
|
+
attr_reader :arguments
|
|
8
|
+
|
|
9
|
+
def initialize(worker_class, service: nil, **options)
|
|
10
|
+
@worker_class = worker_class
|
|
11
|
+
@service = service
|
|
12
|
+
@options = options
|
|
13
|
+
@payload = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.coerce(service:, payload:, **opts)
|
|
17
|
+
Esse::AsyncIndexing::SERVICES.fetch(service).coerce_to_worker(payload, **opts)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
%i[created_at enqueued_at].each do |method_name|
|
|
21
|
+
define_method method_name do |value|
|
|
22
|
+
@payload[method_name.to_s] =
|
|
23
|
+
case value
|
|
24
|
+
when Numeric then value.to_f
|
|
25
|
+
when String then Time.parse(value).to_f
|
|
26
|
+
when Time, DateTime then value.to_f
|
|
27
|
+
else
|
|
28
|
+
raise ArgumentError, format("The %<v>p is not a valid value for %<m>s.", v: value, m: method_name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Adds arguments to the job
|
|
36
|
+
# @return self
|
|
37
|
+
def with_args(*args)
|
|
38
|
+
@payload["args"] = args
|
|
39
|
+
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Schedule the time when a job will be executed. Jobs which are scheduled in the past are enqueued for immediate execution.
|
|
44
|
+
# @param timestamp [Numeric] timestamp, numeric or something that acts numeric.
|
|
45
|
+
# @return self
|
|
46
|
+
def in(timestamp)
|
|
47
|
+
now = Time.now.to_f
|
|
48
|
+
timestamp = Time.parse(timestamp) if timestamp.is_a?(String)
|
|
49
|
+
int = timestamp.respond_to?(:strftime) ? timestamp.to_f : now + timestamp.to_f
|
|
50
|
+
return self if int <= now
|
|
51
|
+
|
|
52
|
+
@payload["at"] = int
|
|
53
|
+
@payload["created_at"] = now
|
|
54
|
+
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
alias_method :at, :in
|
|
58
|
+
|
|
59
|
+
def with_job_jid(jid = nil)
|
|
60
|
+
@payload["jid"] ||= jid || Esse::AsyncIndexing.jid
|
|
61
|
+
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return Response of service
|
|
66
|
+
# @see Esse::AsyncIndexing::Adapters::** for more details
|
|
67
|
+
def push
|
|
68
|
+
unless Esse::AsyncIndexing::SERVICES.key?(service)
|
|
69
|
+
raise Esse::AsyncIndexing::Error, format("Service %<service>p is not implemented. Please use one of #{Esse::AsyncIndexing::SERVICES.keys.map(&:inspect).join(" or ")}.", service: service)
|
|
70
|
+
end
|
|
71
|
+
@payload["created_at"] ||= Time.now.to_f
|
|
72
|
+
worker_to_push = with_job_jid
|
|
73
|
+
Esse::AsyncIndexing::SERVICES[service].push(worker_to_push)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def eql?(other)
|
|
77
|
+
return false unless other.is_a?(self.class)
|
|
78
|
+
|
|
79
|
+
worker_class == other.worker_class &&
|
|
80
|
+
payload == other.payload &&
|
|
81
|
+
options == other.options
|
|
82
|
+
end
|
|
83
|
+
alias_method :==, :eql?
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frizen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "shared_class_methods"
|
|
4
|
+
|
|
5
|
+
module Esse::AsyncIndexing
|
|
6
|
+
module Workers
|
|
7
|
+
module Faktory
|
|
8
|
+
def self.extended(base)
|
|
9
|
+
base.include(::Faktory::Job) if defined?(::Faktory)
|
|
10
|
+
base.extend SharedClassMethods
|
|
11
|
+
base.extend ClassMethods
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module ClassMethods
|
|
15
|
+
def service_worker_options
|
|
16
|
+
default_queue = Esse.config.async_indexing.faktory.workers.dig(name, :queue)
|
|
17
|
+
default_retry = Esse.config.async_indexing.faktory.workers.dig(name, :retry)
|
|
18
|
+
default_queue ||= ::Faktory.default_job_options["queue"] if defined?(::Faktory)
|
|
19
|
+
default_retry ||= ::Faktory.default_job_options["retry"] if defined?(::Faktory)
|
|
20
|
+
{
|
|
21
|
+
queue: default_queue || "default",
|
|
22
|
+
retry: default_retry || 25
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frizen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse::AsyncIndexing
|
|
4
|
+
module Workers
|
|
5
|
+
module SharedClassMethods
|
|
6
|
+
def perform_async(*args)
|
|
7
|
+
build_worker.with_args(*args).push
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def perform_in(interval, *args)
|
|
11
|
+
build_worker.with_args(*args).at(interval).push
|
|
12
|
+
end
|
|
13
|
+
alias_method :perform_at, :perform_in
|
|
14
|
+
|
|
15
|
+
protected
|
|
16
|
+
|
|
17
|
+
def service_worker_options
|
|
18
|
+
{}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def build_worker
|
|
22
|
+
Esse::AsyncIndexing.worker(name, **service_worker_options.merge(bg_worker_options))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frizen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "shared_class_methods"
|
|
4
|
+
|
|
5
|
+
module Esse::AsyncIndexing
|
|
6
|
+
module Workers
|
|
7
|
+
module Sidekiq
|
|
8
|
+
def self.extended(base)
|
|
9
|
+
base.include(::Sidekiq::Worker) if defined?(::Sidekiq)
|
|
10
|
+
base.extend SharedClassMethods
|
|
11
|
+
base.extend ClassMethods
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module ClassMethods
|
|
15
|
+
def service_worker_options
|
|
16
|
+
default_queue = Esse.config.async_indexing.sidekiq.workers.dig(name, :queue)
|
|
17
|
+
default_retry = Esse.config.async_indexing.sidekiq.workers.dig(name, :retry)
|
|
18
|
+
default_queue ||= ::Sidekiq.default_worker_options["queue"] if defined?(::Sidekiq)
|
|
19
|
+
default_retry ||= ::Sidekiq.default_worker_options["retry"] if defined?(::Sidekiq)
|
|
20
|
+
{
|
|
21
|
+
queue: default_queue || "default",
|
|
22
|
+
retry: default_retry || 15
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "worker"
|
|
4
|
+
|
|
5
|
+
module Esse
|
|
6
|
+
module AsyncIndexing
|
|
7
|
+
module Workers
|
|
8
|
+
DEFAULT = {
|
|
9
|
+
"esse/async_indexing/jobs/bulk_update_lazy_document_attribute_job" => "Esse::AsyncIndexing::Jobs::BulkUpdateLazyDocumentAttributeJob",
|
|
10
|
+
"esse/async_indexing/jobs/document_delete_by_id_job" => "Esse::AsyncIndexing::Jobs::DocumentDeleteByIdJob",
|
|
11
|
+
"esse/async_indexing/jobs/document_index_by_id_job" => "Esse::AsyncIndexing::Jobs::DocumentIndexByIdJob",
|
|
12
|
+
"esse/async_indexing/jobs/document_update_by_id_job" => "Esse::AsyncIndexing::Jobs::DocumentUpdateByIdJob",
|
|
13
|
+
"esse/async_indexing/jobs/document_upsert_by_id_job" => "Esse::AsyncIndexing::Jobs::DocumentUpsertByIdJob",
|
|
14
|
+
"esse/async_indexing/jobs/import_all_job" => "Esse::AsyncIndexing::Jobs::ImportAllJob",
|
|
15
|
+
"esse/async_indexing/jobs/import_batch_id_job" => "Esse::AsyncIndexing::Jobs::ImportBatchIdJob",
|
|
16
|
+
"esse/async_indexing/jobs/update_lazy_document_attribute_job" => "Esse::AsyncIndexing::Jobs::UpdateLazyDocumentAttributeJob"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# The backend service may live in a different application, so they are not required by default.
|
|
20
|
+
# That's why we have a separate structure to let enqueue jobs even without having the explicit worker class loaded.
|
|
21
|
+
# This method will require all the internal jobs and configure them according to the defined options.
|
|
22
|
+
def self.install!(service, **options)
|
|
23
|
+
return if @installed_services&.include?(service.to_sym)
|
|
24
|
+
|
|
25
|
+
DEFAULT.each do |job, worker_name|
|
|
26
|
+
Kernel.require(job)
|
|
27
|
+
worker = Esse::AsyncIndexing::Jobs.const_get(worker_name.split("::").last)
|
|
28
|
+
worker.extend(self.for(service, **options))
|
|
29
|
+
end
|
|
30
|
+
@installed_services = Array(@installed_services) << service.to_sym
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.for(service, **options)
|
|
34
|
+
require_relative "workers/#{service}"
|
|
35
|
+
service = service.to_sym
|
|
36
|
+
worker_options = options.merge(service: service)
|
|
37
|
+
module_name = service.to_s.split(/_/i).collect! { |w| w.capitalize }.join
|
|
38
|
+
mod = Esse::AsyncIndexing::Workers.const_get(module_name)
|
|
39
|
+
mod.module_eval do
|
|
40
|
+
define_method(:bg_worker_options) do
|
|
41
|
+
worker_options
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
mod
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "esse"
|
|
4
|
+
require "esse-redis_storage"
|
|
5
|
+
require "forwardable"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
require "time"
|
|
8
|
+
require "multi_json"
|
|
9
|
+
|
|
10
|
+
module Esse
|
|
11
|
+
module AsyncIndexing
|
|
12
|
+
module Actions
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module Jobs
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module Workers
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
require_relative "async_indexing/version"
|
|
24
|
+
require_relative "async_indexing/actions"
|
|
25
|
+
require_relative "async_indexing/adapters"
|
|
26
|
+
require_relative "async_indexing/config"
|
|
27
|
+
require_relative "async_indexing/errors"
|
|
28
|
+
require_relative "async_indexing/workers"
|
|
29
|
+
require_relative "plugins/async_indexing"
|
|
30
|
+
|
|
31
|
+
module Esse::AsyncIndexing
|
|
32
|
+
SERVICES = {
|
|
33
|
+
sidekiq: Adapters::Sidekiq,
|
|
34
|
+
faktory: Adapters::Faktory
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# @param worker_class [String] The worker class name
|
|
38
|
+
# @param options [Hash] Options that will be passed along to the worker instance
|
|
39
|
+
# @return [Esse::AsyncIndexing::Worker] An instance of worker
|
|
40
|
+
def self.worker(worker_class, service: nil, **options)
|
|
41
|
+
serv_name = service_name(service)
|
|
42
|
+
Worker.new(worker_class, **Esse.config.async_indexing.send(service).worker_options(worker_class).merge(options), service: serv_name)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.service_name(identifier = nil)
|
|
46
|
+
identifier ||= Esse.config.async_indexing.services.first
|
|
47
|
+
if identifier.nil?
|
|
48
|
+
raise ArgumentError, "There are no async indexing services configured. Please configure at least one service or pass the service name as an argument."
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if SERVICES[identifier.to_sym].nil?
|
|
52
|
+
raise ArgumentError, "Invalid service: #{identifier.inspect}, valid services are: #{SERVICES.keys.join(", ")}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
identifier.to_sym
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.jid
|
|
59
|
+
SecureRandom.hex(12)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.async_indexing_repo?(repo)
|
|
63
|
+
return false unless repo.is_a?(Class) && repo < Esse::Repository
|
|
64
|
+
|
|
65
|
+
repo.respond_to?(:implement_batch_ids?) && repo.implement_batch_ids?
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
Esse::Config.__send__ :include, Esse::AsyncIndexing::Config
|
|
70
|
+
if defined?(Esse::CLI)
|
|
71
|
+
require_relative "async_indexing/cli"
|
|
72
|
+
end
|