webhookdb 1.4.0 → 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.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/db/migrations/026_undo_integration_backfill_cursor.rb +2 -0
  3. data/db/migrations/032_remove_db_defaults.rb +2 -0
  4. data/db/migrations/043_text_search.rb +2 -0
  5. data/db/migrations/047_sync_parallelism.rb +9 -0
  6. data/db/migrations/048_sync_stats.rb +9 -0
  7. data/db/migrations/049_error_handlers.rb +18 -0
  8. data/db/migrations/050_logged_webhook_indices.rb +25 -0
  9. data/db/migrations/051_partitioning.rb +9 -0
  10. data/integration/async_spec.rb +0 -2
  11. data/integration/service_integrations_spec.rb +0 -2
  12. data/lib/amigo/durable_job.rb +2 -2
  13. data/lib/amigo/job_in_context.rb +12 -0
  14. data/lib/webhookdb/api/entities.rb +6 -2
  15. data/lib/webhookdb/api/error_handlers.rb +104 -0
  16. data/lib/webhookdb/api/helpers.rb +8 -1
  17. data/lib/webhookdb/api/icalproxy.rb +22 -0
  18. data/lib/webhookdb/api/install.rb +2 -1
  19. data/lib/webhookdb/api/saved_queries.rb +1 -0
  20. data/lib/webhookdb/api/saved_views.rb +1 -0
  21. data/lib/webhookdb/api/service_integrations.rb +1 -1
  22. data/lib/webhookdb/api/sync_targets.rb +1 -1
  23. data/lib/webhookdb/api/system.rb +5 -0
  24. data/lib/webhookdb/api/webhook_subscriptions.rb +1 -0
  25. data/lib/webhookdb/api.rb +4 -1
  26. data/lib/webhookdb/apps.rb +4 -0
  27. data/lib/webhookdb/async/autoscaler.rb +10 -0
  28. data/lib/webhookdb/async/job.rb +4 -0
  29. data/lib/webhookdb/async/scheduled_job.rb +4 -0
  30. data/lib/webhookdb/async.rb +2 -0
  31. data/lib/webhookdb/backfiller.rb +17 -4
  32. data/lib/webhookdb/concurrent.rb +96 -0
  33. data/lib/webhookdb/connection_cache.rb +29 -8
  34. data/lib/webhookdb/customer.rb +2 -2
  35. data/lib/webhookdb/database_document.rb +1 -1
  36. data/lib/webhookdb/db_adapter/default_sql.rb +1 -14
  37. data/lib/webhookdb/db_adapter/partition.rb +14 -0
  38. data/lib/webhookdb/db_adapter/partitioning.rb +8 -0
  39. data/lib/webhookdb/db_adapter/pg.rb +77 -5
  40. data/lib/webhookdb/db_adapter/snowflake.rb +15 -6
  41. data/lib/webhookdb/db_adapter.rb +24 -2
  42. data/lib/webhookdb/fixtures/logged_webhooks.rb +4 -0
  43. data/lib/webhookdb/fixtures/organization_error_handlers.rb +20 -0
  44. data/lib/webhookdb/http.rb +29 -15
  45. data/lib/webhookdb/icalendar.rb +30 -9
  46. data/lib/webhookdb/jobs/amigo_test_jobs.rb +1 -1
  47. data/lib/webhookdb/jobs/backfill.rb +21 -25
  48. data/lib/webhookdb/jobs/create_mirror_table.rb +3 -4
  49. data/lib/webhookdb/jobs/deprecated_jobs.rb +2 -0
  50. data/lib/webhookdb/jobs/emailer.rb +2 -1
  51. data/lib/webhookdb/jobs/front_signalwire_message_channel_sync_inbound.rb +15 -0
  52. data/lib/webhookdb/jobs/icalendar_delete_stale_cancelled_events.rb +7 -2
  53. data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +74 -11
  54. data/lib/webhookdb/jobs/icalendar_enqueue_syncs_for_urls.rb +22 -0
  55. data/lib/webhookdb/jobs/icalendar_sync.rb +21 -9
  56. data/lib/webhookdb/jobs/increase_event_handler.rb +3 -2
  57. data/lib/webhookdb/jobs/logged_webhooks_replay.rb +5 -3
  58. data/lib/webhookdb/jobs/message_dispatched.rb +1 -0
  59. data/lib/webhookdb/jobs/model_event_system_log_tracker.rb +7 -0
  60. data/lib/webhookdb/jobs/monitor_metrics.rb +1 -1
  61. data/lib/webhookdb/jobs/organization_database_migration_notify.rb +32 -0
  62. data/lib/webhookdb/jobs/organization_database_migration_run.rb +4 -6
  63. data/lib/webhookdb/jobs/organization_error_handler_dispatch.rb +26 -0
  64. data/lib/webhookdb/jobs/prepare_database_connections.rb +1 -0
  65. data/lib/webhookdb/jobs/process_webhook.rb +11 -12
  66. data/lib/webhookdb/jobs/renew_watch_channel.rb +7 -10
  67. data/lib/webhookdb/jobs/replication_migration.rb +5 -2
  68. data/lib/webhookdb/jobs/reset_code_create_dispatch.rb +1 -2
  69. data/lib/webhookdb/jobs/scheduled_backfills.rb +2 -2
  70. data/lib/webhookdb/jobs/send_invite.rb +3 -2
  71. data/lib/webhookdb/jobs/send_test_webhook.rb +1 -3
  72. data/lib/webhookdb/jobs/send_webhook.rb +4 -5
  73. data/lib/webhookdb/jobs/stale_row_deleter.rb +31 -0
  74. data/lib/webhookdb/jobs/sync_target_enqueue_scheduled.rb +3 -0
  75. data/lib/webhookdb/jobs/sync_target_run_sync.rb +9 -15
  76. data/lib/webhookdb/jobs/webhook_subscription_delivery_event.rb +5 -8
  77. data/lib/webhookdb/liquid/expose.rb +1 -1
  78. data/lib/webhookdb/liquid/filters.rb +1 -1
  79. data/lib/webhookdb/liquid/partial.rb +2 -2
  80. data/lib/webhookdb/logged_webhook/resilient.rb +3 -3
  81. data/lib/webhookdb/logged_webhook.rb +16 -2
  82. data/lib/webhookdb/message/email_transport.rb +1 -1
  83. data/lib/webhookdb/message.rb +2 -2
  84. data/lib/webhookdb/messages/error_generic_backfill.rb +2 -0
  85. data/lib/webhookdb/messages/error_icalendar_fetch.rb +2 -0
  86. data/lib/webhookdb/messages/error_signalwire_send_sms.rb +2 -0
  87. data/lib/webhookdb/organization/alerting.rb +50 -4
  88. data/lib/webhookdb/organization/database_migration.rb +1 -1
  89. data/lib/webhookdb/organization/db_builder.rb +4 -3
  90. data/lib/webhookdb/organization/error_handler.rb +141 -0
  91. data/lib/webhookdb/organization.rb +62 -9
  92. data/lib/webhookdb/postgres/model_utilities.rb +2 -0
  93. data/lib/webhookdb/postgres.rb +1 -3
  94. data/lib/webhookdb/replicator/base.rb +136 -29
  95. data/lib/webhookdb/replicator/base_stale_row_deleter.rb +165 -0
  96. data/lib/webhookdb/replicator/email_octopus_contact_v1.rb +0 -1
  97. data/lib/webhookdb/replicator/fake.rb +100 -88
  98. data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +105 -44
  99. data/lib/webhookdb/replicator/github_repo_v1_mixin.rb +17 -0
  100. data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +144 -23
  101. data/lib/webhookdb/replicator/icalendar_event_v1.rb +20 -44
  102. data/lib/webhookdb/replicator/icalendar_event_v1_partitioned.rb +33 -0
  103. data/lib/webhookdb/replicator/intercom_contact_v1.rb +1 -0
  104. data/lib/webhookdb/replicator/intercom_conversation_v1.rb +1 -0
  105. data/lib/webhookdb/replicator/intercom_v1_mixin.rb +24 -2
  106. data/lib/webhookdb/replicator/partitionable_mixin.rb +116 -0
  107. data/lib/webhookdb/replicator/shopify_v1_mixin.rb +1 -1
  108. data/lib/webhookdb/replicator/signalwire_message_v1.rb +1 -2
  109. data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +1 -1
  110. data/lib/webhookdb/replicator/transistor_episode_stats_v1.rb +0 -1
  111. data/lib/webhookdb/replicator.rb +4 -1
  112. data/lib/webhookdb/service/helpers.rb +4 -0
  113. data/lib/webhookdb/service/middleware.rb +6 -2
  114. data/lib/webhookdb/service_integration.rb +5 -0
  115. data/lib/webhookdb/signalwire.rb +1 -1
  116. data/lib/webhookdb/spec_helpers/async.rb +0 -4
  117. data/lib/webhookdb/spec_helpers/sentry.rb +32 -0
  118. data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +87 -1
  119. data/lib/webhookdb/spec_helpers.rb +1 -0
  120. data/lib/webhookdb/sync_target.rb +195 -29
  121. data/lib/webhookdb/tasks/admin.rb +1 -1
  122. data/lib/webhookdb/tasks/annotate.rb +1 -1
  123. data/lib/webhookdb/tasks/db.rb +13 -1
  124. data/lib/webhookdb/tasks/docs.rb +1 -1
  125. data/lib/webhookdb/tasks/fixture.rb +1 -1
  126. data/lib/webhookdb/tasks/message.rb +1 -1
  127. data/lib/webhookdb/tasks/regress.rb +1 -1
  128. data/lib/webhookdb/tasks/release.rb +1 -1
  129. data/lib/webhookdb/tasks/sidekiq.rb +1 -1
  130. data/lib/webhookdb/tasks/specs.rb +1 -1
  131. data/lib/webhookdb/version.rb +1 -1
  132. data/lib/webhookdb/webhook_subscription.rb +2 -3
  133. data/lib/webhookdb.rb +3 -1
  134. metadata +88 -54
  135. data/lib/webhookdb/jobs/organization_database_migration_notify_finished.rb +0 -21
  136. data/lib/webhookdb/jobs/organization_database_migration_notify_started.rb +0 -21
