mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
perf(metrics): avoid redundant retained-read work
Execute plain retained reconciliation in one current SQLite snapshot and reuse bounded compiled statements. Preserve per-series chronology without a metric sort while keeping display aggregation ordering explicit. Exact-base worker comparisons cover the prior PR benchmark failures. Full metrics/database and focused concurrent race checks pass. Final CI and real diagnostic outcome qualification remain open.
This commit is contained in:
@@ -62,8 +62,8 @@ reproduction evidence, not a representative customer success rate.
|
||||
| Step | Work | Acceptance | Current state |
|
||||
|---|---|---|---|
|
||||
| 1. Product contract and baseline | Map the current loop and sources of judgment. Record telemetry populations and gaps. | Every identified decision has an owner. Activity is not labelled usefulness. | Complete for this redesign scope. Contract, ownership decisions and baseline limits are recorded. |
|
||||
| 2. Shared evidence | Preserve canonical risk reasons and SMART counters, source/time semantics and history across tools/turns. | Regression tests preserve unknown versus zero and all canonical evidence. Real responses can inspect the same facts as the product. | Implemented and qualified for the named shared-evidence defects. Canonical disk detail, risk and cadence pass real data-path proof. Full affected package, concurrency and retained-query performance checks pass. Real-model interpretation failures remain tracked in step 5. |
|
||||
| 3. Diagnostic orchestration | Correct proposal-as-proof. Audit triage budgets, unmatched-signal evaluation, assessment completion and investigation cutoffs. | No code-written causal conclusion. No quality inferred from tool, flag or finding counts. Each retained pass has an objective reason. Safety boundaries and incomplete outcomes remain explicit. | Proposal promotion and capture inference were removed in c5d2f56dda. The current slice removes investigation success-call floors, checkpoint instructions and generic call-count wrap-up rules. Full chat regressions pass. Detection passes and live qualification remain open. |
|
||||
| 2. Shared evidence | Preserve canonical risk reasons and SMART counters, source/time semantics and history across tools/turns. | Regression tests preserve unknown versus zero and all canonical evidence. Real responses can inspect the same facts as the product. | Implemented and qualified for the named shared-evidence defects. Canonical disk detail, risk and cadence pass real data-path proof. Affected package and concurrency checks pass. Final exact-base performance qualification remains open after PR CI exposed regressions. Real-model interpretation failures remain tracked in step 5. |
|
||||
| 3. Diagnostic orchestration | Correct proposal-as-proof. Audit triage budgets, unmatched-signal evaluation, assessment completion and investigation cutoffs. | No code-written causal conclusion. No quality inferred from tool, flag or finding counts. Each retained pass has an objective reason. Safety boundaries and incomplete outcomes remain explicit. | Proposal promotion and capture inference were removed in c5d2f56dda. Commit 668af3fe6b removes investigation success-call floors, checkpoint instructions and generic call-count wrap-up rules. Full chat regressions pass. Detection passes and live qualification remain open. |
|
||||
| 4. Issue through verified outcome | Follow existing issue/investigation/action records into Assistant, approval, execution and independent readback. | Accepted proposal is visibly distinct from execution and verification. Rejected or unsupported actions do not become success. Uncertainty can survive an action proposal. | Existing foundation, full journey qualification pending. |
|
||||
| 5. Ground-truth qualification and landing | Extend existing qualification tooling only where necessary. Exercise healthy/unhealthy, dependency, missing-access, storage/backup and approved/rejected action cases. Inspect the final browser journey at desktop and narrow widths. | Record exact source/model/permissions, evidence, decisions, faults/misses, latency and verification. Fix in-scope failures, pass appropriate proofs and land scoped commits. | Pending. |
|
||||
|
||||
@@ -81,7 +81,8 @@ scoring to make the model pass.
|
||||
| Causal-resource validator | Removed the duplicate resource graph and name/status inference from the working capture boundary. Causal attribution is optional when unknown. | Prove capability/schema validation, parameter isolation and invocation integrity remain enforced. Dependency evidence remains available to the model through canonical queries. |
|
||||
| Flag-count turn ladder | `computeTriageMaxTurns` grants 5 + 3 turns per flag, bounded to 8–40, with a separate quick limit. | Replace quality/urgency proxies with explicit execution resource limits. More flags must not imply a better investigation budget. |
|
||||
| Unmatched-signal evaluation | `runAIAnalysisState` detects signals from tool output and triage, then starts a second model pass when they lack matching findings. | Audit for removal in favour of complete initial evidence and model-owned decisions. Preserve negative-control and missed-fault qualification rather than force reports. |
|
||||
| Missing-finding assessment sweep | A bounded continuation requests missing explicit present/resolved/uncertain verdicts for known findings. | Retain only the mechanical completeness obligation, with sufficient original evidence and no fabricated resolution or requirement to discover new issues. |
|
||||
| Missing-finding assessment sweep | A separate session receives old finding excerpts after the main run. It lacks the original run evidence. | Remove the separate session. Keep explicit present/resolved/uncertain decisions in the original conversation and preserve incomplete status for omissions. |
|
||||
| First accepted finding ends investigation | The main loop replaces its prompt and removes evidence tools after a finding write succeeds. | Remove this interpretation of persistence as evidential sufficiency. Recording one issue must not prevent reads needed for another issue or a known finding. |
|
||||
| Investigation evidence-call floor | Removed in the current slice. Seed-only and failed-read conclusions survive without forced extra calls. | Completion is not diagnostic correctness. Preserve explicit limits, failed/unavailable evidence and independent action freshness checks. |
|
||||
| Generic wrap-up counters | Removed the 12/18-call tool-result instructions and four silent-turn cutoff. | Explicit run limits bound work. Counts and silence do not establish evidential sufficiency. A twenty-read regression preserves available tools, observations and the model conclusion. |
|
||||
| Authority and execution boundaries | Tenant identity, capability schemas, approvals, invocation IDs, parameter redaction and independent readback. | Keep and prove unchanged when diagnostic policy is simplified. These enforce objective invariants. |
|
||||
@@ -746,3 +747,46 @@ comparison against the actual base 4f9de86e failed nine benchmark comparisons,
|
||||
including small single-resource retained reads. Landing is blocked while the
|
||||
exact base/candidate comparison is reproduced and corrected. Do not treat the
|
||||
previous narrower performance pass as complete CI qualification.
|
||||
|
||||
### Exact-base retained-read correction
|
||||
|
||||
The small retained-read path performed an extra tier-presence query and opened
|
||||
an explicit transaction even when the reconciliation was already one SQLite
|
||||
statement. Plain reads now reuse bounded compiled reconciliation statements.
|
||||
All-metric plain reads retain index time order instead of sorting by metric.
|
||||
Each output series remains chronological. Display aggregation retains its
|
||||
required series grouping and same-snapshot presence optimization. No data or
|
||||
tier-presence result is cached.
|
||||
|
||||
The corrected `store.go` SHA256 is
|
||||
`41132f7eae5a987e72092aceb8813a20f3dc44986c849023ffa0654e5bfe58aa`.
|
||||
Ten alternating 100 ms worker samples against exact PR base 4f9de86e report:
|
||||
|
||||
| Query | Base | Correction | Comparison |
|
||||
|---|---:|---:|---|
|
||||
| Bounded single-metric API chart | 848.6 us | 894.5 us | No significant difference, p=.436 |
|
||||
| Single series across resources | 916.98 us | 47.12 us | -94.86%, p<.001 |
|
||||
| Single query, 500-node load fixture | 18.78 ms | 51.50 us | -99.73%, p<.001 |
|
||||
| All-metric dashboard | 309.3 us | 174.3 us | -43.66%, p<.001 |
|
||||
| Batch dashboard, 10 nodes | 1.912 ms | 1.723 ms | -9.86%, p=.007 |
|
||||
| Batch dashboard, 50 nodes | 8.807 ms | 8.337 ms | -5.34%, p=.023 |
|
||||
| Batch dashboard, 100 nodes | 17.56 ms | 16.08 ms | -8.43%, p=.002 |
|
||||
| Batch dashboard, 500 nodes | 87.56 ms | 82.34 ms | No significant difference, p=.052 |
|
||||
|
||||
These are worker measurements, not a production latency guarantee. An exact
|
||||
query-plan reproduction shows the old single-query SQL choosing the broad
|
||||
tier/time index on this worker. The corrected runtime uses the series/range
|
||||
index explicitly. This explains the large worker single-query difference but
|
||||
does not establish which plan the prior CI runner chose. No benchmark threshold
|
||||
was relaxed. The final full metrics suite passes in 87.114s, the database
|
||||
suite in 0.119s and focused concurrent read/write race proof in 19.065s. The
|
||||
remaining failed CI comparisons have no significant worker latency difference:
|
||||
API memory fallback, 163.6 versus 167.3 us (p=.315), and the fifty-guest chart
|
||||
batch, 128.0 versus 121.3 ms (p=.190). These comparisons cover the earlier failed
|
||||
cases. A fresh CI run must still qualify the final landing commit. The earlier
|
||||
unexplained native SQLite fault remains an open observation.
|
||||
|
||||
Private worker evidence is retained under
|
||||
`/opt/pulse-release-worker/pr1920-bench-4f9-c5d2/`, with source-bound
|
||||
`full-metrics-second.log`, `full-db-second.log`, `race-metrics-second.log`,
|
||||
`second-candidate-*` and `adj-*` artifacts.
|
||||
|
||||
@@ -10201,7 +10201,7 @@
|
||||
},
|
||||
{
|
||||
"id": "patrol-assistant-customer-outcome-qualification",
|
||||
"summary": "The maintainer requested a whole-design assessment and an explicit completion goal for Patrol and Assistant. The ordered plan and source-bound evidence are in docs/qualification/PATROL_ASSISTANT_CUSTOMER_JOURNEY.md. The contract separates observations, model conclusions, proposals, execution and independently verified outcomes. Assistant continues the same issue and governed action records. The explicit explanation handoff is an implemented frontend foundation, not outcome qualification. The recorded 2026-09-05 latest-report, monitoring-active, multi-ping review excluded development and deployment proof and contained 127 paid installs, 71 with Patrol enabled and 23 with Assistant calls. Fourteen reported verified resolutions came from one install. Paid includes all non-free tiers. Activity cooccurrence does not establish a successful linked task, and schema 17 outcome/provider/cost fields had no adoption in that review. Real read-only homelab testing with Claude Opus 5 exposed missing canonical evidence, history target/tier gaps, premature context loss, unsupported causal claims and multi-minute responses. Prior history and handoff changes are recorded in the plan. The current working slice removes proposal rationale promotion into Root Cause and duplicate causal inference, preserves disk SMART/risk/source freshness and explicit remaining life, supports canonical disk get/health, and corrects collector cadence and status recovery. Targeted regressions and the live disk data path pass. Affected full worker package suites pass. The single-metric downsample regression and an adjacent bounded chart regression are corrected. Compiled presence statements are reused within a bounded set, but every query reads current data in one snapshot. The chart comparison shows no significant slowdown. Final metrics/database suites, focused concurrency race checks and the existing benchmark guard pass. Single-metric downsampling remains close to the performance threshold. PR #1920 CI subsequently failed nine benchmark comparisons against the actual base 4f9de86e, including small retained reads. The prior worker comparison used local source 5288b64d and does not establish final CI performance. Landing remains blocked pending exact-base reproduction and correction. One earlier SQLite native binding fault has no established root cause, although subsequent final full and focused race proofs pass. The latest live answers still infer collector startup from timestamp coincidence, claim continuous coverage from retained spacing and infer backup recovery from an online datastore. They fail diagnostic qualification. A serialized routing-envelope leak prompted a subscription transport correction with targeted regression proof, verified without routing prose in a completed real Assistant answer. A narrow answer-table overflow defect is corrected in the shared sanitized renderer with desktop, intermediate and mobile browser proof. The selected subscription provider explicitly refused autonomous Patrol readiness. The runtime and API preserve that refusal and prevent execution. Ordinary Assistant success is not autonomous proof. Required healthy/unhealthy, dependency, missing-access, backup/capacity and approved/rejected action qualification remains incomplete. Independent volunteered Pro environments are required before claiming repeatable customer value or wider readiness. The next diagnostic slice removes successful-read completion floors, evidence checkpoint instructions and generic call-count/silent-turn wrap-up policy. Full worker chat and AI suites pass. Browser qualification exposed and corrected mirrored-alert finding selection in the shared review lookup. Ordinary/mirrored records and linked uncertainty/failed-read context pass Playwright at 1440/900/390 widths. Detection orchestration and real-model/action outcome qualification remain open.",
|
||||
"summary": "The maintainer requested a whole-design assessment and an explicit completion goal for Patrol and Assistant. The ordered plan and source-bound evidence are in docs/qualification/PATROL_ASSISTANT_CUSTOMER_JOURNEY.md. The contract separates observations, model conclusions, proposals, execution and independently verified outcomes. Assistant continues the same issue and governed action records. The explicit explanation handoff is an implemented frontend foundation, not outcome qualification. The recorded 2026-09-05 latest-report, monitoring-active, multi-ping review excluded development and deployment proof and contained 127 paid installs, 71 with Patrol enabled and 23 with Assistant calls. Fourteen reported verified resolutions came from one install. Paid includes all non-free tiers. Activity cooccurrence does not establish a successful linked task, and schema 17 outcome/provider/cost fields had no adoption in that review. Real read-only homelab testing with Claude Opus 5 exposed missing canonical evidence, history target/tier gaps, premature context loss, unsupported causal claims and multi-minute responses. Prior history and handoff changes are recorded in the plan. The current working slice removes proposal rationale promotion into Root Cause and duplicate causal inference, preserves disk SMART/risk/source freshness and explicit remaining life, supports canonical disk get/health, and corrects collector cadence and status recovery. Targeted regressions and the live disk data path pass. Affected full worker package suites pass. The single-metric downsample regression and an adjacent bounded chart regression are corrected. Compiled presence statements are reused within a bounded set, but every query reads current data in one snapshot. The chart comparison shows no significant slowdown. Final metrics/database suites, focused concurrency race checks and the existing benchmark guard pass. Single-metric downsampling remains close to the performance threshold. PR #1920 CI subsequently failed nine benchmark comparisons against the actual base 4f9de86e, including small retained reads. The prior worker comparison used local source 5288b64d and does not establish final CI performance. The exact-base correction removes unnecessary plain-read presence probes, transaction wrappers and metric sorting while retaining canonical reconciliation and current snapshots. Full metrics/database and focused race suites pass. Ten alternating worker samples cover the previously failed CI comparisons without a significant greater-than-10% regression. Final CI landing qualification remains pending. One earlier SQLite native binding fault has no established root cause, although subsequent final full and focused race proofs pass. The latest live answers still infer collector startup from timestamp coincidence, claim continuous coverage from retained spacing and infer backup recovery from an online datastore. They fail diagnostic qualification. A serialized routing-envelope leak prompted a subscription transport correction with targeted regression proof, verified without routing prose in a completed real Assistant answer. A narrow answer-table overflow defect is corrected in the shared sanitized renderer with desktop, intermediate and mobile browser proof. The selected subscription provider explicitly refused autonomous Patrol readiness. The runtime and API preserve that refusal and prevent execution. Ordinary Assistant success is not autonomous proof. Required healthy/unhealthy, dependency, missing-access, backup/capacity and approved/rejected action qualification remains incomplete. Independent volunteered Pro environments are required before claiming repeatable customer value or wider readiness. The next diagnostic slice removes successful-read completion floors, evidence checkpoint instructions and generic call-count/silent-turn wrap-up policy. Full worker chat and AI suites pass. Browser qualification exposed and corrected mirrored-alert finding selection in the shared review lookup. Ordinary/mirrored records and linked uncertainty/failed-read context pass Playwright at 1440/900/390 widths. Detection orchestration and real-model/action outcome qualification remain open.",
|
||||
"owner": "project-owner",
|
||||
"status": "planned",
|
||||
"recorded_at": "2026-09-05",
|
||||
|
||||
@@ -34,17 +34,25 @@ buckets cross into Go. Fleet queries stream ordered observations into bounded
|
||||
display buckets without retaining every input point. Both preserve identical
|
||||
bucket, mean and extrema semantics, and reuse scan destinations across rows.
|
||||
|
||||
Each bounded resource chunk uses one read transaction. Indexed existence checks
|
||||
identify which retention tiers have any observations in that snapshot. Every
|
||||
present tier participates in the shared indexed overlap query. Presence never
|
||||
stands in for per-series coverage. The store retains at most 32 compiled
|
||||
presence-statement shapes and evaluates them again inside each read snapshot.
|
||||
Each bounded resource chunk reads one consistent snapshot. Plain reads execute
|
||||
the canonical reconciliation query as one SQLite statement. They require no
|
||||
separate presence probe or explicit transaction. All-metric plain reads use
|
||||
resource/time index order, which preserves each output series' chronology
|
||||
without an unnecessary metric sort. Streaming display aggregation still
|
||||
requires contiguous series and requests that ordering explicitly.
|
||||
|
||||
Display-aggregated reads use one transaction. Indexed existence checks identify
|
||||
which retention tiers have observations in that snapshot. Every present tier
|
||||
participates in the shared indexed overlap query. Presence never stands in for
|
||||
per-series coverage. The store retains at most 32 compiled read-statement shapes
|
||||
and evaluates them again inside each current read snapshot.
|
||||
It never caches tier presence, query results or timestamp windows. Less common
|
||||
shapes run uncached after the bound is reached. Preparation occurs before
|
||||
acquiring the transaction, including with a single-connection pool. The shared
|
||||
database instrumentation preserves timing for transaction-bound prepared
|
||||
statements, and database closure owns their lifetime. Empty tiers need no per-observation probe,
|
||||
and an all-raw series uses a direct range query. Fixed query dimensions need
|
||||
statements, and database closure owns their lifetime. In display-aggregated
|
||||
reads, empty tiers need no per-observation probe and an all-raw series uses a
|
||||
direct range query. Fixed query dimensions need
|
||||
not be decoded again for every returned point. Query-plan tests exercise the runtime SQL builder,
|
||||
including all tier priorities and metric filters. Multi-stage rollups preserve
|
||||
recorded minima and maxima instead of taking extrema from bucket averages.
|
||||
|
||||
+108
-77
@@ -196,10 +196,10 @@ type Store struct {
|
||||
db *pdb.InstrumentedDB
|
||||
config StoreConfig
|
||||
|
||||
// Cache compiled presence SQL only. Results always come from the current
|
||||
// read transaction. Bound the number of parameter-count shapes retained.
|
||||
presenceMu sync.Mutex
|
||||
presenceStatements map[string]*pdb.InstrumentedStmt
|
||||
// Cache compiled read SQL only. Results always come from the current
|
||||
// database snapshot. Bound the number of parameter-count shapes retained.
|
||||
readMu sync.Mutex
|
||||
readStatements map[string]*pdb.InstrumentedStmt
|
||||
|
||||
// Write buffer
|
||||
bufferMu sync.Mutex
|
||||
@@ -1343,7 +1343,7 @@ func normalizeMetricTypes(metricTypes []string) []string {
|
||||
// buckets, and a coarser fallback is omitted if a preferred point overlaps it.
|
||||
// A bucket's presence does not establish continuous underlying collection.
|
||||
// All probes are scoped to the same series and requested timestamp window.
|
||||
func retainedQuerySQL(resourceType string, resourceIDs, metricTypes []string, start, end time.Time, stepSecs int64, tiers []Tier) (string, []interface{}) {
|
||||
func retainedQuerySQL(resourceType string, resourceIDs, metricTypes []string, start, end time.Time, stepSecs int64, tiers []Tier, groupSeries bool) (string, []interface{}) {
|
||||
// Query dimensions fixed by the caller need not be decoded per point.
|
||||
identityColumns := ""
|
||||
if len(resourceIDs) != 1 {
|
||||
@@ -1422,7 +1422,18 @@ func retainedQuerySQL(resourceType string, resourceIDs, metricTypes []string, st
|
||||
ORDER BY ` + identityColumns + `bucket_ts ASC`
|
||||
params = append([]interface{}{stepSecs, stepSecs, stepSecs}, params...)
|
||||
} else {
|
||||
query += " ORDER BY " + identityColumns + "timestamp ASC"
|
||||
orderColumns := identityColumns
|
||||
if !groupSeries && len(metricTypes) == 0 {
|
||||
// The all-metric index orders each resource by time. Interleaved
|
||||
// metrics still append in timestamp order within each output series,
|
||||
// so plain reads need no extra sort by metric. Streaming display
|
||||
// aggregation explicitly requests contiguous series instead.
|
||||
orderColumns = ""
|
||||
if len(resourceIDs) != 1 {
|
||||
orderColumns = "resource_id, "
|
||||
}
|
||||
}
|
||||
query += " ORDER BY " + orderColumns + "timestamp ASC"
|
||||
}
|
||||
return query, params
|
||||
}
|
||||
@@ -1443,93 +1454,97 @@ func tierBucketSeconds(tier Tier) int64 {
|
||||
// The database owns statement closure. Once the bounded set is full, uncommon
|
||||
// parameter-count shapes run uncached. No result, tier presence or time window
|
||||
// is cached, and no eviction can close a statement another reader is binding.
|
||||
const maxRetainedPresenceStatements = 32
|
||||
const maxRetainedReadStatements = 32
|
||||
|
||||
func (s *Store) retainedPresenceStatement(query string) (*pdb.InstrumentedStmt, error) {
|
||||
s.presenceMu.Lock()
|
||||
defer s.presenceMu.Unlock()
|
||||
if stmt := s.presenceStatements[query]; stmt != nil {
|
||||
func (s *Store) retainedReadStatement(query string) (*pdb.InstrumentedStmt, error) {
|
||||
s.readMu.Lock()
|
||||
defer s.readMu.Unlock()
|
||||
if stmt := s.readStatements[query]; stmt != nil {
|
||||
return stmt, nil
|
||||
}
|
||||
if len(s.presenceStatements) >= maxRetainedPresenceStatements {
|
||||
if len(s.readStatements) >= maxRetainedReadStatements {
|
||||
return nil, nil
|
||||
}
|
||||
stmt, err := s.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.presenceStatements == nil {
|
||||
s.presenceStatements = make(map[string]*pdb.InstrumentedStmt)
|
||||
if s.readStatements == nil {
|
||||
s.readStatements = make(map[string]*pdb.InstrumentedStmt)
|
||||
}
|
||||
s.presenceStatements[query] = stmt
|
||||
s.readStatements[query] = stmt
|
||||
return stmt, nil
|
||||
}
|
||||
|
||||
func (s *Store) queryRetainedChunk(resourceType string, resourceIDs []string, metricTypes []string, start, end time.Time, stepSecs int64, tiers []Tier) (map[string]map[string][]MetricPoint, error) {
|
||||
// Determine which tiers exist in the same read snapshot used below. An
|
||||
// all-raw series should cost a direct range read, not a UNION and an empty
|
||||
// overlap probe for every observation. Presence does not imply coverage:
|
||||
// every tier with any matching observations still participates.
|
||||
// EXISTS needs only indexed identity and time columns. Do not build or
|
||||
// order the value projection for each presence probe.
|
||||
idSlots := strings.TrimSuffix(strings.Repeat("?,", len(resourceIDs)), ",")
|
||||
presenceScope := "resource_type = ? AND resource_id IN (" + idSlots + ")"
|
||||
index := "idx_metrics_query_all"
|
||||
if len(metricTypes) > 0 {
|
||||
presenceScope += " AND metric_type IN (" + strings.TrimSuffix(strings.Repeat("?,", len(metricTypes)), ",") + ")"
|
||||
index = "idx_metrics_lookup"
|
||||
}
|
||||
presenceScope += " AND tier = ? AND timestamp >= ? AND timestamp <= ?"
|
||||
check := "EXISTS (SELECT 1 FROM metrics INDEXED BY " + index + " WHERE " + presenceScope + ")"
|
||||
checks := make([]string, len(tiers))
|
||||
checkParams := make([]interface{}, 0, len(tiers)*(len(resourceIDs)+len(metricTypes)+4))
|
||||
present := make([]bool, len(tiers))
|
||||
checkDestinations := make([]interface{}, len(tiers))
|
||||
for i, tier := range tiers {
|
||||
checks[i] = check
|
||||
checkParams = append(checkParams, resourceType)
|
||||
for _, id := range resourceIDs {
|
||||
checkParams = append(checkParams, id)
|
||||
var tx *pdb.InstrumentedTx
|
||||
var err error
|
||||
if stepSecs > 1 {
|
||||
// Determine which tiers exist in the same read snapshot used below. An
|
||||
// all-raw series should cost a direct range read, not a UNION and an empty
|
||||
// overlap probe for every observation. Presence does not imply coverage:
|
||||
// every tier with any matching observations still participates.
|
||||
// EXISTS needs only indexed identity and time columns. Do not build or
|
||||
// order the value projection for each presence probe.
|
||||
idSlots := strings.TrimSuffix(strings.Repeat("?,", len(resourceIDs)), ",")
|
||||
presenceScope := "resource_type = ? AND resource_id IN (" + idSlots + ")"
|
||||
index := "idx_metrics_query_all"
|
||||
if len(metricTypes) > 0 {
|
||||
presenceScope += " AND metric_type IN (" + strings.TrimSuffix(strings.Repeat("?,", len(metricTypes)), ",") + ")"
|
||||
index = "idx_metrics_lookup"
|
||||
}
|
||||
for _, metric := range metricTypes {
|
||||
checkParams = append(checkParams, metric)
|
||||
presenceScope += " AND tier = ? AND timestamp >= ? AND timestamp <= ?"
|
||||
check := "EXISTS (SELECT 1 FROM metrics INDEXED BY " + index + " WHERE " + presenceScope + ")"
|
||||
checks := make([]string, len(tiers))
|
||||
checkParams := make([]interface{}, 0, len(tiers)*(len(resourceIDs)+len(metricTypes)+4))
|
||||
present := make([]bool, len(tiers))
|
||||
checkDestinations := make([]interface{}, len(tiers))
|
||||
for i, tier := range tiers {
|
||||
checks[i] = check
|
||||
checkParams = append(checkParams, resourceType)
|
||||
for _, id := range resourceIDs {
|
||||
checkParams = append(checkParams, id)
|
||||
}
|
||||
for _, metric := range metricTypes {
|
||||
checkParams = append(checkParams, metric)
|
||||
}
|
||||
checkParams = append(checkParams, string(tier), start.Unix(), end.Unix())
|
||||
checkDestinations[i] = &present[i]
|
||||
}
|
||||
checkParams = append(checkParams, string(tier), start.Unix(), end.Unix())
|
||||
checkDestinations[i] = &present[i]
|
||||
}
|
||||
presenceQuery := "SELECT " + strings.Join(checks, ", ")
|
||||
statement, err := s.retainedPresenceStatement(presenceQuery)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prepare retained metrics presence: %w", err)
|
||||
}
|
||||
// Prepare before acquiring the transaction so a one-connection pool never
|
||||
// waits for itself while preparing a database-level statement.
|
||||
tx, err := s.db.BeginTx(context.Background(), &sql.TxOptions{ReadOnly: true})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("begin retained metrics snapshot: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
var presenceRow *sql.Row
|
||||
if statement != nil {
|
||||
bound := tx.Stmt(statement)
|
||||
defer bound.Close()
|
||||
presenceRow = bound.QueryRow(checkParams...)
|
||||
} else {
|
||||
presenceRow = tx.QueryRow(presenceQuery, checkParams...)
|
||||
}
|
||||
if err := presenceRow.Scan(checkDestinations...); err != nil {
|
||||
return nil, fmt.Errorf("inspect retained metrics tiers: %w", err)
|
||||
}
|
||||
available := make([]Tier, 0, len(tiers))
|
||||
for i, tier := range tiers {
|
||||
if present[i] {
|
||||
available = append(available, tier)
|
||||
presenceQuery := "SELECT " + strings.Join(checks, ", ")
|
||||
statement, err := s.retainedReadStatement(presenceQuery)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prepare retained metrics presence: %w", err)
|
||||
}
|
||||
// Prepare before acquiring the transaction so a one-connection pool never
|
||||
// waits for itself while preparing a database-level statement.
|
||||
tx, err = s.db.BeginTx(context.Background(), &sql.TxOptions{ReadOnly: true})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("begin retained metrics snapshot: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
var presenceRow *sql.Row
|
||||
if statement != nil {
|
||||
bound := tx.Stmt(statement)
|
||||
defer bound.Close()
|
||||
presenceRow = bound.QueryRow(checkParams...)
|
||||
} else {
|
||||
presenceRow = tx.QueryRow(presenceQuery, checkParams...)
|
||||
}
|
||||
if err := presenceRow.Scan(checkDestinations...); err != nil {
|
||||
return nil, fmt.Errorf("inspect retained metrics tiers: %w", err)
|
||||
}
|
||||
available := make([]Tier, 0, len(tiers))
|
||||
for i, tier := range tiers {
|
||||
if present[i] {
|
||||
available = append(available, tier)
|
||||
}
|
||||
}
|
||||
if len(available) == 0 {
|
||||
return make(map[string]map[string][]MetricPoint), nil
|
||||
}
|
||||
tiers = available
|
||||
}
|
||||
if len(available) == 0 {
|
||||
return make(map[string]map[string][]MetricPoint), nil
|
||||
}
|
||||
tiers = available
|
||||
// Fleet results are already ordered by series and time. Stream their
|
||||
// display buckets to avoid SQLite's fleet-wide temporary GROUP BY tree.
|
||||
// Single-resource charts aggregate in SQLite to bound rows crossing Go.
|
||||
@@ -1538,12 +1553,28 @@ func (s *Store) queryRetainedChunk(resourceType string, resourceIDs []string, me
|
||||
if streamBuckets {
|
||||
queryStep = 0
|
||||
}
|
||||
sqlQuery, params := retainedQuerySQL(resourceType, resourceIDs, metricTypes, start, end, queryStep, tiers)
|
||||
sqlQuery, params := retainedQuerySQL(resourceType, resourceIDs, metricTypes, start, end, queryStep, tiers, streamBuckets)
|
||||
|
||||
queryRows := func() (*sql.Rows, error) { return tx.Query(sqlQuery, params...) }
|
||||
if tx == nil {
|
||||
// One SQLite statement already owns a consistent read snapshot. Plain
|
||||
// retained reads need neither a separate presence probe nor a transaction
|
||||
// wrapper. Compile the canonical reconciliation query once per shape.
|
||||
statement, prepareErr := s.retainedReadStatement(sqlQuery)
|
||||
if prepareErr != nil {
|
||||
return nil, fmt.Errorf("prepare retained metrics query: %w", prepareErr)
|
||||
}
|
||||
if statement != nil {
|
||||
queryRows = func() (*sql.Rows, error) { return statement.Query(params...) }
|
||||
} else {
|
||||
queryRows = func() (*sql.Rows, error) { return s.db.Query(sqlQuery, params...) }
|
||||
}
|
||||
}
|
||||
|
||||
// Retry on SQLITE_BUSY
|
||||
var rows *sql.Rows
|
||||
for i := 0; i < 5; i++ {
|
||||
rows, err = tx.Query(sqlQuery, params...)
|
||||
rows, err = queryRows()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ func TestRetainedQueryPlansUseIndexes(t *testing.T) {
|
||||
metrics = []string{"cpu", "memory"}
|
||||
}
|
||||
end := time.Unix(2000000000, 0)
|
||||
sql, args := retainedQuerySQL("vm", []string{"vm-1", "vm-2", "vm-3"}, metrics, end.Add(-window), end, step, store.tierFallbacks(window))
|
||||
sql, args := retainedQuerySQL("vm", []string{"vm-1", "vm-2", "vm-3"}, metrics, end.Add(-window), end, step, store.tierFallbacks(window), false)
|
||||
plan := explainQueryPlan(t, db, sql, args)
|
||||
searches := 0
|
||||
for _, line := range strings.Split(plan, "\n") {
|
||||
@@ -216,7 +216,7 @@ func TestRetainedSingleTierQueryPlansUseIndexes(t *testing.T) {
|
||||
if filtered {
|
||||
metrics = []string{"cpu"}
|
||||
}
|
||||
query, args := retainedQuerySQL("vm", []string{"vm-1"}, metrics, end.Add(-time.Hour), end, 60, []Tier{tier})
|
||||
query, args := retainedQuerySQL("vm", []string{"vm-1"}, metrics, end.Add(-time.Hour), end, 60, []Tier{tier}, false)
|
||||
plan := explainQueryPlan(t, db, query, args)
|
||||
if strings.Contains(plan, "SCAN m ") || !strings.Contains(plan, "SEARCH m ") {
|
||||
t.Fatalf("single tier must use a bounded lookup: %s", plan)
|
||||
@@ -537,3 +537,14 @@ func explainQueryPlan(t *testing.T, db *sql.DB, query string, args []any) string
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func TestRetainedUnaggregatedQueryAvoidsMetricSort(t *testing.T) {
|
||||
db := newPlanTestDB(t)
|
||||
end := time.Unix(2000000040, 0)
|
||||
query, args := retainedQuerySQL("node", []string{"a", "b"}, nil, end.Add(-time.Hour), end, 0, []Tier{TierRaw, TierMinute, TierHourly}, false)
|
||||
for _, line := range strings.Split(explainQueryPlan(t, db, query, args), "\n") {
|
||||
if strings.Contains(strings.ToUpper(line), "TEMP B-TREE") {
|
||||
t.Fatalf("unaggregated all-metric read sorts despite time-ordered index: %s", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,51 +172,55 @@ func TestStoreRollupPreservesRetainedExtrema(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetainedPresenceStatementsReadCurrentSnapshot(t *testing.T) {
|
||||
db := newPlanTestDB(t)
|
||||
db.SetMaxOpenConns(1)
|
||||
store := &Store{db: pdb.Wrap(db, "presence-snapshot")}
|
||||
end := time.Unix(2000000040, 0)
|
||||
start := end.Add(-time.Hour)
|
||||
query := func(id string) []MetricPoint {
|
||||
t.Helper()
|
||||
points, err := store.Query("node", id, "cpu", start, end, 60)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return points
|
||||
}
|
||||
if points := query("a"); len(points) != 0 {
|
||||
t.Fatalf("empty inventory: %+v", points)
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO metrics(resource_type,resource_id,metric_type,tier,timestamp,value) VALUES ('node','a','cpu','minute',?,17)`, end.Add(-time.Minute).Unix()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if points := query("a"); len(points) != 1 || points[0].Value != 17 {
|
||||
t.Fatalf("newly present tier was hidden: %+v", points)
|
||||
}
|
||||
if points := query("b"); len(points) != 0 {
|
||||
t.Fatalf("statement reused another resource's result: %+v", points)
|
||||
}
|
||||
if _, err := db.Exec("DELETE FROM metrics"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if points := query("a"); len(points) != 0 {
|
||||
t.Fatalf("removed history remained present: %+v", points)
|
||||
}
|
||||
// Vary the parameter-count shape beyond the bound. Uncached shapes must
|
||||
// remain functional without expanding the retained statement set.
|
||||
for n := 1; n <= maxRetainedPresenceStatements+2; n++ {
|
||||
ids := make([]string, n)
|
||||
for i := range ids {
|
||||
ids[i] = fmt.Sprintf("missing-%d", i)
|
||||
}
|
||||
got, err := store.QueryAllBatch("node", ids, start, end, 60)
|
||||
if err != nil || len(got) != 0 {
|
||||
t.Fatalf("shape %d: %+v, %v", n, got, err)
|
||||
}
|
||||
}
|
||||
if len(store.presenceStatements) > maxRetainedPresenceStatements {
|
||||
t.Fatalf("unbounded compiled SQL retention: %d", len(store.presenceStatements))
|
||||
func TestRetainedReadStatementsReadCurrentSnapshot(t *testing.T) {
|
||||
for _, step := range []int64{0, 60} {
|
||||
t.Run(fmt.Sprint(step), func(t *testing.T) {
|
||||
db := newPlanTestDB(t)
|
||||
db.SetMaxOpenConns(1)
|
||||
store := &Store{db: pdb.Wrap(db, "presence-snapshot")}
|
||||
end := time.Unix(2000000040, 0)
|
||||
start := end.Add(-time.Hour)
|
||||
query := func(id string) []MetricPoint {
|
||||
t.Helper()
|
||||
points, err := store.Query("node", id, "cpu", start, end, step)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return points
|
||||
}
|
||||
if points := query("a"); len(points) != 0 {
|
||||
t.Fatalf("empty inventory: %+v", points)
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO metrics(resource_type,resource_id,metric_type,tier,timestamp,value) VALUES ('node','a','cpu','minute',?,17)`, end.Add(-time.Minute).Unix()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if points := query("a"); len(points) != 1 || points[0].Value != 17 {
|
||||
t.Fatalf("newly present tier was hidden: %+v", points)
|
||||
}
|
||||
if points := query("b"); len(points) != 0 {
|
||||
t.Fatalf("statement reused another resource's result: %+v", points)
|
||||
}
|
||||
if _, err := db.Exec("DELETE FROM metrics"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if points := query("a"); len(points) != 0 {
|
||||
t.Fatalf("removed history remained present: %+v", points)
|
||||
}
|
||||
// Vary the parameter-count shape beyond the bound. Uncached shapes must
|
||||
// remain functional without expanding the retained statement set.
|
||||
for n := 1; n <= maxRetainedReadStatements+2; n++ {
|
||||
ids := make([]string, n)
|
||||
for i := range ids {
|
||||
ids[i] = fmt.Sprintf("missing-%d", i)
|
||||
}
|
||||
got, err := store.QueryAllBatch("node", ids, start, end, step)
|
||||
if err != nil || len(got) != 0 {
|
||||
t.Fatalf("shape %d: %+v, %v", n, got, err)
|
||||
}
|
||||
}
|
||||
if len(store.readStatements) > maxRetainedReadStatements {
|
||||
t.Fatalf("unbounded compiled SQL retention: %d", len(store.readStatements))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user