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

Sign up to get free protection for your applications and to get access to all the features.
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: []