Skip to content

Commit bd4bd18

Browse files
committed
Refactor DVZ/P models to multiple service classes
Fixes #2519
1 parent 175f1ee commit bd4bd18

26 files changed

+566
-731
lines changed

app/models/replication/druid_version_zip.rb

Lines changed: 0 additions & 187 deletions
This file was deleted.

app/models/zip_part.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@
55
# This model's data is populated by Replication::DeliveryDispatcherJob.
66
class ZipPart < ApplicationRecord
77
belongs_to :zipped_moab_version, inverse_of: :zip_parts
8-
delegate :zip_endpoint, :preserved_object, :druid_version_zip, to: :zipped_moab_version
8+
delegate :zip_endpoint, :preserved_object, :zip_part_pathfinder, to: :zipped_moab_version
99

1010
validates :md5, presence: true, format: { with: /\A[0-9a-f]{32}\z/ }
1111
validates :size, presence: true, numericality: { only_integer: true, greater_than: 0 }
1212
validates :suffix, presence: true, format: { with: /\A\.z(ip|[0-9]+)\z/ }
1313

14-
def druid_version_zip_part
15-
@druid_version_zip_part ||= Replication::DruidVersionZipPart.new(druid_version_zip, s3_key)
14+
def zip_part_file
15+
@zip_part_file ||= Replication::ZipPartFile.new(filename: s3_key)
1616
end
1717

1818
def s3_key
19-
druid_version_zip.s3_key(suffix)
19+
zip_part_pathfinder.s3_key(suffix:)
2020
end
2121

2222
def s3_part

app/models/zipped_moab_version.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,16 @@ def update_status_details
4444
self.status_details = nil if status_changed? && !status_details_changed?
4545
end
4646

