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
@@ -3,6 +3,9 @@
|
|
3
3
|
require "sequel/advisory_lock"
|
4
4
|
require "sequel/database"
|
5
5
|
|
6
|
+
require "webhookdb/concurrent"
|
7
|
+
require "webhookdb/jobs/sync_target_run_sync"
|
8
|
+
|
6
9
|
# Support exporting WebhookDB data into external services,
|
7
10
|
# such as another Postgres instance or data warehouse (Snowflake, etc).
|
8
11
|
#
|
@@ -21,9 +24,9 @@ class Webhookdb::SyncTarget < Webhookdb::Postgres::Model(:sync_targets)
|
|
21
24
|
include Appydays::Configurable
|
22
25
|
include Webhookdb::Dbutil
|
23
26
|
|
24
|
-
class Deleted <
|
25
|
-
class InvalidConnection <
|
26
|
-
class SyncInProgress <
|
27
|
+
class Deleted < Webhookdb::WebhookdbError; end
|
28
|
+
class InvalidConnection < Webhookdb::WebhookdbError; end
|
29
|
+
class SyncInProgress < Webhookdb::WebhookdbError; end
|
27
30
|
|
28
31
|
# Advisory locks for sync targets use this as the first int, and the id as the second.
|
29
32
|
ADVISORY_LOCK_KEYSPACE = 2_000_000_000
|
@@ -32,6 +35,7 @@ class Webhookdb::SyncTarget < Webhookdb::Postgres::Model(:sync_targets)
|
|
32
35
|
DB_VERIFY_TIMEOUT = 2000
|
33
36
|
DB_VERIFY_STATEMENT = "SELECT 1"
|
34
37
|
RAND = Random.new
|
38
|
+
MAX_STATS = 200
|
35
39
|
|
36
40
|
configurable(:sync_target) do
|
37
41
|
# Allow installs to set this much lower if they want a faster sync.
|
@@ -52,6 +56,12 @@ class Webhookdb::SyncTarget < Webhookdb::Postgres::Model(:sync_targets)
|
|
52
56
|
# we must allow sync targets to use http urls. This should only
|
53
57
|
# be used internally, and never in production.
|
54
58
|
setting :allow_http, false
|
59
|
+
# Syncing may require serverside cursors, which open a transaction.
|
60
|
+
# To avoid long-lived transactions, any sync which has a transaction,
|
61
|
+
# and goes on longer than +max_transaction_seconds+,
|
62
|
+
# will 'soft abort' the sync and reschedule itself to continue
|
63
|
+
# using a new transaction.
|
64
|
+
setting :max_transaction_seconds, 10.minutes.to_i
|
55
65
|
|
56
66
|
after_configured do
|
57
67
|
if Webhookdb::RACK_ENV == "test"
|
@@ -87,8 +97,10 @@ class Webhookdb::SyncTarget < Webhookdb::Postgres::Model(:sync_targets)
|
|
87
97
|
dataset_module do
|
88
98
|
def due_for_sync(as_of:)
|
89
99
|
never_synced = Sequel[last_synced_at: nil]
|
90
|
-
|
91
|
-
|
100
|
+
# Use 'last_synced_at <= (now - internal)' rather than 'last_synced_at + interval <= now'
|
101
|
+
# so we can use the last_synced_at index.
|
102
|
+
cutoff = (Sequel[as_of].cast("TIMESTAMPTZ") - (Sequel.lit("INTERVAL '1 second'") * Sequel[:period_seconds]))
|
103
|
+
due_before_now = Sequel[:last_synced_at] <= cutoff
|
92
104
|
return self.where(never_synced | due_before_now)
|
93
105
|
end
|
94
106
|
end
|
@@ -168,13 +180,29 @@ class Webhookdb::SyncTarget < Webhookdb::Postgres::Model(:sync_targets)
|
|
168
180
|
timeout: HTTP_VERIFY_TIMEOUT,
|
169
181
|
follow_redirects: true,
|
170
182
|
)
|
171
|
-
rescue
|
172
|
-
raise InvalidConnection, "POST to #{cleanurl}
|
173
|
-
|
174
|
-
raise
|
183
|
+
rescue StandardError => e
|
184
|
+
raise InvalidConnection, "POST to #{cleanurl} failed: #{e.message}" if
|
185
|
+
e.is_a?(Webhookdb::Http::Error) || self.transport_error?(e)
|
186
|
+
raise
|
175
187
|
end
|
176
188
|
end
|
177
189
|
|
190
|
+
# Return true if the given error is considered a 'transport' error,
|
191
|
+
# like a timeout, socket error, dns error, etc.
|
192
|
+
# This isn't a consistent class type.
|
193
|
+
def self.transport_error?(e)
|
194
|
+
return true if e.is_a?(Timeout::Error)
|
195
|
+
return true if e.is_a?(SocketError)
|
196
|
+
return true if e.is_a?(OpenSSL::SSL::SSLError)
|
197
|
+
# SystemCallError are Errno errors, we can get them when the url no longer resolves.
|
198
|
+
return true if e.is_a?(SystemCallError)
|
199
|
+
# Socket::ResolutionError is an error but I guess it's defined in C and we can't raise it in tests.
|
200
|
+
# Anything with an error_code assume is some transport-level issue and treat it as a connection issue,
|
201
|
+
# not a coding issue.
|
202
|
+
return true if e.respond_to?(:error_code)
|
203
|
+
return false
|
204
|
+
end
|
205
|
+
|
178
206
|
def next_scheduled_sync(now:)
|
179
207
|
return self.next_sync(self.period_seconds, now)
|
180
208
|
end
|
@@ -200,6 +228,13 @@ class Webhookdb::SyncTarget < Webhookdb::Postgres::Model(:sync_targets)
|
|
200
228
|
return RAND.rand(1..max_jitter)
|
201
229
|
end
|
202
230
|
|
231
|
+
# @return [ActiveSupport::Duration,Integer]
|
232
|
+
def latency(now: Time.now)
|
233
|
+
return 0 if self.last_synced_at.nil?
|
234
|
+
return 0 if self.last_synced_at > now
|
235
|
+
return now - self.last_synced_at
|
236
|
+
end
|
237
|
+
|
203
238
|
# Running a sync involves some work we always do (export, transform),
|
204
239
|
# and then work that varies per-adapter (load).
|
205
240
|
#
|
@@ -239,6 +274,7 @@ class Webhookdb::SyncTarget < Webhookdb::Postgres::Model(:sync_targets)
|
|
239
274
|
# since the session will be ended.
|
240
275
|
Webhookdb::Dbutil.borrow_conn(Webhookdb::Postgres::Model.uri) do |db|
|
241
276
|
self.advisory_lock(db).with_lock? do
|
277
|
+
self.logger.info "starting_sync"
|
242
278
|
routine = if self.connection_url.start_with?("https://", "http://")
|
243
279
|
# Note that http links are not secure and should only be used for development purposes
|
244
280
|
HttpRoutine.new(now, self)
|
@@ -261,6 +297,16 @@ class Webhookdb::SyncTarget < Webhookdb::Postgres::Model(:sync_targets)
|
|
261
297
|
return displaysafe_url(self.connection_url)
|
262
298
|
end
|
263
299
|
|
300
|
+
def log_tags
|
301
|
+
return {
|
302
|
+
sync_target_id: self.id,
|
303
|
+
sync_target_connection_url: self.displaysafe_connection_url,
|
304
|
+
service_integration_id: self.service_integration_id,
|
305
|
+
service_integration_service: self.service_integration.service_name,
|
306
|
+
service_integration_table: self.service_integration.table_name,
|
307
|
+
}
|
308
|
+
end
|
309
|
+
|
264
310
|
# @return [String]
|
265
311
|
def associated_type
|
266
312
|
# Eventually we need to support orgs
|
@@ -284,6 +330,39 @@ class Webhookdb::SyncTarget < Webhookdb::Postgres::Model(:sync_targets)
|
|
284
330
|
return "#{schema_name}.#{table_name}"
|
285
331
|
end
|
286
332
|
|
333
|
+
# :section: Stats
|
334
|
+
|
335
|
+
def add_sync_stat(start, exception: nil, response_status: nil)
|
336
|
+
stat = {"t" => s2ms(start), "d" => s2ms(Time.now - start)}
|
337
|
+
stat["e"] = exception.class.name if exception
|
338
|
+
stat["rs"] = response_status unless response_status.nil?
|
339
|
+
stats = self.sync_stats
|
340
|
+
stats.prepend(stat)
|
341
|
+
stats.pop if stats.size > MAX_STATS
|
342
|
+
self.will_change_column(:sync_stats)
|
343
|
+
end
|
344
|
+
|
345
|
+
protected def s2ms(t) = (t.to_f * 1000).to_i
|
346
|
+
protected def ms2s(ms) = ms / 1000.0
|
347
|
+
|
348
|
+
def sync_stat_summary
|
349
|
+
return {} if self.sync_stats.empty?
|
350
|
+
earliest = self.sync_stats.last
|
351
|
+
latest = self.sync_stats.first
|
352
|
+
average_latency = (self.sync_stats.sum { |st| ms2s(st["d"]) }) / self.sync_stats.size
|
353
|
+
errors = self.sync_stats.count { |st| st["e"] || st["rs"] }
|
354
|
+
calls_per_minute = 60 / average_latency
|
355
|
+
rpm = self.page_size * calls_per_minute
|
356
|
+
rpm *= self.parallelism if self.parallelism.positive?
|
357
|
+
return {
|
358
|
+
latest: Time.at(ms2s(latest["t"]).to_i),
|
359
|
+
earliest: Time.at(ms2s(earliest["t"]).to_i),
|
360
|
+
average_latency: average_latency.round(2),
|
361
|
+
average_rows_minute: rpm.to_i,
|
362
|
+
errors:,
|
363
|
+
}
|
364
|
+
end
|
365
|
+
|
287
366
|
# @return [Webhookdb::Organization]
|
288
367
|
def organization
|
289
368
|
return self.service_integration.organization
|
@@ -341,40 +420,112 @@ class Webhookdb::SyncTarget < Webhookdb::Postgres::Model(:sync_targets)
|
|
341
420
|
end
|
342
421
|
end
|
343
422
|
|
344
|
-
def
|
345
|
-
|
423
|
+
def perform_db_op(&)
|
424
|
+
yield
|
346
425
|
rescue Sequel::NoExistingObject => e
|
347
426
|
raise Webhookdb::SyncTarget::Deleted, e
|
348
427
|
end
|
428
|
+
|
429
|
+
def record(last_synced_at)
|
430
|
+
self.perform_db_op do
|
431
|
+
self.sync_target.update(last_synced_at:)
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
def with_stat(&)
|
436
|
+
start = Time.now
|
437
|
+
begin
|
438
|
+
yield
|
439
|
+
self.sync_target.add_sync_stat(start)
|
440
|
+
rescue Webhookdb::Http::Error => e
|
441
|
+
self.sync_target.add_sync_stat(start, response_status: e.status)
|
442
|
+
raise
|
443
|
+
rescue StandardError => e
|
444
|
+
self.sync_target.add_sync_stat(start, exception: e)
|
445
|
+
raise
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
def to_ms(t)
|
450
|
+
return (t.to_f * 1000).to_i
|
451
|
+
end
|
349
452
|
end
|
350
453
|
|
351
454
|
class HttpRoutine < Routine
|
455
|
+
def initialize(*)
|
456
|
+
super
|
457
|
+
@inflight_timestamps = []
|
458
|
+
@cleanurl, @authparams = Webhookdb::Http.extract_url_auth(self.sync_target.connection_url)
|
459
|
+
@threadpool = if self.sync_target.parallelism.zero?
|
460
|
+
Webhookdb::Concurrent::SerialPool.new
|
461
|
+
else
|
462
|
+
Webhookdb::Concurrent::ParallelizedPool.new(self.sync_target.parallelism)
|
463
|
+
end
|
464
|
+
@mutex = Thread::Mutex.new
|
465
|
+
end
|
466
|
+
|
352
467
|
def run
|
468
|
+
timeout_at = Time.now + Webhookdb::SyncTarget.max_transaction_seconds
|
353
469
|
page_size = self.sync_target.page_size
|
470
|
+
sync_result = :complete
|
354
471
|
self.dataset_to_sync do |ds|
|
355
472
|
chunk = []
|
356
|
-
ds.paged_each(rows_per_fetch: page_size) do |row|
|
473
|
+
ds.paged_each(rows_per_fetch: page_size, cursor_name: "synctarget_#{self.sync_target.id}_cursor") do |row|
|
357
474
|
chunk << row
|
358
|
-
|
475
|
+
if chunk.size >= page_size
|
476
|
+
# Do not share chunks across threads
|
477
|
+
self._flush_http_chunk(chunk.dup)
|
478
|
+
chunk.clear
|
479
|
+
if Time.now >= timeout_at && Thread.current[:sidekiq_context]
|
480
|
+
# If we've hit the timeout, stop any further syncing
|
481
|
+
sync_result = :timeout
|
482
|
+
break
|
483
|
+
end
|
484
|
+
end
|
359
485
|
end
|
360
486
|
self._flush_http_chunk(chunk) unless chunk.empty?
|
361
|
-
|
362
|
-
|
363
|
-
|
487
|
+
@threadpool.join
|
488
|
+
case sync_result
|
489
|
+
when :timeout
|
490
|
+
# If the sync timed out, use the last recorded sync timestamp,
|
491
|
+
# and re-enqueue the job, so the sync will pick up where it left off.
|
492
|
+
self.sync_target.logger.info("sync_target_transaction_timeout", self.sync_target.log_tags)
|
493
|
+
Webhookdb::Jobs::SyncTargetRunSync.perform_async(self.sync_target.id)
|
494
|
+
else
|
495
|
+
# The sync completed normally.
|
496
|
+
# Save 'now' as the timestamp, rather than the last updated row.
|
497
|
+
# This is important because other we'd keep trying to sync the last row synced.
|
498
|
+
self.record(self.now)
|
499
|
+
end
|
500
|
+
end
|
501
|
+
rescue Webhookdb::Concurrent::Timeout => e
|
502
|
+
# This should never really happen, but it does, so record it while we debug it.
|
503
|
+
self.perform_db_op do
|
504
|
+
self.sync_target.save_changes
|
364
505
|
end
|
365
|
-
|
366
|
-
|
506
|
+
self.sync_target.logger.error("sync_target_pool_timeout_error", self.sync_target.log_tags, e)
|
507
|
+
rescue StandardError => e
|
508
|
+
# Errors talking to the http server are handled well so no need to re-raise.
|
367
509
|
# We already committed the last page that was successful,
|
368
510
|
# so we can just stop syncing at this point to try again later.
|
369
|
-
|
511
|
+
raise e unless e.is_a?(Webhookdb::Http::Error) || Webhookdb::SyncTarget.transport_error?(e)
|
512
|
+
self.perform_db_op do
|
513
|
+
# Save any outstanding stats.
|
514
|
+
self.sync_target.save_changes
|
515
|
+
end
|
370
516
|
# Don't spam our logs with downstream errors
|
371
517
|
idem_key = "sync_target_http_error-#{self.sync_target.id}-#{e.class.name}"
|
372
518
|
Webhookdb::Idempotency.every(1.hour).in_memory.under_key(idem_key) do
|
373
|
-
self.sync_target.logger.warn("sync_target_http_error",
|
519
|
+
self.sync_target.logger.warn("sync_target_http_error", self.sync_target.log_tags, e)
|
374
520
|
end
|
375
521
|
end
|
376
522
|
|
377
523
|
def _flush_http_chunk(chunk)
|
524
|
+
chunk_ts = chunk.last.fetch(self.replicator.timestamp_column.name)
|
525
|
+
@mutex.synchronize do
|
526
|
+
@inflight_timestamps << chunk_ts
|
527
|
+
@inflight_timestamps.sort!
|
528
|
+
end
|
378
529
|
sint = self.sync_target.service_integration
|
379
530
|
body = {
|
380
531
|
rows: chunk,
|
@@ -383,19 +534,34 @@ class Webhookdb::SyncTarget < Webhookdb::Postgres::Model(:sync_targets)
|
|
383
534
|
table: sint.table_name,
|
384
535
|
sync_timestamp: self.now,
|
385
536
|
}
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
537
|
+
@threadpool.post do
|
538
|
+
self.with_stat do
|
539
|
+
Webhookdb::Http.post(
|
540
|
+
@cleanurl,
|
541
|
+
body,
|
542
|
+
timeout: sint.organization.sync_target_timeout,
|
543
|
+
logger: self.sync_target.logger,
|
544
|
+
basic_auth: @authparams,
|
545
|
+
)
|
546
|
+
end
|
547
|
+
# On success, we want to commit the latest timestamp we sent to the client,
|
548
|
+
# so it can be recorded. Then in the case of an error on later rows,
|
549
|
+
# we won't re-sync rows we've already processed (with earlier updated timestamps).
|
550
|
+
@mutex.synchronize do
|
551
|
+
this_ts_idx = @inflight_timestamps.index { |t| t == chunk_ts }
|
552
|
+
raise Webhookdb::InvariantViolation, "timestamp no longer found!?" if this_ts_idx.nil?
|
553
|
+
# However, we only want to record the timestamp if this request is the earliest inflight request;
|
554
|
+
# ie, if a later request finishes before an earlier one, we don't want to record the timestamp
|
555
|
+
# of the later request as 'finished' since the earlier one didn't finish.
|
556
|
+
# This does mean though that, if the earliest request errors, we'll throw away the work
|
557
|
+
# done by the later request.
|
558
|
+
# Note that each row can only appear in a sync once, even if it is modified after the sync starts;
|
559
|
+
# thus, parallel httpsync should be fine for most clients to handle,
|
560
|
+
# since race conditions *on the same row* cannot happen even with parallel httpsync.
|
561
|
+
self.record(chunk_ts) if this_ts_idx.zero?
|
562
|
+
@inflight_timestamps.delete_at(this_ts_idx)
|
563
|
+
end
|
564
|
+
end
|
399
565
|
end
|
400
566
|
end
|
401
567
|
|
@@ -445,7 +611,9 @@ class Webhookdb::SyncTarget < Webhookdb::Postgres::Model(:sync_targets)
|
|
445
611
|
schema_expr = schema_lines.join(";\n") + ";"
|
446
612
|
if schema_expr != self.sync_target.last_applied_schema
|
447
613
|
adapter_conn.execute(schema_expr)
|
448
|
-
self.
|
614
|
+
self.perform_db_op do
|
615
|
+
self.sync_target.update(last_applied_schema: schema_expr)
|
616
|
+
end
|
449
617
|
end
|
450
618
|
tempfile = Tempfile.new("whdbsyncout-#{self.sync_target.id}")
|
451
619
|
begin
|
data/lib/webhookdb/tasks/db.rb
CHANGED
@@ -9,7 +9,7 @@ require "webhookdb/postgres"
|
|
9
9
|
module Webhookdb::Tasks
|
10
10
|
class DB < Rake::TaskLib
|
11
11
|
def initialize
|
12
|
-
super
|
12
|
+
super
|
13
13
|
namespace :db do
|
14
14
|
desc "Drop all tables in the public schema."
|
15
15
|
task :drop_tables do
|
@@ -41,6 +41,18 @@ module Webhookdb::Tasks
|
|
41
41
|
desc "Re-create the database tables. Drop tables and migrate."
|
42
42
|
task reset: ["db:drop_tables", "db:migrate"]
|
43
43
|
|
44
|
+
desc "Set all model tables to UNLOGGED. Do this after test migrating. NEVER PROD."
|
45
|
+
task :unlogged do
|
46
|
+
raise "Unly run under RACK_ENV=test" unless Webhookdb::RACK_ENV == "test"
|
47
|
+
require "webhookdb/postgres"
|
48
|
+
Webhookdb::Postgres.load_superclasses
|
49
|
+
Webhookdb::Postgres.each_model_superclass do |sc|
|
50
|
+
sc.tsort.reverse_each do |m|
|
51
|
+
self.exec(sc.db, "ALTER TABLE #{m.tablename} SET UNLOGGED")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
44
56
|
task :drop_replication_databases do
|
45
57
|
require "webhookdb/postgres"
|
46
58
|
Webhookdb::Postgres.load_superclasses
|
data/lib/webhookdb/tasks/docs.rb
CHANGED
@@ -7,7 +7,7 @@ require "webhookdb"
|
|
7
7
|
module Webhookdb::Tasks
|
8
8
|
class Release < Rake::TaskLib
|
9
9
|
def initialize
|
10
|
-
super
|
10
|
+
super
|
11
11
|
desc "Migrate replication tables for each integration, ensure all columns and backfill new columns."
|
12
12
|
task :migrate_replication_tables do
|
13
13
|
Webhookdb.load_app
|
@@ -8,7 +8,7 @@ require "webhookdb"
|
|
8
8
|
module Webhookdb::Tasks
|
9
9
|
class Specs < Rake::TaskLib
|
10
10
|
def initialize
|
11
|
-
super
|
11
|
+
super
|
12
12
|
namespace :specs do
|
13
13
|
desc "Run API integration tests in the 'integration' folder of this gem. " \
|
14
14
|
"To run your own tests, create a task similar to this one, " \
|
data/lib/webhookdb/version.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "webhookdb/jobs/
|
3
|
+
require "webhookdb/jobs/webhook_subscription_delivery_event"
|
4
4
|
|
5
5
|
# Webhook subscriptions have a few parts:
|
6
6
|
#
|
@@ -136,9 +136,8 @@ class Webhookdb::WebhookSubscription < Webhookdb::Postgres::Model(:webhook_subsc
|
|
136
136
|
rescue StandardError => e
|
137
137
|
self.logger.error(
|
138
138
|
"webhook_subscription_delivery_failure",
|
139
|
-
|
140
|
-
|
141
|
-
webhook_subscription_delivery_id: d.id,
|
139
|
+
{webhook_subscription_id: self.id, webhook_subscription_delivery_id: d.id},
|
140
|
+
e,
|
142
141
|
)
|
143
142
|
d.add_attempt(status: e.is_a?(Webhookdb::Http::Error) ? e.status : 0)
|
144
143
|
if attempt < MAX_DELIVERY_ATTEMPTS
|
data/lib/webhookdb.rb
CHANGED
@@ -19,6 +19,8 @@ Money.locale_backend = :i18n
|
|
19
19
|
Money.default_currency = "USD"
|
20
20
|
Money.rounding_mode = BigDecimal::ROUND_HALF_UP
|
21
21
|
|
22
|
+
ActiveSupport.to_time_preserves_timezone = true
|
23
|
+
|
22
24
|
module Appydays::Configurable
|
23
25
|
def self.fetch_env(keys, default=:__keyerror, env: ENV)
|
24
26
|
keys = [keys] unless keys.respond_to?(:to_ary)
|
@@ -35,27 +37,33 @@ module Webhookdb
|
|
35
37
|
include Appydays::Configurable
|
36
38
|
extend Webhookdb::MethodUtilities
|
37
39
|
|
40
|
+
# Base class for all WebhookDB errors.
|
41
|
+
class WebhookdbError < StandardError; end
|
42
|
+
|
43
|
+
# Class for errors that usually should not be rescued from.
|
44
|
+
class ProgrammingError < WebhookdbError; end
|
45
|
+
|
38
46
|
# Error raised when we cannot take an action
|
39
47
|
# because some condition has not been set up right.
|
40
|
-
class InvalidPrecondition <
|
48
|
+
class InvalidPrecondition < ProgrammingError; end
|
41
49
|
|
42
50
|
# Error raised when, after we take an action,
|
43
51
|
# something we expect to have changed has not changed.
|
44
|
-
class InvalidPostcondition <
|
52
|
+
class InvalidPostcondition < ProgrammingError; end
|
45
53
|
|
46
54
|
# Some invariant has been violated, which we never expect to see.
|
47
|
-
class InvariantViolation <
|
55
|
+
class InvariantViolation < ProgrammingError; end
|
48
56
|
|
49
57
|
# Error raised when a customer gives us some invalid input.
|
50
58
|
# Allows the library to raise the error with the message,
|
51
59
|
# and is caught automatically by the service as a 400.
|
52
|
-
class InvalidInput <
|
60
|
+
class InvalidInput < WebhookdbError; end
|
53
61
|
|
54
62
|
# Raised when an organization's database cannot be modified.
|
55
|
-
class DatabaseLocked <
|
63
|
+
class DatabaseLocked < WebhookdbError; end
|
56
64
|
|
57
65
|
# Used in various places that need to short-circuit code in regression mode.
|
58
|
-
class RegressionModeSkip <
|
66
|
+
class RegressionModeSkip < WebhookdbError; end
|
59
67
|
|
60
68
|
APPLICATION_NAME = "Webhookdb"
|
61
69
|
RACK_ENV = Appydays::Configurable.fetch_env(["RACK_ENV", "RUBY_ENV"], "development")
|
@@ -78,7 +86,7 @@ module Webhookdb
|
|
78
86
|
nil,
|
79
87
|
key: "LOG_LEVEL",
|
80
88
|
side_effect: ->(v) { Appydays::Loggable.default_level = v if v }
|
81
|
-
setting :log_format,
|
89
|
+
setting :log_format, :json_trunc
|
82
90
|
setting :app_url, "http://localhost:18002"
|
83
91
|
setting :api_url, "http://localhost:#{ENV.fetch('PORT', 18_001)}"
|
84
92
|
setting :bust_idempotency, false
|
@@ -93,6 +101,8 @@ module Webhookdb
|
|
93
101
|
end
|
94
102
|
end
|
95
103
|
|
104
|
+
def self.admin_url = self.api_url
|
105
|
+
|
96
106
|
# Regression mode is true when we re replaying webhooks locally,
|
97
107
|
# or for some other reason, want to disable certain checks we use in production.
|
98
108
|
# For example, we may want to ignore certain errors (like if integrations are missing dependency rows),
|
@@ -134,7 +144,23 @@ module Webhookdb
|
|
134
144
|
# :section: Errors
|
135
145
|
#
|
136
146
|
|
137
|
-
class LockFailed <
|
147
|
+
class LockFailed < WebhookdbError; end
|
148
|
+
|
149
|
+
# This exception is rescued in the API and returned to the caller via +merror!+.
|
150
|
+
# This is mostly used in synchronously-processed replicators that need to return
|
151
|
+
# very specific types of errors during processing.
|
152
|
+
# This is very rare.
|
153
|
+
class ExceptionCarrier < WebhookdbError
|
154
|
+
attr_reader :status, :code, :more, :headers
|
155
|
+
|
156
|
+
def initialize(status, message, code: nil, more: {}, headers: {})
|
157
|
+
super(message)
|
158
|
+
@status = status
|
159
|
+
@code = code
|
160
|
+
@more = more
|
161
|
+
@headers = headers
|
162
|
+
end
|
163
|
+
end
|
138
164
|
|
139
165
|
### Generate a key for the specified Sequel model +instance+ and
|
140
166
|
### any additional +parts+ that can be used for idempotent requests.
|