Class ::nsmcp::BoundedLogScanner (public)

 ::nx::Class ::nsmcp::BoundedLogScanner[i]

Defined in /usr/local/ns/tcl/nsmcp/log-01-scanner.tcl

Testcases:
No testcase defined.
Source code:
    :property cacheDirectory:required
    :property literals:required
    :property {channel {}}
    :property {limit {}}
    :property {blockBytes:integer 4194304}
    :variable keys {}
    :variable overlap 0
    :variable buffer {}
    :variable bufferFrom 0
    :variable bufferEnd 0
    :variable windowCeiling -1
    :variable windowHits 0
    :variable readBytes 65536
    :variable readMicroseconds 0
    :variable sourceBytes 0
    :variable readCalls 0

    :method init {} {
        if {[llength ${:literals}]<1 || [llength ${:literals}]>20} {
            error "scanner requires 1..20 literals"
        }
        if {${:blockBytes}<1 || ${:blockBytes}>8388608} {
            error "invalid scanner buffer limits"
        }
        foreach literal ${:literals} {
            set bytes [encoding convertto utf-8 $literal]
            set length [string length $bytes]
            if {$length<1 || $length>4096 || [regexp {[\x00\r\n]} $literal]} {
                error "scanner literals must be 1..4096 UTF-8 bytes without CR/LF/NUL"
            }
            lappend :keys $bytes
            set :overlap [expr {max(${:overlap},$length-1)}]
        }
        set :blockBytes [expr {max(${:blockBytes},${:overlap}+1)}]
        set :readBytes [expr {min(${:blockBytes},65536)}]
        if {${:channel} ne "" && (![string is entier -strict ${:limit}] || ${:limit}<0)} {
            error "scanner requires a fixed nonnegative byte limit"
        }
    }

    # A caller can supply an already bounded, record-aligned byte chunk instead
    # of a source channel. No source paths or raw bodies enter persistent state.
    :public method load {bytes} {
        #
        # Load a bounded in-memory byte chunk for candidate scanning.
        #
        # Reject channel-backed scanners and chunks larger than the configured block
        # bound.
        #
        # @param bytes Raw binary chunk, no larger than the configured block limit.
        #
        # @return The new in-memory byte limit.
        #

        if {${:channel} ne ""} {
            error "load requires an in-memory scanner"
        }
        if {[string length $bytes]>${:blockBytes}} {
            error "scanner chunk exceeds its byte bound"
        }
        :setBuffer $bytes 0 [string length $bytes]
        set :limit ${:bufferEnd}
    }

    :method setBuffer {bytes from ceiling} {
        set :buffer $bytes
        set :bufferFrom $from
        set :bufferEnd [expr {$from+[string length $bytes]}]
        set :windowCeiling $ceiling
        set :windowHits 0
    }

    :method first {position ceiling} {
        set relative [expr {$position-${:bufferFrom}}]
        set available [expr {$ceiling-${:bufferFrom}}]
        set found -1
        # Search the existing bytes directly. A scratch file adds writes and
        # descriptor operations without improving the bounded search contract.
        set buffer ${:buffer}
        foreach key ${:keys} {
            set candidate [string first $key $buffer $relative]
            if {$candidate>=0 && $candidate+[string length $key]<=$available
                && ($found<0 || $candidate<$found)} {
                set found $candidate
            }
        }
        if {$found>=0} {
            return [expr {${:bufferFrom}+$found}]
        }
        return -1
    }

    :public method statistics {} {
        #
        # Report source-channel work performed by this scanner.
        #
        # Counts describe Tcl channel reads, including overlapping windows, not
        # physical disk operations. In-memory scanning performs no channel I/O.
        #
        # @return A dictionary with readMicroseconds, readBytes and readCalls.
        #

        return [dict create readMicroseconds ${:readMicroseconds}  readBytes ${:sourceBytes} readCalls ${:readCalls}]
    }

    # Return the earliest raw match fully contained in [from, ceiling). The
    # ceiling can shrink between calls; buffered bytes beyond it never qualify.
    :public method find {from {ceiling {}}} {
        #
        # Find the earliest candidate within a fixed byte interval.
        #
        # Use overlapping bounded windows for channels or the current memory chunk;
        # never read beyond the ceiling.
        #
        # @param from Inclusive starting byte offset within the fixed scan interval.
        # @param ceiling Exclusive upper byte bound; empty uses the configured
        #        scanner limit.
        #
        # @return The candidate byte offset, or -1 when no candidate is found.
        #

        if {$ceiling eq ""} {
            set ceiling ${:limit}
        }
        if {![string is entier -strict $from] || ![string is entier -strict $ceiling]
            || $from<0 || $ceiling<$from || $ceiling>${:limit}} {
            error "invalid scanner byte interval"
        }
        set position $from
        while {$position<$ceiling} {
            if {${:channel} eq ""} {
                set found [:first $position $ceiling]
                if {$found>=0} {
                    incr :windowHits
                }
                return $found
            }
            if {$position<${:bufferFrom} || $position>=${:bufferEnd} || ${:windowCeiling}!=$ceiling} {
                seek ${:channel} $position
                set expected [expr {min(${:readBytes},$ceiling-$position)}]
                set started [clock clicks -microseconds]
                set bytes [read ${:channel} $expected]
                incr :readMicroseconds [expr {max(0,[clock clicks -microseconds]-$started)}]
                incr :sourceBytes [string length $bytes]
                incr :readCalls
                if {[string length $bytes]<$expected} {
                    error "source ended before its fixed scanner bound"
                }
                :setBuffer $bytes $position $ceiling
            }
            set found [:first $position [expr {min($ceiling,${:bufferEnd})}]]
            # A short batch key near the end cannot win before a longer key
            # beginning earlier but crossing this boundary has been checked.
            if {$found>=0 && (${:bufferEnd}>=$ceiling || $found<${:bufferEnd}-${:overlap})} {
                incr :windowHits
                return $found
            }
            if {${:bufferEnd}>=$ceiling} {
                return -1
            }
            if {${:windowHits}==0} {
                set :readBytes [expr {min(${:blockBytes},2*${:readBytes})}]
            } else {
                set :readBytes [expr {min(${:blockBytes},65536)}]
            }
            set position [expr {max($from,${:bufferEnd}-${:overlap})}]
            # Refill even though the overlap is inside the previous buffer.
            set :windowCeiling -1
        }
        return -1
    }

    # In-memory physical-line candidates for access logs. A caller can request
    # sequential lines when normalization makes a raw literal prefilter unsafe.
    :public method nextLine {position {filter true}} {
        #
        # Read an in-memory physical line, optionally using the literal prefilter.
        #
        # Return physical byte bounds and newline status without parsing or
        # redacting the record.
        #
        # @param position Physical byte cursor within the loaded chunk.
        # @param filter Whether to apply the literal candidate prefilter; false
        #        reads sequentially. Defaults to 'true'.
        #
        # @return A dictionary with start, end, raw and terminated fields, or an
        #         empty result at exhaustion.
        #

        if {${:channel} ne ""} {
            error "nextLine requires an in-memory scanner"
        }
        if {$position<0 || $position>${:limit}} {
            error "invalid line cursor"
        }
        if {$position==${:limit}} {
            return {}
        }
        if {$filter} {
            set found [:find $position]
            if {$found<0} {
                return {}
            }
            set start [expr {[string last \n ${:buffer} [expr {$found-1}]]+1}]
        } else {
            set start $position
        }
        set end [string first \n ${:buffer} $start]
        set terminated [expr {$end>=0}]
        if {!$terminated} {
            set end ${:limit}
        }
        return [dict create start $start end [expr {$end+($terminated ? 1 : 0)}]  raw [string range ${:buffer} $start [expr {$end-1}]] terminated $terminated]
    }
XQL Not present:
Generic, PostgreSQL, Oracle
[ hide source ] | [ make this the default ]
Show another procedure: