Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions app/controllers/api/v1/beacons/manifests_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
module Api
module V1
module Beacons
class ManifestsController < Beacons::BaseController
def show
stored_etag = "v#{Current.beacon.manifest_version}"

# During sync, beacon sends If-Match with its current version.
# If the manifest changed since sync started, return 412 so beacon aborts and restarts.
if sync_version_stale?(stored_etag)
response.headers["ETag"] = stored_etag
head :precondition_failed
return
end

# Beacon sends If-None-Match with its cached version to check for updates.
# If versions match, return 304 — no sync needed.
if cached_version_fresh?(stored_etag)
response.headers["ETag"] = stored_etag
head :not_modified
return
end

# Build full manifest only when we need to return the body
manifest = manifest_builder.call
response.headers["ETag"] = manifest[:manifest_version]
render json: manifest
end

private

def manifest_builder
::Beacons::ManifestBuilder.new(Current.beacon)
end

def sync_version_stale?(etag)
if_match = request.headers["If-Match"]
return false if if_match.blank?

if_match != etag
end

def cached_version_fresh?(etag)
if_none_match = request.headers["If-None-Match"]
return false if if_none_match.blank?

if_none_match == etag
end
end
end
end
end
12 changes: 12 additions & 0 deletions app/jobs/beacons/rebuild_manifest_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Beacons
class RebuildManifestJob < ApplicationJob
queue_as :default

def perform(beacon_id)
beacon = Beacon.find_by(id: beacon_id)
return unless beacon

ManifestBuilder.new(beacon).call
end
end
end
17 changes: 16 additions & 1 deletion app/jobs/synchronize_cognates_on_topics_job.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
class SynchronizeCognatesOnTopicsJob < ApplicationJob
def perform(tag)
modified_topic_ids = []

Topic.where(id: tag.taggings.select(:taggable_id)).each do |topic|
tags = topic.tag_list << tag.cognates_tags.uniq.pluck(:name)
topic.tag_list.add(tags)
topic.save
modified_topic_ids << topic.id if topic.save
end

rebuild_beacon_manifests(modified_topic_ids)
end

private

def rebuild_beacon_manifests(topic_ids)
return if topic_ids.empty?

beacon_ids = BeaconTopic.where(topic_id: topic_ids).distinct.pluck(:beacon_id)
beacon_ids.each do |beacon_id|
Beacons::RebuildManifestJob.perform_later(beacon_id)
end
end
end
22 changes: 13 additions & 9 deletions app/models/beacon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@
# Table name: beacons
# Database name: primary
#
# id :bigint not null, primary key
# api_key_digest :string not null
# api_key_prefix :string not null
# name :string not null
# revoked_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# language_id :bigint not null
# region_id :bigint not null
# id :bigint not null, primary key
# api_key_digest :string not null
# api_key_prefix :string not null
# manifest_checksum :string
# manifest_data :jsonb
# manifest_version :integer default(0), not null
# name :string not null
# previous_manifest_data :jsonb
# revoked_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# language_id :bigint not null
# region_id :bigint not null
#
# Indexes
#
Expand Down
8 changes: 8 additions & 0 deletions app/models/beacon_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,12 @@
class BeaconProvider < ApplicationRecord
belongs_to :beacon
belongs_to :provider

after_commit :rebuild_beacon_manifest, on: [ :create, :destroy ]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we add controller/mutator for managing association, move this logic there


private

def rebuild_beacon_manifest
Beacons::RebuildManifestJob.perform_later(beacon_id)
end
end
8 changes: 8 additions & 0 deletions app/models/beacon_topic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,12 @@
class BeaconTopic < ApplicationRecord
belongs_to :beacon
belongs_to :topic

after_commit :rebuild_beacon_manifest, on: [ :create, :destroy ]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we add controller/mutator for managing association, move this logic there

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this logic go in a concern shared by the two models?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will not stay in models. It will go to controller/mutator when we add managing associations


private

def rebuild_beacon_manifest
Beacons::RebuildManifestJob.perform_later(beacon_id)
end
end
136 changes: 136 additions & 0 deletions app/services/beacons/manifest_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
module Beacons
class ManifestBuilder
def initialize(beacon)
@beacon = beacon
end

def call
content = build_content
checksum = "sha256:#{compute_checksum(content)}"
version = resolve_version(checksum, content)

content.merge(
manifest_version: "v#{version}",
manifest_checksum: checksum,
generated_at: Time.current.iso8601,
)
end

private

attr_reader :beacon

def resolve_version(checksum, content)
return beacon.manifest_version if beacon.manifest_checksum == checksum

beacon.update!(
manifest_version: beacon.manifest_version + 1,
manifest_checksum: checksum,
previous_manifest_data: beacon.manifest_data,
manifest_data: content,
)

