Class: Tina4::LogFileSink

Inherits:
Object
  • Object
show all
Defined in:
lib/tina4/log.rb

Overview

One owned log file: bounded, PREDICTIVE rotation guarded by a single in-process (thread) exclusive lock over the size check, rotation and append.

Decision 20 (2026-08-10 owner override): SINGLE FILE + IN-PROCESS LOCK ONLY. Cross-process exclusive locking is deliberately not implemented; concurrent PROCESSES writing the same file may interleave. Run one file per process, or route through a log shipper, for that case.

Constant Summary collapse

LOCK_TIMEOUT_SECONDS =
2.0

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path, rotate_size, rotate_keep) ⇒ LogFileSink

Returns a new instance of LogFileSink.



57
58
59
60
61
62
# File 'lib/tina4/log.rb', line 57

def initialize(path, rotate_size, rotate_keep)
  @path = path
  @rotate_size = rotate_size
  @rotate_keep = rotate_keep
  @mutex = Mutex.new
end

Instance Attribute Details

#pathObject (readonly)

Returns the value of attribute path.



55
56
57
# File 'lib/tina4/log.rb', line 55

def path
  @path
end

Instance Method Details

#closeObject



115
# File 'lib/tina4/log.rb', line 115

def close; end

#openObject

Create the directory and prove the file is writable.



65
66
67
68
69
70
71
72
73
# File 'lib/tina4/log.rb', line 65

def open
  dir = File.dirname(@path)
  FileUtils.mkdir_p(dir)
  File.open(@path, "a") {}
rescue SystemCallError, IOError => e
  raise LogConfigurationError.new(
    "cannot open log sink #{@path}: #{e.message}", sink: @path, operation: "open"
  )
end

#rotate_if_needed(next_record_bytes) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/tina4/log.rb', line 75

def rotate_if_needed(next_record_bytes)
  current_size = File.exist?(@path) ? File.size(@path) : 0
  return if current_size.zero?
  return if current_size + next_record_bytes <= @rotate_size

  if @rotate_keep <= 0
    File.delete(@path)
    return
  end

  oldest = "#{@path}.#{@rotate_keep}"
  File.delete(oldest) if File.exist?(oldest)
  (@rotate_keep - 1).downto(1) do |n|
    src = "#{@path}.#{n}"
    dst = "#{@path}.#{n + 1}"
    File.rename(src, dst) if File.exist?(src)
  end
  File.rename(@path, "#{@path}.1")
end

#write(encoded_line) ⇒ Object

Append one complete encoded record, rotating first if it would cross the threshold. Raises LogWriteError (timeout/write) to the caller, which applies the sink failure policy.

Raises:



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/tina4/log.rb', line 98

def write(encoded_line)
  clean = encoded_line.gsub(/\e\[[0-9;]*m/, "")
  payload = clean.b

  acquired = try_lock_with_timeout
  raise LogWriteError.new("timed out acquiring the log sink lock for #{@path}", sink: @path, operation: "lock") unless acquired

  begin
    rotate_if_needed(payload.bytesize)
    File.open(@path, "ab") { |f| f.write(payload) }
  rescue SystemCallError, IOError => e
    raise LogWriteError.new("cannot write log sink #{@path}: #{e.message}", sink: @path, operation: "write")
  ensure
    @mutex.unlock if @mutex.owned?
  end
end