实现预分配 RunnerService facade

This commit is contained in:
2026-09-20 19:51:32 +00:00
parent b77245e4b9
commit 5fdd39f7ff
9 changed files with 481 additions and 7 deletions
+29 -5
View File
@@ -52,6 +52,11 @@ type Accepter interface {
Accept(context.Context, taskassignment.Assignment) (bool, error)
}
type Claims interface {
Offer(taskassignment.Assignment) (<-chan struct{}, error)
WaitClaimed(context.Context, string) error
}
// Message is the subset of jetstream.Msg needed by one reconciliation.
type Message interface {
Data() []byte
@@ -62,19 +67,24 @@ type Message interface {
// Processor maps one delivery to one idempotent worker reconciliation.
type Processor struct {
TrustDomain string
Accepter Accepter
RetryDelay time.Duration
TrustDomain string
Accepter Accepter
Claims Claims
RetryDelay time.Duration
ClaimTimeout time.Duration
}
func (p Processor) Process(ctx context.Context, message Message) error {
if p.Accepter == nil {
return errors.New("assignment accepter is required")
if p.Accepter == nil || p.Claims == nil {
return errors.New("assignment accepter and claim registry are required")
}
assignment, err := taskassignment.Unmarshal(message.Data(), p.TrustDomain)
if err != nil {
return errors.Join(err, message.TermWithReason("invalid assignment"))
}
if _, err := p.Claims.Offer(assignment); err != nil {
return errors.Join(err, message.TermWithReason("conflicting assignment"))
}
accepted, err := p.Accepter.Accept(ctx, assignment)
if err != nil {
delay := p.RetryDelay
@@ -84,6 +94,20 @@ func (p Processor) Process(ctx context.Context, message Message) error {
return errors.Join(err, message.NakWithDelay(delay))
}
if accepted {
timeout := p.ClaimTimeout
if timeout <= 0 {
timeout = 4 * time.Minute
}
claimContext, cancel := context.WithTimeout(ctx, timeout)
err := p.Claims.WaitClaimed(claimContext, assignment.ID)
cancel()
if err != nil {
delay := p.RetryDelay
if delay <= 0 {
delay = 2 * time.Second
}
return errors.Join(err, message.NakWithDelay(delay))
}
if err := message.DoubleAck(ctx); err != nil {
return fmt.Errorf("ack assignment %s: %w", assignment.ID, err)
}
+23 -2
View File
@@ -57,6 +57,26 @@ type fakeAccepter struct {
err error
}
type fakeClaims struct {
claimed bool
}
func (c *fakeClaims) Offer(taskassignment.Assignment) (<-chan struct{}, error) {
ready := make(chan struct{})
if c.claimed {
close(ready)
}
return ready, nil
}
func (c *fakeClaims) WaitClaimed(ctx context.Context, _ string) error {
if c.claimed {
return nil
}
<-ctx.Done()
return ctx.Err()
}
func (a *fakeAccepter) Accept(context.Context, taskassignment.Assignment) (bool, error) {
return a.accepted, a.err
}
@@ -84,7 +104,7 @@ func encodedAssignment(t *testing.T) []byte {
func TestProcessorAcknowledgesPersistedHandoff(t *testing.T) {
message := &fakeMessage{data: encodedAssignment(t)}
processor := Processor{TrustDomain: "ddupan.top", Accepter: &fakeAccepter{accepted: true}}
processor := Processor{TrustDomain: "ddupan.top", Accepter: &fakeAccepter{accepted: true}, Claims: &fakeClaims{claimed: true}}
if err := processor.Process(context.Background(), message); err != nil {
t.Fatal(err)
}
@@ -95,7 +115,7 @@ func TestProcessorAcknowledgesPersistedHandoff(t *testing.T) {
func TestProcessorRetriesUntilBackendHandoffIsDurable(t *testing.T) {
message := &fakeMessage{data: encodedAssignment(t)}
processor := Processor{TrustDomain: "ddupan.top", Accepter: &fakeAccepter{}, RetryDelay: 2 * time.Second}
processor := Processor{TrustDomain: "ddupan.top", Accepter: &fakeAccepter{}, Claims: &fakeClaims{}, RetryDelay: 2 * time.Second}
if err := processor.Process(context.Background(), message); err != nil {
t.Fatal(err)
}
@@ -109,6 +129,7 @@ func TestProcessorRetriesBackendFailureAndTerminatesPoisonMessage(t *testing.T)
processor := Processor{
TrustDomain: "ddupan.top",
Accepter: &fakeAccepter{err: errors.New("backend unavailable")},
Claims: &fakeClaims{},
RetryDelay: time.Minute,
}
if err := processor.Process(context.Background(), retry); err == nil {