Module: Lich::Common::Buffer
- Defined in:
- documented/common/buffer.rb
Overview
Thread-safe circular buffer for game server output and script input streams.
Buffer maintains a fixed-size ring of lines, each tagged with a stream identifier that indicates the source (game server downstream output, upstream commands, script output, etc.). Each thread maintains its own read cursor and stream filter, allowing independent consumption of the shared buffer while preserving thread safety via mutex.
Threads are registered automatically on their first call to .gets, .gets?, .rewind, or .clear; stale entries are cleaned up automatically via throttled sweeps to prevent unbounded memory growth in long-running sessions.
Constant Summary collapse
- DOWNSTREAM_STRIPPED =
1- DOWNSTREAM_RAW =
2- DOWNSTREAM_MOD =
4- UPSTREAM =
8- UPSTREAM_MOD =
16- SCRIPT_OUTPUT =
32- @@index =
Hash.new
- @@streams =
Hash.new
- @@mutex =
Mutex.new
- @@offset =
0- @@buffer =
Array.new
- @@max_size =
3000- @@cleanup_throttle =
@@index / @@streams are keyed by Thread#object_id and were never pruned, so every thread that ever read the buffer leaked an entry that outlived it. maybe_cleanup sweeps dead-thread entries from the registration path, at most once every 60s via this throttle.
Throttle.new(60.0)
Class Method Summary collapse
-
.cleanup ⇒ Object
rubocop:enable Lint/HashCompareByIdentity Removes @@index / @@streams entries whose thread is no longer alive.
-
.clear ⇒ Array<Object>
Reads all remaining lines from the buffer for the calling thread, returning them as an array and advancing the cursor to the end.
-
.gets ⇒ Object
Reads the next line from the buffer for the calling thread, blocking until one is available.
-
.gets? ⇒ Object?
Reads the next line from the buffer for the calling thread, returning nil if no matching line is available instead of blocking.
-
.rewind ⇒ self
Resets the calling thread's read cursor to the oldest line in the buffer.
-
.streams ⇒ Integer?
rubocop:disable Lint/HashCompareByIdentity Returns the stream filter bitmask for the calling thread.
-
.streams=(val) ⇒ void
Sets the stream filter bitmask for the calling thread.
-
.update(line, stream = nil) ⇒ self
Appends a line to the buffer, optionally overriding its stream identifier.
Class Method Details
.cleanup ⇒ Object
rubocop:enable Lint/HashCompareByIdentity Removes @@index / @@streams entries whose thread is no longer alive. Snapshots the live thread ids once rather than recomputing them per entry, and holds the mutex so it cannot race with a concurrent reader mutating the same hashes.
260 261 262 263 264 265 266 267 |
# File 'documented/common/buffer.rb', line 260 def self.cleanup @@mutex.synchronize { live_ids = Thread.list.map(&:object_id) @@index.delete_if { |k, _v| !live_ids.include?(k) } @@streams.delete_if { |k, _v| !live_ids.include?(k) } } return self end |
.clear ⇒ Array<Object>
Reads all remaining lines from the buffer for the calling thread, returning them as an array and advancing the cursor to the end.
Filters the buffer by the calling thread's current stream mask, returning only lines that match. If the thread's cursor has fallen behind the buffer (e.g., due to buffer rotation), it is advanced to the oldest line. Registers the thread on first call with a default stream filter of DOWNSTREAM_STRIPPED.
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'documented/common/buffer.rb', line 152 def self.clear thread_id = Thread.current.object_id if @@index[thread_id].nil? @@mutex.synchronize { @@index[thread_id] = (@@offset + @@buffer.length) @@streams[thread_id] ||= DOWNSTREAM_STRIPPED } maybe_cleanup end lines = Array.new loop { if (@@index[thread_id] - @@offset) >= @@buffer.length return lines end line = nil @@mutex.synchronize { if @@index[thread_id] < @@offset @@index[thread_id] = @@offset end line = @@buffer[@@index[thread_id] - @@offset] } @@index[thread_id] += 1 lines.push(line) if ((line.stream & @@streams[thread_id]) != 0) } return lines end |
.gets ⇒ Object
Reads the next line from the buffer for the calling thread, blocking until one is available.
Filters the buffer by the calling thread's current stream mask, skipping lines that do not match. If the thread's cursor has fallen behind the buffer (e.g., due to buffer rotation), it is advanced to the oldest line. Registers the thread on first call with a default stream filter of DOWNSTREAM_STRIPPED.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'documented/common/buffer.rb', line 51 def self.gets thread_id = Thread.current.object_id if @@index[thread_id].nil? @@mutex.synchronize { @@index[thread_id] = (@@offset + @@buffer.length) @@streams[thread_id] ||= DOWNSTREAM_STRIPPED } maybe_cleanup end line = nil loop { if (@@index[thread_id] - @@offset) >= @@buffer.length sleep 0.05 while ((@@index[thread_id] - @@offset) >= @@buffer.length) end @@mutex.synchronize { if @@index[thread_id] < @@offset @@index[thread_id] = @@offset end line = @@buffer[@@index[thread_id] - @@offset] } @@index[thread_id] += 1 break if ((line.stream & @@streams[thread_id]) != 0) } return line end |
.gets? ⇒ Object?
Reads the next line from the buffer for the calling thread, returning nil if no matching line is available instead of blocking.
Filters the buffer by the calling thread's current stream mask, skipping lines that do not match. If the thread's cursor has fallen behind the buffer (e.g., due to buffer rotation), it is advanced to the oldest line. Registers the thread on first call with a default stream filter of DOWNSTREAM_STRIPPED.
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'documented/common/buffer.rb', line 90 def self.gets? thread_id = Thread.current.object_id if @@index[thread_id].nil? @@mutex.synchronize { @@index[thread_id] = (@@offset + @@buffer.length) @@streams[thread_id] ||= DOWNSTREAM_STRIPPED } maybe_cleanup end line = nil loop { if (@@index[thread_id] - @@offset) >= @@buffer.length return nil end @@mutex.synchronize { if @@index[thread_id] < @@offset @@index[thread_id] = @@offset end line = @@buffer[@@index[thread_id] - @@offset] } @@index[thread_id] += 1 break if ((line.stream & @@streams[thread_id]) != 0) } return line end |
.rewind ⇒ self
Resets the calling thread's read cursor to the oldest line in the buffer.
Registers the thread with a default stream filter of DOWNSTREAM_STRIPPED if it has not yet accessed the buffer. Acquires the mutex to prevent race conditions during thread registration and cleanup.
127 128 129 130 131 132 133 134 135 136 137 |
# File 'documented/common/buffer.rb', line 127 def self.rewind thread_id = Thread.current.object_id # Hold the mutex: for a thread whose first Buffer call is rewind this # adds new keys, which must not race a concurrent cleanup delete_if # (Ruby raises on a key added during iteration). @@mutex.synchronize { @@index[thread_id] = @@offset @@streams[thread_id] ||= DOWNSTREAM_STRIPPED } return self end |
.streams ⇒ Integer?
rubocop:disable Lint/HashCompareByIdentity Returns the stream filter bitmask for the calling thread.
The result is a bitmask composed of stream constants (DOWNSTREAM_STRIPPED, DOWNSTREAM_RAW, etc.) that determines which lines the thread will receive from .gets, .gets?, and .clear. Returns nil if the thread has never accessed the buffer.
226 227 228 |
# File 'documented/common/buffer.rb', line 226 def self.streams @@mutex.synchronize { @@streams[Thread.current.object_id] } end |
.streams=(val) ⇒ void
This method returns an undefined value.
Sets the stream filter bitmask for the calling thread.
The bitmask must be a non-zero Integer with at least one of the six stream bits set (bits 0-5 corresponding to the six stream constants). Acquires the mutex to prevent race conditions during thread registration and cleanup. Raises no exception on invalid input; instead logs an error message via respond and returns nil.
244 245 246 247 248 249 250 251 252 253 |
# File 'documented/common/buffer.rb', line 244 def self.streams=(val) if (!val.is_a?(Integer)) or ((val & 63) == 0) respond "--- Lich: error: invalid streams value\n\t#{$!.caller[0..2].join("\n\t")}" else # Hold the mutex: setting streams for a thread that has not registered # yet adds a new key, which must not race the cleanup delete_if sweep # (Ruby raises on a key added during iteration). @@mutex.synchronize { @@streams[Thread.current.object_id] = val } end end |
.update(line, stream = nil) ⇒ self
Appends a line to the buffer, optionally overriding its stream identifier.
The line is duplicated and frozen before insertion to prevent external mutation. If the buffer exceeds the maximum size (3000 lines), the oldest line is discarded and the offset is incremented. Acquires the mutex for thread safety.
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'documented/common/buffer.rb', line 197 def self.update(line, stream = nil) @@mutex.synchronize { frozen_line = line.dup unless stream.nil? frozen_line.stream = stream end frozen_line.freeze @@buffer.push(frozen_line) while (@@buffer.length > @@max_size) @@buffer.shift @@offset += 1 end } return self end |