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
-
.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.
- .read(path) ⇒ Object
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 |