@@ -15,6 +15,8 @@ Amigo::DeprecatedJobs.install(
15
15
  "Jobs::ConvertKitSubscriberBackfill",
16
16
  "Jobs::ConvertKitTagBackfill",
17
17
  "Jobs::CustomerCreatedNotifyInternal",
18
+ "Jobs::OrganizationDatabaseMigrationNotifyStarted",
19
+ "Jobs::OrganizationDatabaseMigrationNotifyFinished",
18
20
  "Jobs::RssBackfillPoller",
19
21
  "Jobs::TwilioScheduledBackfill",
20
22
  )
@@ -9,6 +9,7 @@ class Webhookdb::Jobs::Emailer
9
9
  splay 5.seconds
10
10
 
11
11
  def _perform
12
- Webhookdb::Message.send_unsent
12
+ sent = Webhookdb::Message.send_unsent
13
+ self.set_job_tags(sent_messages: sent.count)
13
14
  end
14
15
  end
@@ -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: "icalendar_event_v1").each do |sint|
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.delete_stale_cancelled_events
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
- # Jobs are 'splayed' over 1/4 of the configured calendar sync period (see +Webhookdb::Icalendar+)
9
- # to avoid a thundering herd.
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
- cron "*/30 * * * *" # Every 30 minutes
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
- max_splay = Webhookdb::Icalendar.sync_period_hours.hours.to_i / 4
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
- sint.replicator.admin_dataset do |ds|
20
- sint.replicator.rows_needing_sync(ds).each do |row|
21
- calendar_external_id = row.fetch(:external_id)
22
- self.with_log_tags(sint.log_tags) do
23
- splay = rand(1..max_splay)
24
- enqueued_job_id = Webhookdb::Jobs::IcalendarSync.perform_in(splay, sint.id, calendar_external_id)
25
- self.logger.debug("enqueued_icalendar_sync", calendar_external_id:, enqueued_job_id:)
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.with_log_tags(sint.log_tags.merge(calendar_external_id:)) do
14
- row = sint.replicator.admin_dataset { |ds| ds[external_id: calendar_external_id] }
15
- if row.nil?
16
- self.logger.warn("icalendar_sync_row_miss", calendar_external_id:)
17
- return
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.logger.info("increase_oauth_disconnect", oauth_connection_id: conn_id)
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.logger.info("increase_event_noop")
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.with_log_tags(service_integration_opaque_id: lwh.service_integration_opaque_id) do
14
- lwh.retry_one(truncate_successful: true)
15
- end
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
@@ -9,6 +9,7 @@ class Webhookdb::Jobs::ModelEventSystemLogTracker
9
9
  on "webhookdb.*"
