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}
    :property {native:boolean true}
    :property {nativeThreshold:integer 32}
    :variable keys {}
    :variable overlap 0
    :variable buffer {}
    :variable bufferFrom 0
    :variable bufferEnd 0
    :variable windowCeiling -1
    :variable windowHits 0
    :variable readBytes 65536
    :variable nativePrepared false
    :variable temporaryChannel {}
    :variable temporaryPath {}

    :method init {} {
        if {[llength ${:literals}]<1 || [llength ${:literals}]>20} {error "scanner requires 1..20 literals"}
        if {${:blockBytes}<1 || ${:blockBytes}>8388608 || ${:nativeThreshold}<1} {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"}
    }

    :public method destroy {} {
        if {[info exists :temporaryChannel] && ${:temporaryChannel} ne ""} {catch {close ${:temporaryChannel}}}
        if {[info exists :temporaryPath] && ${:temporaryPath} ne ""} {catch {file delete -- ${:temporaryPath}}}
        next
    }

    # 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} {
        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
        set :nativePrepared false
    }
    :method prepareNative {} {
        if {${:nativePrepared}} {return}
        if {${:temporaryChannel} eq ""} {
            set :temporaryChannel [file tempfile :temporaryPath [file join ${:cacheDirectory} bounded-scan-]]
            fconfigure ${:temporaryChannel} -translation binary -buffering none
        } else {
            ns_ftruncate ${:temporaryChannel} 0
        }
        seek ${:temporaryChannel} 0
        puts -nonewline ${:temporaryChannel} ${:buffer}
        set :nativePrepared true
    }
    :method first {position ceiling} {
        set relative [expr {$position-${:bufferFrom}}]
        set available [expr {$ceiling-${:bufferFrom}}]
        set found -1
        # Native descriptor searching is safe only on this bounded private file.
        # Dense windows switch to C string searching in the cached byte buffer,
        # avoiding one descriptor seek/read per adjacent candidate.
        if {${:native} && [llength ${:keys}]==1 && ${:windowHits}<${:nativeThreshold}
            && [info commands ns_fseekchars] ne ""} {
            :prepareNative
            seek ${:temporaryChannel} $relative
            set candidate [ns_fseekchars ${:temporaryChannel} [lindex ${:literals} 0]]
            if {$candidate>=0 && $candidate+[string length [lindex ${:keys} 0]]<=$available} {set found $candidate}
        } else {
            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
    }

    # 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 {}}} {
        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 bytes [read ${:channel} $expected]
                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}} {
        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: