webhookdb 1.3.1 → 1.5.0
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 +4 -4
- data/admin-dist/assets/{index-6aebf805.js → index-9306dd28.js} +39 -39
- data/admin-dist/index.html +1 -1
- data/data/messages/templates/errors/generic_backfill.email.liquid +30 -0
- data/data/messages/templates/errors/icalendar_fetch.email.liquid +8 -2
- data/data/messages/templates/specs/with_fields.email.liquid +6 -0
- data/db/migrations/026_undo_integration_backfill_cursor.rb +2 -0
- data/db/migrations/032_remove_db_defaults.rb +2 -0
- data/db/migrations/043_text_search.rb +2 -0
- data/db/migrations/045_system_log.rb +15 -0
- data/db/migrations/046_indices.rb +14 -0
- data/db/migrations/047_sync_parallelism.rb +9 -0
- data/db/migrations/048_sync_stats.rb +9 -0
- data/db/migrations/049_error_handlers.rb +18 -0
- data/db/migrations/050_logged_webhook_indices.rb +25 -0
- data/db/migrations/051_partitioning.rb +9 -0
- data/integration/async_spec.rb +0 -2
- data/integration/service_integrations_spec.rb +0 -2
- data/lib/amigo/durable_job.rb +2 -2
- data/lib/amigo/job_in_context.rb +12 -0
- data/lib/webhookdb/admin.rb +6 -0
- data/lib/webhookdb/admin_api/data_provider.rb +1 -0
- data/lib/webhookdb/admin_api/entities.rb +8 -0
- data/lib/webhookdb/aggregate_result.rb +1 -1
- data/lib/webhookdb/api/entities.rb +6 -2
- data/lib/webhookdb/api/error_handlers.rb +104 -0
- data/lib/webhookdb/api/helpers.rb +25 -1
- data/lib/webhookdb/api/icalproxy.rb +22 -0
- data/lib/webhookdb/api/install.rb +2 -1
- data/lib/webhookdb/api/organizations.rb +6 -0
- data/lib/webhookdb/api/saved_queries.rb +1 -0
- data/lib/webhookdb/api/saved_views.rb +1 -0
- data/lib/webhookdb/api/service_integrations.rb +2 -1
- data/lib/webhookdb/api/sync_targets.rb +1 -1
- data/lib/webhookdb/api/system.rb +5 -0
- data/lib/webhookdb/api/webhook_subscriptions.rb +1 -0
- data/lib/webhookdb/api.rb +4 -1
- data/lib/webhookdb/apps.rb +4 -0
- data/lib/webhookdb/async/autoscaler.rb +10 -0
- data/lib/webhookdb/async/job.rb +4 -0
- data/lib/webhookdb/async/scheduled_job.rb +4 -0
- data/lib/webhookdb/async.rb +2 -0
- data/lib/webhookdb/backfiller.rb +17 -4
- data/lib/webhookdb/concurrent.rb +96 -0
- data/lib/webhookdb/connection_cache.rb +57 -10
- data/lib/webhookdb/console.rb +1 -1
- data/lib/webhookdb/customer/reset_code.rb +1 -1
- data/lib/webhookdb/customer.rb +5 -4
- data/lib/webhookdb/database_document.rb +1 -1
- data/lib/webhookdb/db_adapter/default_sql.rb +1 -14
- data/lib/webhookdb/db_adapter/partition.rb +14 -0
- data/lib/webhookdb/db_adapter/partitioning.rb +8 -0
- data/lib/webhookdb/db_adapter/pg.rb +77 -5
- data/lib/webhookdb/db_adapter/snowflake.rb +15 -6
- data/lib/webhookdb/db_adapter.rb +25 -3
- data/lib/webhookdb/dbutil.rb +2 -0
- data/lib/webhookdb/errors.rb +34 -0
- data/lib/webhookdb/fixtures/logged_webhooks.rb +4 -0
- data/lib/webhookdb/fixtures/organization_error_handlers.rb +20 -0
- data/lib/webhookdb/http.rb +30 -16
- data/lib/webhookdb/icalendar.rb +30 -9
- data/lib/webhookdb/jobs/amigo_test_jobs.rb +1 -1
- data/lib/webhookdb/jobs/backfill.rb +21 -25
- data/lib/webhookdb/jobs/create_mirror_table.rb +3 -4
- data/lib/webhookdb/jobs/deprecated_jobs.rb +3 -0
- data/lib/webhookdb/jobs/emailer.rb +2 -1
- data/lib/webhookdb/jobs/front_signalwire_message_channel_sync_inbound.rb +15 -0
- data/lib/webhookdb/jobs/icalendar_delete_stale_cancelled_events.rb +7 -2
- data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +74 -11
- data/lib/webhookdb/jobs/icalendar_enqueue_syncs_for_urls.rb +22 -0
- data/lib/webhookdb/jobs/icalendar_sync.rb +21 -9
- data/lib/webhookdb/jobs/increase_event_handler.rb +3 -2
- data/lib/webhookdb/jobs/{logged_webhook_replay.rb → logged_webhooks_replay.rb} +5 -3
- data/lib/webhookdb/jobs/message_dispatched.rb +1 -0
- data/lib/webhookdb/jobs/model_event_system_log_tracker.rb +112 -0
- data/lib/webhookdb/jobs/monitor_metrics.rb +29 -0
- data/lib/webhookdb/jobs/organization_database_migration_notify.rb +32 -0
- data/lib/webhookdb/jobs/organization_database_migration_run.rb +4 -6
- data/lib/webhookdb/jobs/organization_error_handler_dispatch.rb +26 -0
- data/lib/webhookdb/jobs/prepare_database_connections.rb +1 -0
- data/lib/webhookdb/jobs/process_webhook.rb +11 -12
- data/lib/webhookdb/jobs/renew_watch_channel.rb +10 -10
- data/lib/webhookdb/jobs/replication_migration.rb +5 -2
- data/lib/webhookdb/jobs/reset_code_create_dispatch.rb +1 -2
- data/lib/webhookdb/jobs/scheduled_backfills.rb +2 -2
- data/lib/webhookdb/jobs/send_invite.rb +3 -2
- data/lib/webhookdb/jobs/send_test_webhook.rb +1 -3
- data/lib/webhookdb/jobs/send_webhook.rb +4 -5
- data/lib/webhookdb/jobs/stale_row_deleter.rb +31 -0
- data/lib/webhookdb/jobs/sync_target_enqueue_scheduled.rb +3 -0
- data/lib/webhookdb/jobs/sync_target_run_sync.rb +9 -15
- data/lib/webhookdb/jobs/{webhook_subscription_delivery_attempt.rb → webhook_subscription_delivery_event.rb} +5 -8
- data/lib/webhookdb/liquid/expose.rb +1 -1
- data/lib/webhookdb/liquid/filters.rb +1 -1
- data/lib/webhookdb/liquid/partial.rb +2 -2
- data/lib/webhookdb/logged_webhook/resilient.rb +3 -3
- data/lib/webhookdb/logged_webhook.rb +16 -2
- data/lib/webhookdb/message/email_transport.rb +1 -1
- data/lib/webhookdb/message/transport.rb +1 -1
- data/lib/webhookdb/message.rb +55 -4
- data/lib/webhookdb/messages/error_generic_backfill.rb +47 -0
- data/lib/webhookdb/messages/error_icalendar_fetch.rb +5 -0
- data/lib/webhookdb/messages/error_signalwire_send_sms.rb +2 -0
- data/lib/webhookdb/messages/specs.rb +16 -0
- data/lib/webhookdb/organization/alerting.rb +56 -6
- data/lib/webhookdb/organization/database_migration.rb +2 -2
- data/lib/webhookdb/organization/db_builder.rb +5 -4
- data/lib/webhookdb/organization/error_handler.rb +141 -0
- data/lib/webhookdb/organization.rb +76 -10
- data/lib/webhookdb/postgres/model.rb +1 -0
- data/lib/webhookdb/postgres/model_utilities.rb +2 -0
- data/lib/webhookdb/postgres.rb +3 -4
- data/lib/webhookdb/replicator/base.rb +202 -68
- data/lib/webhookdb/replicator/base_stale_row_deleter.rb +165 -0
- data/lib/webhookdb/replicator/column.rb +2 -0
- data/lib/webhookdb/replicator/email_octopus_contact_v1.rb +0 -1
- data/lib/webhookdb/replicator/fake.rb +106 -88
- data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +131 -61
- data/lib/webhookdb/replicator/github_repo_v1_mixin.rb +17 -0
- data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +197 -32
- data/lib/webhookdb/replicator/icalendar_event_v1.rb +20 -44
- data/lib/webhookdb/replicator/icalendar_event_v1_partitioned.rb +33 -0
- data/lib/webhookdb/replicator/intercom_contact_v1.rb +1 -0
- data/lib/webhookdb/replicator/intercom_conversation_v1.rb +1 -0
- data/lib/webhookdb/replicator/intercom_v1_mixin.rb +49 -6
- data/lib/webhookdb/replicator/partitionable_mixin.rb +116 -0
- data/lib/webhookdb/replicator/shopify_v1_mixin.rb +1 -1
- data/lib/webhookdb/replicator/signalwire_message_v1.rb +31 -1
- data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +1 -1
- data/lib/webhookdb/replicator/transistor_episode_stats_v1.rb +0 -1
- data/lib/webhookdb/replicator/transistor_episode_v1.rb +11 -5
- data/lib/webhookdb/replicator/webhook_request.rb +8 -0
- data/lib/webhookdb/replicator.rb +6 -3
- data/lib/webhookdb/service/helpers.rb +4 -0
- data/lib/webhookdb/service/middleware.rb +6 -2
- data/lib/webhookdb/service/view_api.rb +1 -1
- data/lib/webhookdb/service.rb +10 -10
- data/lib/webhookdb/service_integration.rb +19 -1
- data/lib/webhookdb/signalwire.rb +1 -1
- data/lib/webhookdb/spec_helpers/async.rb +0 -4
- data/lib/webhookdb/spec_helpers/sentry.rb +32 -0
- data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +239 -64
- data/lib/webhookdb/spec_helpers.rb +1 -0
- data/lib/webhookdb/sync_target.rb +202 -34
- data/lib/webhookdb/system_log_event.rb +9 -0
- data/lib/webhookdb/tasks/admin.rb +1 -1
- data/lib/webhookdb/tasks/annotate.rb +1 -1
- data/lib/webhookdb/tasks/db.rb +13 -1
- data/lib/webhookdb/tasks/docs.rb +1 -1
- data/lib/webhookdb/tasks/fixture.rb +1 -1
- data/lib/webhookdb/tasks/message.rb +1 -1
- data/lib/webhookdb/tasks/regress.rb +1 -1
- data/lib/webhookdb/tasks/release.rb +1 -1
- data/lib/webhookdb/tasks/sidekiq.rb +1 -1
- data/lib/webhookdb/tasks/specs.rb +1 -1
- data/lib/webhookdb/version.rb +1 -1
- data/lib/webhookdb/webhook_subscription.rb +3 -4
- data/lib/webhookdb.rb +34 -8
- metadata +114 -64
- data/lib/webhookdb/jobs/customer_created_notify_internal.rb +0 -22
- data/lib/webhookdb/jobs/organization_database_migration_notify_finished.rb +0 -21
- data/lib/webhookdb/jobs/organization_database_migration_notify_started.rb +0 -21
- /data/lib/webhookdb/jobs/{logged_webhook_resilient_replay.rb → logged_webhooks_resilient_replay.rb} +0 -0
- /data/lib/webhookdb/jobs/{webhook_resource_notify_integrations.rb → webhookdb_resource_notify_integrations.rb} +0 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Webhookdb::Errors
|
4
|
+
class << self
|
5
|
+
# Call the given block for the given exception, each cause (see +Exception#cause+),
|
6
|
+
# and each wrapped errors (see +Amigo::Retry::Error#wrapped+).
|
7
|
+
# If the block returns +true+ for any exception, stop walking.
|
8
|
+
def each_cause(ex, &)
|
9
|
+
raise LocalJumpError unless block_given?
|
10
|
+
return true if yield(ex) == true
|
11
|
+
caused_got = ex.cause && each_cause(ex.cause, &)
|
12
|
+
return true if caused_got == true
|
13
|
+
wrapped_got = ex.respond_to?(:wrapped) && ex.wrapped && each_cause(ex.wrapped, &)
|
14
|
+
return true if wrapped_got == true
|
15
|
+
return nil
|
16
|
+
end
|
17
|
+
|
18
|
+
# Run the given block for each cause (see +each_cause),
|
19
|
+
# returning the first exception the block returns +true+ for.
|
20
|
+
def find_cause(ex, &)
|
21
|
+
raise LocalJumpError unless block_given?
|
22
|
+
got = nil
|
23
|
+
each_cause(ex) do |cause|
|
24
|
+
if yield(cause) == true
|
25
|
+
got = cause
|
26
|
+
true
|
27
|
+
else
|
28
|
+
false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
return got
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -35,6 +35,10 @@ module Webhookdb::Fixtures::LoggedWebhooks
|
|
35
35
|
self.response_status = rand(400..599)
|
36
36
|
end
|
37
37
|
|
38
|
+
decorator :truncated do |t=Time.now|
|
39
|
+
self.truncated_at = t
|
40
|
+
end
|
41
|
+
|
38
42
|
decorator :with_organization do |org={}|
|
39
43
|
org = Webhookdb::Fixtures.organization.create(org) unless org.is_a?(Webhookdb::Organization)
|
40
44
|
self.organization = org
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "faker"
|
4
|
+
|
5
|
+
require "webhookdb/fixtures"
|
6
|
+
|
7
|
+
module Webhookdb::Fixtures::OrganizationErrorHandlers
|
8
|
+
extend Webhookdb::Fixtures
|
9
|
+
|
10
|
+
fixtured_class Webhookdb::Organization::ErrorHandler
|
11
|
+
|
12
|
+
base :organization_error_handler do
|
13
|
+
self.url ||= Faker::Internet.url
|
14
|
+
end
|
15
|
+
|
16
|
+
before_saving do |instance|
|
17
|
+
instance.organization ||= Webhookdb::Fixtures.organization.create
|
18
|
+
instance
|
19
|
+
end
|
20
|
+
end
|
data/lib/webhookdb/http.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "appydays/configurable"
|
4
4
|
require "appydays/loggable/httparty_formatter"
|
5
|
+
require "down/httpx"
|
5
6
|
require "httparty"
|
6
7
|
|
7
8
|
module Webhookdb::Http
|
@@ -11,7 +12,7 @@ module Webhookdb::Http
|
|
11
12
|
end
|
12
13
|
|
13
14
|
# Error raised when some API has rate limited us.
|
14
|
-
class BaseError <
|
15
|
+
class BaseError < Webhookdb::WebhookdbError; end
|
15
16
|
|
16
17
|
class Error < BaseError
|
17
18
|
attr_reader :response, :body, :uri, :status, :http_method
|
@@ -96,25 +97,38 @@ module Webhookdb::Http
|
|
96
97
|
options[:log_level] = self.log_level
|
97
98
|
end
|
98
99
|
|
99
|
-
# Convenience wrapper around Down
|
100
|
+
# Convenience wrapper around Down so we can use our preferred implementation.
|
101
|
+
# See commit history for more info.
|
100
102
|
# @return Array<Down::ChunkedIO, IO> Tuple
|
101
103
|
def self.chunked_download(request_url, rewindable: false, **down_kw)
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
# so each line Down yields via #gets will have force_encoding('utf-8').
|
108
|
-
# https://github.com/janko/down/issues/87
|
109
|
-
io.instance_variable_set(:@encoding, "binary")
|
110
|
-
io = Zlib::GzipReader.wrap(io)
|
111
|
-
end
|
104
|
+
uri = URI(request_url)
|
105
|
+
raise URI::InvalidURIError, "#{request_url} must be an http/s url" unless ["http", "https"].include?(uri.scheme)
|
106
|
+
down_kw[:headers] ||= {}
|
107
|
+
down_kw[:headers]["User-Agent"] ||= self.user_agent
|
108
|
+
io = Down::Httpx.open(uri, rewindable:, **down_kw)
|
112
109
|
return io
|
113
110
|
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class Down::Httpx
|
114
|
+
alias _original_response_error! response_error!
|
115
|
+
def response_error!(response)
|
116
|
+
# For some reason, Down's httpx backend uses TooManyRedirects for every status code...
|
117
|
+
raise Down::NotModified if response.status == 304
|
118
|
+
return self._original_response_error!(response)
|
119
|
+
end
|
120
|
+
end
|
114
121
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
122
|
+
class HTTPX::Response::Body
|
123
|
+
alias _original_initialize initialize
|
124
|
+
def initialize(*)
|
125
|
+
_original_initialize(*)
|
126
|
+
# If the encoding is an invalid one like 'utf8' vs 'utf-8', modify what's was in the charset.
|
127
|
+
# See https://github.com/HoneyryderChuck/httpx/issues/66
|
128
|
+
return unless @encoding.is_a?(String) && (md = @encoding.match(/^(utf)(\d+)$/))
|
129
|
+
@encoding = "#{md[1]}-#{md[2]}"
|
119
130
|
end
|
120
131
|
end
|
132
|
+
|
133
|
+
# Not sure why, but Down uses this, loads the plugin, but the constant isn't defined.
|
134
|
+
require "httpx/plugins/follow_redirects"
|
data/lib/webhookdb/icalendar.rb
CHANGED
@@ -7,20 +7,41 @@ module Webhookdb::Icalendar
|
|
7
7
|
# If a manual backfill is attempted, direct customer to this url.
|
8
8
|
DOCUMENTATION_URL = "https://docs.webhookdb.com/guides/icalendar/"
|
9
9
|
|
10
|
+
EVENT_REPLICATORS = ["icalendar_event_v1", "icalendar_event_v1_partitioned"].freeze
|
11
|
+
|
10
12
|
include Appydays::Configurable
|
11
13
|
|
12
14
|
configurable(:icalendar) do
|
13
|
-
# Do not store events older
|
15
|
+
# Do not store events older than this when syncing recurring events.
|
14
16
|
# Many icalendar feeds are misconfigured and this prevents enumerating 2000+ years of recurrence.
|
15
|
-
setting :oldest_recurring_event, "
|
16
|
-
#
|
17
|
-
# Most services only update every day or so.
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
17
|
+
setting :oldest_recurring_event, "2000-01-01", convert: ->(s) { Date.parse(s) }
|
18
|
+
# Calendars feeds are considered 'fresh' if they have been synced this long ago or less.
|
19
|
+
# Most services only update every day or so.
|
20
|
+
# Assume it takes 5s to sync each feed (request, parse, upsert).
|
21
|
+
# If you have 10,000 feeds, that is 50,000 seconds,
|
22
|
+
# or almost 14 hours of processing time, or two threads for 7 hours.
|
21
23
|
setting :sync_period_hours, 6
|
24
|
+
# When stale feeds are scheduled for a resync,
|
25
|
+
# 'smear' them along this duration. Using 0 would immediately enqueue syncs of all stale feeds,
|
26
|
+
# which could saturate the job server. The number here means that feeds will be refreshed between every
|
27
|
+
# +sync_period_hours+ and +sync_period_hours+ + +sync_period_splay_hours+.
|
28
|
+
setting :sync_period_splay_hours, 1
|
29
|
+
# Number of threads for the 'precheck' threadpool, used when enqueing icalendar sync jobs.
|
30
|
+
# Since the precheck process uses many threads, but each check is resource-light and not latency-sensitive,
|
31
|
+
# we use a shared threadpool for it.
|
32
|
+
setting :precheck_feed_change_pool_size, 12
|
33
|
+
|
34
|
+
# Cancelled events that were last updated this long ago are deleted from the database.
|
35
|
+
setting :stale_cancelled_event_threshold_days, 20
|
36
|
+
# The stale row deleter job will look for rows this far before the threshold.
|
37
|
+
setting :stale_cancelled_event_lookback_days, 3
|
22
38
|
|
23
|
-
#
|
24
|
-
|
39
|
+
# The URL of the icalproxy server, if using one.
|
40
|
+
# See https://github.com/webhookdb/icalproxy for more info.
|
41
|
+
# Used to get property HTTP semantics for any icalendar feed, like Etag and HEAD requests.
|
42
|
+
setting :proxy_url, ""
|
43
|
+
# Api key of the icalproxy server, if using one.
|
44
|
+
# See https://github.com/webhookdb/icalproxy
|
45
|
+
setting :proxy_api_key, ""
|
25
46
|
end
|
26
47
|
end
|
@@ -13,40 +13,36 @@ class Webhookdb::Jobs::Backfill
|
|
13
13
|
on "webhookdb.backfilljob.run"
|
14
14
|
sidekiq_options queue: "netout"
|
15
15
|
|
16
|
-
|
17
|
-
# This is really the lowest-priority job so always defer to other queues.
|
18
|
-
return super
|
19
|
-
end
|
16
|
+
# This is really the lowest-priority job so always defer to other queues.
|
20
17
|
|
21
18
|
def _perform(event)
|
22
19
|
begin
|
23
20
|
bfjob = self.lookup_model(Webhookdb::BackfillJob, event.payload)
|
24
21
|
rescue RuntimeError => e
|
25
|
-
self.
|
22
|
+
self.set_job_tags(result: "skipped_missing_backfill_job", exception: e)
|
26
23
|
return
|
27
24
|
end
|
28
25
|
sint = bfjob.service_integration
|
29
26
|
bflock = bfjob.ensure_service_integration_lock
|
30
|
-
self.
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
end
|
27
|
+
self.set_job_tags(sint.log_tags.merge(backfill_job_id: bfjob.opaque_id))
|
28
|
+
sint.db.transaction do
|
29
|
+
unless bflock.lock?
|
30
|
+
self.set_job_tags(result: "skipped_locked_backfill_job")
|
31
|
+
bfjob.update(finished_at: Time.now)
|
32
|
+
break
|
33
|
+
end
|
34
|
+
bfjob.refresh
|
35
|
+
if bfjob.finished?
|
36
|
+
self.set_job_tags(result: "skipped_finished_backfill_job")
|
37
|
+
break
|
38
|
+
end
|
39
|
+
begin
|
40
|
+
sint.replicator.backfill(bfjob)
|
41
|
+
rescue Webhookdb::Replicator::CredentialsMissing
|
42
|
+
# The credentials could have been cleared out, so just finish this job.
|
43
|
+
self.set_job_tags(result: "skipped_backfill_job_without_credentials")
|
44
|
+
bfjob.update(finished_at: Time.now)
|
45
|
+
break
|
50
46
|
end
|
51
47
|
end
|
52
48
|
end
|
@@ -10,9 +10,8 @@ class Webhookdb::Jobs::CreateMirrorTable
|
|
10
10
|
|
11
11
|
def _perform(event)
|
12
12
|
sint = self.lookup_model(Webhookdb::ServiceIntegration, event)
|
13
|
-
self.
|
14
|
-
|
15
|
-
|
16
|
-
end
|
13
|
+
self.set_job_tags(sint.log_tags)
|
14
|
+
svc = Webhookdb::Replicator.create(sint)
|
15
|
+
svc.create_table(if_not_exists: true)
|
17
16
|
end
|
18
17
|
end
|
@@ -14,6 +14,9 @@ Amigo::DeprecatedJobs.install(
|
|
14
14
|
"Jobs::ConvertKitBroadcastBackfill",
|
15
15
|
"Jobs::ConvertKitSubscriberBackfill",
|
16
16
|
"Jobs::ConvertKitTagBackfill",
|
17
|
+
"Jobs::CustomerCreatedNotifyInternal",
|
18
|
+
"Jobs::OrganizationDatabaseMigrationNotifyStarted",
|
19
|
+
"Jobs::OrganizationDatabaseMigrationNotifyFinished",
|
17
20
|
"Jobs::RssBackfillPoller",
|
18
21
|
"Jobs::TwilioScheduledBackfill",
|
19
22
|
)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "webhookdb/async/job"
|
4
|
+
require "webhookdb/jobs"
|
5
|
+
|
6
|
+
# See +Webhookdb::Replicator::FrontSignalwireMessageChannelAppV1#alert_async_failed_signalwire_send+
|
7
|
+
# for why we need to pull this into an async job.
|
8
|
+
class Webhookdb::Jobs::FrontSignalwireMessageChannelSyncInbound
|
9
|
+
extend Webhookdb::Async::Job
|
10
|
+
|
11
|
+
def perform(service_integration_id, kwargs)
|
12
|
+
sint = self.lookup_model(Webhookdb::ServiceIntegration, service_integration_id)
|
13
|
+
sint.replicator.sync_front_inbound_message(**kwargs.symbolize_keys)
|
14
|
+
end
|
15
|
+
end
|
@@ -9,10 +9,15 @@ class Webhookdb::Jobs::IcalendarDeleteStaleCancelledEvents
|
|
9
9
|
cron "37 7 * * *" # Once a day
|
10
10
|
splay 120
|
11
11
|
|
12
|
+
ADVISORY_LOCK_ID = 1_236_432_568
|
13
|
+
|
12
14
|
def _perform
|
13
|
-
Webhookdb::ServiceIntegration.where(service_name:
|
15
|
+
Webhookdb::ServiceIntegration.where(service_name: Webhookdb::Icalendar::EVENT_REPLICATORS).each do |sint|
|
14
16
|
self.with_log_tags(sint.log_tags) do
|
15
|
-
sint.replicator.
|
17
|
+
sint.replicator.with_advisory_lock(ADVISORY_LOCK_ID) do
|
18
|
+
deleted_rows = sint.replicator.stale_row_deleter.run
|
19
|
+
self.set_job_tags("#{sint.organization.key}_#{sint.table_name}" => deleted_rows)
|
20
|
+
end
|
16
21
|
end
|
17
22
|
end
|
18
23
|
end
|
@@ -5,27 +5,90 @@ require "webhookdb/jobs"
|
|
5
5
|
|
6
6
|
# For every IcalendarCalendar row needing a sync (across all service integrations),
|
7
7
|
# enqueue a +Webhookdb::Jobs::IcalendarSync+ job.
|
8
|
-
#
|
9
|
-
# to
|
8
|
+
#
|
9
|
+
# Because icalendars need to be synced periodically,
|
10
|
+
# and there can be quite a lot, we have to be clever with how we sync them to both avoid syncing too often,
|
11
|
+
# and especially avoid saturating workers with syncs.
|
12
|
+
# We also have to handle workers running behind, the app being slow, etc.
|
13
|
+
#
|
14
|
+
# - This job runs every 30 minutes.
|
15
|
+
# - It finds rows never synced,
|
16
|
+
# or haven't been synced in 8 hours (see +Webhookdb::Icalendar.sync_period_hours+
|
17
|
+
# for the actual value, we'll assume 8 hours for this doc).
|
18
|
+
# - Enqueue a row sync sync job for between 1 second and 60 minutes from now
|
19
|
+
# (see +Webhookdb::Icalendar.sync_period_splay_hours+ for actual upper bound value).
|
20
|
+
# - When the sync job runs, if the row has been synced less than 8 hours ago, the job noops.
|
21
|
+
#
|
22
|
+
# This design will lead to the same calendar being enqueued for a sync multiple times
|
23
|
+
# (for a splay of 1 hour, and running this job every 30 minutes, about half the jobs in the queue will be duplicate).
|
24
|
+
# This isn't a problem however, since the first sync will run but the duplicates will noop
|
25
|
+
# since the row will be seen as recently synced.
|
26
|
+
#
|
27
|
+
# Importantly, if there is a thundering herd situation,
|
28
|
+
# because there is a massive traunch of rows that need to be synced,
|
29
|
+
# and it takes maybe 10 hours to sync rather than one,
|
30
|
+
# the herd will be thinned and smeared over time as each row is synced.
|
31
|
+
# There isn't much we can do for the initial herd (other than making sure
|
32
|
+
# only some number of syncs are processing for a given org at a time)
|
33
|
+
# but we won't keep getting thundering herds from the same calendars over time.
|
10
34
|
class Webhookdb::Jobs::IcalendarEnqueueSyncs
|
11
35
|
extend Webhookdb::Async::ScheduledJob
|
12
36
|
|
13
|
-
|
37
|
+
# See docs for explanation of why we run this often.
|
38
|
+
cron "*/30 * * * *"
|
14
39
|
splay 30
|
15
40
|
|
16
41
|
def _perform
|
17
|
-
|
42
|
+
self.advisory_lock.with_lock? do
|
43
|
+
self.__perform
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Just a random big number
|
48
|
+
LOCK_ID = 2_161_457_251_202_716_167
|
49
|
+
|
50
|
+
def advisory_lock
|
51
|
+
return Sequel::AdvisoryLock.new(Webhookdb::Customer.db, LOCK_ID)
|
52
|
+
end
|
53
|
+
|
54
|
+
def __perform
|
55
|
+
max_projected_out_seconds = Webhookdb::Icalendar.sync_period_splay_hours.hours.to_i
|
56
|
+
total_count = 0
|
57
|
+
threadpool = Concurrent::ThreadPoolExecutor.new(
|
58
|
+
name: "ical-precheck",
|
59
|
+
max_threads: Webhookdb::Icalendar.precheck_feed_change_pool_size,
|
60
|
+
min_threads: 1,
|
61
|
+
idletime: 40,
|
62
|
+
max_queue: 0,
|
63
|
+
fallback_policy: :caller_runs,
|
64
|
+
synchronous: false,
|
65
|
+
)
|
18
66
|
Webhookdb::ServiceIntegration.dataset.where_each(service_name: "icalendar_calendar_v1") do |sint|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
67
|
+
sint_count = 0
|
68
|
+
self.with_log_tags(sint.log_tags) do
|
69
|
+
repl = sint.replicator
|
70
|
+
repl.admin_dataset do |ds|
|
71
|
+
row_ds = repl.
|
72
|
+
rows_needing_sync(ds).
|
73
|
+
order(:pk).
|
74
|
+
select(:external_id, :ics_url, :last_fetch_context)
|
75
|
+
row_ds.paged_each(rows_per_fetch: 500, cursor_name: "ical_enqueue_#{sint.id}_cursor") do |row|
|
76
|
+
threadpool.post do
|
77
|
+
break unless repl.feed_changed?(row)
|
78
|
+
calendar_external_id = row.fetch(:external_id)
|
79
|
+
perform_in = rand(1..max_projected_out_seconds)
|
80
|
+
enqueued_job_id = Webhookdb::Jobs::IcalendarSync.perform_in(perform_in, sint.id, calendar_external_id)
|
81
|
+
self.logger.debug("enqueued_icalendar_sync", calendar_external_id:, enqueued_job_id:, perform_in:)
|
82
|
+
sint_count += 1
|
83
|
+
end
|
26
84
|
end
|
27
85
|
end
|
28
86
|
end
|
87
|
+
total_count += sint_count
|
88
|
+
self.set_job_tags("#{sint.organization.key}_#{sint.table_name}" => sint_count)
|
29
89
|
end
|
90
|
+
threadpool.shutdown
|
91
|
+
threadpool.wait_for_termination
|
92
|
+
self.set_job_tags(total_enqueued: total_count)
|
30
93
|
end
|
31
94
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "webhookdb/async/job"
|
4
|
+
|
5
|
+
class Webhookdb::Jobs::IcalendarEnqueueSyncsForUrls
|
6
|
+
extend Webhookdb::Async::Job
|
7
|
+
|
8
|
+
def perform(urls)
|
9
|
+
self.set_job_tags(url_count: urls.length)
|
10
|
+
row_count = 0
|
11
|
+
Webhookdb::ServiceIntegration.where(service_name: "icalendar_calendar_v1").each do |sint|
|
12
|
+
sint.replicator.admin_dataset do |ds|
|
13
|
+
affected_row_ext_ids = ds.where(ics_url: urls).select_map(:external_id)
|
14
|
+
affected_row_ext_ids.each do |ext_id|
|
15
|
+
Webhookdb::Jobs::IcalendarSync.perform_async(sint.id, ext_id)
|
16
|
+
end
|
17
|
+
row_count += affected_row_ext_ids.length
|
18
|
+
end
|
19
|
+
end
|
20
|
+
self.set_job_tags(result: "icalendar_enqueued_syncs", row_count:)
|
21
|
+
end
|
22
|
+
end
|
@@ -1,23 +1,35 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "amigo/semaphore_backoff_job"
|
4
|
+
|
3
5
|
require "webhookdb/async/job"
|
4
6
|
require "webhookdb/jobs"
|
5
7
|
|
6
8
|
class Webhookdb::Jobs::IcalendarSync
|
7
9
|
extend Webhookdb::Async::Job
|
10
|
+
include Amigo::SemaphoreBackoffJob
|
8
11
|
|
9
|
-
sidekiq_options retry: false
|
12
|
+
sidekiq_options retry: false, queue: "netout"
|
10
13
|
|
11
14
|
def perform(sint_id, calendar_external_id)
|
12
15
|
sint = self.lookup_model(Webhookdb::ServiceIntegration, sint_id)
|
13
|
-
self.
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
end
|
19
|
-
self.logger.debug("icalendar_sync_start")
|
20
|
-
sint.replicator.sync_row(row)
|
16
|
+
self.set_job_tags(sint.log_tags.merge(calendar_external_id:))
|
17
|
+
row = sint.replicator.admin_dataset { |ds| ds[external_id: calendar_external_id] }
|
18
|
+
if row.nil?
|
19
|
+
self.set_job_tags(result: "icalendar_sync_row_miss")
|
20
|
+
return
|
21
21
|
end
|
22
|
+
self.logger.debug("icalendar_sync_start")
|
23
|
+
sint.replicator.sync_row(row)
|
24
|
+
self.set_job_tags(result: "icalendar_synced")
|
25
|
+
end
|
26
|
+
|
27
|
+
def before_perform(sint_id, *)
|
28
|
+
@sint = self.lookup_model(Webhookdb::ServiceIntegration, sint_id)
|
22
29
|
end
|
30
|
+
|
31
|
+
def semaphore_key = "semaphore-icalendarsync-#{@sint.organization_id}"
|
32
|
+
def semaphore_size = @sint.organization.job_semaphore_size
|
33
|
+
def semaphore_expiry = 15.minutes
|
34
|
+
def semaphore_backoff = 60 + (rand * 30)
|
23
35
|
end
|
@@ -8,13 +8,14 @@ class Webhookdb::Jobs::IncreaseEventHandler
|
|
8
8
|
on "increase.*"
|
9
9
|
|
10
10
|
def _perform(event)
|
11
|
+
self.set_job_tags(increase_event_name: event.name)
|
11
12
|
case event.name
|
12
13
|
when "increase.oauth_connection.deactivated"
|
13
14
|
conn_id = event.payload[0].fetch("associated_object_id")
|
14
|
-
self.
|
15
|
+
self.set_job_tags(result: "increase_oauth_disconnected", oauth_connection_id: conn_id)
|
15
16
|
Webhookdb::Oauth::IncreaseProvider.disconnect_oauth(conn_id)
|
16
17
|
else
|
17
|
-
self.
|
18
|
+
self.set_job_tags(result: "increase_event_noop")
|
18
19
|
end
|
19
20
|
end
|
20
21
|
end
|
@@ -10,8 +10,10 @@ class Webhookdb::Jobs::LoggedWebhooksReplay
|
|
10
10
|
|
11
11
|
def _perform(event)
|
12
12
|
lwh = self.lookup_model(Webhookdb::LoggedWebhook, event)
|
13
|
-
self.
|
14
|
-
lwh.
|
15
|
-
|
13
|
+
self.set_job_tags(
|
14
|
+
logged_webhook_id: lwh.id,
|
15
|
+
service_integration_opaque_id: lwh.service_integration_opaque_id,
|
16
|
+
)
|
17
|
+
lwh.retry_one(truncate_successful: true)
|
16
18
|
end
|
17
19
|
end
|
@@ -9,6 +9,7 @@ class Webhookdb::Jobs::MessageDispatched
|
|
9
9
|
|
10
10
|
def _perform(event)
|
11
11
|
delivery = self.lookup_model(Webhookdb::Message::Delivery, event)
|
12
|
+
self.set_job_tags(delivery_id: delivery.id, to: delivery.to)
|
12
13
|
Webhookdb::Idempotency.once_ever.under_key("message-dispatched-#{delivery.id}") do
|
13
14
|
delivery.send!
|
14
15
|
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "webhookdb/async/job"
|
4
|
+
require "webhookdb/messages/invite"
|
5
|
+
|
6
|
+
class Webhookdb::Jobs::ModelEventSystemLogTracker
|
7
|
+
extend Webhookdb::Async::Job
|
8
|
+
|
9
|
+
on "webhookdb.*"
|
10
|
+
|
11
|
+
def _perform(event)
|
12
|
+
self.set_job_tags(event_name: event.name)
|
13
|
+
case event.name
|
14
|
+
when "webhookdb.customer.created"
|
15
|
+
self.alert_customer_created(event)
|
16
|
+
when "webhookdb.organization.created"
|
17
|
+
self.alert_org_created(event)
|
18
|
+
when "webhookdb.serviceintegration.created"
|
19
|
+
self.alert_sint_created(event)
|
20
|
+
when "webhookdb.serviceintegration.destroyed"
|
21
|
+
self.alert_sint_destroyed(event)
|
22
|
+
else
|
23
|
+
self.set_job_tags(result: "noop")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_event(title, body, link)
|
28
|
+
Webhookdb::SystemLogEvent.create(
|
29
|
+
at: Time.now,
|
30
|
+
title:,
|
31
|
+
body:,
|
32
|
+
link:,
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def alert_customer_created(event)
|
37
|
+
customer = self.lookup_model(Webhookdb::Customer, event)
|
38
|
+
Webhookdb::DeveloperAlert.new(
|
39
|
+
subsystem: "Customer Created",
|
40
|
+
emoji: ":hook:",
|
41
|
+
fallback: "New customer created: #{customer.inspect}",
|
42
|
+
fields: [
|
43
|
+
{title: "Id", value: customer.id, short: true},
|
44
|
+
{title: "Email", value: customer.email, short: true},
|
45
|
+
{title: "Link", value: customer.admin_link},
|
46
|
+
],
|
47
|
+
).emit
|
48
|
+
create_event("Customer Created", customer.email, customer.admin_link)
|
49
|
+
self.set_job_tags(result: "created_customer", email: customer.email)
|
50
|
+
end
|
51
|
+
|
52
|
+
def alert_org_created(event)
|
53
|
+
org = self.lookup_model(Webhookdb::Organization, event)
|
54
|
+
Webhookdb::DeveloperAlert.new(
|
55
|
+
subsystem: "Organization Created",
|
56
|
+
emoji: ":office:",
|
57
|
+
fallback: "Organization created: #{org.inspect}",
|
58
|
+
fields: [
|
59
|
+
{title: "Id", value: org.id, short: true},
|
60
|
+
{title: "Email", value: org.name, short: true},
|
61
|
+
{title: "Link", value: org.admin_link},
|
62
|
+
],
|
63
|
+
).emit
|
64
|
+
create_event("Organization Created", "#{org.name} (#{org.key})", org.admin_link)
|
65
|
+
self.set_job_tags(result: "created_organization", key: org.key)
|
66
|
+
end
|
67
|
+
|
68
|
+
def alert_sint_created(event)
|
69
|
+
sint = self.lookup_model(Webhookdb::ServiceIntegration, event)
|
70
|
+
Webhookdb::DeveloperAlert.new(
|
71
|
+
subsystem: "Integration Created",
|
72
|
+
emoji: ":fax:",
|
73
|
+
fallback: "Service Integration #{sint.service_name} (#{sint.opaque_id}) created",
|
74
|
+
fields: [
|
75
|
+
{title: "Id", value: sint.opaque_id, short: true},
|
76
|
+
{title: "Service", value: sint.service_name, short: true},
|
77
|
+
{title: "Table", value: sint.table_name, short: true},
|
78
|
+
{title: "Org Name", value: sint.organization.name, short: true},
|
79
|
+
{title: "Link", value: sint.admin_link},
|
80
|
+
],
|
81
|
+
).emit
|
82
|
+
create_event(
|
83
|
+
"Integration Created",
|
84
|
+
"#{sint.service_name} (#{sint.opaque_id}) created in #{sint.organization.name}",
|
85
|
+
sint.admin_link,
|
86
|
+
)
|
87
|
+
self.set_job_tags(result: "created_service_integration", opaque_id: sint.opaque_id, service: sint.service_name)
|
88
|
+
end
|
89
|
+
|
90
|
+
def alert_sint_destroyed(event)
|
91
|
+
pl = event.payload[1].symbolize_keys
|
92
|
+
org = Webhookdb::Organization[pl[:organization_id]]
|
93
|
+
Webhookdb::DeveloperAlert.new(
|
94
|
+
subsystem: "Integration Deleted",
|
95
|
+
emoji: ":funeral_urn:",
|
96
|
+
fallback: "Service Integration #{pl[:service_name]} (#{pl[:opaque_id]}) deleted",
|
97
|
+
fields: [
|
98
|
+
{title: "Id", value: pl[:opaque_id], short: true},
|
99
|
+
{title: "Service", value: pl[:service_name], short: true},
|
100
|
+
{title: "Table", value: pl[:table_name], short: true},
|
101
|
+
{title: "Org Name", value: org.name, short: true},
|
102
|
+
{title: "Link", value: org.admin_link},
|
103
|
+
],
|
104
|
+
).emit
|
105
|
+
create_event(
|
106
|
+
"Integration Deleted",
|
107
|
+
"#{pl[:service_name]} (#{pl[:opaque_id]}) deleted from #{org.name}",
|
108
|
+
org.admin_link,
|
109
|
+
)
|
110
|
+
self.set_job_tags(result: "destroyed_service_integration", opaque_id: pl[:oparque_id], service: pl[:service_name])
|
111
|
+
end
|
112
|
+
end
|