10
10
 
11
11
  def _perform(event)
12
+ self.set_job_tags(event_name: event.name)
12
13
  case event.name
13
14
  when "webhookdb.customer.created"
14
15
  self.alert_customer_created(event)
@@ -18,6 +19,8 @@ class Webhookdb::Jobs::ModelEventSystemLogTracker
18
19
  self.alert_sint_created(event)
19
20
  when "webhookdb.serviceintegration.destroyed"
20
21
  self.alert_sint_destroyed(event)
22
+ else
23
+ self.set_job_tags(result: "noop")
21
24
  end
22
25
  end
23
26
 
@@ -43,6 +46,7 @@ class Webhookdb::Jobs::ModelEventSystemLogTracker
43
46
  ],
44
47
  ).emit
45
48
  create_event("Customer Created", customer.email, customer.admin_link)
49
+ self.set_job_tags(result: "created_customer", email: customer.email)
46
50
  end
47
51
 
48
52
  def alert_org_created(event)
@@ -58,6 +62,7 @@ class Webhookdb::Jobs::ModelEventSystemLogTracker
58
62
  ],
59
63
  ).emit
60
64
  create_event("Organization Created", "#{org.name} (#{org.key})", org.admin_link)
65
+ self.set_job_tags(result: "created_organization", key: org.key)
61
66
  end
