Class: Deb::S3::Lock

Inherits:
Object
  • Object
show all
Defined in:
lib/deb/s3/lock.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(user, host) ⇒ Lock

Returns a new instance of Lock.



12
13
14
15
# File 'lib/deb/s3/lock.rb', line 12

def initialize(user, host)
  @user = user
  @host = host
end

Instance Attribute Details

#hostObject (readonly)

Returns the value of attribute host.



10
11
12
# File 'lib/deb/s3/lock.rb', line 10

def host
  @host
end

#userObject (readonly)

Returns the value of attribute user.



9
10
11
# File 'lib/deb/s3/lock.rb', line 9

def user
  @user
end

Class Method Details

.current(codename, component = nil, architecture = nil, cache_control = nil) ⇒ Object



96
97
98
99
100
101
102
103
104
105
# File 'lib/deb/s3/lock.rb', line 96

def current(codename, component = nil, architecture = nil, cache_control = nil)
  lockbody = Deb::S3::Utils.s3_read(lock_path(codename, component, architecture, cache_control))
  if lockbody
    user, host = lockbody.to_s.split("@", 2)
    lock = Deb::S3::Lock.new(user, host)
  else
    lock = Deb::S3::Lock.new("unknown", "unknown")
  end
  lock
end

.lock(codename, component = nil, architecture = nil, cache_control = nil, max_attempts = 60, max_wait_interval = 10) ⇒ Object

2-phase mutual lock mechanism based on ‘s3:CopyObject`.

This logic isn’t relying on S3’s enhanced features like Object Lock because it imposes some limitation on using other features like S3 Cross-Region replication. This should work more than good enough with S3’s strong read-after-write consistency which we can presume in all region nowadays.

This is relying on S3 to set object’s ETag as object’s MD5 if an object isn’t comprized from multiple parts. We’d be able to presume it as the lock file is usually an object of some smaller bytes.

acquire lock:

  1. call ‘s3:HeadObject` on final lock object

  2. If final lock object exists, restart from the beginning

  3. Otherwise, call ‘s3:PutObject` to create initial lock object

  4. Perform ‘s3:CopyObject` to copy from initial lock object to final lock object with specifying ETag/MD5 of the initial lock object

  5. If copy object fails as ‘PreconditionFailed`, restart from the beginning

  6. Otherwise, lock has been acquired

release lock:

  1. remove final lock object by ‘s3:DeleteObject`



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/deb/s3/lock.rb', line 45

def lock(codename, component = nil, architecture = nil, cache_control = nil, max_attempts=60, max_wait_interval=10)
  lockbody = "#{Etc.getlogin}@#{Socket.gethostname}"
  initial_lockfile = initial_lock_path(codename, component, architecture, cache_control)
  final_lockfile = lock_path(codename, component, architecture, cache_control)

  md5_b64 = Base64.encode64(Digest::MD5.digest(lockbody))
  md5_hex = Digest::MD5.hexdigest(lockbody)
  max_attempts.times do |i|
    wait_interval = [(1<<i)/10, max_wait_interval].min
    if Deb::S3::Utils.s3_exists?(final_lockfile)
      lock = current(codename, component, architecture, cache_control)
      $stderr.puts("Repository is locked by another user: #{lock.user} at host #{lock.host} (phase-1)")
      $stderr.puts("Attempting to obtain a lock after #{wait_interval} secound(s).")
      sleep(wait_interval)
    else
      # upload the file
      Deb::S3::Utils.s3.put_object(
        bucket: Deb::S3::Utils.bucket,
        key: Deb::S3::Utils.s3_path(initial_lockfile),
        body: lockbody,
        content_type: "text/plain",
        content_md5: md5_b64,
        metadata: {
          "md5" => md5_hex,
        },
      )
      begin
        Deb::S3::Utils.s3.copy_object(
          bucket: Deb::S3::Utils.bucket,
          key: Deb::S3::Utils.s3_path(final_lockfile),
          copy_source: "/#{Deb::S3::Utils.bucket}/#{Deb::S3::Utils.s3_path(initial_lockfile)}",
          copy_source_if_match: md5_hex,
        )
        return
      rescue Aws::S3::Errors::PreconditionFailed => error
        lock = current(codename, component, architecture, cache_control)
        $stderr.puts("Repository is locked by another user: #{lock.user} at host #{lock.host} (phase-2)")
        $stderr.puts("Attempting to obtain a lock after #{wait_interval} second(s).")
        sleep(wait_interval)
      end
    end
  end
  # TODO: throw appropriate error class
  raise("Unable to obtain a lock after #{max_attempts}, giving up.")
end

.unlock(codename, component = nil, architecture = nil, cache_control = nil) ⇒ Object



91
92
93
94
# File 'lib/deb/s3/lock.rb', line 91

def unlock(codename, component = nil, architecture = nil, cache_control = nil)
  Deb::S3::Utils.s3_remove(initial_lock_path(codename, component, architecture, cache_control))
  Deb::S3::Utils.s3_remove(lock_path(codename, component, architecture, cache_control))
end