123 lines
3.1 KiB
Go
123 lines
3.1 KiB
Go
package issues
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
|
)
|
|
|
|
// TriageState is the part of an issue a person decided, kept across restarts.
|
|
// The tracker is in-memory, so without this every deploy would reopen every
|
|
// resolved and ignored issue.
|
|
type TriageState struct {
|
|
Status string `json:"status"`
|
|
ResolvedAt int64 `json:"resolvedAt,omitempty"`
|
|
}
|
|
|
|
// StatusStore persists triage state by issue id.
|
|
type StatusStore interface {
|
|
Load(ctx context.Context) (map[string]TriageState, error)
|
|
Save(ctx context.Context, id string, s TriageState) error
|
|
}
|
|
|
|
// FileStatusStore keeps all states in one JSON file, rewritten on each save.
|
|
type FileStatusStore struct {
|
|
mu sync.Mutex
|
|
path string
|
|
states map[string]TriageState
|
|
}
|
|
|
|
func NewFileStatusStore(path string) (*FileStatusStore, error) {
|
|
fs := &FileStatusStore{path: path, states: map[string]TriageState{}}
|
|
b, err := os.ReadFile(path)
|
|
if os.IsNotExist(err) {
|
|
return fs, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(b, &fs.states); err != nil {
|
|
return nil, err
|
|
}
|
|
return fs, nil
|
|
}
|
|
|
|
func (s *FileStatusStore) Load(context.Context) (map[string]TriageState, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
out := make(map[string]TriageState, len(s.states))
|
|
for k, v := range s.states {
|
|
out[k] = v
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *FileStatusStore) Save(_ context.Context, id string, st TriageState) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if st.Status == StatusOpen {
|
|
delete(s.states, id)
|
|
} else {
|
|
s.states[id] = st
|
|
}
|
|
b, err := json.Marshal(s.states)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmp := s.path + ".tmp"
|
|
if err := os.WriteFile(tmp, b, 0o644); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp, s.path)
|
|
}
|
|
|
|
// ClickHouseStatusStore keeps the latest state per issue id.
|
|
type ClickHouseStatusStore struct {
|
|
conn driver.Conn
|
|
db string
|
|
}
|
|
|
|
func NewClickHouseStatusStore(ctx context.Context, conn driver.Conn, db string) (*ClickHouseStatusStore, error) {
|
|
err := conn.Exec(ctx, fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s.issue_status (
|
|
id String,
|
|
status LowCardinality(String),
|
|
resolved_at Int64,
|
|
updated_at DateTime64(3, 'UTC')
|
|
) ENGINE = ReplacingMergeTree(updated_at)
|
|
ORDER BY id`, db))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &ClickHouseStatusStore{conn: conn, db: db}, nil
|
|
}
|
|
|
|
func (s *ClickHouseStatusStore) Load(ctx context.Context) (map[string]TriageState, error) {
|
|
rows, err := s.conn.Query(ctx, fmt.Sprintf(`SELECT id, status, resolved_at FROM %s.issue_status FINAL`, s.db))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := map[string]TriageState{}
|
|
for rows.Next() {
|
|
var id string
|
|
var st TriageState
|
|
if err := rows.Scan(&id, &st.Status, &st.ResolvedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
if st.Status != StatusOpen {
|
|
out[id] = st
|
|
}
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *ClickHouseStatusStore) Save(ctx context.Context, id string, st TriageState) error {
|
|
return s.conn.Exec(ctx, fmt.Sprintf(`INSERT INTO %s.issue_status (id, status, resolved_at, updated_at) VALUES (?, ?, ?, ?)`, s.db),
|
|
id, st.Status, st.ResolvedAt, time.Now().UTC())
|
|
}
|