62
67
 
63
68
  def alert_sint_created(event)
@@ -79,6 +84,7 @@ class Webhookdb::Jobs::ModelEventSystemLogTracker
79
84
  "#{sint.service_name} (#{sint.opaque_id}) created in #{sint.organization.name}",
80
85
  sint.admin_link,
81
86
  )
87
+ self.set_job_tags(result: "created_service_integration", opaque_id: sint.opaque_id, service: sint.service_name)
82
88
  end
83
89
 
84
90
  def alert_sint_destroyed(event)
@@ -101,5 +107,6 @@ class Webhookdb::Jobs::ModelEventSystemLogTracker
101
107
  "#{pl[:service_name]} (#{pl[:opaque_id]}) deleted from #{org.name}",
102
108
  org.admin_link,
103
109
  )
110
+ self.set_job_tags(result: "destroyed_service_integration", opaque_id: pl[:oparque_id], service: pl[:service_name])
104
111
  end
105
112
  end
@@ -24,6 +24,6 @@ class Webhookdb::Jobs::MonitorMetrics
24
24
  end
25
25
  opts[:max_size] = max_size
26
26
  opts[:max_latency] = max_latency
27
- self.logger.info("metrics_monitor_queue", **opts)
27
+ self.set_job_tags(action: "metrics_monitor_queue", **opts)
28
28
  end
29
29
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/async/job"
4
+ require "webhookdb/messages/org_database_migration_finished"
5
+ require "webhookdb/messages/org_database_migration_started"
6
+
7
+ class Webhookdb::Jobs::OrganizationDatabaseMigrationNotify
8
+ extend Webhookdb::Async::Job
9
+
10
+ on "webhookdb.organization.databasemigration.updated"
11
+
12
+ def _perform(event)
13
+ dbm = self.lookup_model(Webhookdb::Organization::DatabaseMigration, event)
14
+ self.set_job_tags(database_migration_id: dbm.id, organization: dbm.organization.key)
15
+ case event.payload[1]
16
+ when changed(:started_at, from: nil)
17
+ Webhookdb::Idempotency.once_ever.under_key("org-dbmigration-start-#{dbm.id}") do
18
+ msg = Webhookdb::Messages::OrgDatabaseMigrationStarted.new(dbm)
19
+ dbm.organization.admin_customers.each { |c| msg.dispatch_email(c) }
20
+ end
21
+ self.set_job_tags(result: "started_message_sent")
22
+ when changed(:finished_at, from: nil)
23
+ Webhookdb::Idempotency.once_ever.under_key("org-dbmigration-finish-#{dbm.id}") do
24
+ msg = Webhookdb::Messages::OrgDatabaseMigrationFinished.new(dbm)
25
+ dbm.organization.admin_customers.each { |c| msg.dispatch_email(c) }
26
+ end
27
+ self.set_job_tags(result: "finished_message_sent")
28
+ else
29
+ self.set_job_tags(result: "noop")
30
+ end
31
+ end
32
+ end
@@ -11,14 +11,12 @@ class Webhookdb::Jobs::OrganizationDatabaseMigrationRun
11
11
 