beacon.manifest_version
end

def build_content
providers_data = build_providers

{
language: build_language,
region: build_region,
tags: build_tags,
providers: providers_data,
total_size_bytes: compute_total_size(providers_data),
total_files: compute_total_files(providers_data),
}
end

def build_language
language = beacon.language

{
id: language.id,
code: language.code,
name: language.name,
}
end

def build_region
region = beacon.region

{
id: region.id,
name: region.name,
}
end

def build_tags
topics
.flat_map(&:tags)
.uniq(&:id)
.sort_by(&:id)
.map { |tag| { id: tag.id, name: tag.name } }
end

def build_providers
topics_by_provider = topics.group_by(&:provider_id)

beacon.providers.sort_by(&:id).map do |provider|
provider_topics = (topics_by_provider[provider.id] || []).sort_by(&:id)

{
id: provider.id,
name: provider.name,
topics: provider_topics.map { |topic| build_topic(topic) },
}
end
end

def build_topic(topic)
{
id: topic.id,
name: topic.title,
tag_ids: topic.tags.map(&:id).sort,
files: topic.documents.sort_by { |d| d.blob.id }.map { |doc| build_file(topic, doc) },
}
end

def build_file(topic, document)
blob = document.blob
filename = topic.custom_file_name(document)

{
id: blob.id,
filename: filename,
path: "providers/#{topic.provider_id}/topics/#{topic.id}/#{filename}",
checksum: blob.checksum,
size_bytes: blob.byte_size,
content_type: blob.content_type,
updated_at: blob.created_at.iso8601,
}
end

def topics
@topics ||= beacon.topics.active.includes(:tags, :provider, documents_attachments: :blob)
end

def compute_total_size(providers_data)
providers_data.sum do |provider|
provider[:topics].sum do |topic|
topic[:files].sum { |file| file[:size_bytes] }
end
end
end

def compute_total_files(providers_data)
providers_data.sum do |provider|
provider[:topics].sum { |topic| topic[:files].size }
end
end

def compute_checksum(content)
OpenSSL::Digest::SHA256.hexdigest(content.to_json)
end
end
end
23 changes: 20 additions & 3 deletions app/services/topics/mutator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ def initialize(topic:, params: nil, document_signed_ids: nil)
end

def create
mutate
mutate.tap { |status, _| rebuild_beacon_manifests if status == :ok }
end

def update
mutate
mutate.tap { |status, _| rebuild_beacon_manifests if status == :ok }
end

def archive
if @topic.archived!
sync_docs_for_topic_archive if topic.documents.any?
rebuild_beacon_manifests
return [ :ok, topic ]
end

Expand All @@ -26,17 +27,23 @@ def archive
def unarchive
if @topic.active!
sync_docs_for_topic_archive if topic.documents.any?
rebuild_beacon_manifests
return [ :ok, topic ]
end

[ :error, topic.errors.full_messages ]
end

def destroy
beacon_ids = topic.beacon_topics.pluck(:beacon_id)

# If topic deletion fails for some reason, documents deletion still will happen
# This case is unlikely, and only admins can delete topics
sync_docs_for_topic_deletion if topic.documents.any?
return [ :ok, topic ] if topic.destroy
if topic.destroy
dispatch_rebuild_jobs(beacon_ids)
return [ :ok, topic ]
end

[ :error, topic.errors.full_messages ]
end
Expand Down Expand Up @@ -148,4 +155,14 @@ def extract_document_ids
documents = params && params[:documents] || []
documents.map { |doc| doc.is_a?(String) ? doc : doc.signed_id }
end

def rebuild_beacon_manifests
dispatch_rebuild_jobs(topic.beacon_topics.pluck(:beacon_id))
end

def dispatch_rebuild_jobs(beacon_ids)
beacon_ids.each do |beacon_id|
Beacons::RebuildManifestJob.perform_later(beacon_id)
end
end
end
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

namespace :beacons do
resource :status, only: :show
resource :manifest, only: :show
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddManifestVersioningToBeacons < ActiveRecord::Migration[8.0]
def change
add_column :beacons, :manifest_version, :integer, default: 0, null: false
add_column :beacons, :manifest_checksum, :string
end
end
5 changes: 5 additions & 0 deletions db/migrate/20260203100006_add_manifest_data_to_beacons.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddManifestDataToBeacons < ActiveRecord::Migration[8.0]
def change
add_column :beacons, :manifest_data, :jsonb
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddPreviousManifestDataToBeacons < ActiveRecord::Migration[8.0]
def change
add_column :beacons, :previous_manifest_data, :jsonb
end
end
6 changes: 5 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading