初版功能完成
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"remlink/internal/model"
|
||||
)
|
||||
|
||||
var ErrSessionNotFound = errors.New("Session not found")
|
||||
|
||||
func (s *Store) CreateSession(ctx context.Context, session model.Session) error {
|
||||
if session.ID == 0 || session.EngineerNodeID == "" || session.SiteNodeID == "" || !session.Status.Valid() || len(session.CIDRs) == 0 {
|
||||
return errors.New("Session requires ID, participants, valid status, and CIDRs")
|
||||
}
|
||||
if session.CreatedAt.IsZero() {
|
||||
session.CreatedAt = time.Now().UTC()
|
||||
}
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin create Session: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
id := strconv.FormatUint(session.ID, 10)
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO sessions(session_id, engineer_node_id, site_node_id, status, created_at, error_code)
|
||||
VALUES(?, ?, ?, ?, ?, NULL)
|
||||
`, id, session.EngineerNodeID, session.SiteNodeID, session.Status, formatTime(session.CreatedAt)); err != nil {
|
||||
return fmt.Errorf("insert Session %s: %w", id, err)
|
||||
}
|
||||
for _, prefix := range session.CIDRs {
|
||||
if !prefix.Addr().Is4() || prefix != prefix.Masked() || prefix.Bits() == 0 {
|
||||
return fmt.Errorf("invalid Session CIDR %s", prefix)
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `INSERT INTO session_cidrs(session_id, cidr) VALUES(?, ?)`, id, prefix.String()); err != nil {
|
||||
return fmt.Errorf("insert Session CIDR: %w", err)
|
||||
}
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO session_stats(session_id, updated_at) VALUES(?, ?)
|
||||
`, id, formatTime(session.CreatedAt)); err != nil {
|
||||
return fmt.Errorf("initialize Session stats: %w", err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("commit create Session: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSession(ctx context.Context, sessionID uint64) (model.Session, error) {
|
||||
id := strconv.FormatUint(sessionID, 10)
|
||||
var (
|
||||
session model.Session
|
||||
status, createdAt string
|
||||
activeAt, closedAt, errorCode sql.NullString
|
||||
)
|
||||
err := s.db.QueryRowContext(ctx, `
|
||||
SELECT s.engineer_node_id, s.site_node_id, s.status, s.created_at, s.active_at, s.closed_at, s.error_code,
|
||||
st.tx_bytes, st.rx_bytes, st.tx_packets, st.rx_packets
|
||||
FROM sessions s JOIN session_stats st ON st.session_id = s.session_id
|
||||
WHERE s.session_id = ?
|
||||
`, id).Scan(&session.EngineerNodeID, &session.SiteNodeID, &status, &createdAt, &activeAt, &closedAt, &errorCode,
|
||||
&session.Counters.UploadBytes, &session.Counters.DownloadBytes,
|
||||
&session.Counters.UploadPackets, &session.Counters.DownloadPackets)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return model.Session{}, fmt.Errorf("%w: %d", ErrSessionNotFound, sessionID)
|
||||
}
|
||||
if err != nil {
|
||||
return model.Session{}, fmt.Errorf("get Session %d: %w", sessionID, err)
|
||||
}
|
||||
session.ID = sessionID
|
||||
session.Status = model.SessionStatus(status)
|
||||
if !session.Status.Valid() {
|
||||
return model.Session{}, fmt.Errorf("invalid stored Session status %q", status)
|
||||
}
|
||||
session.CreatedAt, err = parseTime(createdAt)
|
||||
if err != nil {
|
||||
return model.Session{}, err
|
||||
}
|
||||
if activeAt.Valid {
|
||||
value, err := parseTime(activeAt.String)
|
||||
if err != nil {
|
||||
return model.Session{}, err
|
||||
}
|
||||
session.ActiveAt = &value
|
||||
}
|
||||
if closedAt.Valid {
|
||||
value, err := parseTime(closedAt.String)
|
||||
if err != nil {
|
||||
return model.Session{}, err
|
||||
}
|
||||
session.ClosedAt = &value
|
||||
}
|
||||
if errorCode.Valid {
|
||||
session.ErrorCode = errorCode.String
|
||||
}
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT cidr FROM session_cidrs WHERE session_id = ? ORDER BY cidr`, id)
|
||||
if err != nil {
|
||||
return model.Session{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var raw string
|
||||
if err := rows.Scan(&raw); err != nil {
|
||||
return model.Session{}, err
|
||||
}
|
||||
prefix, err := netip.ParsePrefix(raw)
|
||||
if err != nil {
|
||||
return model.Session{}, err
|
||||
}
|
||||
session.CIDRs = append(session.CIDRs, prefix)
|
||||
}
|
||||
return session, rows.Err()
|
||||
}
|
||||
|
||||
// ListSessions returns complete Sessions newest first for the Admin API.
|
||||
func (s *Store) ListSessions(ctx context.Context) ([]model.Session, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT session_id FROM sessions ORDER BY created_at DESC, session_id DESC`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list Session IDs: %w", err)
|
||||
}
|
||||
var ids []uint64
|
||||
for rows.Next() {
|
||||
var raw string
|
||||
if err := rows.Scan(&raw); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
id, err := strconv.ParseUint(raw, 10, 64)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessions := make([]model.Session, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
session, err := s.GetSession(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessions = append(sessions, session)
|
||||
}
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateSessionStatus(ctx context.Context, sessionID uint64, status model.SessionStatus, errorCode string) error {
|
||||
if !status.Valid() {
|
||||
return fmt.Errorf("invalid Session status %q", status)
|
||||
}
|
||||
now := formatTime(time.Now().UTC())
|
||||
activeAt, closedAt := any(nil), any(nil)
|
||||
if status == model.SessionActive {
|
||||
activeAt = now
|
||||
}
|
||||
if status == model.SessionClosed || status == model.SessionFailed {
|
||||
closedAt = now
|
||||
}
|
||||
var errorValue any
|
||||
if errorCode != "" {
|
||||
errorValue = errorCode
|
||||
}
|
||||
result, err := s.db.ExecContext(ctx, `
|
||||
UPDATE sessions SET status = ?, active_at = COALESCE(?, active_at),
|
||||
closed_at = COALESCE(?, closed_at), error_code = ? WHERE session_id = ?
|
||||
`, status, activeAt, closedAt, errorValue, strconv.FormatUint(sessionID, 10))
|
||||
if err != nil {
|
||||
return fmt.Errorf("update Session %d status: %w", sessionID, err)
|
||||
}
|
||||
count, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return fmt.Errorf("%w: %d", ErrSessionNotFound, sessionID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateSessionStats(ctx context.Context, sessionID uint64, counters model.SessionCounters) error {
|
||||
if counters.UploadBytes > math.MaxInt64 || counters.DownloadBytes > math.MaxInt64 ||
|
||||
counters.UploadPackets > math.MaxInt64 || counters.DownloadPackets > math.MaxInt64 {
|
||||
return errors.New("Session counters exceed SQLite INTEGER capacity")
|
||||
}
|
||||
result, err := s.db.ExecContext(ctx, `
|
||||
UPDATE session_stats SET tx_bytes = ?, rx_bytes = ?, tx_packets = ?, rx_packets = ?, updated_at = ?
|
||||
WHERE session_id = ?
|
||||
`, counters.UploadBytes, counters.DownloadBytes, counters.UploadPackets, counters.DownloadPackets,
|
||||
formatTime(time.Now().UTC()), strconv.FormatUint(sessionID, 10))
|
||||
if err != nil {
|
||||
return fmt.Errorf("update Session %d stats: %w", sessionID, err)
|
||||
}
|
||||
count, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return fmt.Errorf("%w: %d", ErrSessionNotFound, sessionID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloseOpenSessions enforces the v1 no-resume rule on Server startup.
|
||||
func (s *Store) CloseOpenSessions(ctx context.Context) (int64, error) {
|
||||
now := formatTime(time.Now().UTC())
|
||||
result, err := s.db.ExecContext(ctx, `
|
||||
UPDATE sessions SET status = 'CLOSED', closed_at = ?, error_code = NULL
|
||||
WHERE status NOT IN ('CLOSED', 'FAILED')
|
||||
`, now)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("close open Sessions at startup: %w", err)
|
||||
}
|
||||
return result.RowsAffected()
|
||||
}
|
||||
Reference in New Issue
Block a user