12
12
  def _perform(event)
13
13
  dbm = self.lookup_model(Webhookdb::Organization::DatabaseMigration, event)
14
- self.with_log_tags(
15
- organization_id: dbm.organization.id,
16
- organization_name: dbm.organization.name,
17
- organization_database_migration_id: dbm.id,
18
- ) do
14
+ self.set_job_tags(organization: dbm.organization.key, database_migration_id: dbm.id)
15
+ begin
19
16
  dbm.migrate
17
+ self.set_job_tags(result: "migration_finished")
20
18
  rescue Webhookdb::Organization::DatabaseMigration::MigrationAlreadyFinished
21
- self.logger.warn("org_database_migration_already_finished")
19
+ self.set_job_tags(result: "migration_already_finished")
22
20
  end
23
21
  end
24
22
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/async/job"
4
+
5
+ class Webhookdb::Jobs::OrganizationErrorHandlerDispatch
6
+ extend Webhookdb::Async::Job
7
+
8
+ sidekiq_options queue: "netout"
9
+
10
+ def perform(error_handler_id, payload)
11
+ eh = self.lookup_model(Webhookdb::Organization::ErrorHandler, error_handler_id)
12
+ self.set_job_tags(error_handler_id: eh.id, **eh.organization.log_tags)
13
+ begin
14
+ eh.dispatch(payload)
15
+ self.set_job_tags(result: "success")
16
+ rescue StandardError => e
17
+ # Don't bother logging these errors out
18
+ self.set_job_tags(result: "error")
19
+ self.logger.debug("organization_error_handler_post_error", error: e)
20
+ raise Amigo::Retry::OrDie.new(
21
+ Webhookdb::Organization::Alerting.error_handler_retries,
22
+ Webhookdb::Organization::Alerting.error_handler_retry_interval,
23
+ )
24
+ end
25
+ end
26
+ end
@@ -12,6 +12,7 @@ class Webhookdb::Jobs::PrepareDatabaseConnections
12
12
 
13
13
  def _perform(event)
14
14
  org = self.lookup_model(Webhookdb::Organization, event)
15
+ self.set_job_tags(organization: org.key)
15
16
  org.db.transaction do
16
17
  # If creating the public host fails, we end up with an orphaned database,
17
18
  # but that's not a big deal- we can eventually see it's empty/unlinked and drop it.
@@ -23,18 +23,17 @@ class Webhookdb::Jobs::ProcessWebhook
23
23
  end
24
24
 
25
25
  def _perform(event)
26
- self.with_log_tags(@sint.log_tags) do
27
- kw = event.payload[1].symbolize_keys
28
- svc = Webhookdb::Replicator.create(@sint)
29
- # kwargs contains: :headers, :body, :request_path, :request_method
30
- req = Webhookdb::Replicator::WebhookRequest.new(
31
- body: kw.fetch(:body),
32
- headers: kw.fetch(:headers),
33
- path: kw.fetch(:request_path),
34
- method: kw.fetch(:request_method),
35
- )
36
- svc.upsert_webhook(req)
37
- end
26
+ self.set_job_tags(@sint.log_tags)
27
+ kw = event.payload[1].symbolize_keys
28
+ svc = Webhookdb::Replicator.create(@sint)
29
+ # kwargs contains: :headers, :body, :request_path, :request_method
30
+ req = Webhookdb::Replicator::WebhookRequest.new(
31
+ body: kw.fetch(:body),
32
+ headers: kw.fetch(:headers),
33
+ path: kw.fetch(:request_path),
34
+ method: kw.fetch(:request_method),
35
+ )
36
+ svc.upsert_webhook(req)
38
37
  end
