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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +35 -0
  3. data/CHANGELOG.md +14 -0
  4. data/Gemfile +12 -0
  5. data/Gemfile.lock +168 -0
  6. data/LICENSE +21 -0
  7. data/README.md +262 -0
  8. data/Rakefile +4 -0
  9. data/docker-compose.yml +11 -0
  10. data/lib/esse/async_indexing/actions/batch_delete.rb +15 -0
  11. data/lib/esse/async_indexing/actions/batch_import.rb +16 -0
  12. data/lib/esse/async_indexing/actions/batch_import_all.rb +11 -0
  13. data/lib/esse/async_indexing/actions/batch_update.rb +32 -0
  14. data/lib/esse/async_indexing/actions/bulk_update_lazy_document_attribute.rb +20 -0
  15. data/lib/esse/async_indexing/actions/coerce_index_repository.rb +28 -0
  16. data/lib/esse/async_indexing/actions/delete_document.rb +16 -0
  17. data/lib/esse/async_indexing/actions/import_batch_id.rb +21 -0
  18. data/lib/esse/async_indexing/actions/index_document.rb +20 -0
  19. data/lib/esse/async_indexing/actions/update_document.rb +20 -0
  20. data/lib/esse/async_indexing/actions/update_lazy_document_attribute.rb +14 -0
  21. data/lib/esse/async_indexing/actions/upsert_document.rb +25 -0
  22. data/lib/esse/async_indexing/actions.rb +21 -0
  23. data/lib/esse/async_indexing/active_record.rb +102 -0
  24. data/lib/esse/async_indexing/active_record_callbacks/callback.rb +27 -0
  25. data/lib/esse/async_indexing/active_record_callbacks/lazy_update_attribute.rb +26 -0
  26. data/lib/esse/async_indexing/active_record_callbacks/on_create.rb +15 -0
  27. data/lib/esse/async_indexing/active_record_callbacks/on_destroy.rb +15 -0
  28. data/lib/esse/async_indexing/active_record_callbacks/on_update.rb +27 -0
  29. data/lib/esse/async_indexing/adapters/adapter.rb +29 -0
  30. data/lib/esse/async_indexing/adapters/faktory.rb +114 -0
  31. data/lib/esse/async_indexing/adapters/sidekiq.rb +94 -0
  32. data/lib/esse/async_indexing/adapters.rb +12 -0
  33. data/lib/esse/async_indexing/cli/async_import.rb +58 -0
  34. data/lib/esse/async_indexing/cli.rb +32 -0
  35. data/lib/esse/async_indexing/config.rb +27 -0
  36. data/lib/esse/async_indexing/configuration/base.rb +65 -0
  37. data/lib/esse/async_indexing/configuration/faktory.rb +6 -0
  38. data/lib/esse/async_indexing/configuration/sidekiq.rb +12 -0
  39. data/lib/esse/async_indexing/configuration.rb +45 -0
  40. data/lib/esse/async_indexing/errors.rb +12 -0
  41. data/lib/esse/async_indexing/jobs/bulk_update_lazy_document_attribute_job.rb +7 -0
  42. data/lib/esse/async_indexing/jobs/document_delete_by_id_job.rb +7 -0
  43. data/lib/esse/async_indexing/jobs/document_index_by_id_job.rb +7 -0
  44. data/lib/esse/async_indexing/jobs/document_update_by_id_job.rb +7 -0
  45. data/lib/esse/async_indexing/jobs/document_upsert_by_id_job.rb +7 -0
  46. data/lib/esse/async_indexing/jobs/import_all_job.rb +7 -0
  47. data/lib/esse/async_indexing/jobs/import_batch_id_job.rb +34 -0
  48. data/lib/esse/async_indexing/jobs/update_lazy_document_attribute_job.rb +7 -0
  49. data/lib/esse/async_indexing/testing.rb +79 -0
  50. data/lib/esse/async_indexing/version.rb +7 -0
  51. data/lib/esse/async_indexing/worker.rb +85 -0
  52. data/lib/esse/async_indexing/workers/faktory.rb +28 -0
  53. data/lib/esse/async_indexing/workers/shared_class_methods.rb +26 -0
  54. data/lib/esse/async_indexing/workers/sidekiq.rb +28 -0
  55. data/lib/esse/async_indexing/workers.rb +48 -0
  56. data/lib/esse/async_indexing.rb +72 -0
  57. data/lib/esse/plugins/async_indexing.rb +106 -0
  58. data/lib/esse-async-indexing.rb +3 -0
  59. data/lib/esse-async_indexing.rb +3 -0
  60. 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ module AsyncIndexing
5
+ module Adapters
6
+ end
7
+ end
8
+ end
9
+
10
+ require_relative "adapters/adapter"
11
+ require_relative "adapters/sidekiq"
12
+ require_relative "adapters/faktory"
@@ -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