mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
feat: add durable result policy core
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ResultDeliveryAlertAfter = 5 * time.Minute
|
||||
ResultDeliveryReviewAfter = 30 * time.Minute
|
||||
)
|
||||
|
||||
type IntegrityState string
|
||||
|
||||
const (
|
||||
IntegrityCertified IntegrityState = "CERTIFIED"
|
||||
IntegritySuppressed IntegrityState = "SUPPRESSED"
|
||||
IntegrityReview IntegrityState = "REVIEW"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrResultBinding = fmt.Errorf("result workload binding rejected")
|
||||
ErrResultConflict = fmt.Errorf("conflicting result")
|
||||
ErrResultInvalid = fmt.Errorf("invalid match result")
|
||||
ErrReceiptMissing = fmt.Errorf("result receipt not found")
|
||||
)
|
||||
|
||||
// WorkloadBinding is the identity extracted and validated by the secure
|
||||
// credential adapter. The domain compares every binding dimension recorded by
|
||||
// allocation; a shared service-account class is not sufficient on its own.
|
||||
type WorkloadBinding struct {
|
||||
Issuer string
|
||||
Audience string
|
||||
Namespace string
|
||||
ServiceAcct string
|
||||
PodUID string
|
||||
GameServerUID string
|
||||
MatchID string
|
||||
ServerID string
|
||||
}
|
||||
|
||||
type MatchResult struct {
|
||||
MatchID string
|
||||
ServerID string
|
||||
ResultNonce string
|
||||
Team0Score int
|
||||
Team1Score int
|
||||
IntegrityState IntegrityState
|
||||
}
|
||||
|
||||
type ResultReceipt struct {
|
||||
ResultID string
|
||||
MatchID string
|
||||
ResultNonce string
|
||||
PayloadDigest [32]byte
|
||||
IntegrityState IntegrityState
|
||||
ReceivedAt time.Time
|
||||
CommittedAt time.Time
|
||||
}
|
||||
|
||||
type ResultStore struct {
|
||||
expected WorkloadBinding
|
||||
receipts map[string]ResultReceipt
|
||||
}
|
||||
|
||||
func NewResultStore(expected WorkloadBinding) (*ResultStore, error) {
|
||||
if err := validateBinding(expected); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ResultStore{expected: expected, receipts: make(map[string]ResultReceipt)}, nil
|
||||
}
|
||||
|
||||
// Submit is the durable-transaction boundary in miniature. Production code
|
||||
// must persist the receipt, match transition, participant penalties/ratings,
|
||||
// and outbox event atomically around this same decision.
|
||||
func (s *ResultStore) Submit(resultID string, result MatchResult, binding WorkloadBinding, now time.Time) (ResultReceipt, bool, error) {
|
||||
if resultID == "" || !sameBinding(s.expected, binding) {
|
||||
return ResultReceipt{}, false, ErrResultBinding
|
||||
}
|
||||
if err := validateResult(s.expected, result); err != nil {
|
||||
return ResultReceipt{}, false, err
|
||||
}
|
||||
digest := resultDigest(result)
|
||||
if prior, ok := s.receipts[result.MatchID]; ok {
|
||||
if prior.ResultID == resultID && prior.PayloadDigest == digest {
|
||||
return prior, false, nil
|
||||
}
|
||||
return prior, false, ErrResultConflict
|
||||
}
|
||||
receipt := ResultReceipt{ResultID: resultID, MatchID: result.MatchID, ResultNonce: result.ResultNonce, PayloadDigest: digest, IntegrityState: result.IntegrityState, ReceivedAt: now}
|
||||
s.receipts[result.MatchID] = receipt
|
||||
return receipt, true, nil
|
||||
}
|
||||
|
||||
func (s *ResultStore) Commit(resultID, matchID string, now time.Time) (ResultReceipt, error) {
|
||||
receipt, ok := s.receipts[matchID]
|
||||
if !ok || receipt.ResultID != resultID {
|
||||
return ResultReceipt{}, ErrReceiptMissing
|
||||
}
|
||||
if receipt.CommittedAt.IsZero() {
|
||||
receipt.CommittedAt = now
|
||||
s.receipts[matchID] = receipt
|
||||
}
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
type DeliveryHealth string
|
||||
|
||||
const (
|
||||
DeliveryHealthy DeliveryHealth = "HEALTHY"
|
||||
DeliveryAlert DeliveryHealth = "ALERT"
|
||||
DeliveryReview DeliveryHealth = "REVIEW"
|
||||
)
|
||||
|
||||
func DeliveryStatus(receipt ResultReceipt, now time.Time) DeliveryHealth {
|
||||
if !receipt.CommittedAt.IsZero() {
|
||||
return DeliveryHealthy
|
||||
}
|
||||
age := now.Sub(receipt.ReceivedAt)
|
||||
if age >= ResultDeliveryReviewAfter {
|
||||
return DeliveryReview
|
||||
}
|
||||
if age >= ResultDeliveryAlertAfter {
|
||||
return DeliveryAlert
|
||||
}
|
||||
return DeliveryHealthy
|
||||
}
|
||||
|
||||
func RatingEligible(receipt ResultReceipt) bool {
|
||||
return receipt.IntegrityState == IntegrityCertified
|
||||
}
|
||||
|
||||
func validateBinding(binding WorkloadBinding) error {
|
||||
if binding.Issuer == "" || binding.Audience == "" || binding.Namespace == "" || binding.ServiceAcct == "" || binding.PodUID == "" || binding.GameServerUID == "" || binding.MatchID == "" || binding.ServerID == "" {
|
||||
return ErrResultBinding
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sameBinding(a, b WorkloadBinding) bool { return a == b }
|
||||
|
||||
func validateResult(expected WorkloadBinding, result MatchResult) error {
|
||||
if result.MatchID != expected.MatchID || result.ServerID != expected.ServerID || len(result.ResultNonce) < 16 || len(result.ResultNonce) > 128 || result.Team0Score < 0 || result.Team1Score < 0 {
|
||||
return ErrResultInvalid
|
||||
}
|
||||
switch result.IntegrityState {
|
||||
case IntegrityCertified, IntegritySuppressed, IntegrityReview:
|
||||
return nil
|
||||
default:
|
||||
return ErrResultInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func resultDigest(result MatchResult) [32]byte {
|
||||
canonical := result.MatchID + "\x00" + result.ServerID + "\x00" + result.ResultNonce + "\x00" + strconv.Itoa(result.Team0Score) + "\x00" + strconv.Itoa(result.Team1Score) + "\x00" + string(result.IntegrityState)
|
||||
return sha256.Sum256([]byte(canonical))
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func testBinding() WorkloadBinding {
|
||||
return WorkloadBinding{Issuer: "https://issuer", Audience: "cosmic-result", Namespace: "games", ServiceAcct: "match-server", PodUID: "pod-1", GameServerUID: "gs-1", MatchID: "match-1", ServerID: "server-1"}
|
||||
}
|
||||
|
||||
func testResult() MatchResult {
|
||||
return MatchResult{MatchID: "match-1", ServerID: "server-1", ResultNonce: "nonce-1234567890", Team0Score: 3, Team1Score: 2, IntegrityState: IntegrityCertified}
|
||||
}
|
||||
|
||||
func TestResultStoreBindsWorkloadAndMakesIdenticalDuplicateInert(t *testing.T) {
|
||||
now := time.Unix(1000, 0)
|
||||
binding := testBinding()
|
||||
store, err := NewResultStore(binding)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first, created, err := store.Submit("result-1", testResult(), binding, now)
|
||||
if err != nil || !created || !RatingEligible(first) {
|
||||
t.Fatalf("first result = %+v created=%v err=%v", first, created, err)
|
||||
}
|
||||
replay, created, err := store.Submit("result-1", testResult(), binding, now.Add(time.Minute))
|
||||
if err != nil || created || replay.ReceivedAt != now {
|
||||
t.Fatalf("duplicate result = %+v created=%v err=%v", replay, created, err)
|
||||
}
|
||||
wrong := binding
|
||||
wrong.PodUID = "pod-2"
|
||||
if _, _, err := store.Submit("result-2", testResult(), wrong, now); !errors.Is(err, ErrResultBinding) {
|
||||
t.Fatalf("wrong pod accepted: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictingResultIsInertAndIntegritySuppressesRating(t *testing.T) {
|
||||
now := time.Unix(1000, 0)
|
||||
binding := testBinding()
|
||||
store, _ := NewResultStore(binding)
|
||||
if _, _, err := store.Submit("result-1", testResult(), binding, now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conflict := testResult()
|
||||
conflict.Team0Score = 99
|
||||
prior, _, err := store.Submit("result-2", conflict, binding, now)
|
||||
if !errors.Is(err, ErrResultConflict) || prior.ResultID != "result-1" || prior.CommittedAt != (time.Time{}) {
|
||||
t.Fatalf("conflict mutated receipt: %+v err=%v", prior, err)
|
||||
}
|
||||
suppressed := testResult()
|
||||
suppressed.MatchID = "match-2"
|
||||
suppressed.IntegrityState = IntegritySuppressed
|
||||
secondBinding := binding
|
||||
secondBinding.MatchID = "match-2"
|
||||
secondStore, _ := NewResultStore(secondBinding)
|
||||
got, _, err := secondStore.Submit("result-2", suppressed, secondBinding, now)
|
||||
if err != nil || RatingEligible(got) {
|
||||
t.Fatalf("suppressed result eligibility = %+v err=%v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResultDeliveryHealthSeparatesOutageFromIntegrity(t *testing.T) {
|
||||
now := time.Unix(1000, 0)
|
||||
binding := testBinding()
|
||||
store, _ := NewResultStore(binding)
|
||||
receipt, _, err := store.Submit("result-1", testResult(), binding, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if DeliveryStatus(receipt, now.Add(5*time.Minute-time.Nanosecond)) != DeliveryHealthy || DeliveryStatus(receipt, now.Add(ResultDeliveryAlertAfter)) != DeliveryAlert || DeliveryStatus(receipt, now.Add(ResultDeliveryReviewAfter)) != DeliveryReview {
|
||||
t.Fatal("pending delivery thresholds are wrong")
|
||||
}
|
||||
committed, err := store.Commit("result-1", "match-1", now.Add(31*time.Minute))
|
||||
if err != nil || DeliveryStatus(committed, now.Add(2*time.Hour)) != DeliveryHealthy {
|
||||
t.Fatalf("committed delivery status = %+v err=%v", committed, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user