Class: FlossFunding::LockfileBase

Inherits:
Object
  • Object
show all
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: at: type: <on_load|at_exit> nags:

: namespace: env_variable_name: state: pid: at:

Direct Known Subclasses

AtExitLockfile, OnLoadLockfile

Constant Summary collapse

MIN_SECONDS =

10 minutes (enforced minimum)

600
MAX_SECONDS =

7 days (enforced maximum)

604_800

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeLockfileBase

: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

#pathObject (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.

Parameters:

  • library_or_name (Object)

Returns:

  • (Boolean)


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.

Parameters:



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