2021-10-11 11:11:14 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"encoding/json"
|
2021-10-20 09:30:50 +02:00
|
|
|
"fmt"
|
2021-10-11 11:11:14 +02:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2021-10-20 09:30:50 +02:00
|
|
|
"time"
|
|
|
|
|
2022-01-31 15:14:37 +01:00
|
|
|
"github.com/ClusterCockpit/cc-backend/log"
|
2022-01-27 09:40:59 +01:00
|
|
|
"github.com/ClusterCockpit/cc-backend/schema"
|
2021-10-20 09:30:50 +02:00
|
|
|
"github.com/jmoiron/sqlx"
|
2021-10-11 11:11:14 +02:00
|
|
|
)
|
|
|
|
|
2022-01-20 10:00:55 +01:00
|
|
|
// `AUTO_INCREMENT` is in a comment because of this hack:
|
|
|
|
// https://stackoverflow.com/a/41028314 (sqlite creates unique ids automatically)
|
2021-12-16 13:17:48 +01:00
|
|
|
const JOBS_DB_SCHEMA string = `
|
2022-01-20 10:00:55 +01:00
|
|
|
DROP TABLE IF EXISTS jobtag;
|
2021-10-11 11:11:14 +02:00
|
|
|
DROP TABLE IF EXISTS job;
|
|
|
|
DROP TABLE IF EXISTS tag;
|
|
|
|
|
|
|
|
CREATE TABLE job (
|
2022-01-20 10:00:55 +01:00
|
|
|
id INTEGER PRIMARY KEY /*!40101 AUTO_INCREMENT */,
|
2021-12-16 13:17:48 +01:00
|
|
|
job_id BIGINT NOT NULL,
|
|
|
|
cluster VARCHAR(255) NOT NULL,
|
2022-01-20 10:00:55 +01:00
|
|
|
start_time BIGINT NOT NULL, -- Unix timestamp
|
2021-12-16 13:17:48 +01:00
|
|
|
|
|
|
|
user VARCHAR(255) NOT NULL,
|
|
|
|
project VARCHAR(255) NOT NULL,
|
2022-01-20 10:00:55 +01:00
|
|
|
` + "`partition`" + ` VARCHAR(255) NOT NULL, -- partition is a keyword in mysql -.-
|
2021-12-16 13:17:48 +01:00
|
|
|
array_job_id BIGINT NOT NULL,
|
|
|
|
duration INT,
|
2022-01-20 10:00:55 +01:00
|
|
|
job_state VARCHAR(255) NOT NULL CHECK(job_state IN ('running', 'completed', 'failed', 'canceled', 'stopped', 'timeout')),
|
|
|
|
meta_data TEXT, -- JSON
|
|
|
|
resources TEXT NOT NULL, -- JSON
|
2021-12-16 13:17:48 +01:00
|
|
|
|
|
|
|
num_nodes INT NOT NULL,
|
|
|
|
num_hwthreads INT NOT NULL,
|
|
|
|
num_acc INT NOT NULL,
|
2022-01-20 10:00:55 +01:00
|
|
|
smt TINYINT NOT NULL DEFAULT 1 CHECK(smt IN (0, 1 )),
|
|
|
|
exclusive TINYINT NOT NULL DEFAULT 1 CHECK(exclusive IN (0, 1, 2)),
|
2022-02-15 14:25:39 +01:00
|
|
|
monitoring_status TINYINT NOT NULL DEFAULT 1 CHECK(monitoring_status IN (0, 1, 2, 3)),
|
2021-12-16 13:17:48 +01:00
|
|
|
|
|
|
|
mem_used_max REAL NOT NULL DEFAULT 0.0,
|
|
|
|
flops_any_avg REAL NOT NULL DEFAULT 0.0,
|
|
|
|
mem_bw_avg REAL NOT NULL DEFAULT 0.0,
|
|
|
|
load_avg REAL NOT NULL DEFAULT 0.0,
|
|
|
|
net_bw_avg REAL NOT NULL DEFAULT 0.0,
|
|
|
|
net_data_vol_total REAL NOT NULL DEFAULT 0.0,
|
|
|
|
file_bw_avg REAL NOT NULL DEFAULT 0.0,
|
|
|
|
file_data_vol_total REAL NOT NULL DEFAULT 0.0);
|
|
|
|
|
2021-10-11 11:11:14 +02:00
|
|
|
CREATE TABLE tag (
|
|
|
|
id INTEGER PRIMARY KEY,
|
2021-12-16 13:17:48 +01:00
|
|
|
tag_type VARCHAR(255) NOT NULL,
|
2022-02-08 12:49:28 +01:00
|
|
|
tag_name VARCHAR(255) NOT NULL,
|
|
|
|
CONSTRAINT be_unique UNIQUE (tag_type, tag_name));
|
2021-12-16 13:17:48 +01:00
|
|
|
|
2021-10-11 11:11:14 +02:00
|
|
|
CREATE TABLE jobtag (
|
|
|
|
job_id INTEGER,
|
|
|
|
tag_id INTEGER,
|
|
|
|
PRIMARY KEY (job_id, tag_id),
|
2021-12-16 13:17:48 +01:00
|
|
|
FOREIGN KEY (job_id) REFERENCES job (id) ON DELETE CASCADE,
|
|
|
|
FOREIGN KEY (tag_id) REFERENCES tag (id) ON DELETE CASCADE);
|
|
|
|
`
|
|
|
|
|
2022-02-09 15:03:12 +01:00
|
|
|
const JOBS_DB_INDEXES string = `
|
|
|
|
CREATE INDEX job_by_user ON job (user);
|
|
|
|
CREATE INDEX job_by_starttime ON job (start_time);
|
|
|
|
CREATE INDEX job_by_job_id ON job (job_id);
|
|
|
|
`
|
|
|
|
|
2021-12-16 13:17:48 +01:00
|
|
|
// Delete the tables "job", "tag" and "jobtag" from the database and
|
|
|
|
// repopulate them using the jobs found in `archive`.
|
|
|
|
func initDB(db *sqlx.DB, archive string) error {
|
|
|
|
starttime := time.Now()
|
2022-01-31 15:14:37 +01:00
|
|
|
log.Print("Building job table...")
|
2021-12-16 13:17:48 +01:00
|
|
|
|
|
|
|
// Basic database structure:
|
|
|
|
_, err := db.Exec(JOBS_DB_SCHEMA)
|
2021-10-11 11:11:14 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-12-08 10:08:41 +01:00
|
|
|
clustersDir, err := os.ReadDir(archive)
|
2021-10-11 11:11:14 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-10-20 09:30:50 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-12-17 15:49:22 +01:00
|
|
|
tx, err := db.Beginx()
|
2021-12-08 10:08:41 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-12-17 15:49:22 +01:00
|
|
|
|
2022-01-10 16:13:40 +01:00
|
|
|
stmt, err := tx.PrepareNamed(`INSERT INTO job (
|
2022-01-20 10:00:55 +01:00
|
|
|
job_id, user, project, cluster, ` + "`partition`" + `, array_job_id, num_nodes, num_hwthreads, num_acc,
|
2022-01-10 16:13:40 +01:00
|
|
|
exclusive, monitoring_status, smt, job_state, start_time, duration, resources, meta_data,
|
|
|
|
mem_used_max, flops_any_avg, mem_bw_avg, load_avg, net_bw_avg, net_data_vol_total, file_bw_avg, file_data_vol_total
|
|
|
|
) VALUES (
|
|
|
|
:job_id, :user, :project, :cluster, :partition, :array_job_id, :num_nodes, :num_hwthreads, :num_acc,
|
|
|
|
:exclusive, :monitoring_status, :smt, :job_state, :start_time, :duration, :resources, :meta_data,
|
|
|
|
:mem_used_max, :flops_any_avg, :mem_bw_avg, :load_avg, :net_bw_avg, :net_data_vol_total, :file_bw_avg, :file_data_vol_total
|
|
|
|
);`)
|
2021-12-17 15:49:22 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-01-31 15:14:37 +01:00
|
|
|
fmt.Printf("%d jobs inserted...\r", 0)
|
2021-12-08 10:08:41 +01:00
|
|
|
i := 0
|
2021-10-11 11:11:14 +02:00
|
|
|
tags := make(map[string]int64)
|
2021-12-08 10:08:41 +01:00
|
|
|
handleDirectory := func(filename string) error {
|
|
|
|
// Bundle 100 inserts into one transaction for better performance:
|
|
|
|
if i%100 == 0 {
|
|
|
|
if tx != nil {
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-17 15:49:22 +01:00
|
|
|
tx, err = db.Beginx()
|
2021-12-08 10:08:41 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-12-17 15:49:22 +01:00
|
|
|
stmt = tx.NamedStmt(stmt)
|
2021-12-08 10:08:41 +01:00
|
|
|
fmt.Printf("%d jobs inserted...\r", i)
|
|
|
|
}
|
|
|
|
|
2021-12-17 15:49:22 +01:00
|
|
|
err := loadJob(tx, stmt, tags, filename)
|
2021-12-08 10:08:41 +01:00
|
|
|
if err == nil {
|
|
|
|
i += 1
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, clusterDir := range clustersDir {
|
|
|
|
lvl1Dirs, err := os.ReadDir(filepath.Join(archive, clusterDir.Name()))
|
2021-10-11 11:11:14 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-12-08 10:08:41 +01:00
|
|
|
for _, lvl1Dir := range lvl1Dirs {
|
|
|
|
if !lvl1Dir.IsDir() {
|
|
|
|
// Could be the cluster.json file
|
2021-10-11 11:11:14 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2021-12-08 10:08:41 +01:00
|
|
|
lvl2Dirs, err := os.ReadDir(filepath.Join(archive, clusterDir.Name(), lvl1Dir.Name()))
|
2021-10-11 11:11:14 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-12-08 10:08:41 +01:00
|
|
|
for _, lvl2Dir := range lvl2Dirs {
|
|
|
|
dirpath := filepath.Join(archive, clusterDir.Name(), lvl1Dir.Name(), lvl2Dir.Name())
|
|
|
|
startTimeDirs, err := os.ReadDir(dirpath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2021-10-20 09:30:50 +02:00
|
|
|
}
|
|
|
|
|
2021-12-17 15:49:22 +01:00
|
|
|
for _, startTimeDir := range startTimeDirs {
|
|
|
|
if startTimeDir.Type().IsRegular() && startTimeDir.Name() == "meta.json" {
|
2021-12-08 10:08:41 +01:00
|
|
|
if err := handleDirectory(dirpath); err != nil {
|
2022-01-31 15:14:37 +01:00
|
|
|
log.Errorf("in %s: %s", dirpath, err.Error())
|
2021-12-08 10:08:41 +01:00
|
|
|
}
|
2021-12-17 15:49:22 +01:00
|
|
|
} else if startTimeDir.IsDir() {
|
|
|
|
if err := handleDirectory(filepath.Join(dirpath, startTimeDir.Name())); err != nil {
|
2022-01-31 15:14:37 +01:00
|
|
|
log.Errorf("in %s: %s", filepath.Join(dirpath, startTimeDir.Name()), err.Error())
|
2021-12-08 10:08:41 +01:00
|
|
|
}
|
|
|
|
}
|
2021-10-11 11:11:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-20 09:30:50 +02:00
|
|
|
if err := tx.Commit(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create indexes after inserts so that they do not
|
|
|
|
// need to be continually updated.
|
2022-02-09 15:03:12 +01:00
|
|
|
if _, err := db.Exec(JOBS_DB_INDEXES); err != nil {
|
2021-10-20 09:30:50 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-12-08 10:08:41 +01:00
|
|
|
log.Printf("A total of %d jobs have been registered in %.3f seconds.\n", i, time.Since(starttime).Seconds())
|
2021-10-11 11:11:14 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-10-26 10:24:43 +02:00
|
|
|
// Read the `meta.json` file at `path` and insert it to the database using the prepared
|
|
|
|
// insert statement `stmt`. `tags` maps all existing tags to their database ID.
|
2021-12-17 15:49:22 +01:00
|
|
|
func loadJob(tx *sqlx.Tx, stmt *sqlx.NamedStmt, tags map[string]int64, path string) error {
|
2021-10-11 11:11:14 +02:00
|
|
|
f, err := os.Open(filepath.Join(path, "meta.json"))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
2022-01-07 09:39:00 +01:00
|
|
|
var jobMeta schema.JobMeta = schema.JobMeta{BaseJob: schema.JobDefaults}
|
|
|
|
if err := json.NewDecoder(bufio.NewReader(f)).Decode(&jobMeta); err != nil {
|
2021-10-11 11:11:14 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-01-07 09:39:00 +01:00
|
|
|
job := schema.Job{
|
2022-01-20 10:00:55 +01:00
|
|
|
BaseJob: jobMeta.BaseJob,
|
|
|
|
StartTime: time.Unix(jobMeta.StartTime, 0),
|
|
|
|
StartTimeUnix: jobMeta.StartTime,
|
2022-01-07 09:39:00 +01:00
|
|
|
}
|
|
|
|
|
2021-12-16 13:17:48 +01:00
|
|
|
// TODO: Other metrics...
|
2022-01-07 09:39:00 +01:00
|
|
|
job.FlopsAnyAvg = loadJobStat(&jobMeta, "flops_any")
|
|
|
|
job.MemBwAvg = loadJobStat(&jobMeta, "mem_bw")
|
2021-10-11 11:11:14 +02:00
|
|
|
|
2021-12-17 15:49:22 +01:00
|
|
|
job.RawResources, err = json.Marshal(job.Resources)
|
2021-12-16 13:17:48 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-12-17 15:49:22 +01:00
|
|
|
res, err := stmt.Exec(job)
|
2021-10-11 11:11:14 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
id, err := res.LastInsertId()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tag := range job.Tags {
|
|
|
|
tagstr := tag.Name + ":" + tag.Type
|
|
|
|
tagId, ok := tags[tagstr]
|
|
|
|
if !ok {
|
2021-10-20 09:30:50 +02:00
|
|
|
res, err := tx.Exec(`INSERT INTO tag (tag_name, tag_type) VALUES (?, ?)`, tag.Name, tag.Type)
|
2021-10-11 11:11:14 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
tagId, err = res.LastInsertId()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
tags[tagstr] = tagId
|
|
|
|
}
|
|
|
|
|
2021-10-20 09:30:50 +02:00
|
|
|
if _, err := tx.Exec(`INSERT INTO jobtag (job_id, tag_id) VALUES (?, ?)`, id, tagId); err != nil {
|
2021-10-11 11:11:14 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-12-17 15:49:22 +01:00
|
|
|
func loadJobStat(job *schema.JobMeta, metric string) float64 {
|
2021-10-11 11:11:14 +02:00
|
|
|
if stats, ok := job.Statistics[metric]; ok {
|
2021-12-17 15:49:22 +01:00
|
|
|
return stats.Avg
|
2021-10-11 11:11:14 +02:00
|
|
|
}
|
|
|
|
|
2021-12-17 15:49:22 +01:00
|
|
|
return 0.0
|
2021-10-11 11:11:14 +02:00
|
|
|
}
|