47-
def druid_version_zip
48-
@druid_version_zip ||= Replication::DruidVersionZip.new(
47+
def zip_part_pathfinder
48+
@zip_part_pathfinder ||= Replication::ZipPartPathfinder.new(
49+
druid: preserved_object.druid,
50+
version:,
4951
# In tests, a PreservedObject may not have a MoabRecord, hence the safe navigation.
50-
preserved_object.druid, version, preserved_object&.moab_record&.moab_storage_root&.storage_location # rubocop:disable Style/SafeNavigationChainLength
52+
storage_location: preserved_object&.moab_record&.moab_storage_root&.storage_location # rubocop:disable Style/SafeNavigationChainLength
5153
)
5254
end
55+
56+
def filesystem_size
57+
@filesystem_size ||= Replication::MoabVersionFiles.new(root: zip_part_pathfinder.moab_version_root).size
58+
end
5359
end

app/services/replication/errors.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
module Replication
4+
# Replication-related exceptions
5+
module Errors
6+
# Raised when Moab version root is not found
7+
class MoabVersionNotFound < RuntimeError; end
8+
9+
# Raised when a file is unreadable due to access restrictions, stale
10+
# handles, I/O snafus, and so on
11+
class UnreadableFile < RuntimeError; end
12+
13+
# Raised when the zip command fails
14+
class ZipmakerFailure < RuntimeError; end
15+
end
16+
end
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# frozen_string_literal: true
2+
3+
module Replication
4+
# Calculate the total size of all files in a Moab and ensure they are readable
5+
class MoabVersionFiles
6+
# @param [String] root The root path for a particular Moab
7+
def initialize(root:)
8+
@root = root
9+
end
10+
11+
# @raise [Replication::Errors::MoabVersionNotFound] when Moab version root is not found
12+
# @return [Integer] the sum of all file sizes in the Moab
13+
def size
14+
@size ||= files.sum { |f| File.size(f) }
15+
end
16+
17+
# Raises an error if any of the files in the moab are not readable, for example, due to
18+
# Ceph MDS instance for a prescat worker VM thinking that a file is a stray as a result of our
19+
# particular use of hard-linking in preservation ingest. Allows for a quick directory walk before
20+
# attempting to create the zip file(s).
21+
#
22+
# @see https://github.com/sul-dlss/preservation_catalog/issues/1633
23+
# @raise [Replication::Errors::UnreadableFile] wraps an underlying file access / readability exception
24+
# @raise [Replication::Errors::MoabVersionNotFound] when Moab version root is not found
25+
# @return [NilClass] indicates success
26+
def ensure_readable!
27+
files.each { |f| File.stat(f) }
28+
29+
nil
30+
rescue StandardError => e # e.g., Errno::EACCES, Errno::EIO, Errno::ENOENT, Errno::ESTALE
31+
raise Errors::UnreadableFile, "Error reading files (#{e.class}): #{e.message}", e.backtrace
32+
end
33+
34+
private
35+
36+
def files
37+
raise Errors::MoabVersionNotFound, "Moab version does not exist: #{root}" unless File.exist?(root)
38+
39+
@files ||= Dir.glob("#{root}/**/*").select { |path| File.file?(path) }
40+
end
41+
42+
attr_reader :root
43+
end
44+
end

app/services/replication/replicate_version_service.rb

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def call # rubocop:disable Metrics/AbcSize
3737
replicate_incomplete_zipped_moab_versions
3838

3939
# Delete the local zip part files
40-
druid_version_zip.cleanup_zip_parts!
40+
ZipPartCleaner.clean!(pathfinder: zip_part_pathfinder)
4141
end
4242

4343
private
@@ -50,15 +50,19 @@ def zipped_moab_versions
5050
preserved_object.zipped_moab_versions.where(version:)
5151
end
5252

53-
def druid_version_zip
54-
@druid_version_zip ||= zipped_moab_versions.first.druid_version_zip
53+
def zip_part_pathfinder
54+
@zip_part_pathfinder ||= zipped_moab_versions.first.zip_part_pathfinder
55+
end
56+
57+
def zip_part_files
58+
@zip_part_files ||= zip_part_pathfinder.zip_keys.map { |zip_key| ZipPartFile.new(filename: zip_key) }
5559
end
5660

5761
def create_zip_if_necessary
58-
return if druid_version_zip.complete?
62+
return if ZipPartCompletenessChecker.complete?(pathfinder: zip_part_pathfinder)
5963

60-
druid_version_zip.cleanup_zip_parts!
61-
druid_version_zip.create_zip!
64+
ZipPartCleaner.clean!(pathfinder: zip_part_pathfinder)
65+
ZipPartCreator.create!(pathfinder: zip_part_pathfinder)
6266
end
6367

6468
def reset_to_created!(zipped_moab_version)
@@ -83,11 +87,11 @@ def check_zip_parts_to_zip_file(zipped_moab_version)
8387

8488
def populate_zip_parts!(zipped_moab_version)
8589
ZippedMoabVersion.transaction do
86-
druid_version_zip.druid_version_zip_parts.each do |druid_version_zip_part|
90+
zip_part_files.each do |zip_part_file|
8791
zipped_moab_version.zip_parts.create!(
88-
suffix: druid_version_zip_part.extname,
89-
size: druid_version_zip_part.size,
90-
md5: druid_version_zip_part.read_md5
92+
suffix: zip_part_file.extname,
93+
size: zip_part_file.size,
94+
md5: zip_part_file.read_md5
9195
)
9296
end
9397
zipped_moab_version.update!(zip_parts_count: zipped_moab_version.zip_parts.count)

app/services/replication/replicate_zip_part_service.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def call
1717

1818
return results if already_replicated? || !check_existing_part_file_on_endpoint
1919

20-
transfer_manager.upload_file(druid_version_zip_part.file_path,
20+
transfer_manager.upload_file(zip_part_file.file_path,
2121
bucket: zip_part.zip_endpoint.bucket_name,
2222
key: zip_part.s3_key, metadata:)
2323
results
@@ -27,7 +27,7 @@ def call
2727

2828
attr_reader :zip_part
2929

30-
delegate :s3_part, :druid_version_zip_part, :preserved_object, :zip_endpoint, to: :zip_part
30+
delegate :s3_part, :zip_part_file, :preserved_object, :zip_endpoint, to: :zip_part
3131
delegate :druid, to: :preserved_object
3232

3333
def zip_part_file_exists_on_endpoint?
@@ -53,8 +53,8 @@ def set_hb_context
5353

5454
def metadata
5555
{
56-
checksum_md5: druid_version_zip_part.read_md5,
57-
size: druid_version_zip_part.size.to_s # S3 metadata values must be strings
56+
checksum_md5: zip_part_file.read_md5,
57+
size: zip_part_file.size.to_s # S3 metadata values must be strings
5858
}
5959
end
6060

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
module Replication
4+
# Deletes all zip part files and their md5 sidecar files from local zip storage
5+
class ZipPartCleaner
6+
# @param [ZipPartPathfinder] pathfinder The pathfinder instance for a zip part
7+
def self.clean!(pathfinder:)
8+
FileUtils.rm_f(pathfinder.all_file_paths)
9+
end
10+
end
11+
end

0 commit comments

Comments
 (0)