mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-07-27 00:37:14 +02:00
cc-lib changed JobData, ScopedJobStats and Job.Statistics from flat maps to structs (a .Metrics map plus array-valued Groups) to represent filesystem (and future interconnect) metric groups. Migrate all construction, indexing and iteration to the new API across the archive backends, metricstore query path, metric dispatcher, archiver, importer, tagger, taskmanager, repository and API layers. Semantics: DecodeJobStats now projects JobData.Groups into ScopedJobStats.Groups, and the archiver derives per-filesystem node-scope statistics into Job.Statistics.Groups. deepCopy, resampling and the metric/scope filter are group-aware. The metricstore internal storage (buffers, selector tree, checkpoint/parquet) is unchanged; all conversion stays at the LoadData/archive-codec seam. Note: requires the corresponding cc-lib release; bump the cc-lib dependency version once tagged (a local go.mod replace was used during development and is intentionally not committed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
692 lines
20 KiB
Go
692 lines
20 KiB
Go
// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
|
|
// All rights reserved. This file is part of cc-backend.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package archive
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"os"
|
|
"slices"
|
|
"strconv"
|
|
"sync"
|
|
"text/tabwriter"
|
|
"time"
|
|
|
|
"github.com/ClusterCockpit/cc-backend/internal/config"
|
|
cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
|
|
"github.com/ClusterCockpit/cc-lib/v2/schema"
|
|
"github.com/ClusterCockpit/cc-lib/v2/util"
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
// SqliteArchiveConfig holds the configuration for the SQLite archive backend.
|
|
type SqliteArchiveConfig struct {
|
|
DBPath string `json:"db-path"` // Path to SQLite database file
|
|
}
|
|
|
|
// SqliteArchive implements ArchiveBackend using a SQLite database with BLOB storage.
|
|
// Job metadata and data are stored as JSON BLOBs with indexes for fast queries.
|
|
//
|
|
// Uses WAL (Write-Ahead Logging) mode for better concurrency and a 64MB cache.
|
|
type SqliteArchive struct {
|
|
db *sql.DB // SQLite database connection
|
|
clusters []string // List of discovered cluster names
|
|
}
|
|
|
|
// sqliteSchema defines the database schema for SQLite archive backend.
|
|
// Jobs table: stores job metadata and data as BLOBs with compression flag
|
|
// Clusters table: stores cluster configurations
|
|
// Metadata table: stores version and other key-value pairs
|
|
const sqliteSchema = `
|
|
CREATE TABLE IF NOT EXISTS jobs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
job_id INTEGER NOT NULL,
|
|
cluster TEXT NOT NULL,
|
|
start_time INTEGER NOT NULL,
|
|
meta_json BLOB NOT NULL,
|
|
data_json BLOB,
|
|
data_compressed BOOLEAN DEFAULT 0,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
UNIQUE(job_id, cluster, start_time)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_jobs_cluster ON jobs(cluster);
|
|
CREATE INDEX IF NOT EXISTS idx_jobs_start_time ON jobs(start_time);
|
|
CREATE INDEX IF NOT EXISTS idx_jobs_order ON jobs(cluster, start_time);
|
|
CREATE INDEX IF NOT EXISTS idx_jobs_lookup ON jobs(cluster, job_id, start_time);
|
|
|
|
CREATE TABLE IF NOT EXISTS clusters (
|
|
name TEXT PRIMARY KEY,
|
|
config_json BLOB NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS metadata (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
`
|
|
|
|
func (sa *SqliteArchive) Init(rawConfig json.RawMessage) (uint64, error) {
|
|
var cfg SqliteArchiveConfig
|
|
if err := json.Unmarshal(rawConfig, &cfg); err != nil {
|
|
cclog.Warnf("SqliteArchive Init() > Unmarshal error: %#v", err)
|
|
return 0, err
|
|
}
|
|
|
|
if cfg.DBPath == "" {
|
|
err := fmt.Errorf("SqliteArchive Init(): empty database path")
|
|
cclog.Errorf("SqliteArchive Init() > config error: %v", err)
|
|
return 0, err
|
|
}
|
|
|
|
// Open SQLite database
|
|
db, err := sql.Open("sqlite3", cfg.DBPath)
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive Init() > failed to open database: %v", err)
|
|
return 0, err
|
|
}
|
|
sa.db = db
|
|
|
|
// Set pragmas for better performance
|
|
pragmas := []string{
|
|
"PRAGMA journal_mode=WAL",
|
|
"PRAGMA synchronous=NORMAL",
|
|
"PRAGMA cache_size=-64000", // 64MB cache
|
|
"PRAGMA busy_timeout=60000",
|
|
}
|
|
for _, pragma := range pragmas {
|
|
if _, err := sa.db.Exec(pragma); err != nil {
|
|
cclog.Warnf("SqliteArchive Init() > pragma failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Create schema
|
|
if _, err := sa.db.Exec(sqliteSchema); err != nil {
|
|
cclog.Errorf("SqliteArchive Init() > schema creation failed: %v", err)
|
|
return 0, err
|
|
}
|
|
|
|
// Check/set version
|
|
var versionStr string
|
|
err = sa.db.QueryRow("SELECT value FROM metadata WHERE key = 'version'").Scan(&versionStr)
|
|
if err == sql.ErrNoRows {
|
|
// First time initialization, set version
|
|
_, err = sa.db.Exec("INSERT INTO metadata (key, value) VALUES ('version', ?)", fmt.Sprintf("%d", Version))
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive Init() > failed to set version: %v", err)
|
|
return 0, err
|
|
}
|
|
versionStr = fmt.Sprintf("%d", Version)
|
|
} else if err != nil {
|
|
cclog.Errorf("SqliteArchive Init() > failed to read version: %v", err)
|
|
return 0, err
|
|
}
|
|
|
|
version, err := strconv.ParseUint(versionStr, 10, 64)
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive Init() > version parse error: %v", err)
|
|
return 0, err
|
|
}
|
|
|
|
if version != Version {
|
|
return version, fmt.Errorf("unsupported version %d, need %d", version, Version)
|
|
}
|
|
|
|
// Discover clusters
|
|
sa.clusters = []string{}
|
|
rows, err := sa.db.Query("SELECT DISTINCT cluster FROM jobs ORDER BY cluster")
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive Init() > failed to query clusters: %v", err)
|
|
return 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var cluster string
|
|
if err := rows.Scan(&cluster); err != nil {
|
|
cclog.Errorf("SqliteArchive Init() > failed to scan cluster: %v", err)
|
|
continue
|
|
}
|
|
sa.clusters = append(sa.clusters, cluster)
|
|
}
|
|
|
|
cclog.Infof("SqliteArchive initialized with database '%s', found %d clusters", cfg.DBPath, len(sa.clusters))
|
|
return version, nil
|
|
}
|
|
|
|
func (sa *SqliteArchive) Info() {
|
|
fmt.Printf("SQLite Job archive database\n")
|
|
|
|
ci := make(map[string]*clusterInfo)
|
|
|
|
rows, err := sa.db.Query(`
|
|
SELECT cluster, COUNT(*), MIN(start_time), MAX(start_time),
|
|
SUM(LENGTH(meta_json) + COALESCE(LENGTH(data_json), 0))
|
|
FROM jobs
|
|
GROUP BY cluster
|
|
`)
|
|
if err != nil {
|
|
cclog.Fatalf("SqliteArchive Info() > query failed: %s", err.Error())
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var cluster string
|
|
var numJobs int
|
|
var dateFirst, dateLast int64
|
|
var diskSize int64
|
|
|
|
if err := rows.Scan(&cluster, &numJobs, &dateFirst, &dateLast, &diskSize); err != nil {
|
|
cclog.Errorf("SqliteArchive Info() > scan failed: %v", err)
|
|
continue
|
|
}
|
|
|
|
ci[cluster] = &clusterInfo{
|
|
numJobs: numJobs,
|
|
dateFirst: dateFirst,
|
|
dateLast: dateLast,
|
|
diskSize: float64(diskSize) / (1024 * 1024), // Convert to MB
|
|
}
|
|
}
|
|
|
|
cit := clusterInfo{dateFirst: time.Now().Unix()}
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', tabwriter.Debug)
|
|
fmt.Fprintln(w, "cluster\t#jobs\tfrom\tto\tsize (MB)")
|
|
for cluster, clusterInfo := range ci {
|
|
fmt.Fprintf(w, "%s\t%d\t%s\t%s\t%.2f\n", cluster,
|
|
clusterInfo.numJobs,
|
|
time.Unix(clusterInfo.dateFirst, 0),
|
|
time.Unix(clusterInfo.dateLast, 0),
|
|
clusterInfo.diskSize)
|
|
|
|
cit.numJobs += clusterInfo.numJobs
|
|
cit.dateFirst = util.Min(cit.dateFirst, clusterInfo.dateFirst)
|
|
cit.dateLast = util.Max(cit.dateLast, clusterInfo.dateLast)
|
|
cit.diskSize += clusterInfo.diskSize
|
|
}
|
|
|
|
fmt.Fprintf(w, "TOTAL\t%d\t%s\t%s\t%.2f\n",
|
|
cit.numJobs, time.Unix(cit.dateFirst, 0), time.Unix(cit.dateLast, 0), cit.diskSize)
|
|
w.Flush()
|
|
}
|
|
|
|
func (sa *SqliteArchive) Exists(job *schema.Job) bool {
|
|
var count int
|
|
err := sa.db.QueryRow("SELECT COUNT(*) FROM jobs WHERE job_id = ? AND cluster = ? AND start_time = ?",
|
|
job.JobID, job.Cluster, job.StartTime).Scan(&count)
|
|
return err == nil && count > 0
|
|
}
|
|
|
|
func (sa *SqliteArchive) LoadJobMeta(job *schema.Job) (*schema.Job, error) {
|
|
var metaBlob []byte
|
|
err := sa.db.QueryRow("SELECT meta_json FROM jobs WHERE job_id = ? AND cluster = ? AND start_time = ?",
|
|
job.JobID, job.Cluster, job.StartTime).Scan(&metaBlob)
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive LoadJobMeta() > query error: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
if config.Keys.Validate {
|
|
if err := schema.Validate(schema.Meta, bytes.NewReader(metaBlob)); err != nil {
|
|
return nil, fmt.Errorf("validate job meta: %v", err)
|
|
}
|
|
}
|
|
|
|
return DecodeJobMeta(bytes.NewReader(metaBlob))
|
|
}
|
|
|
|
func (sa *SqliteArchive) LoadJobData(job *schema.Job) (schema.JobData, error) {
|
|
var dataBlob []byte
|
|
var compressed bool
|
|
err := sa.db.QueryRow("SELECT data_json, data_compressed FROM jobs WHERE job_id = ? AND cluster = ? AND start_time = ?",
|
|
job.JobID, job.Cluster, job.StartTime).Scan(&dataBlob, &compressed)
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive LoadJobData() > query error: %v", err)
|
|
return schema.JobData{}, err
|
|
}
|
|
key := fmt.Sprintf("%s:%d:%d", job.Cluster, job.JobID, job.StartTime)
|
|
|
|
var reader io.Reader = bytes.NewReader(dataBlob)
|
|
if compressed {
|
|
gzipReader, err := gzip.NewReader(reader)
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive LoadJobData() > gzip error: %v", err)
|
|
return schema.JobData{}, err
|
|
}
|
|
defer gzipReader.Close()
|
|
reader = gzipReader
|
|
}
|
|
|
|
if config.Keys.Validate {
|
|
data, _ := io.ReadAll(reader)
|
|
if err := schema.Validate(schema.Data, bytes.NewReader(data)); err != nil {
|
|
return schema.JobData{}, fmt.Errorf("validate job data: %v", err)
|
|
}
|
|
return DecodeJobData(bytes.NewReader(data), key)
|
|
}
|
|
|
|
return DecodeJobData(reader, key)
|
|
}
|
|
|
|
func (sa *SqliteArchive) LoadJobStats(job *schema.Job) (schema.ScopedJobStats, error) {
|
|
var dataBlob []byte
|
|
var compressed bool
|
|
err := sa.db.QueryRow("SELECT data_json, data_compressed FROM jobs WHERE job_id = ? AND cluster = ? AND start_time = ?",
|
|
job.JobID, job.Cluster, job.StartTime).Scan(&dataBlob, &compressed)
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive LoadJobStats() > query error: %v", err)
|
|
return schema.ScopedJobStats{}, err
|
|
}
|
|
key := fmt.Sprintf("%s:%d:%d", job.Cluster, job.JobID, job.StartTime)
|
|
|
|
var reader io.Reader = bytes.NewReader(dataBlob)
|
|
if compressed {
|
|
gzipReader, err := gzip.NewReader(reader)
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive LoadJobStats() > gzip error: %v", err)
|
|
return schema.ScopedJobStats{}, err
|
|
}
|
|
defer gzipReader.Close()
|
|
reader = gzipReader
|
|
}
|
|
|
|
if config.Keys.Validate {
|
|
data, _ := io.ReadAll(reader)
|
|
if err := schema.Validate(schema.Data, bytes.NewReader(data)); err != nil {
|
|
return schema.ScopedJobStats{}, fmt.Errorf("validate job data: %v", err)
|
|
}
|
|
return DecodeJobStats(bytes.NewReader(data), key)
|
|
}
|
|
|
|
return DecodeJobStats(reader, key)
|
|
}
|
|
|
|
func (sa *SqliteArchive) LoadClusterCfg(name string) (*schema.Cluster, error) {
|
|
var configBlob []byte
|
|
err := sa.db.QueryRow("SELECT config_json FROM clusters WHERE name = ?", name).Scan(&configBlob)
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive LoadClusterCfg() > query error: %v", err)
|
|
return nil, err
|
|
}
|
|
|
|
if config.Keys.Validate {
|
|
if err := schema.Validate(schema.ClusterCfg, bytes.NewReader(configBlob)); err != nil {
|
|
cclog.Warnf("Validate cluster config: %v\n", err)
|
|
return &schema.Cluster{}, fmt.Errorf("validate cluster config: %v", err)
|
|
}
|
|
}
|
|
|
|
return DecodeCluster(bytes.NewReader(configBlob))
|
|
}
|
|
|
|
func (sa *SqliteArchive) StoreJobMeta(job *schema.Job) error {
|
|
var metaBuf bytes.Buffer
|
|
if err := EncodeJobMeta(&metaBuf, job); err != nil {
|
|
cclog.Error("SqliteArchive StoreJobMeta() > encoding error")
|
|
return err
|
|
}
|
|
|
|
now := time.Now().Unix()
|
|
_, err := sa.db.Exec(`
|
|
INSERT INTO jobs (job_id, cluster, start_time, meta_json, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(job_id, cluster, start_time) DO UPDATE SET
|
|
meta_json = excluded.meta_json,
|
|
updated_at = excluded.updated_at
|
|
`, job.JobID, job.Cluster, job.StartTime, metaBuf.Bytes(), now, now)
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive StoreJobMeta() > insert error: %v", err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (sa *SqliteArchive) ImportJob(jobMeta *schema.Job, jobData *schema.JobData) error {
|
|
var metaBuf, dataBuf bytes.Buffer
|
|
if err := EncodeJobMeta(&metaBuf, jobMeta); err != nil {
|
|
cclog.Error("SqliteArchive ImportJob() > encoding meta error")
|
|
return err
|
|
}
|
|
if err := EncodeJobData(&dataBuf, jobData); err != nil {
|
|
cclog.Error("SqliteArchive ImportJob() > encoding data error")
|
|
return err
|
|
}
|
|
|
|
var dataBytes []byte
|
|
var compressed bool
|
|
|
|
if dataBuf.Len() > 2000 {
|
|
var compressedBuf bytes.Buffer
|
|
gzipWriter := gzip.NewWriter(&compressedBuf)
|
|
if _, err := gzipWriter.Write(dataBuf.Bytes()); err != nil {
|
|
cclog.Errorf("SqliteArchive ImportJob() > gzip write error: %v", err)
|
|
return err
|
|
}
|
|
if err := gzipWriter.Close(); err != nil {
|
|
cclog.Errorf("SqliteArchive ImportJob() > gzip close error: %v", err)
|
|
return err
|
|
}
|
|
dataBytes = compressedBuf.Bytes()
|
|
compressed = true
|
|
} else {
|
|
dataBytes = dataBuf.Bytes()
|
|
compressed = false
|
|
}
|
|
|
|
now := time.Now().Unix()
|
|
_, err := sa.db.Exec(`
|
|
INSERT INTO jobs (job_id, cluster, start_time, meta_json, data_json, data_compressed, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(job_id, cluster, start_time) DO UPDATE SET
|
|
meta_json = excluded.meta_json,
|
|
data_json = excluded.data_json,
|
|
data_compressed = excluded.data_compressed,
|
|
updated_at = excluded.updated_at
|
|
`, jobMeta.JobID, jobMeta.Cluster, jobMeta.StartTime, metaBuf.Bytes(), dataBytes, compressed, now, now)
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive ImportJob() > insert error: %v", err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (sa *SqliteArchive) GetClusters() []string {
|
|
return sa.clusters
|
|
}
|
|
|
|
func (sa *SqliteArchive) CleanUp(jobs []*schema.Job) {
|
|
start := time.Now()
|
|
count := 0
|
|
|
|
tx, err := sa.db.Begin()
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive CleanUp() > transaction error: %v", err)
|
|
return
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
stmt, err := tx.Prepare("DELETE FROM jobs WHERE job_id = ? AND cluster = ? AND start_time = ?")
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive CleanUp() > prepare error: %v", err)
|
|
return
|
|
}
|
|
defer stmt.Close()
|
|
|
|
for _, job := range jobs {
|
|
if job == nil {
|
|
cclog.Errorf("SqliteArchive CleanUp() error: job is nil")
|
|
continue
|
|
}
|
|
|
|
if _, err := stmt.Exec(job.JobID, job.Cluster, job.StartTime); err != nil {
|
|
cclog.Errorf("SqliteArchive CleanUp() > delete error: %v", err)
|
|
} else {
|
|
count++
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
cclog.Errorf("SqliteArchive CleanUp() > commit error: %v", err)
|
|
return
|
|
}
|
|
|
|
cclog.Infof("Retention Service - Remove %d jobs from SQLite in %s", count, time.Since(start))
|
|
}
|
|
|
|
func (sa *SqliteArchive) Move(jobs []*schema.Job, targetPath string) {
|
|
// For SQLite, "move" means updating the cluster field or similar
|
|
// This is interpretation-dependent; for now we'll just log
|
|
cclog.Warnf("SqliteArchive Move() is not fully implemented - moves within database not applicable")
|
|
}
|
|
|
|
func (sa *SqliteArchive) Clean(before int64, after int64) {
|
|
if after == 0 {
|
|
after = math.MaxInt64
|
|
}
|
|
|
|
result, err := sa.db.Exec("DELETE FROM jobs WHERE start_time < ? OR start_time > ?", before, after)
|
|
if err != nil {
|
|
cclog.Fatalf("SqliteArchive Clean() > delete error: %s", err.Error())
|
|
}
|
|
|
|
rowsAffected, _ := result.RowsAffected()
|
|
cclog.Infof("SqliteArchive Clean() removed %d jobs", rowsAffected)
|
|
}
|
|
|
|
func (sa *SqliteArchive) Compress(jobs []*schema.Job) {
|
|
var cnt int
|
|
start := time.Now()
|
|
|
|
tx, err := sa.db.Begin()
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive Compress() > transaction error: %v", err)
|
|
return
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
stmt, err := tx.Prepare("UPDATE jobs SET data_json = ?, data_compressed = 1 WHERE job_id = ? AND cluster = ? AND start_time = ?")
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive Compress() > prepare error: %v", err)
|
|
return
|
|
}
|
|
defer stmt.Close()
|
|
|
|
for _, job := range jobs {
|
|
var dataBlob []byte
|
|
var compressed bool
|
|
err := sa.db.QueryRow("SELECT data_json, data_compressed FROM jobs WHERE job_id = ? AND cluster = ? AND start_time = ?",
|
|
job.JobID, job.Cluster, job.StartTime).Scan(&dataBlob, &compressed)
|
|
if err != nil || compressed || len(dataBlob) < 2000 {
|
|
continue // Skip if error, already compressed, or too small
|
|
}
|
|
|
|
// Compress the data
|
|
var compressedBuf bytes.Buffer
|
|
gzipWriter := gzip.NewWriter(&compressedBuf)
|
|
if _, err := gzipWriter.Write(dataBlob); err != nil {
|
|
cclog.Errorf("SqliteArchive Compress() > gzip error: %v", err)
|
|
gzipWriter.Close()
|
|
continue
|
|
}
|
|
gzipWriter.Close()
|
|
|
|
if _, err := stmt.Exec(compressedBuf.Bytes(), job.JobID, job.Cluster, job.StartTime); err != nil {
|
|
cclog.Errorf("SqliteArchive Compress() > update error: %v", err)
|
|
} else {
|
|
cnt++
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
cclog.Errorf("SqliteArchive Compress() > commit error: %v", err)
|
|
return
|
|
}
|
|
|
|
cclog.Infof("Compression Service - %d jobs in SQLite took %s", cnt, time.Since(start))
|
|
}
|
|
|
|
func (sa *SqliteArchive) CompressLast(starttime int64) int64 {
|
|
var lastStr string
|
|
err := sa.db.QueryRow("SELECT value FROM metadata WHERE key = 'compress_last'").Scan(&lastStr)
|
|
|
|
var last int64
|
|
if err == sql.ErrNoRows {
|
|
last = starttime
|
|
} else if err != nil {
|
|
cclog.Errorf("SqliteArchive CompressLast() > query error: %v", err)
|
|
last = starttime
|
|
} else {
|
|
last, err = strconv.ParseInt(lastStr, 10, 64)
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive CompressLast() > parse error: %v", err)
|
|
last = starttime
|
|
}
|
|
}
|
|
|
|
cclog.Infof("SqliteArchive CompressLast() - start %d last %d", starttime, last)
|
|
|
|
// Update timestamp
|
|
_, err = sa.db.Exec(`
|
|
INSERT INTO metadata (key, value) VALUES ('compress_last', ?)
|
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
`, fmt.Sprintf("%d", starttime))
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive CompressLast() > update error: %v", err)
|
|
}
|
|
|
|
return last
|
|
}
|
|
|
|
type sqliteJobRow struct {
|
|
metaBlob []byte
|
|
dataBlob []byte
|
|
compressed bool
|
|
}
|
|
|
|
func (sa *SqliteArchive) Iter(loadMetricData bool) <-chan JobContainer {
|
|
ch := make(chan JobContainer)
|
|
|
|
go func() {
|
|
defer close(ch)
|
|
|
|
const chunkSize = 1000
|
|
offset := 0
|
|
|
|
var query string
|
|
if loadMetricData {
|
|
query = "SELECT meta_json, data_json, data_compressed FROM jobs ORDER BY cluster, start_time LIMIT ? OFFSET ?"
|
|
} else {
|
|
query = "SELECT meta_json FROM jobs ORDER BY cluster, start_time LIMIT ? OFFSET ?"
|
|
}
|
|
|
|
numWorkers := 4
|
|
jobRows := make(chan sqliteJobRow, numWorkers*2)
|
|
var wg sync.WaitGroup
|
|
|
|
for range numWorkers {
|
|
wg.Go(func() {
|
|
for row := range jobRows {
|
|
job, err := DecodeJobMeta(bytes.NewReader(row.metaBlob))
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive Iter() > decode meta error: %v", err)
|
|
continue
|
|
}
|
|
|
|
if loadMetricData && row.dataBlob != nil {
|
|
var reader io.Reader = bytes.NewReader(row.dataBlob)
|
|
if row.compressed {
|
|
gzipReader, err := gzip.NewReader(reader)
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive Iter() > gzip error: %v", err)
|
|
ch <- JobContainer{Meta: job, Data: nil}
|
|
continue
|
|
}
|
|
decompressed, err := io.ReadAll(gzipReader)
|
|
gzipReader.Close()
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive Iter() > decompress error: %v", err)
|
|
ch <- JobContainer{Meta: job, Data: nil}
|
|
continue
|
|
}
|
|
reader = bytes.NewReader(decompressed)
|
|
}
|
|
|
|
key := fmt.Sprintf("%s:%d:%d", job.Cluster, job.JobID, job.StartTime)
|
|
jobData, err := DecodeJobData(reader, key)
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive Iter() > decode data error: %v", err)
|
|
ch <- JobContainer{Meta: job, Data: nil}
|
|
} else {
|
|
ch <- JobContainer{Meta: job, Data: &jobData}
|
|
}
|
|
} else {
|
|
ch <- JobContainer{Meta: job, Data: nil}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
for {
|
|
rows, err := sa.db.Query(query, chunkSize, offset)
|
|
if err != nil {
|
|
cclog.Fatalf("SqliteArchive Iter() > query error: %s", err.Error())
|
|
}
|
|
|
|
rowCount := 0
|
|
for rows.Next() {
|
|
var row sqliteJobRow
|
|
|
|
if loadMetricData {
|
|
if err := rows.Scan(&row.metaBlob, &row.dataBlob, &row.compressed); err != nil {
|
|
cclog.Errorf("SqliteArchive Iter() > scan error: %v", err)
|
|
continue
|
|
}
|
|
} else {
|
|
if err := rows.Scan(&row.metaBlob); err != nil {
|
|
cclog.Errorf("SqliteArchive Iter() > scan error: %v", err)
|
|
continue
|
|
}
|
|
row.dataBlob = nil
|
|
row.compressed = false
|
|
}
|
|
|
|
jobRows <- row
|
|
rowCount++
|
|
}
|
|
rows.Close()
|
|
|
|
if rowCount < chunkSize {
|
|
break
|
|
}
|
|
|
|
offset += chunkSize
|
|
}
|
|
|
|
close(jobRows)
|
|
wg.Wait()
|
|
}()
|
|
|
|
return ch
|
|
}
|
|
|
|
func (sa *SqliteArchive) StoreClusterCfg(name string, config *schema.Cluster) error {
|
|
var configBuf bytes.Buffer
|
|
if err := EncodeCluster(&configBuf, config); err != nil {
|
|
cclog.Error("SqliteArchive StoreClusterCfg() > encoding error")
|
|
return err
|
|
}
|
|
|
|
now := time.Now().Unix()
|
|
_, err := sa.db.Exec(`
|
|
INSERT INTO clusters (name, config_json, updated_at)
|
|
VALUES (?, ?, ?)
|
|
ON CONFLICT(name) DO UPDATE SET
|
|
config_json = excluded.config_json,
|
|
updated_at = excluded.updated_at
|
|
`, name, configBuf.Bytes(), now)
|
|
if err != nil {
|
|
cclog.Errorf("SqliteArchive StoreClusterCfg() > insert error: %v", err)
|
|
return err
|
|
}
|
|
|
|
// Update clusters list if new
|
|
found := slices.Contains(sa.clusters, name)
|
|
if !found {
|
|
sa.clusters = append(sa.clusters, name)
|
|
}
|
|
|
|
return nil
|
|
}
|