|
| 1 | +package client |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "testing" |
| 6 | + "time" |
| 7 | + |
| 8 | + "github.com/https-cert/deploy/internal/client/dnsconfig" |
| 9 | + "github.com/https-cert/deploy/internal/config" |
| 10 | + "github.com/https-cert/deploy/pb/deployPB" |
| 11 | +) |
| 12 | + |
| 13 | +// blockingDNS 模拟写入已发出而 SDK 暂时不响应取消的情况。 |
| 14 | +type blockingDNS struct { |
| 15 | + started chan struct{} // started 通知新增请求已经进入云 SDK。 |
| 16 | + release chan struct{} // release 控制 SDK 完成的时刻。 |
| 17 | +} |
| 18 | + |
| 19 | +// FindZone 返回精确托管区,不依赖证书绑定。 |
| 20 | +func (b *blockingDNS) FindZone(context.Context, string) (string, error) { |
| 21 | + return "zone-id", nil |
| 22 | +} |
| 23 | + |
| 24 | +// List 返回空记录目录,确保测试进入新增路径。 |
| 25 | +func (b *blockingDNS) List(context.Context, string, *deployPB.DnsRecord) ([]dnsconfig.Entry, error) { |
| 26 | + return nil, nil |
| 27 | +} |
| 28 | + |
| 29 | +// Create 保持阻塞直到 SDK 自行收敛,故意忽略取消信号。 |
| 30 | +func (b *blockingDNS) Create(context.Context, string, *deployPB.DnsRecord) (string, error) { |
| 31 | + close(b.started) |
| 32 | + <-b.release |
| 33 | + return "create-id", nil |
| 34 | +} |
| 35 | + |
| 36 | +// TestDNSTimeoutRetainsLocksAndSlot 验证超时只回包一次,底层写入结束前不释放锁和并发槽。 |
| 37 | +func TestDNSTimeoutRetainsLocksAndSlot(t *testing.T) { |
| 38 | + parent, cancel := context.WithCancel(context.Background()) |
| 39 | + defer cancel() |
| 40 | + runner := newOperationRunner(1) |
| 41 | + backend := &blockingDNS{started: make(chan struct{}), release: make(chan struct{})} |
| 42 | + defer close(backend.release) |
| 43 | + replies := make(chan *deployPB.DeploymentRequest, 4) |
| 44 | + service := &deploymentService{ |
| 45 | + client: &WSClient{ops: runner}, ops: runner, parent: parent, clientID: "dns-client", |
| 46 | + sendEnvelope: func(reply *deployPB.DeploymentRequest) { replies <- reply }, |
| 47 | + newDNSBackend: func(*config.Runtime, deployPB.DnsProvider) (dnsconfig.Backend, error) { return backend, nil }, |
| 48 | + } |
| 49 | + record := &deployPB.DnsRecord{Zone: "example.com", Fqdn: "_acme-challenge.example.com", Host: "_acme-challenge", Value: "target.example.net"} |
| 50 | + service.runDNS("first", &deployPB.DnsOperationRequest{Record: record}, true) |
| 51 | + select { |
| 52 | + case <-backend.started: |
| 53 | + case <-time.After(time.Second): |
| 54 | + t.Fatal("新增没有启动") |
| 55 | + } |
| 56 | + cancel() |
| 57 | + select { |
| 58 | + case reply := <-replies: |
| 59 | + if reply.ClientId != "dns-client" || reply.RequestId != "first" || reply.GetDnsConfigureResponse().GetStatus() != deployPB.DnsOperationStatus_DNS_OPERATION_STATUS_UNCONFIRMED { |
| 60 | + t.Fatal("超时响应关联或状态错误", reply) |
| 61 | + } |
| 62 | + case <-time.After(time.Second): |
| 63 | + t.Fatal("取消后没有及时回包") |
| 64 | + } |
| 65 | + lockCtx, lockCancel := context.WithTimeout(context.Background(), 20*time.Millisecond) |
| 66 | + defer lockCancel() |
| 67 | + if release, err := runner.LockResource(lockCtx, "dns\x00example.com\x00_acme-challenge.example.com"); err == nil { |
| 68 | + release() |
| 69 | + t.Fatal("未收敛写入的串行锁被提前释放") |
| 70 | + } |
| 71 | + service.runDNS("second", &deployPB.DnsOperationRequest{Record: record}, true) |
| 72 | + if reply := <-replies; reply.RequestId != "second" || reply.GetDnsConfigureResponse().GetStatus() != deployPB.DnsOperationStatus_DNS_OPERATION_STATUS_FAILED { |
| 73 | + t.Fatal("未收敛写入的并发槽被提前释放", reply) |
| 74 | + } |
| 75 | + backend.release <- struct{}{} |
| 76 | + runner.Wait() |
| 77 | + select { |
| 78 | + case reply := <-replies: |
| 79 | + t.Fatal("已超时的任务再次回包", reply) |
| 80 | + default: |
| 81 | + } |
| 82 | + if len(runner.sem) != 0 { |
| 83 | + t.Fatal("SDK 收敛后未释放并发槽") |
| 84 | + } |
| 85 | +} |
0 commit comments