39
38
 
40
39
  def semaphore_key
@@ -6,22 +6,19 @@ require "webhookdb/jobs"
6
6
 
7
7
  # Generic helper to renew watch channels, enqueued by replicator-specific jobs
8
8
  # like RenewGoogleWatchChannels.
9
- # Must be emitted with [service integration id, {row_pk:, expirng_before:}]
9
+ # Must be emitted with [service_integration_id, {row_pk:, expirng_before:}]
10
10
  # Calls #renew_watch_channel(row_pk:, expiring_before:).
11
11
  class Webhookdb::Jobs::RenewWatchChannel
12
12
  extend Webhookdb::Async::Job
13
13
  include Amigo::QueueBackoffJob
14
14
 
15
- on "webhookdb.serviceintegration.renewwatchchannel"
16
15
  sidekiq_options queue: "netout"
17
16
 
18
- def _perform(event)
19
- sint = self.lookup_model(Webhookdb::ServiceIntegration, event)
20
- self.with_log_tags(sint.log_tags) do
21
- opts = event.payload[1]
22
- row_pk = opts.fetch("row_pk")
23
- expiring_before = Time.parse(opts.fetch("expiring_before"))
24
- sint.replicator.renew_watch_channel(row_pk:, expiring_before:)
25
- end
17
+ def perform(service_integration_id, renew_watch_criteria)
18
+ sint = self.lookup_model(Webhookdb::ServiceIntegration, service_integration_id)
19
+ self.set_job_tags(sint.log_tags)
20
+ row_pk = renew_watch_criteria.fetch("row_pk")
21
+ expiring_before = Time.parse(renew_watch_criteria.fetch("expiring_before"))
22
+ sint.replicator.renew_watch_channel(row_pk:, expiring_before:)
26
23
  end
27
24
  end
@@ -2,18 +2,21 @@
2
2
 
3
3
  # See Organization::enqueue_migrate_all_replication_tables for more info.
4
4
  class Webhookdb::Jobs::ReplicationMigration
5
- include Sidekiq::Worker
5
+ extend Webhookdb::Async::Job
6
6
 
7
7
  def perform(org_id, target_release_created_at)
8
8
  (org = Webhookdb::Organization[org_id]) or raise "Organization[#{org_id}] does not exist"
9
9
  target_rca = Time.parse(target_release_created_at)
10
10
  current_rca = Time.parse(Webhookdb::RELEASE_CREATED_AT)
11
+ self.set_job_tags(organization_id: org_id, target_release_created_at:)
11
12
  if target_rca == current_rca
12
13
  self.class.migrate_org(org)
14
+ self.set_job_tags(result: "ran_replication_migration_job")
13
15
  elsif target_rca > current_rca
14
16
  self.class.perform_in(1, org_id, target_release_created_at)
17
+ self.set_job_tags(result: "reenqueued_replication_migration_job")
15
18
  else
16
- self.logger.warn("stale_replication_migration_job", target_release_created_at:)
19
+ self.set_job_tags(result: "stale_replication_migration_job")
17
20
  end
18
21
  end
19
22
 
@@ -10,13 +10,12 @@ class Webhookdb::Jobs::ResetCodeCreateDispatch
10
10
 
11
11
  def _perform(event)
12
12
  code = self.lookup_model(Webhookdb::Customer::ResetCode, event)
13
+ self.set_job_tags(code_id: code.id, customer: code.customer.email, transport: code.transport)
13
14
  Webhookdb::Idempotency.once_ever.under_key("reset-code-#{code.customer_id}-#{code.id}") do
14
15
  msg = Webhookdb::Messages::Verification.new(code)
15
16
  case code.transport
16
17
  when "email"
17
18
  msg.dispatch_email(code.customer)
18
- else
19
- raise "Unknown transport for #{code.inspect}"
20
19
  end
21
20
  end
22
21
  end
@@ -51,9 +51,9 @@ module Webhookdb::Jobs
51
51
  Webhookdb::Github.activity_cron_expression, 30.seconds, false,
52
52
  ),
