deb-s3-lock-fix 0.11.8.fix0.6 → 0.11.8.fix1

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 (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -12
  3. data/lib/deb/s3/cli.rb +10 -14
  4. data/lib/deb/s3/lock.rb +98 -112
  5. metadata +7 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4adef09b894b92582b5cceeaddddcf77282d6ca042071c0231a7b59c5c8e3000
4
- data.tar.gz: 30cfff54c7f98238ec3190ded8e617ba981946ad30af4f5c573e97882e52bd2f
3
+ metadata.gz: 180e005ba32e429abe5a462bb81cd03d6897b02fb10e079a655008589fa687df
4
+ data.tar.gz: df8aeb29f17894bdfca5f82caaaf5ad6576d07de0fed3789d532de3c93486a13
5
5
  SHA512:
6
- metadata.gz: b92554e2f5573e41a8fcb3c5dcdec4c013cd8d6d3dd1f1c4398f0bc705fa449cebc6b917355cf293925c4bbce536aa1ed6a9f43bf4b61e6020462dc28e286cdc
7
- data.tar.gz: fb679c6b500598a25ec76aaf253dcbdb1c0d35608fed399987b1c553c19e57186c829220207208344bbbb917fbadd144791f45a3e84081ddc70eeb898b17ec01
6
+ metadata.gz: '09eaff5677bd51d239794dd5b5bcf9f9ac18275593cd51088f22a0bded0bdd1c2bc4c82cf771aaa8c32d7fd525c973919ac6a5fe4b85818923922e576fa75b13'
7
+ data.tar.gz: c463aba5d27113100687654aebd7952ca32142f2835190038dfaf401918d004cdf9bdca74e641866b4e90e5ec91cacbd94bde28c504d0ea637c798181d35e9aa
data/README.md CHANGED
@@ -1,18 +1,8 @@
1
- # deb-s3-lock-fix
1
+ # deb-s3
2
2
 
3
3
  [![Build Status](https://travis-ci.org/deb-s3/deb-s3.svg?branch=master)](https://travis-ci.org/deb-s3/deb-s3)
4
4
 
5
- **This repository is a fork of [deb-s3](https://github.com/deb-s3/deb-s3).**
6
-
7
- Note: The locking mechanism in the original deb-s3 library does not prevent race conditions.
8
- It relies on S3 for distributed locking, which will not work consistently due to S3's eventual
9
- consistency model. This fork uses DynamoDB for distributed locking, since it ensures atomic
10
- conditional put operations on the lock.
11
-
12
- To use this library create a DynamoDB table and export the following three env variables:
13
- `DEB_S3_ACCESS_KEY_ID`
14
- `DEB_S3_SECRET_ACCESS_KEY`
15
- `AWS_BUILDERS_REGION`
5
+ **This repository is a fork of [krobertson/deb-s3](https://github.com/krobertson/deb-s3).**
16
6
 
17
7
  `deb-s3` is a simple utility to make creating and managing APT repositories on
18
8
  S3.
data/lib/deb/s3/cli.rb CHANGED
@@ -165,7 +165,7 @@ class Deb::S3::CLI < Thor
165
165
  if options[:lock]
166
166
  log("Checking for existing lock file")
167
167
  log("Locking repository for updates")
168
- Deb::S3::Lock.lock(options[:bucket], options[:codename])
168
+ Deb::S3::Lock.lock(options[:codename], component, options[:arch], options[:cache_control])
169
169
  @lock_acquired = true
170
170
  end
171
171
 
@@ -242,22 +242,18 @@ class Deb::S3::CLI < Thor
242
242
  log("Uploading packages and new manifests to S3")
243
243
  manifests.each_value do |manifest|
244
244
  begin
245
- manifest.write_to_s3 do |f|
246
- sublog("Transferring #{f} at #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}")
247
- end
245
+ manifest.write_to_s3 { |f| sublog("Transferring #{f}") }
248
246
  rescue Deb::S3::Utils::AlreadyExistsError => e
249
247
  error("Uploading manifest failed because: #{e}")
250
248
  end
251
249
  release.update_manifest(manifest)
252
250
  end
253
- release.write_to_s3 do |f|
254
- sublog("Transferring #{f} at #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}")
255
- end
251
+ release.write_to_s3 { |f| sublog("Transferring #{f}") }
256
252
 
257
253
  log("Update complete.")
258
254
  ensure
259
255
  if options[:lock] && @lock_acquired
260
- Deb::S3::Lock.unlock(options[:bucket], options[:codename])
256
+ Deb::S3::Lock.unlock(options[:codename], component, options[:arch], options[:cache_control])
261
257
  log("Lock released.")
262
258
  end
263
259
  end
@@ -406,7 +402,7 @@ class Deb::S3::CLI < Thor
406
402
  if options[:lock]
407
403
  log("Checking for existing lock file")
408
404
  log("Locking repository for updates")
409
- Deb::S3::Lock.lock(options[:bucket], options[:codename])
405
+ Deb::S3::Lock.lock(options[:codename], to_component, options[:arch], options[:cache_control])
410
406
  @lock_acquired = true
411
407
  end
412
408
 
@@ -448,7 +444,7 @@ class Deb::S3::CLI < Thor
448
444
  log "Copy complete."
449
445
  ensure
450
446
  if options[:lock] && @lock_acquired
451
- Deb::S3::Lock.unlock(options[:bucket], options[:codename])
447
+ Deb::S3::Lock.unlock(options[:codename], component, options[:arch], options[:cache_control])
452
448
  log("Lock released.")
453
449
  end
454
450
  end
@@ -501,7 +497,7 @@ class Deb::S3::CLI < Thor
501
497
  if options[:lock]
502
498
  log("Checking for existing lock file")
503
499
  log("Locking repository for updates")
504
- Deb::S3::Lock.lock(options[:bucket], options[:codename])
500
+ Deb::S3::Lock.lock(options[:codename], component, options[:arch], options[:cache_control])
505
501
  @lock_acquired = true
506
502
  end
507
503
 
@@ -549,7 +545,7 @@ class Deb::S3::CLI < Thor
549
545
  end
550
546
  ensure
551
547
  if options[:lock] && @lock_acquired
552
- Deb::S3::Lock.unlock(options[:bucket], options[:codename])
548
+ Deb::S3::Lock.unlock(options[:codename], component, options[:arch], options[:cache_control])
553
549
  log("Lock released.")
554
550
  end
555
551
  end
@@ -615,7 +611,7 @@ class Deb::S3::CLI < Thor
615
611
  if options[:lock]
616
612
  log("Checking for existing lock file")
617
613
  log("Locking repository for updates")
618
- Deb::S3::Lock.lock(options[:bucket], options[:codename])
614
+ Deb::S3::Lock.lock(options[:codename], component, options[:arch], options[:cache_control])
619
615
  @lock_acquired = true
620
616
  end
621
617
 
@@ -679,7 +675,7 @@ class Deb::S3::CLI < Thor
679
675
  end
680
676
  ensure
681
677
  if options[:lock] && @lock_acquired
682
- Deb::S3::Lock.unlock(options[:bucket], options[:codename])
678
+ Deb::S3::Lock.unlock(options[:codename], component, options[:arch], options[:cache_control])
683
679
  log("Lock released.")
684
680
  end
685
681
  end
data/lib/deb/s3/lock.rb CHANGED
@@ -1,139 +1,125 @@
1
- require "aws-sdk-dynamodb"
2
- require "securerandom"
1
+ # -*- encoding : utf-8 -*-
2
+ require "base64"
3
+ require "digest/md5"
3
4
  require "etc"
4
- require "time"
5
+ require "socket"
6
+ require "tempfile"
7
+ require "securerandom"
5
8
 
6
9
  class Deb::S3::Lock
7
- attr_reader :user, :host_with_uuid
8
-
9
- DYNAMODB_TABLE_NAME = 'deb-s3-lock'
10
+ attr_reader :user
11
+ attr_reader :host
10
12
 
11
- def initialize(user, host_with_uuid)
13
+ def initialize(user, host)
12
14
  @user = user
13
- @host_with_uuid = host_with_uuid
14
- end
15
-
16
- def self.dynamodb
17
- @dynamodb ||= begin
18
- validate_environment_variables!
19
- Aws::DynamoDB::Client.new(
20
- access_key_id: ENV['DEB_S3_ACCESS_KEY_ID'],
21
- secret_access_key: ENV['DEB_S3_SECRET_ACCESS_KEY'],
22
- region: ENV['AWS_BUILDERS_REGION']
23
- )
24
- end
25
- end
26
-
27
- def self.validate_environment_variables!
28
- %w[DEB_S3_ACCESS_KEY_ID DEB_S3_SECRET_ACCESS_KEY AWS_BUILDERS_REGION].each do |var|
29
- raise "Environment variable #{var} not set." unless ENV[var]
30
- end
15
+ @host = host
31
16
  end
32
17
 
33
18
  class << self
34
- def lock(bucket, codename, max_attempts = 60, max_wait_interval = 10)
19
+ #
20
+ # 2-phase mutual lock mechanism based on `s3:CopyObject`.
21
+ #
22
+ # This logic isn't relying on S3's enhanced features like Object Lock
23
+ # because it imposes some limitation on using other features like
24
+ # S3 Cross-Region replication. This should work more than good enough
25
+ # with S3's strong read-after-write consistency which we can presume
26
+ # in all region nowadays.
27
+ #
28
+ # This is relying on S3 to set object's ETag as object's MD5 if an
29
+ # object isn't comprized from multiple parts. We'd be able to presume
30
+ # it as the lock file is usually an object of some smaller bytes.
31
+ #
32
+ # acquire lock:
33
+ # 1. call `s3:HeadObject` on final lock object
34
+ # 1. If final lock object exists, restart from the beginning
35
+ # 2. Otherwise, call `s3:PutObject` to create initial lock object
36
+ # 2. Perform `s3:CopyObject` to copy from initial lock object
37
+ # to final lock object with specifying ETag/MD5 of the initial
38
+ # lock object
39
+ # 1. If copy object fails as `PreconditionFailed`, restart
40
+ # from the beginning
41
+ # 2. Otherwise, lock has been acquired
42
+ #
43
+ # release lock:
44
+ # 1. remove final lock object by `s3:DeleteObject`
45
+ #
46
+ def lock(codename, component = nil, architecture = nil, cache_control = nil, max_attempts=60, max_wait_interval=10)
35
47
  uuid = SecureRandom.uuid
36
- lock_body = "#{Etc.getlogin}@#{Socket.gethostname}-#{uuid}"
37
- lock_key = "#{bucket}/#{codename}"
48
+ lockbody = "#{Etc.getlogin}@#{Socket.gethostname}-#{uuid}"
38
49
 
39
- $stderr.puts("Current job's hostname with UUID: #{lock_body}")
50
+ initial_lockfile = initial_lock_path(codename, component, architecture, cache_control)
51
+ final_lockfile = lock_path(codename, component, architecture, cache_control)
40
52
 
53
+ md5_b64 = Base64.encode64(Digest::MD5.digest(lockbody))
54
+ md5_hex = Digest::MD5.hexdigest(lockbody)
41
55
  max_attempts.times do |i|
42
- delete_expired_lock(bucket, codename)
43
- wait_interval = [2**i, max_wait_interval].min
44
-
45
- expiration_time = Time.now.to_i + 45
46
- begin
47
- dynamodb.put_item({
48
- table_name: DYNAMODB_TABLE_NAME,
49
- item: {
50
- 'lock_key' => "52",
51
- 'lock_body' => lock_body,
52
- 'ttl' => expiration_time
53
- },
54
- condition_expression: "attribute_not_exists(lock_key)"
55
- })
56
- return
57
- rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
58
- lock_holder = current_lock_holder(bucket, codename)
59
- current_time = Time.now.strftime("%Y-%m-%d %H:%M:%S")
60
- $stderr.puts("[#{current_time}] Repository is locked by another user: #{lock_holder.user} at host #{lock_holder.host_with_uuid}")
61
- $stderr.puts("Attempting to obtain a lock after #{wait_interval} second(s).")
56
+ wait_interval = [(1<<i)/10, max_wait_interval].min
57
+ if Deb::S3::Utils.s3_exists?(final_lockfile)
58
+ lock = current(codename, component, architecture, cache_control)
59
+ $stderr.puts("Repository is locked by another user: #{lock.user} at host #{lock.host} (phase-1)")
60
+ $stderr.puts("Attempting to obtain a lock after #{wait_interval} secound(s).")
62
61
  sleep(wait_interval)
62
+ else
63
+ # upload the file
64
+ Deb::S3::Utils.s3.put_object(
65
+ bucket: Deb::S3::Utils.bucket,
66
+ key: Deb::S3::Utils.s3_path(initial_lockfile),
67
+ body: lockbody,
68
+ content_type: "text/plain",
69
+ content_md5: md5_b64,
70
+ metadata: {
71
+ "md5" => md5_hex,
72
+ },
73
+ )
74
+ begin
75
+ Deb::S3::Utils.s3.copy_object(
76
+ bucket: Deb::S3::Utils.bucket,
77
+ key: Deb::S3::Utils.s3_path(final_lockfile),
78
+ copy_source: "/#{Deb::S3::Utils.bucket}/#{Deb::S3::Utils.s3_path(initial_lockfile)}",
79
+ copy_source_if_match: md5_hex,
80
+ )
81
+ return
82
+ rescue Aws::S3::Errors::PreconditionFailed => error
83
+ lock = current(codename, component, architecture, cache_control)
84
+ $stderr.puts("Repository is locked by another user: #{lock.user} at host #{lock.host} (phase-2)")
85
+ $stderr.puts("Attempting to obtain a lock after #{wait_interval} second(s).")
86
+ sleep(wait_interval)
87
+ end
63
88
  end
64
89
  end
65
-
66
- raise "Unable to obtain a lock after #{max_attempts} attemtps, giving up."
90
+ # TODO: throw appropriate error class
91
+ raise("Unable to obtain a lock after #{max_attempts}, giving up.")
67
92
  end
68
93
 
69
- def unlock(bucket, codename)
70
- # dynamodb.delete_item({
71
- # table_name: DYNAMODB_TABLE_NAME,
72
- # key: {
73
- # 'lock_key' => "#{bucket}/#{codename}"
74
- # }
75
- # })
94
+ def unlock(codename, component = nil, architecture = nil, cache_control = nil)
95
+ Deb::S3::Utils.s3_remove(initial_lock_path(codename, component, architecture, cache_control))
96
+ Deb::S3::Utils.s3_remove(lock_path(codename, component, architecture, cache_control))
76
97
  end
77
98
 
78
- def current_lock_holder(bucket, codename)
79
- response = dynamodb.get_item({
80
- table_name: DYNAMODB_TABLE_NAME,
81
- key: {
82
- 'lock_key' => "#{bucket}/#{codename}"
83
- }
84
- })
85
-
86
- if response.item
87
- lockdata = response.item['lock_body']
88
- user, host_with_uuid = lockdata.split("@", 2)
89
- Deb::S3::Lock.new(user, host_with_uuid)
99
+ def current(codename, component = nil, architecture = nil, cache_control = nil)
100
+ lockbody = Deb::S3::Utils.s3_read(lock_path(codename, component, architecture, cache_control))
101
+ if lockbody
102
+ user, host_with_uuid = lockbody.to_s.split("@", 2)
103
+ lock = Deb::S3::Lock.new(user, host_with_uuid)
90
104
  else
91
- Deb::S3::Lock.new("unknown", "unknown")
105
+ lock = Deb::S3::Lock.new("unknown", "unknown")
92
106
  end
107
+ lock
93
108
  end
94
109
 
95
- def delete_expired_lock(bucket, codename)
96
- response = dynamodb.get_item({
97
- table_name: DYNAMODB_TABLE_NAME,
98
- key: {
99
- 'lock_key' => "52"
100
- }
101
- })
102
- $stderr.puts("hi: #{response.item['ttl']} #{response.item['lock_body']} #{response.item['lock_key']}]")
103
-
104
- return unless response.item
105
-
106
- if response.item['ttl'] && Time.now.to_i > response.item['ttl'].to_i
107
- lockdata = response.item['lock_body']
108
- user, _ = lockdata.split("@", 2)
109
- $stderr.puts("hello")
110
-
111
- delete_response = nil
112
-
113
- begin
114
- delete_response = dynamodb.delete_item({
115
- table_name: DYNAMODB_TABLE_NAME,
116
- key: {
117
- 'lock_key' => "52"
118
- },
119
- return_values: 'ALL_OLD'
120
- })
121
- rescue Aws::DynamoDB::Errors::ServiceError => e
122
- $stderr.puts("Error deleting lock: #{e.message}")
123
- return
124
- end
125
- $stderr.puts("Expired lock detected and deleted for #{bucket}/#{codename}. Lock was held by user: #{user}.")
126
-
127
- if delete_response.attributes
128
- current_time = Time.now.strftime("%Y-%m-%d %H:%M:%S")
129
- $stderr.puts("[#{current_time}] Expired lock detected and deleted for #{bucket}/#{codename}. Lock was held by user: #{user}.")
130
- end
131
- end
110
+ private
111
+ def initial_lock_path(codename, component = nil, architecture = nil, cache_control = nil)
112
+ "dists/#{codename}/lockfile.lock"
132
113
  end
133
-
134
-
135
-
136
114
 
137
- private :dynamodb, :validate_environment_variables!
115
+ def lock_path(codename, component = nil, architecture = nil, cache_control = nil)
116
+ #
117
+ # Acquire repository lock at `codename` level to avoid race between concurrent upload attempts.
118
+ #
119
+ # * `deb-s3 upload --arch=all` touchs multiples of `dists/{codename}/{component}/binary-*/Packages*`
120
+ # * All `deb-s3 upload` touchs `dists/{codename}/Release`
121
+ #
122
+ "dists/#{codename}/lockfile"
123
+ end
138
124
  end
139
125
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deb-s3-lock-fix
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.8.fix0.6
4
+ version: 0.11.8.fix1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Braeden Wolf & Ken Robertson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-17 00:00:00.000000000 Z
11
+ date: 2023-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -38,20 +38,6 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1'
41
- - !ruby/object:Gem::Dependency
42
- name: aws-sdk-dynamodb
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '1'
48
- type: :runtime
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '1'
55
41
  - !ruby/object:Gem::Dependency
56
42
  name: minitest
57
43
  requirement: !ruby/object:Gem::Requirement
@@ -80,8 +66,9 @@ dependencies:
80
66
  - - "~>"
81
67
  - !ruby/object:Gem::Version
82
68
  version: '11'
83
- description: Fork of deb-s3 with fix for locking. Original work by Ken Robertson.
84
- email: braeden.wolf@sanctuary.ai & ken@invalidlogic.com
69
+ description: Fork of deb-s3 with a specific fix for locking. Original work by Ken
70
+ Robertson.
71
+ email: braedenwolf@outlook.com & ken@invalidlogic.com
85
72
  executables:
86
73
  - deb-s3
87
74
  extensions: []
@@ -98,7 +85,7 @@ files:
98
85
  - lib/deb/s3/templates/package.erb
99
86
  - lib/deb/s3/templates/release.erb
100
87
  - lib/deb/s3/utils.rb
101
- homepage: https://gitlab.com/sanctuaryai/precog2/infrastructure/deb-s3-lock-fix
88
+ homepage: https://github.com/braedenwolf/deb-s3-lock-fix
102
89
  licenses:
103
90
  - MIT
104
91
  metadata: {}
@@ -120,5 +107,5 @@ requirements: []
120
107
  rubygems_version: 3.1.2
121
108
  signing_key:
122
109
  specification_version: 4
123
- summary: Easily create and manage an APT repository on S3 with lock fix.
110
+ summary: Easily create and manage an APT repository on S3 (with specific lock fix).
124
111
  test_files: []