deb-s3-lock-fix 0.11.8.fix1 → 0.11.8.fix2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/lib/deb/s3/cli.rb +8 -8
  3. data/lib/deb/s3/lock.rb +68 -101
  4. metadata +16 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 180e005ba32e429abe5a462bb81cd03d6897b02fb10e079a655008589fa687df
4
- data.tar.gz: df8aeb29f17894bdfca5f82caaaf5ad6576d07de0fed3789d532de3c93486a13
3
+ metadata.gz: dc14627ff3d5d03feb6e36c26ba8ff41f6652ee3e085e43f750968018d662114
4
+ data.tar.gz: 0f628316a7fd3923201ee26ee4ab35d1d5e522d486faf08e24646c93c288572c
5
5
  SHA512:
6
- metadata.gz: '09eaff5677bd51d239794dd5b5bcf9f9ac18275593cd51088f22a0bded0bdd1c2bc4c82cf771aaa8c32d7fd525c973919ac6a5fe4b85818923922e576fa75b13'
7
- data.tar.gz: c463aba5d27113100687654aebd7952ca32142f2835190038dfaf401918d004cdf9bdca74e641866b4e90e5ec91cacbd94bde28c504d0ea637c798181d35e9aa
6
+ metadata.gz: 7b054fdfccd8dd2b573bbe2f702bd2532a5b7218dc0f049e0d8e89356aafc6d22f75054c5edd717e4bb293ff035b274e1a3ced4dc0f79d259b51f8a291e58531
7
+ data.tar.gz: 40cfc56aa2c87fbaadd9753562e2550f45431defcd5fede8c787ae6964dac56a11a332bffa058f62cf18b75739f734395660eba9aca0d7591da4fd06acaef891
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[:codename], component, options[:arch], options[:cache_control])
168
+ Deb::S3::Lock.lock(options[:codename])
169
169
  @lock_acquired = true
170
170
  end
171
171
 
@@ -253,7 +253,7 @@ class Deb::S3::CLI < Thor
253
253
  log("Update complete.")
254
254
  ensure
255
255
  if options[:lock] && @lock_acquired
256
- Deb::S3::Lock.unlock(options[:codename], component, options[:arch], options[:cache_control])
256
+ Deb::S3::Lock.unlock(options[:codename])
257
257
  log("Lock released.")
258
258
  end
259
259
  end
@@ -402,7 +402,7 @@ class Deb::S3::CLI < Thor
402
402
  if options[:lock]
403
403
  log("Checking for existing lock file")
404
404
  log("Locking repository for updates")
405
- Deb::S3::Lock.lock(options[:codename], to_component, options[:arch], options[:cache_control])
405
+ Deb::S3::Lock.lock(options[:codename])
406
406
  @lock_acquired = true
407
407
  end
408
408
 
@@ -444,7 +444,7 @@ class Deb::S3::CLI < Thor
444
444
  log "Copy complete."
445
445
  ensure
446
446
  if options[:lock] && @lock_acquired
447
- Deb::S3::Lock.unlock(options[:codename], component, options[:arch], options[:cache_control])
447
+ Deb::S3::Lock.unlock(options[:codename])
448
448
  log("Lock released.")
449
449
  end
450
450
  end
@@ -497,7 +497,7 @@ class Deb::S3::CLI < Thor
497
497
  if options[:lock]
498
498
  log("Checking for existing lock file")
499
499
  log("Locking repository for updates")
500
- Deb::S3::Lock.lock(options[:codename], component, options[:arch], options[:cache_control])
500
+ Deb::S3::Lock.lock(options[:codename])
501
501
  @lock_acquired = true
502
502
  end
503
503
 
@@ -545,7 +545,7 @@ class Deb::S3::CLI < Thor
545
545
  end
546
546
  ensure
547
547
  if options[:lock] && @lock_acquired
548
- Deb::S3::Lock.unlock(options[:codename], component, options[:arch], options[:cache_control])
548
+ Deb::S3::Lock.unlock(options[:codename])
549
549
  log("Lock released.")
550
550
  end
551
551
  end
@@ -611,7 +611,7 @@ class Deb::S3::CLI < Thor
611
611
  if options[:lock]
612
612
  log("Checking for existing lock file")
613
613
  log("Locking repository for updates")
614
- Deb::S3::Lock.lock(options[:codename], component, options[:arch], options[:cache_control])
614
+ Deb::S3::Lock.lock(options[:codename])
615
615
  @lock_acquired = true
616
616
  end
617
617
 
@@ -675,7 +675,7 @@ class Deb::S3::CLI < Thor
675
675
  end
676
676
  ensure
677
677
  if options[:lock] && @lock_acquired
678
- Deb::S3::Lock.unlock(options[:codename], component, options[:arch], options[:cache_control])
678
+ Deb::S3::Lock.unlock(options[:codename])
679
679
  log("Lock released.")
680
680
  end
681
681
  end
