Class: Agent::Lock::Store::FileSystemStore

Inherits:
Object
  • Object
show all
Defined in:
lib/agent/lock/store/file_system_store.rb

Overview

Locks as files, which is the default and the one that needs nothing installed. See Tree#store_dir for why they live inside .git.

Constant Summary collapse

SUFFIX =
".lock.md"
MUTEX =
".mutex"
MUTEX_TIMEOUT =

Seconds. A claim's critical section is a directory scan and one write, so anything near this is somebody stuck, not somebody busy.

15

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(tree) ⇒ FileSystemStore

Returns a new instance of FileSystemStore.



23
24
25
# File 'lib/agent/lock/store/file_system_store.rb', line 23

def initialize(tree)
  @tree = tree
end

Instance Attribute Details

#tree ⇒ Object (readonly)

Returns the value of attribute tree.



21
22
23
# File 'lib/agent/lock/store/file_system_store.rb', line 21

def tree
  @tree
end

Instance Method Details

#all ⇒ Array<Record>

FNM_DOTMATCH is load-bearing. A scope like .plans/** slugs to a filename that starts with a dot, and a plain glob skips it, so the lock was written, listed nowhere, and blocked nobody. Redis matches those keys either way, and a store that enumerates less than it holds is worse than no store at all.

Returns:

  • (Array<Record>) —

    every lock here, other worktrees included



40
41
42
43
44
# File 'lib/agent/lock/store/file_system_store.rb', line 40

def all
  Dir.glob(File.join(dir, "*#{SUFFIX}"), File::FNM_DOTMATCH)
     .sort
     .filter_map { |path| Record.read(path) }
end

#create(record) ⇒ Boolean

Atomic: two agents racing for one scope cannot both win, because only one File::EXCL create succeeds.

Parameters:

Returns:

  • (Boolean) —

    false when somebody else got there first



83
84
85
86
87
88
89
90
91
# File 'lib/agent/lock/store/file_system_store.rb', line 83

def create(record)
  FileUtils.mkdir_p(dir)
  File.open(path_for(record.id), File::WRONLY | File::CREAT | File::EXCL) do |file|
    file.write(record.to_markdown)
  end
  true
rescue Errno::EEXIST
  false
end

#delete(record) ⇒ void

This method returns an undefined value.

Parameters:



108
# File 'lib/agent/lock/store/file_system_store.rb', line 108

def delete(record) = FileUtils.rm_f(record.path || path_for(record.id))

#describe ⇒ String

Returns how a listing names this store.

Returns:

  • (String) —

    how a listing names this store



31
# File 'lib/agent/lock/store/file_system_store.rb', line 31

def describe = dir

#dir ⇒ String

Returns:

  • (String)


28
# File 'lib/agent/lock/store/file_system_store.rb', line 28

def dir = tree.store_dir

#find(scope) ⇒ Record?

Parameters:

Returns:



76
# File 'lib/agent/lock/store/file_system_store.rb', line 76

def find(scope) = Record.read(path_for(Record.id_for(tree, scope)))

#mutex_path ⇒ String

Named so that all's *.lock.md can never match it, dot or no dot.

Returns:

  • (String)


117
# File 'lib/agent/lock/store/file_system_store.rb', line 117

def mutex_path = File.join(dir, MUTEX)

#path_for(id) ⇒ String

Parameters:

  • id (String)

Returns:

  • (String)


112
# File 'lib/agent/lock/store/file_system_store.rb', line 112

def path_for(id) = File.join(dir, "#{id}#{SUFFIX}")

#synchronize { ... } ⇒ Object

Runs the block with every other process in this store shut out, so a scan for conflicts and the write it justifies cannot be interleaved. O_EXCL alone settles a race for one scope, but lib/** and lib/a1.rb are two files, and two agents that both scanned an empty store before either wrote both won.

An exclusive flock on .mutex beside the locks. The kernel drops it when the holder exits, crash included, so a dead agent cannot leave the store wedged. A live one that stops mid-claim can, which is why the wait is bounded and ends in an error rather than a hung agent.

Not re-entrant. flock belongs to an open file, not to a process, so a nested call opens a second one, waits on the first, and raises once the timeout runs out.

Yields:

  • the critical section

Returns:

  • (Object) —

    whatever the block returns

Raises:

  • (Error) —

    when the mutex stayed held for longer than the timeout



64
65
66
67
68
69
70
71
72
# File 'lib/agent/lock/store/file_system_store.rb', line 64

def synchronize
  FileUtils.mkdir_p(dir)
  # Closing the file releases the flock, and the block form closes it
  # on the way out whether the critical section returned or raised.
  File.open(mutex_path, File::RDWR | File::CREAT, 0o644) do |file|
    wait_for(file)
    yield
  end
end

#update(record) ⇒ void

This method returns an undefined value.

Replace a lock in place. Written beside itself and renamed, so a reader never sees half a document, and so a crash mid-write leaves the previous version rather than nothing.

Parameters:



99
100
101
102
103
104
# File 'lib/agent/lock/store/file_system_store.rb', line 99

def update(record)
  path = record.path || path_for(record.id)
  temp = "#{path}.#{Process.pid}.tmp"
  File.write(temp, record.to_markdown)
  File.rename(temp, path)
end