Module: Ace::Hitl::Lifecycle::AtomicJson

Defined in:
lib/ace/hitl/lifecycle/atomic_json.rb

Overview

Durable, permission-preserving JSON record writes (port of atomic_json): O_EXCL|O_NOFOLLOW tempfile in the target directory, fsync, chmod, chown, rename. Ownership transitions go through the injectable ownership strategy; production performs real chown(2), tests map names onto the current identity (spec 8wm.t.y21 §2).

Defined Under Namespace

Classes: Ownership

Constant Summary collapse

DEFAULT_OWNERSHIP =
Object.new.tap do |strategy|
  strategy.define_singleton_method(:chown) do |path, ownership|
    File.chown(ownership.uid, ownership.gid, path) if ownership
  end
end.freeze

Class Method Summary collapse

Class Method Details

.call(path, value, mode:, ownership: nil, ownership_strategy: DEFAULT_OWNERSHIP, exclusive: false) ⇒ Object

mode is an Integer permission bits value; ownership nil keeps the current identity. exclusive commits with link(2) instead of rename(2), so an existing destination raises Errno::EEXIST instead of being silently replaced (create-once writes).



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
# File 'lib/ace/hitl/lifecycle/atomic_json.rb', line 30

def call(path, value, mode:, ownership: nil, ownership_strategy: DEFAULT_OWNERSHIP,
  exclusive: false)
  path = Pathname.new(path)
  path.parent.mkpath
  # The random suffix makes the temporary unique per invocation:
  # a fixed pid-based name lets a competing writer's cleanup
  # delete our in-flight temporary and fail both writes (review
  # 8wq2zttt on PR#336). Cleanup only ever touches this call's
  # own file.
  temporary = path.parent.join(".#{path.basename}.tmp.#{$PROCESS_ID}.#{SecureRandom.hex(4)}")
  flags = File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW
  fd = IO.sysopen(temporary, flags, mode)
  begin
    io = IO.for_fd(fd, mode: "w")
    begin
      io.write(JSON.generate(value))
      io.write("\n")
      io.flush
      io.fsync
    ensure
      io.close
    end
  rescue
    begin
      temporary.unlink
    rescue
      nil
    end
    raise
  end
  File.chmod(mode, temporary)
  ownership_strategy.chown(temporary, ownership)
  if exclusive
    File.link(temporary, path)
  else
    File.rename(temporary, path)
  end
ensure
  temporary.unlink if temporary&.exist?
end

.read(path) ⇒ Object



71
72
73
74
75
# File 'lib/ace/hitl/lifecycle/atomic_json.rb', line 71

def read(path)
  JSON.parse(File.read(path))
rescue SystemCallError, JSON::ParserError
  nil
end