Skip to content

Commit 43b69e8

Browse files
rsaksidaAriel Rolfo
andauthored
Publish to S3 via a debounced queue (#1022)
- Debounces sync events initiated by publishing - Locks the API while syncing is in progress - Uploads manifest with all uploaded S3 files - Triggers Argo workflow for copying items and indexing - Add rake task to mark baseline sync start point Full information about each item is available [here](https://github.com/CredentialEngine/CredentialRegistry/blob/090c7e0ba58284d7c79d48b09b1c5e971d85aca0/docs/11_registry_changeset_sync.md). --------- Co-authored-by: Ariel Rolfo <arielr-lt+username@users.noreply.github.com>
1 parent 77f7acf commit 43b69e8

50 files changed

Lines changed: 3076 additions & 217 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/api/entities/version.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ class Version < Grape::Entity
1111
expose :created_at,
1212
documentation: { type: 'datetime',
1313
desc: 'When the version was created' }
14-
expose :whodunnit,
15-
as: :actor,
14+
expose :actor,
1615
documentation: { type: 'string',
1716
desc: 'Who performed the changes' }
1817
expose :url,
1918
documentation: { type: 'string', desc: 'Version URL' }
2019

20+
def actor
21+
object.author.presence || object.whodunnit
22+
end
23+
2124
def created_at
2225
Time.zone.parse(object.created_at) if object.created_at?
2326
end

app/api/helpers/community_helpers.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'registry_changeset_sync'
2+
13
# EnvelopeCommunity specific helpers
24
module CommunityHelpers
35
extend Grape::API::Helpers
@@ -21,6 +23,12 @@ def community_error(msg)
2123
json_error! [msg], nil, :unprocessable_entity
2224
end
2325

26+
def assert_publish_unlocked!
27+
return unless RegistryChangesetSync.syncing?(current_community)
28+
29+
json_error!([RegistryChangesetSync::PUBLISH_LOCKED], nil, 503)
30+
end
31+
2432
def normalized_community_names
2533
[
2634
params[:community_name].try(:underscore),

app/api/v1/publish.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
require 'policies/envelope_policy'
22
require 'services/publish_interactor'
3-
require 'services/sync_envelope_graph_with_s3'
43

54
module API
65
module V1
@@ -37,6 +36,7 @@ module Publish
3736
end
3837
post do
3938
authorize Envelope, :create?
39+
assert_publish_unlocked!
4040

4141
secondary_token_header = request.headers['Secondary-Token']
4242
secondary_token = if secondary_token_header.present?

app/api/v2/publish.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class Publish < MountableAPI # rubocop:todo Style/Documentation
4141
use :update_if_exists
4242
end
4343
post do
44+
assert_publish_unlocked!
45+
4446
secondary_token_header = request.headers['Secondary-Token']
4547
secondary_token = (secondary_token_header.split.last if secondary_token_header.present?)
4648

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,38 @@
11
require 'extract_envelope_resources'
2+
require 'registry_changeset_sync'
23
require 'index_envelope_job'
34
require 'precalculate_description_sets_job'
45

56
# Runs the ExtractEnvelopeResources service in background
67
class ExtractEnvelopeResourcesJob < ActiveJob::Base
8+
self.enqueue_after_transaction_commit = true
9+
710
def perform(envelope_id)
811
envelope = Envelope.find(envelope_id)
912
ExtractEnvelopeResources.call(envelope:)
13+
record_s3_sync_activity(envelope)
1014
IndexEnvelopeJob.perform_later(envelope_id)
1115
end
16+
17+
private
18+
19+
def record_s3_sync_activity(envelope)
20+
RegistryChangesetSync.record_activity!(
21+
envelope.envelope_community,
22+
version_id: version_id_for_s3_sync(envelope),
23+
resource_event_id: latest_resource_event_id(envelope)
24+
)
25+
end
26+
27+
def version_id_for_s3_sync(envelope)
28+
return if envelope.envelope_ceterms_ctid.blank?
29+
30+
EnvelopeVersion.where(item_type: 'Envelope', item_id: envelope.id).maximum(:id)
31+
end
32+
33+
def latest_resource_event_id(envelope)
34+
EnvelopeResourceSyncEvent.where(
35+
envelope_community: envelope.envelope_community
36+
).maximum(:id)
37+
end
1238
end

app/jobs/publish_envelope_job.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,18 @@ def build_args(request_params) # rubocop:todo Metrics/AbcSize, Metrics/MethodLen
2626
current_user: User.find(request_params[:user_id])
2727
}
2828

29-
if request_params.key?(:envelope_id)
29+
if request_params[:envelope_id].present?
3030
interactor_args[:envelope] = Envelope.find(request_params[:envelope_id])
3131
else
3232
interactor_args[:raw_resource] = request_params[:raw_resource]
3333
end
3434

35-
if request_params.key?(:publishing_organization_id)
35+
if request_params[:publishing_organization_id].present?
3636
interactor_args[:publishing_organization] =
3737
Organization.find(request_params[:publishing_organization_id])
3838
end
3939

40-
if request_params.key?(:secondary_token)
40+
if request_params[:secondary_token].present?
4141
interactor_args[:secondary_token] =
4242
request_params[:secondary_token]
4343
end
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
require 'registry_changeset_sync'
2+
require 'sync_pending_registry_changesets'
3+
4+
class SyncRegistryChangesetsJob < ActiveJob::Base # rubocop:todo Style/Documentation
5+
def perform(sync_id)
6+
sync = RegistryChangesetSync.find(sync_id)
7+
last_activity_at = nil
8+
last_activity_version_id = nil
9+
last_activity_resource_event_id = nil
10+
pending_sync = false
11+
wait_until = nil
12+
13+
sync.with_lock do
14+
last_activity_at = sync.last_activity_at
15+
last_activity_version_id = sync.last_activity_version_id
16+
last_activity_resource_event_id = sync.last_activity_resource_event_id
17+
pending_graph_sync = last_activity_version_id.present? &&
18+
(sync.last_synced_version_id.blank? ||
19+
sync.last_synced_version_id < last_activity_version_id)
20+
pending_resource_sync = last_activity_resource_event_id.present? &&
21+
(sync.last_synced_resource_event_id.blank? ||
22+
sync.last_synced_resource_event_id < last_activity_resource_event_id)
23+
pending_sync = pending_graph_sync || pending_resource_sync
24+
25+
return if sync.scheduled_for_at.blank? && !pending_sync
26+
27+
if sync.scheduled_for_at.present?
28+
wait_until = last_activity_at + RegistryChangesetSync.debounce_window
29+
30+
if wait_until > Time.current
31+
sync.update!(scheduled_for_at: wait_until)
32+
else
33+
wait_until = nil
34+
sync.update!(scheduled_for_at: nil)
35+
end
36+
end
37+
end
38+
39+
if wait_until
40+
self.class.set(wait_until: wait_until).perform_later(sync.id)
41+
return
42+
end
43+
44+
return unless last_activity_at && pending_sync && sync.mark_syncing!
45+
46+
begin
47+
SyncPendingRegistryChangesets.new(
48+
envelope_community: sync.envelope_community,
49+
cutoff_version_id: last_activity_version_id,
50+
cutoff_resource_event_id: last_activity_resource_event_id,
51+
sync: sync
52+
).call
53+
rescue StandardError => e
54+
sync.mark_sync_error!(e)
55+
raise
56+
ensure
57+
sync.clear_syncing! if sync.reload.argo_workflows.blank?
58+
end
59+
end
60+
end

app/models/envelope.rb

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
require 'authorized_key'
44
require 'export_to_ocn_job'
55
require 'delete_from_ocn_job'
6+
require 'registry_changeset_sync'
7+
require 'envelope_resource_sync_event'
68
require 'envelope_version'
79
require_relative 'extensions/transactionable_envelope'
810
require_relative 'extensions/learning_registry_resources'
@@ -46,10 +48,10 @@ class Envelope < ActiveRecord::Base
4648
before_validation :process_resource, :process_headers
4749
before_save :assign_last_verified_on
4850
after_save :update_headers
49-
after_save :upload_to_s3
5051
before_destroy :delete_description_sets, prepend: true
52+
before_destroy :record_resource_sync_delete_events, prepend: true
5153
after_destroy :delete_from_ocn
52-
after_destroy :delete_from_s3
54+
after_commit :schedule_s3_delete, on: :destroy
5355
after_commit :export_to_ocn
5456

5557
validates :envelope_community, :envelope_type, :envelope_version,
@@ -271,11 +273,48 @@ def export_to_ocn
271273
ExportToOCNJob.perform_later(id)
272274
end
273275

274-
def upload_to_s3
275-
SyncEnvelopeGraphWithS3.upload(self)
276+
def schedule_s3_delete
277+
return if envelope_ceterms_ctid.blank? || envelope_community.blank?
278+
279+
RegistryChangesetSync.record_activity!(
280+
envelope_community,
281+
version_id: latest_s3_sync_version_id,
282+
resource_event_id: @last_resource_sync_event_id_for_destroy
283+
)
284+
end
285+
286+
def latest_s3_sync_version_id
287+
EnvelopeVersion
288+
.where(item_type: self.class.name, item_id: id)
289+
.maximum(:id)
290+
end
291+
292+
def latest_resource_sync_event_id
293+
EnvelopeResourceSyncEvent
294+
.where(envelope_community: envelope_community)
295+
.maximum(:id)
276296
end
277297

278-
def delete_from_s3
279-
SyncEnvelopeGraphWithS3.remove(self)
298+
def record_resource_sync_delete_events
299+
return if envelope_ceterms_ctid.blank? || envelope_community.blank?
300+
301+
resource_ids = envelope_resources.pluck(:resource_id)
302+
return if resource_ids.empty?
303+
304+
now = Time.current
305+
306+
EnvelopeResourceSyncEvent.insert_all!(
307+
resource_ids.map do |resource_id|
308+
{
309+
envelope_community_id: envelope_community.id,
310+
resource_id: resource_id,
311+
action: EnvelopeResourceSyncEvent::ACTIONS[:delete],
312+
created_at: now,
313+
updated_at: now
314+
}
315+
end
316+
)
317+
318+
@last_resource_sync_event_id_for_destroy = latest_resource_sync_event_id
280319
end
281320
end

app/models/envelope_metadata.rb

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
class EnvelopeMetadata
2+
attr_reader :envelope
3+
4+
def self.from_envelope(envelope)
5+
new(envelope)
6+
end
7+
8+
def initialize(envelope)
9+
@envelope = envelope
10+
end
11+
12+
def as_json(*)
13+
{
14+
envelope_community: envelope.envelope_community_name,
15+
envelope_id: envelope.envelope_id,
16+
envelope_ceterms_ctid: envelope.envelope_ceterms_ctid,
17+
envelope_ctdl_type: envelope.envelope_ctdl_type,
18+
envelope_type: envelope.envelope_type,
19+
envelope_version: envelope.envelope_version,
20+
publisher_id: envelope.publisher_id,
21+
secondary_publisher_id: envelope.secondary_publisher_id,
22+
resource_publish_type: envelope.resource_publish_type,
23+
node_headers: node_headers,
24+
owned_by: envelope.organization&._ctid,
25+
published_by: envelope.publishing_organization&._ctid,
26+
changed: false,
27+
updated_at: envelope.updated_at,
28+
last_verified_on: envelope.last_verified_on
29+
}
30+
end
31+
32+
private
33+
34+
def node_headers
35+
headers = envelope.decoded_node_headers
36+
37+
{
38+
resource_digest: headers.resource_digest,
39+
revision_history: Array(headers.versions).map { |version| revision_history_entry(version) },
40+
created_at: parse_time(headers.created_at),
41+
updated_at: parse_time(headers.updated_at),
42+
deleted_at: parse_time(headers.deleted_at)
43+
}
44+
end
45+
46+
def revision_history_entry(version)
47+
{
48+
head: version.head,
49+
event: version.event,
50+
created_at: parse_time(version.created_at),
51+
actor: revision_actor(version),
52+
url: version.url
53+
}
54+
end
55+
56+
def revision_actor(version)
57+
version.author.presence || version.whodunnit
58+
end
59+
60+
def parse_time(value)
61+
return if value.blank?
62+
63+
Time.zone.parse(value)
64+
end
65+
end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
class EnvelopeResourceSyncEvent < ActiveRecord::Base
2+
ACTIONS = {
3+
upsert: 0,
4+
delete: 1
5+
}.freeze
6+
7+
belongs_to :envelope_community
8+
9+
validates :envelope_community_id, :resource_id, :action, presence: true
10+
validates :action, inclusion: { in: ACTIONS.values }
11+
12+
def delete?
13+
action == ACTIONS[:delete]
14+
end
15+
16+
def upsert?
17+
action == ACTIONS[:upsert]
18+
end
19+
20+
def action_name
21+
ACTIONS.key(action).to_s
22+
end
23+
end

0 commit comments

Comments
 (0)