53
53
  Spec.new(
54
- # I think we can get rid of this once we're more confident webhooks are working reliably.
54
+ # This incremental sync is a backstop for any missed webhooks.
55
55
  "IntercomScheduledBackfill", "intercom_marketplace_root_v1",
56
- "46 4 * * *", 0, false, true,
56
+ "46 4 * * *", 0, true, true,
57
57
  ),
58
58
  Spec.new(
59
59
  "AtomSingleFeedPoller", "atom_single_feed_v1",
@@ -9,7 +9,8 @@ class Webhookdb::Jobs::SendInvite
9
9
  on "webhookdb.organizationmembership.invite"
10
10
 
11
11
  def _perform(event)
12
- membership = self.lookup_model(Webhookdb::OrganizationMembership, event)
13
- Webhookdb::Messages::Invite.new(membership).dispatch(membership.customer)
12
+ m = self.lookup_model(Webhookdb::OrganizationMembership, event)
13
+ self.set_job_tags(membership_id: m.id, organization: m.organization.key, customer: m.customer.email)
14
+ Webhookdb::Messages::Invite.new(m).dispatch(m.customer)
14
15
  end
15
16
  end
@@ -14,9 +14,7 @@ class Webhookdb::Jobs::SendTestWebhook
14
14
  # we don't want to retry and randomly send a payload later.
15
15
  sidekiq_options retry: false
16
16
 
17
- def dependent_queues
18
- return ["critical"]
19
- end
17
+ def dependent_queues = ["critical"]
20
18
 
21
19
  def _perform(event)
22
20
  webhook_sub = self.lookup_model(Webhookdb::WebhookSubscription, event)
@@ -10,11 +10,10 @@ class Webhookdb::Jobs::SendWebhook
10
10
 
11
11
  def _perform(event)
12
12
  sint = self.lookup_model(Webhookdb::ServiceIntegration, event)
13
- self.with_log_tags(sint.log_tags) do
14
- sint.all_webhook_subscriptions_dataset.to_notify.each do |sub|
15
- payload = {service_name: sint.service_name, table_name: sint.table_name, **event.payload[1]}
16
- sub.enqueue_delivery(payload)
17
- end
13
+ self.set_job_tags(sint.log_tags)
14
+ sint.all_webhook_subscriptions_dataset.to_notify.each do |sub|
15
+ payload = {service_name: sint.service_name, table_name: sint.table_name, **event.payload[1]}
16
+ sub.enqueue_delivery(payload)
18
17
  end
19
18
  end
20
19
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/async/job"
4
+
5
+ # Run the +stale_row_deleter+ for each service integration
6
+ # which match the given where/exclude clauses.
7
+ # This is generally used to delete old stale rows in the backend
8
+ # (by passing initial: true) when a new stale row deleter is deployed.
9
+ class Webhookdb::Jobs::StaleRowDeleter
10
+ extend Webhookdb::Async::Job
11
+
12
+ def perform(opts={})
13
+ opts = opts.deep_symbolize_keys
14
+ opts[:where] ||= {}
15
+ opts[:exclude] ||= {}
16
+ opts[:initial] ||= false
17
+ ds = Webhookdb::ServiceIntegration.dataset
18
+ ds = ds.where(opts[:where]) if opts[:where]
19
+ ds = ds.exclude(opts[:exclude]) if opts[:exclude]
20
+ self.set_job_tags(dataset: ds.sql)
21
+ count = 0
22
+ ds.each do |sint|
23
+ self.with_log_tags(sint.log_tags) do
24
+ d = sint.replicator.stale_row_deleter
25
+ opts[:initial] ? d.run_initial : d.run
26
+ count += 1
27
+ end
28
+ end
29
+ self.set_job_tags(run_count: count)
30
+ end
31
+ end
@@ -9,8 +9,11 @@ class Webhookdb::Jobs::SyncTargetEnqueueScheduled
9
9
  splay 0
10
10
 
11
11
  def _perform
12
+ count = 0
12
13
  Webhookdb::SyncTarget.due_for_sync(as_of: Time.now).select(:id, :period_seconds).each do |st|
14
+ count += 1
13
15
  Webhookdb::Jobs::SyncTargetRunSync.perform_in(st.jitter, st.id)
14
16
  end
17
+ self.set_job_tags(enqueued_count: count)
15
18
  end
16
19
  end