feat: sync multilayer td integration
This commit is contained in:
parent
20a310f6ca
commit
766c5db992
491 changed files with 26235 additions and 35340 deletions
|
|
@ -4,16 +4,17 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gotd/td/bin"
|
||||
"github.com/gotd/td/crypto"
|
||||
"github.com/gotd/td/proto"
|
||||
"github.com/gotd/td/tg"
|
||||
"github.com/iamxvbaba/td/bin"
|
||||
"github.com/iamxvbaba/td/crypto"
|
||||
"github.com/iamxvbaba/td/proto"
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
)
|
||||
|
||||
type opaqueRPCResult struct{ body []byte }
|
||||
|
|
@ -26,9 +27,9 @@ func (o opaqueRPCResult) Encode(b *bin.Buffer) error {
|
|||
|
||||
func TestEncodeRPCResultUsesAdaptiveGZIP(t *testing.T) {
|
||||
s := New(Options{})
|
||||
c := &Conn{metrics: NopMetrics{}}
|
||||
c := legacyCanonicalTestConn(t, &Conn{metrics: NopMetrics{}})
|
||||
large := &tg.DataJSON{Data: string(bytes.Repeat([]byte("sticker-metadata-"), 16<<10))}
|
||||
encoded, err := s.encodeRPCResult(c, 123, large)
|
||||
encoded, err := s.encodeRPCResult(c, 123, exactTestRPCResult(large))
|
||||
if err != nil {
|
||||
t.Fatalf("encode compressed rpc_result: %v", err)
|
||||
}
|
||||
|
|
@ -57,12 +58,12 @@ func TestEncodeRPCResultUsesAdaptiveGZIP(t *testing.T) {
|
|||
|
||||
func TestEncodeRPCResultKeepsIncompressibleBodyRaw(t *testing.T) {
|
||||
s := New(Options{})
|
||||
c := &Conn{metrics: NopMetrics{}}
|
||||
c := legacyCanonicalTestConn(t, &Conn{metrics: NopMetrics{}})
|
||||
raw := make([]byte, 96<<10)
|
||||
if _, err := rand.Read(raw); err != nil {
|
||||
t.Fatalf("random body: %v", err)
|
||||
}
|
||||
encoded, err := s.encodeRPCResult(c, 456, opaqueRPCResult{body: raw})
|
||||
encoded, err := s.encodeRPCResult(c, 456, exactTestRPCResult(opaqueRPCResult{body: raw}))
|
||||
if err != nil {
|
||||
t.Fatalf("encode incompressible rpc_result: %v", err)
|
||||
}
|
||||
|
|
@ -79,6 +80,349 @@ func TestEncodeRPCResultKeepsIncompressibleBodyRaw(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestEncodeRPCResultReservedChargesBodyBeforeReturning(t *testing.T) {
|
||||
budget := newOutboundTrackedBudget(1 << 20)
|
||||
c := legacyCanonicalTestConn(t, &Conn{metrics: NopMetrics{}, outboundTrackedBudget: budget})
|
||||
s := New(Options{})
|
||||
|
||||
encoded, reserved, err := s.encodeRPCResultReservedContext(
|
||||
context.Background(), c, 789, exactTestRPCResult(&tg.DataJSON{Data: "bounded"}),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("encode reserved rpc_result: %v", err)
|
||||
}
|
||||
if reserved == nil {
|
||||
t.Fatal("encode returned no retained-byte reservation")
|
||||
}
|
||||
if got, want := budget.used.Load(), int64(len(encoded.body)); got != want {
|
||||
t.Fatalf("reserved bytes = %d, want encoded body %d", got, want)
|
||||
}
|
||||
reserved.release()
|
||||
if got := budget.used.Load(); got != 0 {
|
||||
t.Fatalf("reserved bytes after release = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeRPCResultReservedDropsBodyOnBudgetTimeout(t *testing.T) {
|
||||
const maxBytes = 1 << 20
|
||||
budget := newOutboundTrackedBudget(maxBytes)
|
||||
if !budget.reserve(maxBytes) {
|
||||
t.Fatal("saturate outbound body budget")
|
||||
}
|
||||
c := legacyCanonicalTestConn(t, &Conn{metrics: NopMetrics{}, outboundTrackedBudget: budget})
|
||||
s := New(Options{})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
encoded, reserved, err := s.encodeRPCResultReservedContext(
|
||||
ctx, c, 790, exactTestRPCResult(&tg.DataJSON{Data: "must-not-escape"}),
|
||||
)
|
||||
if !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatalf("saturated reservation error = %v, want deadline exceeded", err)
|
||||
}
|
||||
if encoded != nil || reserved != nil {
|
||||
t.Fatalf("untracked result escaped encode slot: encoded=%p reserved=%p", encoded, reserved)
|
||||
}
|
||||
if got := len(outboundEncodeSlots); got != 0 {
|
||||
t.Fatalf("encode slots retained after timeout = %d, want 0", got)
|
||||
}
|
||||
if got := budget.snapshot(); got != maxBytes {
|
||||
t.Fatalf("primary budget after timeout = %d, want saturated %d", got, maxBytes)
|
||||
}
|
||||
budget.release(maxBytes)
|
||||
if got := budget.snapshot(); got != 0 {
|
||||
t.Fatalf("primary budget after release = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeRPCResultFailedRetentionHandoffDropsBodyInSlot(t *testing.T) {
|
||||
budget := newOutboundTrackedBudget(1)
|
||||
if !budget.reserve(1) {
|
||||
t.Fatal("saturate outbound body budget")
|
||||
}
|
||||
c := legacyCanonicalTestConn(t, &Conn{metrics: NopMetrics{}, outboundTrackedBudget: budget})
|
||||
s := New(Options{})
|
||||
observedInSlot := false
|
||||
|
||||
encoded, reserved, retained, err := s.encodeRPCResultReservedWithHandoffContext(
|
||||
context.Background(), c, 791, exactTestRPCResult(&tg.DataJSON{Data: "handoff-fails"}),
|
||||
func(body *encodedOutboundMessage, admissionErr error) error {
|
||||
observedInSlot = body != nil && len(body.body) > 0 && len(outboundEncodeSlots) > 0 &&
|
||||
errors.Is(admissionErr, ErrOutboundTrackedBudget)
|
||||
return errors.New("forced retention failure")
|
||||
},
|
||||
)
|
||||
if !errors.Is(err, errRPCResultRetentionHandoff) {
|
||||
t.Fatalf("retention error = %v, want handoff sentinel", err)
|
||||
}
|
||||
if !observedInSlot {
|
||||
t.Fatal("retention handoff did not run while encoded body was slot-confined")
|
||||
}
|
||||
if retained || encoded != nil || reserved != nil {
|
||||
t.Fatalf("failed handoff escaped ownership: retained=%v encoded=%p reserved=%p", retained, encoded, reserved)
|
||||
}
|
||||
if got := len(outboundEncodeSlots); got != 0 {
|
||||
t.Fatalf("encode slots retained after failed handoff = %d, want 0", got)
|
||||
}
|
||||
if got := budget.snapshot(); got != 1 {
|
||||
t.Fatalf("primary budget after failed handoff = %d, want 1", got)
|
||||
}
|
||||
budget.release(1)
|
||||
}
|
||||
|
||||
const saturatedSlotWaveResultData = "exact-business-success"
|
||||
|
||||
type saturatedSlotWaveGate struct {
|
||||
firstWave int32
|
||||
encodes atomic.Int32
|
||||
entered chan struct{}
|
||||
release chan struct{}
|
||||
}
|
||||
|
||||
type saturatedSlotWaveResult struct{ gate *saturatedSlotWaveGate }
|
||||
|
||||
func (r saturatedSlotWaveResult) Encode(b *bin.Buffer) error {
|
||||
call := r.gate.encodes.Add(1)
|
||||
if call <= r.gate.firstWave {
|
||||
r.gate.entered <- struct{}{}
|
||||
<-r.gate.release
|
||||
}
|
||||
return (&tg.DataJSON{Data: saturatedSlotWaveResultData}).Encode(b)
|
||||
}
|
||||
|
||||
type saturatedSlotWaveRPC struct {
|
||||
calls atomic.Int32
|
||||
gate *saturatedSlotWaveGate
|
||||
}
|
||||
|
||||
func (h *saturatedSlotWaveRPC) Dispatch(context.Context, [8]byte, int64, *bin.Buffer) (bin.Encoder, error) {
|
||||
h.calls.Add(1)
|
||||
return exactTestRPCResult(saturatedSlotWaveResult{gate: h.gate}), nil
|
||||
}
|
||||
|
||||
func (*saturatedSlotWaveRPC) NegotiatedLayer([8]byte, int64) (int, bool) { return 227, true }
|
||||
|
||||
func TestPublishRPCResultSaturatedBudgetRetainsExactResultsAcrossSlotWaves(t *testing.T) {
|
||||
slotCount := cap(outboundEncodeSlots)
|
||||
requestCount := slotCount*2 + 1
|
||||
gate := &saturatedSlotWaveGate{
|
||||
firstWave: int32(slotCount),
|
||||
entered: make(chan struct{}, slotCount),
|
||||
release: make(chan struct{}),
|
||||
}
|
||||
handler := &saturatedSlotWaveRPC{gate: gate}
|
||||
s := New(Options{legacyRPC: handler})
|
||||
now := time.Unix(1_700_000_000, 0)
|
||||
s.rpcResults = newRPCResultCacheWithFlightLimit(func() time.Time { return now }, requestCount+1)
|
||||
|
||||
const primaryMax = 1 << 20
|
||||
primary := newOutboundTrackedBudget(primaryMax)
|
||||
if !primary.reserve(primaryMax) {
|
||||
t.Fatal("saturate shared primary outbound budget")
|
||||
}
|
||||
|
||||
conns := make([]*Conn, requestCount)
|
||||
tasks := make([]inboundRPC, requestCount)
|
||||
owners := make([]*rpcResultOwnerLease, requestCount)
|
||||
reqMsgIDs := make([]int64, requestCount)
|
||||
requestBody := mustEncodeTL(t, &tg.PhoneGetCallConfigRequest{})
|
||||
for i := 0; i < requestCount; i++ {
|
||||
var authKeyID [8]byte
|
||||
authKeyID[0] = byte(i + 1)
|
||||
authKeyID[1] = byte((i + 1) >> 8)
|
||||
reqMsgID := int64(10_000 + i)
|
||||
c := &Conn{
|
||||
metrics: NopMetrics{},
|
||||
writeTimeout: time.Second,
|
||||
authKeyID: authKeyID,
|
||||
sessionID: int64(20_000 + i),
|
||||
outboundTrackedBudget: primary,
|
||||
}
|
||||
legacyLayerWireTestConn(t, c, 227)
|
||||
claim, err := s.rpcResults.Acquire(c.authKeyID, c.sessionID, reqMsgID)
|
||||
if err != nil || claim.state != rpcResultAcquireOwner {
|
||||
t.Fatalf("acquire request %d = %+v err=%v", i, claim, err)
|
||||
}
|
||||
conns[i] = c
|
||||
owners[i] = claim.owner
|
||||
reqMsgIDs[i] = reqMsgID
|
||||
tasks[i] = s.newInboundRPCTask(c, reqMsgID, "phone.getCallConfig", requestBody, claim.owner)
|
||||
}
|
||||
|
||||
start := make(chan struct{})
|
||||
errs := make([]error, requestCount)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(requestCount)
|
||||
for i := range tasks {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
<-start
|
||||
errs[i] = tasks[i].run(context.Background())
|
||||
if tasks[i].release != nil {
|
||||
tasks[i].release()
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
close(start)
|
||||
for i := 0; i < slotCount; i++ {
|
||||
select {
|
||||
case <-gate.entered:
|
||||
case <-time.After(time.Second):
|
||||
close(gate.release)
|
||||
t.Fatalf("first encode wave entered %d/%d slots; handlers=%d encodes=%d first_err=%v", i, slotCount, handler.calls.Load(), gate.encodes.Load(), errs[0])
|
||||
}
|
||||
}
|
||||
if got := gate.encodes.Load(); got != int32(slotCount) {
|
||||
close(gate.release)
|
||||
t.Fatalf("encodes before releasing first wave = %d, want slot cap %d", got, slotCount)
|
||||
}
|
||||
close(gate.release)
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("slot waves did not converge after saturated-budget retention")
|
||||
}
|
||||
|
||||
if got := handler.calls.Load(); got != int32(requestCount) {
|
||||
t.Fatalf("business executions = %d, want %d", got, requestCount)
|
||||
}
|
||||
if got := gate.encodes.Load(); got != int32(requestCount) {
|
||||
t.Fatalf("successful result encodes = %d, want %d", got, requestCount)
|
||||
}
|
||||
if got := len(outboundEncodeSlots); got != 0 {
|
||||
t.Fatalf("encode slots after both waves = %d, want 0", got)
|
||||
}
|
||||
if got := primary.snapshot(); got != primaryMax {
|
||||
t.Fatalf("primary budget changed under saturation = %d, want %d", got, primaryMax)
|
||||
}
|
||||
|
||||
var completedBytes int64
|
||||
for i, c := range conns {
|
||||
if !errors.Is(errs[i], ErrOutboundTrackedBudget) {
|
||||
t.Fatalf("publish request %d error = %v, want terminal budget saturation", i, errs[i])
|
||||
}
|
||||
if !c.isRetired() {
|
||||
t.Fatalf("request %d connection was not explicitly fenced", i)
|
||||
}
|
||||
if !owners[i].handedOff.Load() {
|
||||
t.Fatalf("request %d owner was not handed to completed cache", i)
|
||||
}
|
||||
cached, ok := s.rpcResults.Get(c.authKeyID, c.sessionID, reqMsgIDs[i])
|
||||
if !ok || cached == nil {
|
||||
t.Fatalf("request %d exact result missing from completed cache", i)
|
||||
}
|
||||
completedBytes += int64(len(cached.body))
|
||||
var envelope proto.Result
|
||||
if err := envelope.Decode(&bin.Buffer{Buf: cached.body}); err != nil {
|
||||
t.Fatalf("decode request %d cached rpc_result: %v", i, err)
|
||||
}
|
||||
if envelope.RequestMessageID != reqMsgIDs[i] {
|
||||
t.Fatalf("request %d cached req_msg_id = %d, want %d", i, envelope.RequestMessageID, reqMsgIDs[i])
|
||||
}
|
||||
var result tg.DataJSON
|
||||
if err := result.Decode(&bin.Buffer{Buf: envelope.Result}); err != nil {
|
||||
t.Fatalf("decode request %d exact business result (possibly INTERNAL): %v", i, err)
|
||||
}
|
||||
if result.Data != saturatedSlotWaveResultData {
|
||||
t.Fatalf("request %d cached result = %q, want %q", i, result.Data, saturatedSlotWaveResultData)
|
||||
}
|
||||
retry, err := s.rpcResults.Acquire(c.authKeyID, c.sessionID, reqMsgIDs[i])
|
||||
if err != nil || retry.state != rpcResultAcquireCompleted || retry.encoded != cached {
|
||||
t.Fatalf("retry request %d = %+v err=%v, want exact completed result", i, retry, err)
|
||||
}
|
||||
}
|
||||
if got := handler.calls.Load(); got != int32(requestCount) {
|
||||
t.Fatalf("business executions after retries = %d, want unchanged %d", got, requestCount)
|
||||
}
|
||||
if got := s.rpcResults.completedBytes.snapshot(); got != completedBytes {
|
||||
t.Fatalf("completed-cache charge = %d, want exact retained bytes %d", got, completedBytes)
|
||||
}
|
||||
|
||||
// Expiry is the completed cache's ownership release point. Force it
|
||||
// deterministically and prove every retained byte is returned exactly once.
|
||||
now = now.Add(rpcResultCacheTTL + time.Second)
|
||||
for i, c := range conns {
|
||||
if _, ok := s.rpcResults.Get(c.authKeyID, c.sessionID, reqMsgIDs[i]); ok {
|
||||
t.Fatalf("request %d remained cached after forced expiry", i)
|
||||
}
|
||||
}
|
||||
if got := s.rpcResults.completedBytes.snapshot(); got != 0 {
|
||||
t.Fatalf("completed-cache bytes after expiry = %d, want 0", got)
|
||||
}
|
||||
primary.release(primaryMax)
|
||||
if got := primary.snapshot(); got != 0 {
|
||||
t.Fatalf("primary budget after release = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachedReplayRestoreIsSynchronousAndIndependentOfGlobalHookExecutor(t *testing.T) {
|
||||
// Occupy the entire executor. The replay-state callback must not reserve a
|
||||
// ticket there: slow auth/store restoration has its own bounded path.
|
||||
executor := newRPCDeliveryHookExecutor(1, 1)
|
||||
started := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
ticket, ok := executor.reserve()
|
||||
if !ok || !ticket.submit(func() {
|
||||
close(started)
|
||||
<-release
|
||||
}) {
|
||||
t.Fatal("occupy delivery hook executor")
|
||||
}
|
||||
<-started
|
||||
oldExecutor := defaultRPCDeliveryHookExecutor
|
||||
defaultRPCDeliveryHookExecutor = executor
|
||||
defer func() {
|
||||
defaultRPCDeliveryHookExecutor = oldExecutor
|
||||
close(release)
|
||||
}()
|
||||
|
||||
s := New(Options{WriteTimeout: time.Second})
|
||||
transport := &collectingSessionTransport{}
|
||||
key := newTestAuthKey(t)
|
||||
c := s.newConn(transport, key, 777, 1)
|
||||
legacyCanonicalTestConn(t, c)
|
||||
t.Cleanup(c.ForceClose)
|
||||
encoded := encodedRPCResultForPriorityTest(9001, 0)
|
||||
encoded.delivery = newRPCResultDelivery(encoded.reqMsgID)
|
||||
var restoreOrder atomic.Int32
|
||||
encoded.setDeliveryHook(func() {
|
||||
if !restoreOrder.CompareAndSwap(1, 2) {
|
||||
panic("logical replay hook did not run after replacement metadata restore")
|
||||
}
|
||||
})
|
||||
|
||||
var restored atomic.Bool
|
||||
if err := s.sendCachedRPCResultWithHook(context.Background(), c, encoded, func() error {
|
||||
if got := len(transport.snapshot()); got != 1 {
|
||||
return errors.New("replay restore ran before physical write")
|
||||
}
|
||||
if !restoreOrder.CompareAndSwap(0, 1) {
|
||||
return errors.New("replacement replay restore ran out of order")
|
||||
}
|
||||
restored.Store(true)
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatalf("send cached replay with saturated global executor: %v", err)
|
||||
}
|
||||
if !restored.Load() {
|
||||
t.Fatal("cached replay returned before state restore completed")
|
||||
}
|
||||
if got := restoreOrder.Load(); got != 2 {
|
||||
t.Fatalf("ordered replay restore stage = %d, want replacement then logical hook", got)
|
||||
}
|
||||
c.rpcMu.Lock()
|
||||
pending := c.rpcReplayRestores
|
||||
c.rpcMu.Unlock()
|
||||
if pending != 0 {
|
||||
t.Fatalf("replay restore barriers = %d, want 0", pending)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapBarriersAlwaysUseConvergenceLane(t *testing.T) {
|
||||
large := &encodedOutboundMessage{body: make([]byte, bulkOutboundThreshold)}
|
||||
for _, method := range []string{
|
||||
|
|
@ -144,13 +488,20 @@ func encodedRPCResultForPriorityTest(reqMsgID int64, payloadBytes int) *encodedO
|
|||
if payloadBytes > 0 {
|
||||
b.Put(make([]byte, payloadBytes))
|
||||
}
|
||||
return &encodedOutboundMessage{typeID: proto.ResultTypeID, reqMsgID: reqMsgID, body: b.Raw()}
|
||||
return &encodedOutboundMessage{
|
||||
typeID: proto.ResultTypeID, reqMsgID: reqMsgID, body: b.Raw(),
|
||||
layer: &outboundLayerBinding{
|
||||
profile: tg.LayerProfileCanonical,
|
||||
typ: tg.LayerClassBoolType().Ref(),
|
||||
kind: outboundLayerBindingRequest,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvergenceResultPassesQueuedBulkAfterBlockedWrite(t *testing.T) {
|
||||
tr := newGatedRecordingTransport()
|
||||
c := newOutboundTestConn(t, tr, newOutboundTrackedBudget(2<<20))
|
||||
gate := &encodedOutboundMessage{typeID: tg.UpdatesTooLongTypeID, body: []byte{0x0b, 0xa1, 0x01, 0xe3}}
|
||||
gate := exactTestUpdatesTooLong(t, c)
|
||||
if err := c.SendBestEffortEncoded(context.Background(), proto.MessageFromServer, gate, 0); err != nil {
|
||||
t.Fatalf("enqueue gate: %v", err)
|
||||
}
|
||||
|
|
@ -270,7 +621,7 @@ func TestRPCResultPipelineExportsPreparationAndDeliveryMetrics(t *testing.T) {
|
|||
t.Fatalf("acquire flight = %+v err=%v", claim, err)
|
||||
}
|
||||
result := &tg.DataJSON{Data: string(bytes.Repeat([]byte("sticker-data"), 12<<10))}
|
||||
if err := s.publishRPCResult(c, reqMsgID, "updates.getDifference#25939651", claim.owner, result, nil); err != nil {
|
||||
if err := s.publishRPCResult(c, reqMsgID, "updates.getDifference#25939651", claim.owner, exactTestRPCResult(result), nil); err != nil {
|
||||
t.Fatalf("publish result: %v", err)
|
||||
}
|
||||
select {
|
||||
|
|
@ -293,7 +644,7 @@ func TestRPCResultPipelineExportsPreparationAndDeliveryMetrics(t *testing.T) {
|
|||
|
||||
func TestWrappedConvergenceMethodDrivesEgressAndReplayPriority(t *testing.T) {
|
||||
metrics := &captureRPCResultMetrics{delivered: make(chan error, 1)}
|
||||
s := New(Options{RPC: wrappedConvergenceRPC{}, Metrics: metrics})
|
||||
s := New(Options{legacyRPC: wrappedConvergenceRPC{}, Metrics: metrics})
|
||||
c := newOutboundTestConn(t, &failAfterTransport{}, newOutboundTrackedBudget(1<<20))
|
||||
const reqMsgID = int64(9051)
|
||||
claim, err := s.rpcResults.Acquire(c.authKeyID, c.sessionID, reqMsgID)
|
||||
|
|
@ -302,7 +653,7 @@ func TestWrappedConvergenceMethodDrivesEgressAndReplayPriority(t *testing.T) {
|
|||
}
|
||||
body := mustEncodeTL(t, &tg.HelpGetConfigRequest{})
|
||||
if err := s.handleRPC(context.Background(), c, reqMsgID, "invokeWithLayer#da9b0d0d", &bin.Buffer{Buf: body}, claim.owner); err != nil {
|
||||
t.Fatalf("handle wrapped convergence RPC: %v", err)
|
||||
t.Fatalf("handle wrapped convergence legacyRPC: %v", err)
|
||||
}
|
||||
select {
|
||||
case err := <-metrics.delivered:
|
||||
|
|
@ -336,7 +687,7 @@ func TestWrappedConvergenceMethodDrivesEgressAndReplayPriority(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRPCWorkerReleasesAfterEgressAdmissionWhileWriteBlocked(t *testing.T) {
|
||||
s := New(Options{RPC: immediateLargeRPC{}, WriteTimeout: time.Second})
|
||||
s := New(Options{legacyRPC: immediateLargeRPC{}, WriteTimeout: time.Second})
|
||||
tr := newGatedRecordingTransport()
|
||||
c := newOutboundTestConn(t, tr, newOutboundTrackedBudget(2<<20))
|
||||
const reqMsgID = int64(9001)
|
||||
|
|
@ -396,7 +747,7 @@ func TestDeliveryHookRunsOnceAfterReplayNotFailedWrite(t *testing.T) {
|
|||
}
|
||||
var hooks atomic.Int32
|
||||
if err := s.publishRPCResult(oldConn, reqMsgID, "updates.getDifference", claim.owner,
|
||||
&tg.DataJSON{Data: "difference"}, func() { hooks.Add(1) }); err != nil {
|
||||
exactTestRPCResult(&tg.DataJSON{Data: "difference"}), func() { hooks.Add(1) }); err != nil {
|
||||
// Admission succeeds; the asynchronous physical failure is observed below.
|
||||
t.Fatalf("publish result: %v", err)
|
||||
}
|
||||
|
|
@ -431,8 +782,12 @@ func TestDeliveryHookRunsOnceAfterReplayNotFailedWrite(t *testing.T) {
|
|||
if got := hooks.Load(); got != 1 {
|
||||
t.Fatalf("delivery hooks after replay = %d, want 1", got)
|
||||
}
|
||||
if got := cached.deliveryState(); got != rpcResultDeliveryDelivered {
|
||||
t.Fatalf("replayed delivery state = %d, want delivered", got)
|
||||
if got := cached.deliveryState(); got != rpcResultDeliveryReplayable {
|
||||
t.Fatalf("cached representation state = %d, want original replayable attempt", got)
|
||||
}
|
||||
if cached.delivery == nil || cached.delivery.coordinator == nil ||
|
||||
cached.delivery.coordinator.hookState() != rpcResultDeliveryHookDone {
|
||||
t.Fatal("successful replay did not complete shared delivery coordinator")
|
||||
}
|
||||
if err := s.sendCachedRPCResult(context.Background(), replayConn, cached); err != nil {
|
||||
t.Fatalf("second replay: %v", err)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue