Class: FlossFunding::LockfileBase
- Inherits:
-
Object
- Object
- FlossFunding::LockfileBase
- Defined in:
- lib/floss_funding/lockfile.rb
Overview
Lockfile re-architecture: YAML-based sentinels for per-library nags.
There are two lockfiles with identical structure, but different purposes:
- OnLoadLockfile (“.floss_funding.ruby.on_load.lock”): sentinel for on_load nags
- AtExitLockfile (“.floss_funding.ruby.at_exit.lock”): sentinel for at_exit nags
YAML structure:
created:
pid:
Direct Known Subclasses
Constant Summary collapse
- MIN_SECONDS =
10 minutes (enforced minimum)
600
- MAX_SECONDS =
7 days (enforced maximum)
604_800
Instance Attribute Summary collapse
-
#path ⇒ Object
readonly
Absolute path or nil when project_root unknown.
Instance Method Summary collapse
-
#initialize ⇒ LockfileBase
constructor
:nocov: NOTE: Initialization includes early persistence and rotation attempts.
-
#nagged?(library_or_name) ⇒ Boolean
Has this library already nagged within this lockfile’s lifetime? Accepts either a String key or a library-like object (responds to :library_name/:namespace).
-
#record_nag(library, event, type) ⇒ Object
Record a nag for the provided library.
-
#rotate_if_expired! ⇒ Object
Remove and recreate lockfile if expired.
-
#touch! ⇒ Object
:nocov:.
Constructor Details
#initialize ⇒ LockfileBase
:nocov:
NOTE: Initialization includes early persistence and rotation attempts.
The error-handling paths depend on filesystem behavior and are not
deterministic across CI environments. The functional behavior is
exercised via higher-level specs.
33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/floss_funding/lockfile.rb', line 33 def initialize @path = resolve_path @data = load_or_initialize # Ensure file exists on first touch begin persist! if @path rescue StandardError => e ::FlossFunding.error!(e, "LockfileBase#initialize/persist!") end rotate_if_expired! end |
Instance Attribute Details
#path ⇒ Object (readonly)
Absolute path or nil when project_root unknown
47 48 49 |
# File 'lib/floss_funding/lockfile.rb', line 47 def path @path end |
Instance Method Details
#nagged?(library_or_name) ⇒ Boolean
Has this library already nagged within this lockfile’s lifetime?
Accepts either a String key or a library-like object (responds to :library_name/:namespace).
:nocov:
NOTE: Defensive behavior for malformed input; trivial, but error paths are
hard to exercise meaningfully. Covered indirectly via higher-level flows.
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/floss_funding/lockfile.rb', line 55 def nagged?(library_or_name) d = @data return false unless d && d["nags"].is_a?(Hash) key = if library_or_name.respond_to?(:library_name) key_name_for(library_or_name) else library_or_name.to_s end d["nags"].key?(key) rescue StandardError => e ::FlossFunding.error!(e, "LockfileBase#nagged?") false end |
#record_nag(library, event, type) ⇒ Object
Record a nag for the provided library.
:nocov:
NOTE: Defensive logging and filesystem writes make this method’s error paths
difficult to trigger deterministically. The successful path is exercised by
specs that verify lockfile contents.
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/floss_funding/lockfile.rb', line 80 def record_nag(library, event, type) return unless @path rotate_if_expired! @data["nags"] ||= {} key = key_name_for(library) return if key.empty? || @data["nags"].key?(key) env_name = begin ::FlossFunding::UnderBar.env_variable_name(library.namespace) rescue StandardError => e ::FlossFunding.error!(e, "LockfileBase#record_nag/env_variable_name") nil end @data["nags"][key] = { "namespace" => library.namespace, "env_variable_name" => env_name, "state" => event.state, "pid" => Process.pid, "at" => Time.now.utc.iso8601, } persist! rescue StandardError => e ::FlossFunding.error!(e, "LockfileBase#record_nag") end |
#rotate_if_expired! ⇒ Object
Remove and recreate lockfile if expired.
:nocov:
NOTE: This method exercises time-based file rotation and filesystem errors.
Creating deterministic, cross-platform tests for the rescue branches and
file deletion failures is brittle in CI (race conditions, permissions).
The happy path is exercised by higher-level specs; we exclude this method’s
internals from coverage to avoid flaky thresholds while keeping behavior robust.
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/floss_funding/lockfile.rb', line 113 def rotate_if_expired! return unless @path && File.exist?(@path) created_at = parse_time(@data.dig("created", "at")) return unless created_at age = Time.now.utc - created_at return unless age > max_age_seconds begin File.delete(@path) rescue StandardError => e ::FlossFunding.error!(e, "LockfileBase#rotate_if_expired!/delete") end @data = fresh_payload persist! rescue StandardError => e ::FlossFunding.error!(e, "LockfileBase#rotate_if_expired!") end |
#touch! ⇒ Object
:nocov:
132 133 134 135 136 137 |
# File 'lib/floss_funding/lockfile.rb', line 132 def touch! persist! rescue StandardError => e ::FlossFunding.error!(e, "LockfileBase#touch!") nil end |