ns::stats::SQLiteHistory method history save (public)

 <instance of ns::stats::SQLiteHistory[i]> history save

Defined in

Persist the latest published sample if history is enabled, no write is in progress and its timestamp has not already been saved. Claim the write under the shared coordination lock, then perform database operations outside that lock. Store the compressed sample and associated topology in one transaction, deduplicating samples by timestamp. Apply age and storage-budget eviction, retain coverage events and remove topology no longer referenced by retained samples. On failure, roll back the transaction, retain the pending sample for retry and update failure status. Repeated identical warnings are throttled. A subsequent successful write records the failed interval for coverage reporting. Release the database handle and clear the write claim on completion. Does not collect observations or advance the sampling baseline; callers inspect history status for the persistence outcome.

Testcases:
No testcase defined.
Source code:
set config {}
set generation {}
set stamp {}
:locked {
    set candidate [nsv_get ns_stats history]
    set sample [nsv_get ns_stats pair]
    if {$candidate ne "" && [dict get $candidate enabled] && ![dict get $candidate busy] && $sample ne ""} {
        set stamp [dict get $sample timestamp]
        if {$stamp ne [dict get $candidate lastSaved]} {
            set config $candidate
            set generation [dict get $config generation]
            dict set candidate busy true
            nsv_set ns_stats history $candidate
        }
    }
}
if {$config eq ""} {
    return
}
set db {}
set transaction false
set ok false
set errorMessage {}
try {
    # Collection and SQLite work happen outside the shared control mutex.
    set now [clock seconds]
    set topology [dict get $config topology]
    if {$topology eq "" || $now-[dict get $config topologyAt]>=60} {
        set topology [:describe]
        foreach k {pid bootTime uptimeSeconds} {dict unset topology process $k}
        dict set config topology $topology
        dict set config topologyAt $now
    }
    # Stable Tcl serialization is enough for a private cache key; store
    # the full topology in the table to detect the unlikely CRC collision.
    set topologyId [format %08x [zlib crc32 [encoding convertto utf-8 $topology]]]
    set record $sample
    dict unset record raw
    dict unset record cpu
    dict set record topologyId $topologyId
    set payload [encoding convertto utf-8 $record]
    set rawSize [string length $payload]
    if {$rawSize > 1048576} {
        error "history sample exceeds 1 MiB"
    }
    set packed [::ns::stats::pack $record]
    set packedTopology [::ns::stats::pack $topology]
    set size [expr {[string length $packed]+[string length $packedTopology]}]
    set db [:historyConnect $config]
    :historySchema $db
    ::ns::stats::sql $db {BEGIN IMMEDIATE}
    set transaction true
    set cutoff [expr {([clock seconds]-[dict get $config retention])*1000000}]
    :historyDrop $db "at < $cutoff" age
    set page [::ns::stats::scalar $db {PRAGMA page_size}]
    set max [::ns::stats::scalar $db {PRAGMA max_page_count}]
    while {1} {
        set used [expr {([::ns::stats::scalar $db {PRAGMA page_count}] - [::ns::stats::scalar $db {PRAGMA freelist_count}]) * $page}]
        if {$used+3*$size < $max*$page*0.8} {
            break
        }
        if {![::ns::stats::scalar $db {SELECT count(*) FROM ns_stats_samples}]} {
            error "history metadata or sample exceeds storage budget"
        }
        :historyDrop $db {at IN (SELECT at FROM ns_stats_samples ORDER BY at LIMIT 100)} size
    }
    set stored [::ns::stats::scalar $db "SELECT payload FROM ns_stats_topology WHERE id=[::ns::stats::quote $topologyId]"]
    if {$stored ne "" && [::ns::stats::unpack $stored] ne $topology} {
        error "history topology fingerprint collision"
    }
    ::ns::stats::sql $db "INSERT OR IGNORE INTO ns_stats_topology VALUES([::ns::stats::quote $topologyId],[::ns::stats::quote $packedTopology])"
    ::ns::stats::sql $db "INSERT OR IGNORE INTO ns_stats_samples VALUES($stamp,[::ns::stats::quote [dict get $record instance]],[::ns::stats::quote $topologyId],[::ns::stats::quote $packed])"
    foreach gap [dict get $config gaps] {
        lassign $gap first last n
        ::ns::stats::sql $db "INSERT INTO ns_stats_events(at,first,last,n,reason) VALUES($stamp,$first,$last,$n,'write-failure')"
    }
    ::ns::stats::sql $db {DELETE FROM ns_stats_events WHERE id NOT IN (SELECT id FROM ns_stats_events ORDER BY id DESC LIMIT 64)}
    ::ns::stats::sql $db {DELETE FROM ns_stats_topology WHERE id NOT IN (SELECT DISTINCT topology FROM ns_stats_samples)}
    ::ns::stats::sql $db {COMMIT}
    set transaction false
    set ok true

} on error {message options} {
    set errorMessage $message
    if {$transaction} {
        catch {::ns::stats::sql $db {ROLLBACK}}
    }
    if {$message ne [dict get $config lastFailure] || [clock seconds]-[dict get $config lastLoggedAt] >= 60} {
        ns_log warning "ns_stats: history write failed: $message"
        dict set config lastLoggedAt [clock seconds]
    }
} finally {
    if {$db ne ""} {
        ns_db releasehandle $db
    }
    :locked {
        set current [nsv_get ns_stats history]
        if {$generation eq [dict get $current generation]} {
            dict set current busy false
            dict set current lastLoggedAt [dict get $config lastLoggedAt]
            if {$ok} {
                dict set current lastSaved $stamp
                dict set current lastFailure {}
                dict set current gaps {}
                dict set current topology [dict get $config topology]
                dict set current topologyAt [dict get $config topologyAt]
            } else {
                dict incr current failures
                dict set current lastFailure $errorMessage
                set gaps [dict get $current gaps]
                if {$gaps eq ""} {
                    set gaps [list [list $stamp $stamp 1]]
                } else {
                    lassign [lindex $gaps 0] first last n
                    set gaps [list [list $first $stamp [expr {$n+1}]]]
                }
                dict set current gaps $gaps
            }
            nsv_set ns_stats history $current
        }
    }
}
XQL Not present:
Generic, PostgreSQL, Oracle
[ hide source ] | [ make this the default ]
Show another procedure: