connectors_utility 8.4.0.1 → 8.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e33f6b71650c6bf509ad4dec8065a97dae7b48e01feb442a163c63aa55acbeee
4
- data.tar.gz: 6813830e1050d3c3361936976860ea08418c4d2ad7ae6764640096f83fb4572c
3
+ metadata.gz: cee1c65ea2f4b7c4b2b289aa7cb5dd82d5afc83775a3dad31eae3a38b4ba81bd
4
+ data.tar.gz: 166fe864c3616ebb24f8c75fd1412b4392558346957562e5d47ad739e4700b93
5
5
  SHA512:
6
- metadata.gz: 73ac602ac9da3526e9104ee52c91b417a965c3e7ee1e8bd937695742ea0e5b8dd09fc72e56ae019c8b1ced48b1a72a26e2d1252e7fc2c99d35e495df6e5144b9
7
- data.tar.gz: b97eef3df6c98684477ecd6d709504602e32add75f32e1df0c0c04f83d034631dde36458cd78d1c506d71309f4df0ab0eb7ac5411d16ae79800eb3652bd87d3e
6
+ metadata.gz: 02a3eb1ac4c223a38ae3201125eb3ea53c436311d00eced489c7897574d498f58628d727293be904b729640c47599a56cb238a4f120f878e0035ca8cd0191818
7
+ data.tar.gz: bdcfb1f6d622291f7bc17d5fc3d173b0a43e16efcd5dc0ce2a7f002df6c3e59b7ce314e4da43b42d96729456dcd853db01a22fa8c75461f107012185561687b7
@@ -0,0 +1,31 @@
1
+ #
2
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ # or more contributor license agreements. Licensed under the Elastic License;
4
+ # you may not use this file except in compliance with the Elastic License.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ module Connectors
10
+ class ConnectorStatus
11
+ CREATED = 'created'
12
+ NEEDS_CONFIGURATION = 'needs_configuration'
13
+ CONFIGURED = 'configured'
14
+ CONNECTED = 'connected'
15
+ ERROR = 'error'
16
+
17
+ STATUSES = [
18
+ CREATED,
19
+ NEEDS_CONFIGURATION,
20
+ CONFIGURED,
21
+ CONNECTED,
22
+ ERROR
23
+ ]
24
+
25
+ STATUSES_ALLOWING_SYNC = [
26
+ CONFIGURED,
27
+ CONNECTED,
28
+ ERROR
29
+ ]
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ #
2
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ # or more contributor license agreements. Licensed under the Elastic License;
4
+ # you may not use this file except in compliance with the Elastic License.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require 'core/scheduler'
10
+ require 'core/connector_settings'
11
+ require 'core/elastic_connector_actions'
12
+ require 'utility/logger'
13
+ require 'utility/exception_tracking'
14
+
15
+ module Connectors
16
+ module Crawler
17
+ class Scheduler < Core::Scheduler
18
+ def connector_settings
19
+ Core::ConnectorSettings.fetch_crawler_connectors || []
20
+ rescue StandardError => e
21
+ Utility::ExceptionTracking.log_exception(e, 'Could not retrieve Crawler connectors due to unexpected error.')
22
+ []
23
+ end
24
+
25
+ private
26
+
27
+ def connector_registered?(service_type)
28
+ service_type == 'elastic-crawler'
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,21 @@
1
+ #
2
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ # or more contributor license agreements. Licensed under the Elastic License;
4
+ # you may not use this file except in compliance with the Elastic License.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ module Connectors
10
+ class SyncStatus
11
+ COMPLETED = 'completed'
12
+ IN_PROGRESS = 'in_progress'
13
+ FAILED = 'failed'
14
+
15
+ STATUSES = [
16
+ COMPLETED,
17
+ IN_PROGRESS,
18
+ FAILED
19
+ ]
20
+ end
21
+ end
@@ -6,5 +6,11 @@
6
6
 
7
7
  # frozen_string_literal: true
8
8
 
9
- require_relative 'utility/elasticsearch/index/text_analysis_settings'
10
- require_relative 'utility/elasticsearch/index/mappings'
9
+ require_relative 'utility'
10
+
11
+ require_relative 'connectors/connector_status'
12
+ require_relative 'connectors/sync_status'
13
+ require_relative 'core/scheduler'
14
+ require_relative 'core/elastic_connector_actions'
15
+
16
+ require_relative 'connectors/crawler/scheduler'
@@ -0,0 +1,142 @@
1
+ #
2
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ # or more contributor license agreements. Licensed under the Elastic License;
4
+ # you may not use this file except in compliance with the Elastic License.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require 'active_support/core_ext/hash/indifferent_access'
10
+ require 'connectors/connector_status'
11
+ require 'core/elastic_connector_actions'
12
+ require 'utility'
13
+
14
+ module Core
15
+ class ConnectorSettings
16
+
17
+ DEFAULT_REQUEST_PIPELINE = 'ent-search-generic-ingestion'
18
+ DEFAULT_EXTRACT_BINARY_CONTENT = true
19
+ DEFAULT_REDUCE_WHITESPACE = true
20
+ DEFAULT_RUN_ML_INFERENCE = true
21
+
22
+ DEFAULT_PAGE_SIZE = 100
23
+
24
+ # Error Classes
25
+ class ConnectorNotFoundError < StandardError; end
26
+
27
+ def self.fetch_by_id(connector_id)
28
+ es_response = ElasticConnectorActions.get_connector(connector_id)
29
+ connectors_meta = ElasticConnectorActions.connectors_meta
30
+
31
+ raise ConnectorNotFoundError.new("Connector with id=#{connector_id} was not found.") unless es_response[:found]
32
+ new(es_response, connectors_meta)
33
+ end
34
+
35
+ def initialize(es_response, connectors_meta)
36
+ @elasticsearch_response = es_response.with_indifferent_access
37
+ @connectors_meta = connectors_meta.with_indifferent_access
38
+ end
39
+
40
+ def self.fetch_native_connectors(page_size = DEFAULT_PAGE_SIZE)
41
+ query = { term: { is_native: true } }
42
+ fetch_connectors_by_query(query, page_size)
43
+ end
44
+
45
+ def self.fetch_crawler_connectors(page_size = DEFAULT_PAGE_SIZE)
46
+ query = { term: { service_type: Utility::Constants::CRAWLER_SERVICE_TYPE } }
47
+ fetch_connectors_by_query(query, page_size)
48
+ end
49
+
50
+ def id
51
+ @elasticsearch_response[:_id]
52
+ end
53
+
54
+ def [](property_name)
55
+ # TODO: handle not found
56
+ @elasticsearch_response[:_source][property_name]
57
+ end
58
+
59
+ def index_name
60
+ self[:index_name]
61
+ end
62
+
63
+ def connector_status
64
+ self[:status]
65
+ end
66
+
67
+ def connector_status_allows_sync?
68
+ Connectors::ConnectorStatus::STATUSES_ALLOWING_SYNC.include?(connector_status)
69
+ end
70
+
71
+ def service_type
72
+ self[:service_type]
73
+ end
74
+
75
+ def configuration
76
+ self[:configuration]
77
+ end
78
+
79
+ def scheduling_settings
80
+ self[:scheduling]
81
+ end
82
+
83
+ def request_pipeline
84
+ return_if_present(@elasticsearch_response.dig(:pipeline, :name), @connectors_meta.dig(:pipeline, :default_name), DEFAULT_REQUEST_PIPELINE)
85
+ end
86
+
87
+ def extract_binary_content?
88
+ return_if_present(@elasticsearch_response.dig(:pipeline, :extract_binary_content), @connectors_meta.dig(:pipeline, :default_extract_binary_content), DEFAULT_EXTRACT_BINARY_CONTENT)
89
+ end
90
+
91
+ def reduce_whitespace?
92
+ return_if_present(@elasticsearch_response.dig(:pipeline, :reduce_whitespace), @connectors_meta.dig(:pipeline, :default_reduce_whitespace), DEFAULT_REDUCE_WHITESPACE)
93
+ end
94
+
95
+ def run_ml_inference?
96
+ return_if_present(@elasticsearch_response.dig(:pipeline, :run_ml_inference), @connectors_meta.dig(:pipeline, :default_run_ml_inference), DEFAULT_RUN_ML_INFERENCE)
97
+ end
98
+
99
+ def formatted
100
+ properties = ["ID: #{id}"]
101
+ properties << "Service type: #{service_type}" if service_type
102
+ "connector (#{properties.join(', ')})"
103
+ end
104
+
105
+ def needs_service_type?
106
+ service_type.to_s.strip.empty?
107
+ end
108
+
109
+ def valid_index_name?
110
+ index_name&.start_with?(Utility::Constants::CONTENT_INDEX_PREFIX)
111
+ end
112
+
113
+ private
114
+
115
+ def self.fetch_connectors_by_query(query, page_size)
116
+ connectors_meta = ElasticConnectorActions.connectors_meta
117
+
118
+ results = []
119
+ offset = 0
120
+ loop do
121
+ response = ElasticConnectorActions.search_connectors(query, page_size, offset)
122
+
123
+ hits = response['hits']['hits']
124
+ total = response['hits']['total']['value']
125
+ results += hits.map do |hit|
126
+ Core::ConnectorSettings.new(hit, connectors_meta)
127
+ end
128
+ break if results.size >= total
129
+ offset += hits.size
130
+ end
131
+
132
+ results
133
+ end
134
+
135
+ def return_if_present(*args)
136
+ args.each do |arg|
137
+ return arg unless arg.nil?
138
+ end
139
+ nil
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,269 @@
1
+ #
2
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ # or more contributor license agreements. Licensed under the Elastic License;
4
+ # you may not use this file except in compliance with the Elastic License.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+ #
9
+ require 'active_support/core_ext/hash'
10
+ require 'connectors/connector_status'
11
+ require 'connectors/sync_status'
12
+ require 'utility'
13
+
14
+ module Core
15
+ class ElasticConnectorActions
16
+ class << self
17
+
18
+ def force_sync(connector_id)
19
+ update_connector_fields(connector_id, :scheduling => { :enabled => true }, :sync_now => true)
20
+ end
21
+
22
+ def create_connector(index_name, service_type)
23
+ body = {
24
+ :scheduling => { :enabled => true },
25
+ :index_name => index_name,
26
+ :service_type => service_type
27
+ }
28
+ response = client.index(:index => Utility::Constants::CONNECTORS_INDEX, :body => body)
29
+ response['_id']
30
+ end
31
+
32
+ def get_connector(connector_id)
33
+ client.get(:index => Utility::Constants::CONNECTORS_INDEX, :id => connector_id, :ignore => 404).with_indifferent_access
34
+ end
35
+
36
+ def connectors_meta
37
+ alias_mappings = client.indices.get_mapping(:index => Utility::Constants::CONNECTORS_INDEX).with_indifferent_access
38
+ index = get_latest_index_in_alias(Utility::Constants::CONNECTORS_INDEX, alias_mappings.keys)
39
+ alias_mappings.dig(index, 'mappings', '_meta') || {}
40
+ end
41
+
42
+ def search_connectors(query, page_size, offset)
43
+ client.search(
44
+ :index => Utility::Constants::CONNECTORS_INDEX,
45
+ :ignore => 404,
46
+ :body => {
47
+ :size => page_size,
48
+ :from => offset,
49
+ :query => query,
50
+ :sort => ['name']
51
+ }
52
+ )
53
+ end
54
+
55
+ def update_connector_configuration(connector_id, configuration)
56
+ update_connector_fields(connector_id, :configuration => configuration)
57
+ end
58
+
59
+ def enable_connector_scheduling(connector_id, cron_expression)
60
+ payload = { :enabled => true, :interval => cron_expression }
61
+ update_connector_fields(connector_id, :scheduling => payload)
62
+ end
63
+
64
+ def disable_connector_scheduling(connector_id)
65
+ payload = { :enabled => false }
66
+ update_connector_fields(connector_id, :scheduling => payload)
67
+ end
68
+
69
+ def set_configurable_field(connector_id, field_name, label, value)
70
+ payload = { field_name => { :value => value, :label => label } }
71
+ update_connector_configuration(connector_id, payload)
72
+ end
73
+
74
+ def claim_job(connector_id)
75
+ update_connector_fields(connector_id,
76
+ :sync_now => false,
77
+ :last_sync_status => Connectors::SyncStatus::IN_PROGRESS,
78
+ :last_synced => Time.now)
79
+
80
+ body = {
81
+ :connector_id => connector_id,
82
+ :status => Connectors::SyncStatus::IN_PROGRESS,
83
+ :worker_hostname => Socket.gethostname,
84
+ :created_at => Time.now
85
+ }
86
+ job = client.index(:index => Utility::Constants::JOB_INDEX, :body => body)
87
+
88
+ job['_id']
89
+ end
90
+
91
+ def update_connector_status(connector_id, status, error_message = nil)
92
+ if status == Connectors::ConnectorStatus::ERROR && error_message.nil?
93
+ raise ArgumentError, 'error_message is required when status is error'
94
+ end
95
+ body = {
96
+ :status => status,
97
+ :error => status == Connectors::ConnectorStatus::ERROR ? error_message : nil
98
+ }
99
+ update_connector_fields(connector_id, body)
100
+ end
101
+
102
+ def complete_sync(connector_id, job_id, status)
103
+ sync_status = status[:error] ? Connectors::SyncStatus::FAILED : Connectors::SyncStatus::COMPLETED
104
+
105
+ update_connector_fields(connector_id,
106
+ :last_sync_status => sync_status,
107
+ :last_sync_error => status[:error],
108
+ :error => status[:error],
109
+ :last_synced => Time.now,
110
+ :last_indexed_document_count => status[:indexed_document_count],
111
+ :last_deleted_document_count => status[:deleted_document_count])
112
+
113
+ body = {
114
+ :doc => {
115
+ :status => sync_status,
116
+ :completed_at => Time.now
117
+ }.merge(status)
118
+ }
119
+ client.update(:index => Utility::Constants::JOB_INDEX, :id => job_id, :body => body)
120
+ end
121
+
122
+ def fetch_document_ids(index_name)
123
+ page_size = 1000
124
+ result = []
125
+ begin
126
+ pit_id = client.open_point_in_time(:index => index_name, :keep_alive => '1m', :expand_wildcards => 'all')['id']
127
+ body = {
128
+ :query => { :match_all => {} },
129
+ :sort => [{ :id => { :order => :asc } }],
130
+ :pit => {
131
+ :id => pit_id,
132
+ :keep_alive => '1m'
133
+ },
134
+ :size => page_size,
135
+ :_source => false
136
+ }
137
+ loop do
138
+ response = client.search(:body => body)
139
+ hits = response['hits']['hits']
140
+
141
+ ids = hits.map { |h| h['_id'] }
142
+ result += ids
143
+ break if hits.size < page_size
144
+
145
+ body[:search_after] = hits.last['sort']
146
+ body[:pit][:id] = response['pit_id']
147
+ end
148
+ ensure
149
+ client.close_point_in_time(:index => index_name, :body => { :id => pit_id })
150
+ end
151
+
152
+ result
153
+ end
154
+
155
+ def ensure_content_index_exists(index_name, use_icu_locale = false, language_code = nil)
156
+ settings = Utility::Elasticsearch::Index::TextAnalysisSettings.new(:language_code => language_code, :analysis_icu => use_icu_locale).to_h
157
+ mappings = Utility::Elasticsearch::Index::Mappings.default_text_fields_mappings(:connectors_index => true)
158
+
159
+ body_payload = { settings: settings, mappings: mappings }
160
+ ensure_index_exists(index_name, body_payload)
161
+ end
162
+
163
+ def ensure_index_exists(index_name, body = {})
164
+ if client.indices.exists?(:index => index_name)
165
+ return unless body[:mappings]
166
+ Utility::Logger.debug("Index #{index_name} already exists. Checking mappings...")
167
+ Utility::Logger.debug("New mappings: #{body[:mappings]}")
168
+ response = client.indices.get_mapping(:index => index_name)
169
+ existing = response[index_name]['mappings']
170
+ if existing.empty?
171
+ Utility::Logger.debug("Index #{index_name} has no mappings. Adding mappings...")
172
+ client.indices.put_mapping(:index => index_name, :body => body[:mappings], :expand_wildcards => 'all')
173
+ Utility::Logger.debug("Index #{index_name} mappings added.")
174
+ else
175
+ Utility::Logger.debug("Index #{index_name} already has mappings: #{existing}. Skipping...")
176
+ end
177
+ else
178
+ client.indices.create(:index => index_name, :body => body)
179
+ Utility::Logger.debug("Created index #{index_name}")
180
+ end
181
+ end
182
+
183
+ def system_index_body(alias_name: nil, mappings: nil)
184
+ body = {
185
+ :settings => {
186
+ :index => {
187
+ :hidden => true,
188
+ :number_of_replicas => 0,
189
+ :auto_expand_replicas => '0-5'
190
+ }
191
+ }
192
+ }
193
+ body[:aliases] = { alias_name => { :is_write_index => true } } unless alias_name.nil? || alias_name.empty?
194
+ body[:mappings] = mappings unless mappings.nil?
195
+ body
196
+ end
197
+
198
+ # DO NOT USE this method
199
+ # Creation of connector index should be handled by Kibana, this method is only used by ftest.rb
200
+ def ensure_connectors_index_exists
201
+ mappings = {
202
+ :properties => {
203
+ :api_key_id => { :type => :keyword },
204
+ :configuration => { :type => :object },
205
+ :error => { :type => :text },
206
+ :index_name => { :type => :keyword },
207
+ :last_seen => { :type => :date },
208
+ :last_synced => { :type => :date },
209
+ :last_indexed_document_count => { :type => :integer },
210
+ :last_deleted_document_count => { :type => :integer },
211
+ :scheduling => {
212
+ :properties => {
213
+ :enabled => { :type => :boolean },
214
+ :interval => { :type => :text }
215
+ }
216
+ },
217
+ :service_type => { :type => :keyword },
218
+ :status => { :type => :keyword },
219
+ :sync_error => { :type => :text },
220
+ :sync_now => { :type => :boolean },
221
+ :sync_status => { :type => :keyword }
222
+ }
223
+ }
224
+ ensure_index_exists("#{Utility::Constants::CONNECTORS_INDEX}-v1", system_index_body(:alias_name => Utility::Constants::CONNECTORS_INDEX, :mappings => mappings))
225
+ end
226
+
227
+ # DO NOT USE this method
228
+ # Creation of job index should be handled by Kibana, this method is only used by ftest.rb
229
+ def ensure_job_index_exists
230
+ mappings = {
231
+ :properties => {
232
+ :connector_id => { :type => :keyword },
233
+ :status => { :type => :keyword },
234
+ :error => { :type => :text },
235
+ :worker_hostname => { :type => :keyword },
236
+ :indexed_document_count => { :type => :integer },
237
+ :deleted_document_count => { :type => :integer },
238
+ :created_at => { :type => :date },
239
+ :completed_at => { :type => :date }
240
+ }
241
+ }
242
+ ensure_index_exists("#{Utility::Constants::JOB_INDEX}-v1", system_index_body(:alias_name => Utility::Constants::JOB_INDEX, :mappings => mappings))
243
+ end
244
+
245
+ def update_connector_fields(connector_id, doc = {})
246
+ return if doc.empty?
247
+ client.update(
248
+ :index => Utility::Constants::CONNECTORS_INDEX,
249
+ :id => connector_id,
250
+ :body => { :doc => doc },
251
+ :refresh => true,
252
+ :retry_on_conflict => 3
253
+ )
254
+ end
255
+
256
+ private
257
+
258
+ def client
259
+ @client ||= Utility::EsClient.new(App::Config[:elasticsearch])
260
+ end
261
+
262
+ def get_latest_index_in_alias(alias_name, indicies)
263
+ index_versions = indicies.map { |index| index.gsub("#{alias_name}-v", '').to_i }
264
+ index_version = index_versions.max # gets the largest suffix number
265
+ "#{alias_name}-v#{index_version}"
266
+ end
267
+ end
268
+ end
269
+ end
@@ -0,0 +1,160 @@
1
+ #
2
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ # or more contributor license agreements. Licensed under the Elastic License;
4
+ # you may not use this file except in compliance with the Elastic License.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require 'time'
10
+ require 'fugit'
11
+ require 'core/connector_settings'
12
+ require 'utility/cron'
13
+ require 'utility/logger'
14
+ require 'utility/exception_tracking'
15
+
16
+ module Core
17
+ class Scheduler
18
+ def initialize(poll_interval, heartbeat_interval)
19
+ @poll_interval = poll_interval
20
+ @heartbeat_interval = heartbeat_interval
21
+ @is_shutting_down = false
22
+ end
23
+
24
+ def connector_settings
25
+ raise 'Not implemented'
26
+ end
27
+
28
+ def when_triggered
29
+ loop do
30
+ connector_settings.each do |cs|
31
+ if sync_triggered?(cs)
32
+ yield cs, :sync
33
+ end
34
+ if heartbeat_triggered?(cs)
35
+ yield cs, :heartbeat
36
+ end
37
+ if configuration_triggered?(cs)
38
+ yield cs, :configuration
39
+ end
40
+ end
41
+ if @is_shutting_down
42
+ break
43
+ end
44
+ rescue StandardError => e
45
+ Utility::ExceptionTracking.log_exception(e, 'Sync failed due to unexpected error.')
46
+ ensure
47
+ if @poll_interval > 0 && !@is_shutting_down
48
+ Utility::Logger.debug("Sleeping for #{@poll_interval} seconds in #{self.class}.")
49
+ sleep(@poll_interval)
50
+ end
51
+ end
52
+ end
53
+
54
+ def shutdown
55
+ Utility::Logger.info("Shutting down scheduler #{self.class.name}.")
56
+ @is_shutting_down = true
57
+ end
58
+
59
+ private
60
+
61
+ def sync_triggered?(connector_settings)
62
+ return false unless connector_registered?(connector_settings.service_type)
63
+
64
+ unless connector_settings.valid_index_name?
65
+ Utility::Logger.warn("The index name of #{connector_settings.formatted} is invalid.")
66
+ return false
67
+ end
68
+
69
+ unless connector_settings.connector_status_allows_sync?
70
+ Utility::Logger.info("#{connector_settings.formatted.capitalize} is in status \"#{connector_settings.connector_status}\" and won't sync yet. Connector needs to be in one of the following statuses: #{Connectors::ConnectorStatus::STATUSES_ALLOWING_SYNC} to run.")
71
+
72
+ return false
73
+ end
74
+
75
+ # Sync when sync_now flag is true for the connector
76
+ if connector_settings[:sync_now] == true
77
+ Utility::Logger.info("#{connector_settings.formatted.capitalize} is manually triggered to sync now.")
78
+ return true
79
+ end
80
+
81
+ # Don't sync if sync is explicitly disabled
82
+ scheduling_settings = connector_settings.scheduling_settings
83
+ unless scheduling_settings.present? && scheduling_settings[:enabled] == true
84
+ Utility::Logger.debug("#{connector_settings.formatted.capitalize} scheduling is disabled.")
85
+ return false
86
+ end
87
+
88
+ # We want to sync when sync never actually happened
89
+ last_synced = connector_settings[:last_synced]
90
+ if last_synced.nil? || last_synced.empty?
91
+ Utility::Logger.info("#{connector_settings.formatted.capitalize} has never synced yet, running initial sync.")
92
+ return true
93
+ end
94
+
95
+ current_schedule = scheduling_settings[:interval]
96
+
97
+ # Don't sync if there is no actual scheduling interval
98
+ if current_schedule.nil? || current_schedule.empty?
99
+ Utility::Logger.warn("No sync schedule configured for #{connector_settings.formatted}.")
100
+ return false
101
+ end
102
+
103
+ current_schedule = begin
104
+ Utility::Cron.quartz_to_crontab(current_schedule)
105
+ rescue StandardError => e
106
+ Utility::ExceptionTracking.log_exception(e, "Unable to convert quartz (#{current_schedule}) to crontab.")
107
+ return false
108
+ end
109
+ cron_parser = Fugit::Cron.parse(current_schedule)
110
+
111
+ # Don't sync if the scheduling interval is non-parsable
112
+ unless cron_parser
113
+ Utility::Logger.error("Unable to parse sync schedule for #{connector_settings.formatted}: expression #{current_schedule} is not a valid Quartz Cron definition.")
114
+ return false
115
+ end
116
+
117
+ next_trigger_time = cron_parser.next_time(Time.parse(last_synced))
118
+
119
+ # Sync if next trigger for the connector is in past
120
+ if next_trigger_time < Time.now
121
+ Utility::Logger.info("#{connector_settings.formatted.capitalize} sync is triggered by cron schedule #{current_schedule}.")
122
+ return true
123
+ end
124
+
125
+ false
126
+ end
127
+
128
+ def heartbeat_triggered?(connector_settings)
129
+ return false unless connector_registered?(connector_settings.service_type)
130
+
131
+ last_seen = connector_settings[:last_seen]
132
+ return true if last_seen.nil? || last_seen.empty?
133
+ last_seen = begin
134
+ Time.parse(last_seen)
135
+ rescue StandardError
136
+ Utility::Logger.warn("Unable to parse last_seen #{last_seen}")
137
+ nil
138
+ end
139
+ return true unless last_seen
140
+ last_seen + @heartbeat_interval < Time.now
141
+ end
142
+
143
+ def configuration_triggered?(connector_settings)
144
+ if connector_settings.needs_service_type? || connector_registered?(connector_settings.service_type)
145
+ return connector_settings.connector_status == Connectors::ConnectorStatus::CREATED
146
+ end
147
+
148
+ false
149
+ end
150
+
151
+ def connector_registered?(service_type)
152
+ if Connectors::REGISTRY.registered?(service_type)
153
+ true
154
+ else
155
+ Utility::Logger.info("The service type (#{service_type}) is not supported.")
156
+ false
157
+ end
158
+ end
159
+ end
160
+ end