feat: backend executor implement
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
package transaction
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const backendService = "backend"
|
||||
|
||||
// BackendContainerDeployment is the last container slot committed for backend.
|
||||
// Runtime inspection remains authoritative for the current external state; this
|
||||
// record distinguishes a fresh installation from loss or drift after deployment.
|
||||
type BackendContainerDeployment struct {
|
||||
ActivePort int
|
||||
ContainerName string
|
||||
ImageDigest string
|
||||
ContainerID string
|
||||
TransactionID string
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// BackendContainerDeployment returns the single committed backend container state.
|
||||
func (s *Store) BackendContainerDeployment(ctx context.Context) (BackendContainerDeployment, error) {
|
||||
return scanBackendContainerDeployment(s.db.QueryRowContext(ctx, `
|
||||
SELECT active_port, container_name, image_digest, container_id,
|
||||
transaction_id, updated_at
|
||||
FROM backend_container_deployment
|
||||
WHERE singleton_id = 1`))
|
||||
}
|
||||
|
||||
// HasCommittedBackendContainerTransactionHistory reports whether this store
|
||||
// contains a committed container backend transaction. This is the legacy
|
||||
// deployment evidence used when a database predates the deployment table.
|
||||
// Failed and rolled-back first-install attempts do not mark a machine deployed.
|
||||
func (s *Store) HasCommittedBackendContainerTransactionHistory(ctx context.Context) (bool, error) {
|
||||
var found int
|
||||
if err := s.db.QueryRowContext(ctx, `
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM transactions
|
||||
WHERE service = ?
|
||||
AND idempotency_key GLOB 'backend:container:*'
|
||||
AND state = ?
|
||||
)`, backendService, StateCommitted).Scan(&found); err != nil {
|
||||
return false, fmt.Errorf("query committed backend container transaction history: %w", err)
|
||||
}
|
||||
return found == 1, nil
|
||||
}
|
||||
|
||||
// CommitBackendContainerDeployment atomically records the active container and
|
||||
// commits its transaction. A crash cannot leave COMMITTED without the matching
|
||||
// deployment row, or publish a deployment row for an unfinished transaction.
|
||||
func (s *Store) CommitBackendContainerDeployment(
|
||||
ctx context.Context,
|
||||
transactionID string,
|
||||
deployment BackendContainerDeployment,
|
||||
message string,
|
||||
) (Transaction, error) {
|
||||
if transactionID == "" || strings.TrimSpace(transactionID) != transactionID {
|
||||
return Transaction{}, errors.New("exact transaction ID is required")
|
||||
}
|
||||
if err := validateBackendContainerDeployment(deployment); err != nil {
|
||||
return Transaction{}, err
|
||||
}
|
||||
|
||||
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
|
||||
if err != nil {
|
||||
return Transaction{}, fmt.Errorf("begin backend container deployment commit: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
record, err := getTransactionByID(ctx, tx, transactionID)
|
||||
if err != nil {
|
||||
return Transaction{}, err
|
||||
}
|
||||
if record.Service != backendService {
|
||||
return Transaction{}, fmt.Errorf("transaction %s service must be %s", transactionID, backendService)
|
||||
}
|
||||
if record.State != StateDraining {
|
||||
return Transaction{}, &TransitionError{From: record.State, To: StateCommitted}
|
||||
}
|
||||
|
||||
now := s.now().UTC()
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO backend_container_deployment (
|
||||
singleton_id, active_port, container_name, image_digest,
|
||||
container_id, transaction_id, updated_at
|
||||
) VALUES (1, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(singleton_id) DO UPDATE SET
|
||||
active_port = excluded.active_port,
|
||||
container_name = excluded.container_name,
|
||||
image_digest = excluded.image_digest,
|
||||
container_id = excluded.container_id,
|
||||
transaction_id = excluded.transaction_id,
|
||||
updated_at = excluded.updated_at`,
|
||||
deployment.ActivePort,
|
||||
deployment.ContainerName,
|
||||
deployment.ImageDigest,
|
||||
deployment.ContainerID,
|
||||
transactionID,
|
||||
formatTime(now),
|
||||
)
|
||||
if err != nil {
|
||||
return Transaction{}, fmt.Errorf("write backend container deployment: %w", err)
|
||||
}
|
||||
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
UPDATE transactions
|
||||
SET state = ?, version = version + 1, updated_at = ?
|
||||
WHERE id = ? AND version = ? AND state = ?`,
|
||||
StateCommitted,
|
||||
formatTime(now),
|
||||
transactionID,
|
||||
record.Version,
|
||||
StateDraining,
|
||||
)
|
||||
if err != nil {
|
||||
return Transaction{}, fmt.Errorf("commit backend container transaction state: %w", err)
|
||||
}
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return Transaction{}, fmt.Errorf("read committed backend container transaction rows: %w", err)
|
||||
}
|
||||
if rows != 1 {
|
||||
return Transaction{}, errors.New("backend container transaction changed concurrently")
|
||||
}
|
||||
if err := insertEvent(ctx, tx, transactionID, "", "TRANSACTION_STATE_CHANGED", record.State, StateCommitted, message, now); err != nil {
|
||||
return Transaction{}, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return Transaction{}, fmt.Errorf("commit backend container deployment: %w", err)
|
||||
}
|
||||
|
||||
record.State = StateCommitted
|
||||
record.Version++
|
||||
record.UpdatedAt = now
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func validateBackendContainerDeployment(deployment BackendContainerDeployment) error {
|
||||
if deployment.ActivePort != 8080 && deployment.ActivePort != 8081 {
|
||||
return fmt.Errorf("backend container deployment port must be 8080 or 8081: %d", deployment.ActivePort)
|
||||
}
|
||||
for _, field := range []struct {
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{"container name", deployment.ContainerName},
|
||||
{"image digest", deployment.ImageDigest},
|
||||
{"container ID", deployment.ContainerID},
|
||||
} {
|
||||
if field.value == "" || strings.TrimSpace(field.value) != field.value {
|
||||
return fmt.Errorf("exact backend container %s is required", field.name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func scanBackendContainerDeployment(row rowScanner) (BackendContainerDeployment, error) {
|
||||
var deployment BackendContainerDeployment
|
||||
var updatedAt string
|
||||
if err := row.Scan(
|
||||
&deployment.ActivePort,
|
||||
&deployment.ContainerName,
|
||||
&deployment.ImageDigest,
|
||||
&deployment.ContainerID,
|
||||
&deployment.TransactionID,
|
||||
&updatedAt,
|
||||
); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return BackendContainerDeployment{}, ErrNotFound
|
||||
}
|
||||
return BackendContainerDeployment{}, fmt.Errorf("scan backend container deployment: %w", err)
|
||||
}
|
||||
parsed, err := parseTime(updatedAt)
|
||||
if err != nil {
|
||||
return BackendContainerDeployment{}, fmt.Errorf("parse backend container deployment update time: %w", err)
|
||||
}
|
||||
deployment.UpdatedAt = parsed
|
||||
return deployment, nil
|
||||
}
|
||||
Reference in New Issue
Block a user