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,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse::AsyncIndexing::Actions
|
|
4
|
+
class ImportBatchId
|
|
5
|
+
def self.call(index_class_name, repo_name, batch_id, options = {})
|
|
6
|
+
_index_class, repo_class = CoerceIndexRepository.call(index_class_name, repo_name)
|
|
7
|
+
queue = Esse::RedisStorage::Queue.for(repo: repo_class)
|
|
8
|
+
|
|
9
|
+
kwargs = Esse::HashUtils.deep_transform_keys(options, &:to_sym)
|
|
10
|
+
kwargs[:context] ||= {}
|
|
11
|
+
result = 0
|
|
12
|
+
ids_from_batch = []
|
|
13
|
+
queue.fetch(batch_id) do |ids|
|
|
14
|
+
ids_from_batch = ids
|
|
15
|
+
kwargs[:context][:id] = ids
|
|
16
|
+
result = repo_class.import(**kwargs)
|
|
17
|
+
end
|
|
18
|
+
[result, ids_from_batch]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse::AsyncIndexing::Actions
|
|
4
|
+
class IndexDocument
|
|
5
|
+
DOC_ARGS = %i[lazy_attributes]
|
|
6
|
+
|
|
7
|
+
def self.call(index_class_name, repo_name, document_id, options = {})
|
|
8
|
+
index_class, repo_class = CoerceIndexRepository.call(index_class_name, repo_name)
|
|
9
|
+
bulk_opts = Esse::HashUtils.deep_transform_keys(options, &:to_sym)
|
|
10
|
+
bulk_opts.delete_if { |k, _| DOC_ARGS.include?(k) }
|
|
11
|
+
find_opts = options.slice(*DOC_ARGS)
|
|
12
|
+
|
|
13
|
+
doc = repo_class.documents(**find_opts, id: document_id).first
|
|
14
|
+
return :not_found unless doc
|
|
15
|
+
|
|
16
|
+
index_class.index(doc, **bulk_opts)
|
|
17
|
+
:indexed
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse::AsyncIndexing::Actions
|
|
4
|
+
class UpdateDocument
|
|
5
|
+
DOC_ARGS = %i[lazy_attributes]
|
|
6
|
+
|
|
7
|
+
def self.call(index_class_name, repo_name, document_id, options = {})
|
|
8
|
+
index_class, repo_class = CoerceIndexRepository.call(index_class_name, repo_name)
|
|
9
|
+
bulk_opts = Esse::HashUtils.deep_transform_keys(options, &:to_sym)
|
|
10
|
+
bulk_opts.delete_if { |k, _| DOC_ARGS.include?(k) }
|
|
11
|
+
find_opts = options.slice(*DOC_ARGS)
|
|
12
|
+
|
|
13
|
+
doc = repo_class.documents(**find_opts, id: document_id).first
|
|
14
|
+
return :not_found unless doc
|
|
15
|
+
|
|
16
|
+
index_class.update(doc, **bulk_opts)
|
|
17
|
+
:indexed
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse::AsyncIndexing::Actions
|
|
4
|
+
class UpdateLazyDocumentAttribute
|
|
5
|
+
def self.call(index_class_name, repo_name, attr_name, ids, options = {})
|
|
6
|
+
_index_class, repo_class = CoerceIndexRepository.call(index_class_name, repo_name)
|
|
7
|
+
kwargs = Esse::HashUtils.deep_transform_keys(options, &:to_sym)
|
|
8
|
+
|
|
9
|
+
attr_name = repo_class.lazy_document_attributes.keys.find { |key| key.to_s == attr_name.to_s }
|
|
10
|
+
repo_class.update_documents_attribute(attr_name, ids, **kwargs)
|
|
11
|
+
ids
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse::AsyncIndexing::Actions
|
|
4
|
+
class UpsertDocument
|
|
5
|
+
DOC_ARGS = %i[lazy_attributes]
|
|
6
|
+
# OPERATIONS = %w[index update delete]
|
|
7
|
+
|
|
8
|
+
def self.call(index_class_name, repo_name, document_id, operation = "index", options = {})
|
|
9
|
+
case operation
|
|
10
|
+
when "delete"
|
|
11
|
+
DeleteDocument.call(index_class_name, repo_name, document_id, options)
|
|
12
|
+
when "update"
|
|
13
|
+
result = UpdateDocument.call(index_class_name, repo_name, document_id, options)
|
|
14
|
+
return result if result != :not_found
|
|
15
|
+
DeleteDocument.call(index_class_name, repo_name, document_id, options)
|
|
16
|
+
when "index"
|
|
17
|
+
result = IndexDocument.call(index_class_name, repo_name, document_id, options)
|
|
18
|
+
return result if result != :not_found
|
|
19
|
+
DeleteDocument.call(index_class_name, repo_name, document_id, options)
|
|
20
|
+
else
|
|
21
|
+
raise ArgumentError, "operation must be one of index, update, delete"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse
|
|
4
|
+
module AsyncIndexing
|
|
5
|
+
module Actions
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
require_relative "actions/coerce_index_repository"
|
|
11
|
+
require_relative "actions/batch_import_all"
|
|
12
|
+
require_relative "actions/batch_import"
|
|
13
|
+
require_relative "actions/batch_delete"
|
|
14
|
+
require_relative "actions/batch_update"
|
|
15
|
+
require_relative "actions/bulk_update_lazy_document_attribute"
|
|
16
|
+
require_relative "actions/delete_document"
|
|
17
|
+
require_relative "actions/import_batch_id"
|
|
18
|
+
require_relative "actions/index_document"
|
|
19
|
+
require_relative "actions/update_document"
|
|
20
|
+
require_relative "actions/upsert_document"
|
|
21
|
+
require_relative "actions/update_lazy_document_attribute"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse
|
|
4
|
+
module AsyncIndexing
|
|
5
|
+
ESSE_ACTIVE_RECORD_MINIMAL_VERSION = ::Gem::Version.new("0.3.5")
|
|
6
|
+
|
|
7
|
+
def self.__validate_active_record_version!
|
|
8
|
+
unless defined?(Esse::ActiveRecord::Callbacks)
|
|
9
|
+
raise <<~MSG
|
|
10
|
+
To use async indexing ActiveRecord callbacks you need to install and require the `esse-active_record` gem.
|
|
11
|
+
|
|
12
|
+
Add this line to your application's Gemfile:
|
|
13
|
+
gem 'esse-active_record', '~> 0.3.5'
|
|
14
|
+
MSG
|
|
15
|
+
end
|
|
16
|
+
require "esse/active_record/version"
|
|
17
|
+
if ::Gem::Version.new(Esse::ActiveRecord::VERSION) < ESSE_ACTIVE_RECORD_MINIMAL_VERSION
|
|
18
|
+
raise <<~MSG
|
|
19
|
+
The esse-active_record gem version #{ESSE_ACTIVE_RECORD_MINIMAL_VERSION} or higher is required. Please update the gem to the latest version.
|
|
20
|
+
MSG
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.__register_active_record_callbacks!
|
|
25
|
+
callbacks = Esse::ActiveRecord::Callbacks
|
|
26
|
+
unless callbacks.registered?(:async_indexing, :create)
|
|
27
|
+
callbacks.register_callback(:async_indexing, :create, Esse::AsyncIndexing::ActiveRecordCallbacks::OnCreate)
|
|
28
|
+
end
|
|
29
|
+
unless callbacks.registered?(:async_indexing, :update)
|
|
30
|
+
callbacks.register_callback(:async_indexing, :update, Esse::AsyncIndexing::ActiveRecordCallbacks::OnUpdate)
|
|
31
|
+
end
|
|
32
|
+
unless callbacks.registered?(:async_indexing, :destroy)
|
|
33
|
+
callbacks.register_callback(:async_indexing, :destroy, Esse::AsyncIndexing::ActiveRecordCallbacks::OnDestroy)
|
|
34
|
+
end
|
|
35
|
+
unless callbacks.registered?(:async_update_lazy_attribute, :create)
|
|
36
|
+
callbacks.register_callback(:async_update_lazy_attribute, :create, Esse::AsyncIndexing::ActiveRecordCallbacks::LazyUpdateAttribute)
|
|
37
|
+
end
|
|
38
|
+
unless callbacks.registered?(:async_update_lazy_attribute, :update)
|
|
39
|
+
callbacks.register_callback(:async_update_lazy_attribute, :update, Esse::AsyncIndexing::ActiveRecordCallbacks::LazyUpdateAttribute)
|
|
40
|
+
end
|
|
41
|
+
unless callbacks.registered?(:async_update_lazy_attribute, :destroy)
|
|
42
|
+
callbacks.register_callback(:async_update_lazy_attribute, :destroy, Esse::AsyncIndexing::ActiveRecordCallbacks::LazyUpdateAttribute)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Usage:
|
|
47
|
+
# class User < ApplicationRecord
|
|
48
|
+
# include Esse::AsyncIndexing::ActiveRecord::Model
|
|
49
|
+
module ActiveRecord
|
|
50
|
+
module Model
|
|
51
|
+
def self.included(base)
|
|
52
|
+
unless base.respond_to?(:esse_index)
|
|
53
|
+
base.include(Esse::ActiveRecord::Model)
|
|
54
|
+
end
|
|
55
|
+
base.extend(ClassMethods)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
module ClassMethods
|
|
59
|
+
# Define callback on create/update/delete to push a job to the async indexing the document.
|
|
60
|
+
#
|
|
61
|
+
# @param [String] index_repo_name The path of index and repository name.
|
|
62
|
+
# For example a index with a single repository named `users` is `users`. And a index with
|
|
63
|
+
# multiple repositories named `animals` and `dog` as the repository name is `animals/dog`.
|
|
64
|
+
# For namespace, use `/` as the separator.
|
|
65
|
+
# @raise [ArgumentError] when the repo and events are already registered
|
|
66
|
+
# @raise [ArgumentError] when the specified index have multiple repos
|
|
67
|
+
def async_index_callback(index_repo_name, on: %i[create update destroy], with: nil, **options, &block)
|
|
68
|
+
options[:service_name] = ::Esse::AsyncIndexing.service_name(options[:service_name])
|
|
69
|
+
Array(on).each do |event|
|
|
70
|
+
esse_callback(index_repo_name, :async_indexing, on: event, with: with, **options, &block)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Define callback on create/update/delete to push a job to the async update a lazy attribute.
|
|
75
|
+
#
|
|
76
|
+
# @param [String] index_repo_name The path of index and repository name.
|
|
77
|
+
# For example a index with a single repository named `users` is `users`. And a index with
|
|
78
|
+
# multiple repositories named `animals` and `dog` as the repository name is `animals/dog`.
|
|
79
|
+
# For namespace, use `/` as the separator.
|
|
80
|
+
# @param [String, Symbol] attribute_name The name of the lazy attribute to update.
|
|
81
|
+
# @raise [ArgumentError] when the repo and events are already registered
|
|
82
|
+
# @raise [ArgumentError] when the specified index have multiple repos
|
|
83
|
+
def async_update_lazy_attribute_callback(index_repo_name, attribute_name, on: %i[create update destroy], **options, &block)
|
|
84
|
+
options[:attribute_name] = attribute_name
|
|
85
|
+
options[:service_name] = ::Esse::AsyncIndexing.service_name(options[:service_name])
|
|
86
|
+
esse_callback(index_repo_name, :async_update_lazy_attribute, identifier_suffix: attribute_name.to_sym, on: on, **options, &block)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
Esse::AsyncIndexing.__validate_active_record_version!
|
|
95
|
+
|
|
96
|
+
require_relative "active_record_callbacks/callback"
|
|
97
|
+
require_relative "active_record_callbacks/on_create"
|
|
98
|
+
require_relative "active_record_callbacks/on_update"
|
|
99
|
+
require_relative "active_record_callbacks/on_destroy"
|
|
100
|
+
require_relative "active_record_callbacks/lazy_update_attribute"
|
|
101
|
+
|
|
102
|
+
Esse::AsyncIndexing.__register_active_record_callbacks!
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse::AsyncIndexing
|
|
4
|
+
module ActiveRecordCallbacks
|
|
5
|
+
class Callback < ::Esse::ActiveRecord::Callback
|
|
6
|
+
attr_reader :service_name
|
|
7
|
+
|
|
8
|
+
def initialize(service_name:, with: nil, **kwargs)
|
|
9
|
+
@service_name = service_name
|
|
10
|
+
@with = with
|
|
11
|
+
super(**kwargs)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
protected
|
|
15
|
+
|
|
16
|
+
def resolve_document_id(model)
|
|
17
|
+
resolve_document_ids(model).first
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def resolve_document_ids(model)
|
|
21
|
+
::Esse::ArrayUtils.wrap(block_result || model).map do |record|
|
|
22
|
+
record.is_a?(::ActiveRecord::Base) ? record.id : record
|
|
23
|
+
end.compact
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse::AsyncIndexing
|
|
4
|
+
module ActiveRecordCallbacks
|
|
5
|
+
class LazyUpdateAttribute < Callback
|
|
6
|
+
LAZY_ATTR_WORKER = "Esse::AsyncIndexing::Jobs::UpdateLazyDocumentAttributeJob"
|
|
7
|
+
|
|
8
|
+
attr_reader :attribute_name
|
|
9
|
+
|
|
10
|
+
def initialize(service_name:, attribute_name:, with: nil, **kwargs)
|
|
11
|
+
@attribute_name = attribute_name
|
|
12
|
+
super(service_name: service_name, **kwargs)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(model)
|
|
16
|
+
if (doc_ids = resolve_document_ids(model))
|
|
17
|
+
Esse::AsyncIndexing.worker(LAZY_ATTR_WORKER, service: service_name)
|
|
18
|
+
.with_args(repo.index.name, repo.repo_name, attribute_name.to_s, doc_ids, Esse::HashUtils.deep_transform_keys(options, &:to_s))
|
|
19
|
+
.push
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
true
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse::AsyncIndexing
|
|
4
|
+
module ActiveRecordCallbacks
|
|
5
|
+
class OnCreate < Callback
|
|
6
|
+
def call(model)
|
|
7
|
+
if (doc_id = resolve_document_id(model))
|
|
8
|
+
repo.async_indexing_job_for(:index).call(**options, service: service_name, repo: repo, operation: :index, id: doc_id)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse::AsyncIndexing
|
|
4
|
+
module ActiveRecordCallbacks
|
|
5
|
+
class OnDestroy < Callback
|
|
6
|
+
def call(model)
|
|
7
|
+
if (doc_id = resolve_document_id(model))
|
|
8
|
+
repo.async_indexing_job_for(:delete).call(**options, service: service_name, repo: repo, operation: :delete, id: doc_id)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse::AsyncIndexing
|
|
4
|
+
module ActiveRecordCallbacks
|
|
5
|
+
class OnUpdate < Callback
|
|
6
|
+
def call(model)
|
|
7
|
+
doc_id = resolve_document_id(model)
|
|
8
|
+
return true unless doc_id
|
|
9
|
+
|
|
10
|
+
kwargs = {service: service_name, repo: repo, id: doc_id}
|
|
11
|
+
if with == :update
|
|
12
|
+
repo.async_indexing_job_for(:update).call(**options, **kwargs, operation: :update)
|
|
13
|
+
else
|
|
14
|
+
repo.async_indexing_job_for(:index).call(**options, **kwargs, operation: :index)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
protected
|
|
21
|
+
|
|
22
|
+
def with
|
|
23
|
+
@with || :index
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse::AsyncIndexing
|
|
4
|
+
module Adapters
|
|
5
|
+
class Adapter
|
|
6
|
+
# Push the worker job to the service
|
|
7
|
+
# @param _worker [Esse::AsyncIndexing::Worker] An instance of background worker
|
|
8
|
+
# @abstract Child classes should override this method
|
|
9
|
+
def self.push(_worker)
|
|
10
|
+
raise NotImplemented
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Coerces the raw payload into an instance of Worker
|
|
14
|
+
# @param payload [Object] the object that should be coerced to a Worker
|
|
15
|
+
# @options options [Hash] list of options that will be passed along to the Worker instance
|
|
16
|
+
# @return [Esse::AsyncIndexing::Worker] and instance of Esse::AsyncIndexing::Worker
|
|
17
|
+
# @abstract Child classes should override this method
|
|
18
|
+
def self.coerce_to_worker(payload, **options)
|
|
19
|
+
raise NotImplemented
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
protected
|
|
23
|
+
|
|
24
|
+
def normalize_before_push
|
|
25
|
+
# noop
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse::AsyncIndexing
|
|
4
|
+
module Adapters
|
|
5
|
+
# This is a Faktory adapter that converts Esse::AsyncIndexing::Worker object into a faktory readable format
|
|
6
|
+
# and then push the jobs into the service.
|
|
7
|
+
class Faktory < Adapter
|
|
8
|
+
attr_reader :worker, :queue
|
|
9
|
+
|
|
10
|
+
def initialize(worker)
|
|
11
|
+
@worker = worker
|
|
12
|
+
@queue = worker.options.fetch(:queue, "default")
|
|
13
|
+
|
|
14
|
+
@payload = worker.payload.merge(
|
|
15
|
+
"jobtype" => worker.worker_class,
|
|
16
|
+
"queue" => @queue,
|
|
17
|
+
"retry" => parse_retry(worker.options[:retry])
|
|
18
|
+
)
|
|
19
|
+
@payload["created_at"] ||= Time.now.to_f
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Coerces the raw payload into an instance of Worker
|
|
23
|
+
# @param payload [Hash] The job as json from redis
|
|
24
|
+
# @options options [Hash] list of options that will be passed along to the Worker instance
|
|
25
|
+
# @return [Esse::AsyncIndexing::Worker] and instance of Esse::AsyncIndexing::Worker
|
|
26
|
+
def self.coerce_to_worker(payload, **options)
|
|
27
|
+
raise(Error, "invalid payload") unless payload.is_a?(Hash)
|
|
28
|
+
raise(Error, "invalid payload") unless payload["jobtype"].is_a?(String)
|
|
29
|
+
|
|
30
|
+
options[:retry] ||= payload["retry"] if payload.key?("retry")
|
|
31
|
+
options[:queue] ||= payload["queue"] if payload.key?("queue")
|
|
32
|
+
|
|
33
|
+
Esse::AsyncIndexing.worker(payload["jobtype"], **options, service: :faktory).tap do |worker|
|
|
34
|
+
worker.with_args(*Array(payload["args"])) if payload.key?("args")
|
|
35
|
+
worker.with_job_jid(payload["jid"]) if payload.key?("jid")
|
|
36
|
+
worker.created_at(payload["created_at"]) if payload.key?("created_at")
|
|
37
|
+
worker.enqueued_at(payload["enqueued_at"]) if payload.key?("enqueued_at")
|
|
38
|
+
worker.at(payload["at"]) if payload.key?("at")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Initializes adapter and push job into the faktory service
|
|
43
|
+
#
|
|
44
|
+
# @param worker [Esse::AsyncIndexing::Worker] An instance of Esse::AsyncIndexing::Worker
|
|
45
|
+
# @return [Hash] Job payload
|
|
46
|
+
# @see push method for more details
|
|
47
|
+
def self.push(worker)
|
|
48
|
+
new(worker).push
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Push job to Faktory
|
|
52
|
+
# * If job has the 'at' key. Then schedule it
|
|
53
|
+
# * Otherwise enqueue for immediate execution
|
|
54
|
+
#
|
|
55
|
+
# @raise [Esse::AsyncIndexing::Error] raise and error when faktory dependency is not loaded
|
|
56
|
+
# @return [Hash] Payload that was sent to server
|
|
57
|
+
def push
|
|
58
|
+
unless Object.const_defined?(:Faktory)
|
|
59
|
+
raise Esse::AsyncIndexing::Error, <<~ERR
|
|
60
|
+
Faktory client for ruby is not loaded. You must install and require https://github.com/contribsys/faktory_worker_ruby.
|
|
61
|
+
ERR
|
|
62
|
+
end
|
|
63
|
+
normalize_before_push
|
|
64
|
+
|
|
65
|
+
pool = Thread.current[:faktory_via_pool] || ::Faktory.server_pool
|
|
66
|
+
::Faktory.client_middleware.invoke(@payload, pool) do
|
|
67
|
+
pool.with do |c|
|
|
68
|
+
c.push(@payload)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
@payload
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
protected
|
|
75
|
+
|
|
76
|
+
# Convert worker retry value acording to the Go struct datatype.
|
|
77
|
+
#
|
|
78
|
+
# * 25 is the default.
|
|
79
|
+
# * 0 means the job is completely ephemeral. No matter if it fails or succeeds, it will be discarded.
|
|
80
|
+
# * -1 means the job will go straight to the Dead set if it fails, no retries.
|
|
81
|
+
def parse_retry(value)
|
|
82
|
+
case value
|
|
83
|
+
when Numeric then value.to_i
|
|
84
|
+
when false then -1
|
|
85
|
+
else
|
|
86
|
+
25
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def parse_time(value)
|
|
91
|
+
case value
|
|
92
|
+
when Numeric then Time.at(value).to_datetime.rfc3339(9)
|
|
93
|
+
when Time then value.to_datetime.rfc3339(9)
|
|
94
|
+
when DateTime then value.rfc3339(9)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def to_json(value)
|
|
99
|
+
MultiJson.dump(value, mode: :compat)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def normalize_before_push
|
|
103
|
+
@payload["enqueued_at"] ||= Time.now.to_f
|
|
104
|
+
{"created_at" => false, "enqueued_at" => false, "at" => true}.each do |field, past_remove|
|
|
105
|
+
# Optimization to enqueue something now that is scheduled to go out now or in the past
|
|
106
|
+
if (time = @payload.delete(field)) &&
|
|
107
|
+
(!past_remove || (past_remove && time > Time.now.to_f))
|
|
108
|
+
@payload[field] = parse_time(time)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Esse::AsyncIndexing
|
|
4
|
+
module Adapters
|
|
5
|
+
# This is a Sidekiq adapter that converts Esse::AsyncIndexing::Worker object into a sidekiq readable format
|
|
6
|
+
# and then push the jobs into the service.
|
|
7
|
+
class Sidekiq < Adapter
|
|
8
|
+
attr_reader :worker, :queue
|
|
9
|
+
|
|
10
|
+
def initialize(worker)
|
|
11
|
+
@worker = worker
|
|
12
|
+
@queue = worker.options.fetch(:queue, "default")
|
|
13
|
+
|
|
14
|
+
@payload = worker.payload.merge(
|
|
15
|
+
"class" => worker.worker_class,
|
|
16
|
+
"retry" => worker.options.fetch(:retry, true),
|
|
17
|
+
"queue" => @queue
|
|
18
|
+
)
|
|
19
|
+
@payload["created_at"] ||= Time.now.to_f
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Coerces the raw payload into an instance of Worker
|
|
23
|
+
# @param payload [Hash] The job as json from redis
|
|
24
|
+
# @options options [Hash] list of options that will be passed along to the Worker instance
|
|
25
|
+
# @return [Esse::AsyncIndexing::Worker] and instance of Esse::AsyncIndexing::Worker
|
|
26
|
+
def self.coerce_to_worker(payload, **options)
|
|
27
|
+
raise(Error, "invalid payload") unless payload.is_a?(Hash)
|
|
28
|
+
raise(Error, "invalid payload") unless payload["class"].is_a?(String)
|
|
29
|
+
|
|
30
|
+
options[:retry] ||= payload["retry"] if payload.key?("retry")
|
|
31
|
+
options[:queue] ||= payload["queue"] if payload.key?("queue")
|
|
32
|
+
|
|
33
|
+
Esse::AsyncIndexing.worker(payload["class"], **options, service: :sidekiq).tap do |worker|
|
|
34
|
+
worker.with_args(*Array(payload["args"])) if payload.key?("args")
|
|
35
|
+
worker.with_job_jid(payload["jid"]) if payload.key?("jid")
|
|
36
|
+
worker.created_at(payload["created_at"]) if payload.key?("created_at")
|
|
37
|
+
worker.enqueued_at(payload["enqueued_at"]) if payload.key?("enqueued_at")
|
|
38
|
+
worker.at(payload["at"]) if payload.key?("at")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Initializes adapter and push job into the sidekiq service
|
|
43
|
+
#
|
|
44
|
+
# @param worker [Esse::AsyncIndexing::Worker] An instance of Esse::AsyncIndexing::Worker
|
|
45
|
+
# @return [Hash] Job payload
|
|
46
|
+
# @see push method for more details
|
|
47
|
+
def self.push(worker)
|
|
48
|
+
new(worker).push
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Push sidekiq to the Sidekiq(Redis actually).
|
|
52
|
+
# * If job has the 'at' key. Then schedule it
|
|
53
|
+
# * Otherwise enqueue for immediate execution
|
|
54
|
+
#
|
|
55
|
+
# @return [Hash] Payload that was sent to redis
|
|
56
|
+
def push
|
|
57
|
+
normalize_before_push
|
|
58
|
+
# Optimization to enqueue something now that is scheduled to go out now or in the past
|
|
59
|
+
if (timestamp = @payload.delete("at")) && (timestamp > Time.now.to_f)
|
|
60
|
+
Esse.config.async_indexing.sidekiq.redis_pool.with do |redis|
|
|
61
|
+
redis.zadd(scheduled_queue_name, timestamp.to_f.to_s, to_json(@payload))
|
|
62
|
+
end
|
|
63
|
+
else
|
|
64
|
+
Esse.config.async_indexing.sidekiq.redis_pool.with do |redis|
|
|
65
|
+
redis.lpush(immediate_queue_name, to_json(@payload))
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
@payload
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
protected
|
|
72
|
+
|
|
73
|
+
def namespace
|
|
74
|
+
Esse.config.async_indexing.sidekiq.namespace
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def scheduled_queue_name
|
|
78
|
+
"#{namespace}:schedule"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def immediate_queue_name
|
|
82
|
+
"#{namespace}:queue:#{queue}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def to_json(value)
|
|
86
|
+
MultiJson.dump(value, mode: :compat)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def normalize_before_push
|
|
90
|
+
@payload["enqueued_at"] = Time.now.to_f
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "esse/cli/index/base_operation"
|
|
4
|
+
|
|
5
|
+
class Esse::AsyncIndexing::CLI::AsyncImport < Esse::CLI::Index::BaseOperation
|
|
6
|
+
WORKER_NAME = "Esse::AsyncIndexing::Jobs::ImportBatchIdJob"
|
|
7
|
+
|
|
8
|
+
def run
|
|
9
|
+
validate_options!
|
|
10
|
+
indices.each do |index|
|
|
11
|
+
repos = if (repo = @options[:repo])
|
|
12
|
+
[index.repo(repo)]
|
|
13
|
+
else
|
|
14
|
+
index.repo_hash.values
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
repos.each do |repo|
|
|
18
|
+
unless Esse::AsyncIndexing.async_indexing_repo?(repo)
|
|
19
|
+
raise Esse::CLI::InvalidOption, <<~MSG
|
|
20
|
+
The #{repo} repository does not support async indexing. Make sure you have the `plugin :async_indexing` in your `#{index}` class and the :#{repo.repo_name} collection implements the `#each_batch_ids` method.
|
|
21
|
+
MSG
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
enqueuer = if (caller = repo.async_indexing_jobs[:import])
|
|
25
|
+
->(ids) { caller.call(service_name, repo, :import, ids, **bulk_options) }
|
|
26
|
+
else
|
|
27
|
+
queue = Esse::RedisStorage::Queue.for(repo: repo)
|
|
28
|
+
->(ids) do
|
|
29
|
+
batch_id = queue.enqueue(values: ids)
|
|
30
|
+
Esse::AsyncIndexing.worker(WORKER_NAME, service: service_name)
|
|
31
|
+
.with_args(repo.index.name, repo.repo_name, batch_id, Esse::HashUtils.deep_transform_keys(bulk_options, &:to_s))
|
|
32
|
+
.push
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
repo.batch_ids.each(&enqueuer)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def bulk_options
|
|
44
|
+
@bulk_options ||= begin
|
|
45
|
+
hash = @options.slice(*@options.keys - Esse::CLI_IGNORE_OPTS - [:repo, :service])
|
|
46
|
+
hash.delete(:context) if hash[:context].nil? || hash[:context].empty?
|
|
47
|
+
hash
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate_options!
|
|
52
|
+
validate_indices_option!
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def service_name
|
|
56
|
+
(@options[:service] || Esse.config.async_indexing.services.first)&.to_sym
|
|
57
|
+
end
|
|
58
|
+
end
|