data/lib/deb/s3/lock.rb CHANGED
@@ -1,125 +1,92 @@
1
- # -*- encoding : utf-8 -*-
2
- require "base64"
3
- require "digest/md5"
4
- require "etc"
5
- require "socket"
6
- require "tempfile"
1
+ require "aws-sdk-dynamodb"
7
2
  require "securerandom"
3
+ require "etc"
8
4
 
9
5
  class Deb::S3::Lock
10
- attr_reader :user
11
- attr_reader :host
6
+ DYNAMODB_TABLE_NAME = 'deb-s3-lock'
12
7
 
13
- def initialize(user, host)
8
+ def initialize(user, host_with_uuid)
14
9
  @user = user
15
- @host = host
10
+ @host_with_uuid = host_with_uuid
11
+ end
12
+
13
+ def self.dynamodb
14
+ @dynamodb ||= begin
15
+ validate_environment_variables!
16
+ Aws::DynamoDB::Client.new(
17
+ access_key_id: ENV['DEB_S3_LOCK_ACCESS_KEY_ID'],
18
+ secret_access_key: ENV['DEB_S3_LOCK_SECRET_ACCESS_KEY'],
19
+ region: ENV['AWS_REGION']
20
+ )
21
+ end
22
+ end
23
+
24
+ def self.validate_environment_variables!
25
+ %w[DEB_S3_LOCK_ACCESS_KEY_ID DEB_S3_LOCK_SECRET_ACCESS_KEY AWS_REGION].each do |var|
26
+ raise "Environment variable #{var} not set." unless ENV[var]
27
+ end
16
28
  end
17
29
 
18
30
  class << self
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)
31
+ def lock(codename, max_attempts = 60, max_wait_interval = 10)
47
32
  uuid = SecureRandom.uuid
48
- lockbody = "#{Etc.getlogin}@#{Socket.gethostname}-#{uuid}"
49
-
50
- initial_lockfile = initial_lock_path(codename, component, architecture, cache_control)
51
- final_lockfile = lock_path(codename, component, architecture, cache_control)
33
+ lock_body = "#{Etc.getlogin}@#{Socket.gethostname}-#{uuid}"
34
+ lock_key = codename
52
35
 
53
- md5_b64 = Base64.encode64(Digest::MD5.digest(lockbody))
54
- md5_hex = Digest::MD5.hexdigest(lockbody)
55
36
  max_attempts.times do |i|
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).")
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,
37
+ wait_interval = [2**i, max_wait_interval].min
38
+
39
+ begin
40
+ dynamodb.put_item({
41
+ table_name: DYNAMODB_TABLE_NAME,
42
+ item: {
43
+ 'lock_key' => lock_key,
44
+ 'lock_body' => lock_body
72
45
  },
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
46
+ condition_expression: "attribute_not_exists(lock_key)"
47
+ })
48
+ return
49
+ rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
50
+ lock_holder = current_lock_holder(codename)
51
+ current_time = Time.now.strftime("%Y-%m-%d %H:%M:%S")
52
+ $stderr.puts("[#{current_time}] Repository is locked by another user: #{lock_holder.user} at host #{lock_holder.host_with_uuid}")
53
+ $stderr.puts("Attempting to obtain a lock after #{wait_interval} second(s).")
54
+ sleep(wait_interval)
55
+ rescue => e
56
+ $stderr.puts("Unexpected error: #{e.message}")
57
+ sleep(wait_interval)
88
58
  end
89
59
  end
90
- # TODO: throw appropriate error class
91
- raise("Unable to obtain a lock after #{max_attempts}, giving up.")
60
+
61
+ raise "Unable to obtain a lock after #{max_attempts}, giving up."
92
62
  end
93
63
 
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))
64
+ def unlock(codename)
65
+ dynamodb.delete_item({
66
+ table_name: DYNAMODB_TABLE_NAME,
67
+ key: {
68
+ 'lock_key' => codename
69
+ }
70
+ })
97
71
  end
98
72
 
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)
73
+ def current_lock_holder(codename)
74
+ response = dynamodb.get_item({
75
+ table_name: DYNAMODB_TABLE_NAME,
76
+ key: {
77
+ 'lock_key' => codename
78
+ }
79
+ })
80
+
81
+ if response.item
82
+ lockdata = response.item['lock_body']
83
+ user, host_with_uuid = lockdata.split("@", 2)
84
+ Deb::S3::Lock.new(user, host_with_uuid)
104
85
  else
105
- lock = Deb::S3::Lock.new("unknown", "unknown")
86
+ Deb::S3::Lock.new("unknown", "unknown")
106
87
  end
107
- lock
108
- end
109
-
110
- private
111
- def initial_lock_path(codename, component = nil, architecture = nil, cache_control = nil)
112
- "dists/#{codename}/lockfile.lock"
113
88
  end
114
89
 
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
90
+ private :dynamodb, :validate_environment_variables!
124
91
  end
125
92
  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.fix1
4
+ version: 0.11.8.fix2
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-09-13 00:00:00.000000000 Z
11
+ date: 2023-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -38,6 +38,20 @@ 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'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: minitest
43
57
  requirement: !ruby/object:Gem::Requirement