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>
201 lines
5.2 KiB
Go
201 lines
5.2 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 parquet
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/ClusterCockpit/cc-lib/v2/schema"
|
|
)
|
|
|
|
// JobToParquetRow converts job metadata and metric data into a flat ParquetJobRow.
|
|
// Nested fields are marshaled to JSON; metric data is gzip-compressed JSON.
|
|
func JobToParquetRow(meta *schema.Job, data *schema.JobData) (*ParquetJobRow, error) {
|
|
resourcesJSON, err := json.Marshal(meta.Resources)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal resources: %w", err)
|
|
}
|
|
|
|
var statisticsJSON []byte
|
|
if meta.Statistics.Metrics != nil {
|
|
statisticsJSON, err = json.Marshal(meta.Statistics)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal statistics: %w", err)
|
|
}
|
|
}
|
|
|
|
var tagsJSON []byte
|
|
if len(meta.Tags) > 0 {
|
|
tagsJSON, err = json.Marshal(meta.Tags)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal tags: %w", err)
|
|
}
|
|
}
|
|
|
|
var metaDataJSON []byte
|
|
if meta.MetaData != nil {
|
|
metaDataJSON, err = json.Marshal(meta.MetaData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal metadata: %w", err)
|
|
}
|
|
}
|
|
|
|
var footprintJSON []byte
|
|
if meta.Footprint != nil {
|
|
footprintJSON, err = json.Marshal(meta.Footprint)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal footprint: %w", err)
|
|
}
|
|
}
|
|
|
|
var energyFootJSON []byte
|
|
if meta.EnergyFootprint != nil {
|
|
energyFootJSON, err = json.Marshal(meta.EnergyFootprint)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal energy footprint: %w", err)
|
|
}
|
|
}
|
|
|
|
metricDataGz, err := compressJobData(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("compress metric data: %w", err)
|
|
}
|
|
|
|
return &ParquetJobRow{
|
|
JobID: meta.JobID,
|
|
Cluster: meta.Cluster,
|
|
SubCluster: meta.SubCluster,
|
|
Partition: meta.Partition,
|
|
Project: meta.Project,
|
|
User: meta.User,
|
|
State: string(meta.State),
|
|
StartTime: meta.StartTime,
|
|
Duration: meta.Duration,
|
|
Walltime: meta.Walltime,
|
|
NumNodes: meta.NumNodes,
|
|
NumHWThreads: meta.NumHWThreads,
|
|
NumAcc: meta.NumAcc,
|
|
Energy: meta.Energy,
|
|
SMT: meta.SMT,
|
|
ResourcesJSON: resourcesJSON,
|
|
StatisticsJSON: statisticsJSON,
|
|
TagsJSON: tagsJSON,
|
|
MetaDataJSON: metaDataJSON,
|
|
FootprintJSON: footprintJSON,
|
|
EnergyFootJSON: energyFootJSON,
|
|
MetricDataGz: metricDataGz,
|
|
}, nil
|
|
}
|
|
|
|
// ParquetRowToJob converts a ParquetJobRow back into job metadata and metric data.
|
|
// This is the reverse of JobToParquetRow.
|
|
func ParquetRowToJob(row *ParquetJobRow) (*schema.Job, *schema.JobData, error) {
|
|
meta := &schema.Job{
|
|
JobID: row.JobID,
|
|
Cluster: row.Cluster,
|
|
SubCluster: row.SubCluster,
|
|
Partition: row.Partition,
|
|
Project: row.Project,
|
|
User: row.User,
|
|
State: schema.JobState(row.State),
|
|
StartTime: row.StartTime,
|
|
Duration: row.Duration,
|
|
Walltime: row.Walltime,
|
|
NumNodes: row.NumNodes,
|
|
NumHWThreads: row.NumHWThreads,
|
|
NumAcc: row.NumAcc,
|
|
Energy: row.Energy,
|
|
SMT: row.SMT,
|
|
}
|
|
|
|
if len(row.ResourcesJSON) > 0 {
|
|
if err := json.Unmarshal(row.ResourcesJSON, &meta.Resources); err != nil {
|
|
return nil, nil, fmt.Errorf("unmarshal resources: %w", err)
|
|
}
|
|
}
|
|
|
|
if len(row.StatisticsJSON) > 0 {
|
|
if err := json.Unmarshal(row.StatisticsJSON, &meta.Statistics); err != nil {
|
|
return nil, nil, fmt.Errorf("unmarshal statistics: %w", err)
|
|
}
|
|
}
|
|
|
|
if len(row.TagsJSON) > 0 {
|
|
if err := json.Unmarshal(row.TagsJSON, &meta.Tags); err != nil {
|
|
return nil, nil, fmt.Errorf("unmarshal tags: %w", err)
|
|
}
|
|
}
|
|
|
|
if len(row.MetaDataJSON) > 0 {
|
|
if err := json.Unmarshal(row.MetaDataJSON, &meta.MetaData); err != nil {
|
|
return nil, nil, fmt.Errorf("unmarshal metadata: %w", err)
|
|
}
|
|
}
|
|
|
|
if len(row.FootprintJSON) > 0 {
|
|
if err := json.Unmarshal(row.FootprintJSON, &meta.Footprint); err != nil {
|
|
return nil, nil, fmt.Errorf("unmarshal footprint: %w", err)
|
|
}
|
|
}
|
|
|
|
if len(row.EnergyFootJSON) > 0 {
|
|
if err := json.Unmarshal(row.EnergyFootJSON, &meta.EnergyFootprint); err != nil {
|
|
return nil, nil, fmt.Errorf("unmarshal energy footprint: %w", err)
|
|
}
|
|
}
|
|
|
|
data, err := decompressJobData(row.MetricDataGz)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("decompress metric data: %w", err)
|
|
}
|
|
|
|
return meta, data, nil
|
|
}
|
|
|
|
func decompressJobData(data []byte) (*schema.JobData, error) {
|
|
gz, err := gzip.NewReader(bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer gz.Close()
|
|
|
|
var buf bytes.Buffer
|
|
if _, err := buf.ReadFrom(gz); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var jobData schema.JobData
|
|
if err := json.Unmarshal(buf.Bytes(), &jobData); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &jobData, nil
|
|
}
|
|
|
|
func compressJobData(data *schema.JobData) ([]byte, error) {
|
|
jsonBytes, err := json.Marshal(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
gz, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := gz.Write(jsonBytes); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := gz.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|