172 lines
6.1 KiB
Go
172 lines
6.1 KiB
Go
package rpc
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
agentv1 "git.ipao.vip/rogee/go-sip/gen/agent/v1"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
type savedOperation struct {
|
|
Digest string `json:"digest"`
|
|
Receipt *agentv1.OperationReceipt `json:"receipt"`
|
|
Control *agentv1.ApplyTaskControlResponse `json:"control,omitempty"`
|
|
}
|
|
type savedExecution struct {
|
|
Binding *agentv1.ExecutionBinding `json:"binding"`
|
|
Digest string `json:"digest"`
|
|
State agentv1.ExecutionState `json:"state"`
|
|
Revision int64 `json:"revision"`
|
|
CallState string `json:"call_state"`
|
|
ControlAction agentv1.ControlAction `json:"control_action"`
|
|
}
|
|
type executionJournal struct {
|
|
Version int `json:"version"`
|
|
Mode string `json:"mode"`
|
|
AgentID string `json:"agent_id"`
|
|
CellID string `json:"cell_id"`
|
|
Operations map[string]savedOperation `json:"operations"`
|
|
Executions map[string]savedExecution `json:"executions"`
|
|
}
|
|
|
|
func (s *Server) loadExecutionJournal() error {
|
|
if s.executionPath == "" {
|
|
return nil
|
|
}
|
|
raw, err := os.ReadFile(s.executionPath)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
if _, sessionErr := os.Stat(s.sessions.statePath); sessionErr == nil {
|
|
return errors.New("execution journal missing beside existing session journal")
|
|
} else if !errors.Is(sessionErr, os.ErrNotExist) {
|
|
return sessionErr
|
|
}
|
|
// Establish the empty execution journal before any activation can be
|
|
// acknowledged; later absence must not silently erase execution history.
|
|
return s.persistExecutionJournalLocked()
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(raw))
|
|
decoder.DisallowUnknownFields()
|
|
var journal executionJournal
|
|
if err := decoder.Decode(&journal); err != nil {
|
|
return err
|
|
}
|
|
if err := decoder.Decode(new(any)); err != io.EOF {
|
|
return errors.New("execution journal has trailing data")
|
|
}
|
|
if journal.Version != 1 || journal.Mode != s.mode || journal.AgentID != s.status.AgentId || journal.CellID != s.status.CellId || journal.Operations == nil || journal.Executions == nil {
|
|
return errors.New("execution journal version or identity is invalid")
|
|
}
|
|
for key, record := range journal.Operations {
|
|
if record.Receipt == nil || record.Receipt.Meta == nil || len(record.Digest) != 64 {
|
|
return errors.New("invalid persisted operation")
|
|
}
|
|
if record.Control != nil && !proto.Equal(record.Control.Receipt, record.Receipt) {
|
|
return errors.New("control receipt differs from operation receipt")
|
|
}
|
|
s.operations[key] = operationRecord{digest: record.Digest, receipt: record.Receipt, control: record.Control}
|
|
}
|
|
for id, record := range journal.Executions {
|
|
if record.Binding == nil || record.Binding.ExecutionId != id || record.Revision != record.Binding.TaskRevision {
|
|
return errors.New("invalid persisted execution binding")
|
|
}
|
|
if _, ok := agentv1.ExecutionState_name[int32(record.State)]; !ok {
|
|
return errors.New("invalid persisted execution state")
|
|
}
|
|
if _, ok := agentv1.ControlAction_name[int32(record.ControlAction)]; !ok {
|
|
return errors.New("invalid persisted control action")
|
|
}
|
|
execution := &executionRecord{binding: record.Binding, executeDigest: record.Digest, taskRevision: record.Revision, callState: record.CallState, controlAction: record.ControlAction, state: record.State}
|
|
// A new process cannot infer Asterisk's state from an old local snapshot.
|
|
// Never restore permits or clear unknown occupancy because a process restarted.
|
|
if execution.state != agentv1.ExecutionState_EXECUTION_STATE_TERMINAL {
|
|
execution.state = agentv1.ExecutionState_EXECUTION_STATE_UNKNOWN
|
|
execution.unknown = true
|
|
}
|
|
s.executions[id] = execution
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Caller holds s.mu. Failure poisons further admission: in-memory state must
|
|
// never be acknowledged as durable after an unsuccessful journal write.
|
|
func (s *Server) persistExecutionJournalLocked() error {
|
|
if s.executionErr != nil {
|
|
return status.Errorf(codes.Internal, "execution journal unavailable: %v", s.executionErr)
|
|
}
|
|
if s.executionPath == "" {
|
|
return nil
|
|
}
|
|
journal := executionJournal{Version: 1, Mode: s.mode, AgentID: s.status.AgentId, CellID: s.status.CellId, Operations: make(map[string]savedOperation, len(s.operations)), Executions: make(map[string]savedExecution, len(s.executions))}
|
|
for key, record := range s.operations {
|
|
journal.Operations[key] = savedOperation{Digest: record.digest, Receipt: record.receipt, Control: record.control}
|
|
}
|
|
for id, record := range s.executions {
|
|
journal.Executions[id] = savedExecution{Binding: record.binding, Digest: record.executeDigest, State: record.state, Revision: record.taskRevision, CallState: record.callState, ControlAction: record.controlAction}
|
|
}
|
|
if err := writeRPCJournal(s.executionPath, journal); err != nil {
|
|
s.executionErr = err
|
|
return status.Errorf(codes.Internal, "persist execution journal: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
func (s *Server) executionJournalReady() error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if s.executionErr != nil {
|
|
return status.Errorf(codes.Internal, "execution journal unavailable: %v", s.executionErr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeRPCJournal(path string, value any) error {
|
|
directory := filepath.Dir(path)
|
|
if err := os.MkdirAll(directory, 0700); err != nil {
|
|
return err
|
|
}
|
|
file, err := os.CreateTemp(directory, ".rpc-journal-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
name := file.Name()
|
|
defer os.Remove(name)
|
|
if err := file.Chmod(0600); err != nil {
|
|
_ = file.Close()
|
|
return err
|
|
}
|
|
if err := json.NewEncoder(file).Encode(value); err != nil {
|
|
_ = file.Close()
|
|
return err
|
|
}
|
|
if err := file.Sync(); err != nil {
|
|
_ = file.Close()
|
|
return err
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Rename(name, path); err != nil {
|
|
return err
|
|
}
|
|
dir, err := os.Open(directory)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
syncErr := dir.Sync()
|
|
closeErr := dir.Close()
|
|
if err := errors.Join(syncErr, closeErr); err != nil {
|
|
return fmt.Errorf("sync journal directory: %w", err)
|
|
}
|
|
return nil
|
|
}
|