Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6f3bafd1f |
+7
-7
@@ -1719,12 +1719,12 @@ issue #3 未关闭,`feat/fix-3-downlink-deadlock` 未合入 `main`。下面是
|
|||||||
- 备选方案:为每个未测子项补验收用例(本波不做,避免为变绿放松断言)。
|
- 备选方案:为每个未测子项补验收用例(本波不做,避免为变绿放松断言)。
|
||||||
- 影响:汇总改为通过 19、部分通过 4(F03/F08/F21/F22)、失败 0。F19 仍引用仓库内 SDK 清单、本波不重跑。
|
- 影响:汇总改为通过 19、部分通过 4(F03/F08/F21/F22)、失败 0。F19 仍引用仓库内 SDK 清单、本波不重跑。
|
||||||
|
|
||||||
### 复审修复 R3-01
|
### 复审修复 R3-07
|
||||||
|
|
||||||
1. **推送 worker 漏唤醒:定时分发回执、超限后续、撤回腾窗**
|
1. **客户端建群事务内复核创建者**
|
||||||
- 日期:2026-09-30
|
- 日期:2026-09-30
|
||||||
- 原条款:Gitea #65;推送 worker 只在 `WakePush` 时跑一轮 `PushPending`。
|
- 原条款:Gitea #71。`group.Create` 在 `Queue.Do` 里直接 `insertMemberTx` 群主,不跑 `endpointCheckTx`;停用/删除与建群抢在事务外对话密码窗口时,可插入已停用甚至刚删除的 `owner_id`。
|
||||||
- 实际做法:`dispatchDueBatch` 本条已分发时除 pending 接收方外始终 `WakePush` 发送方;`PushPending` 遇 `too_large` 等跳过且本轮未占满窗口时再 `WakePush` 当前接收方(不在同一次递归扫表);`Recall` 成功改成 `recalled` 的接收方各 `WakePush` 一次,已推送撤回仍走 `flushRevokes`。不改 `PublishDown` 签名。
|
- 实际做法:`Create` 同一写事务里,插入群与群主成员之前对 `actorID` 做与 `createAdmin` 相同的 `endpointCheckTx`;已停用 `endpoint_disabled`,不存在 `invalid_target`,且不插入群。不改 `emit`、不改迁移编号。
|
||||||
- 原因:终态回执、超限后的后续 pending、撤回腾出窗口后都依赖再唤醒,否则空闲在线端收不到。
|
- 原因:成员侧已有事务内复核,群主侧缺对称校验,竞态会留下非法群主。
|
||||||
- 备选方案:在同一次 `PushPending` 里循环扫完整 pending 表(否决,指令要求合并唤醒下一轮)。
|
- 备选方案:事务外再查一次创建者(否决,无法覆盖密码窗口与写事务之间的竞态)。
|
||||||
- 影响:仅 `internal/app/message/`;相关单测见 `review_r3_65_test.go`。
|
- 影响:创建者在进入写事务前被停用/删除时建群失败且无新群行;与后台建群错误码对齐。
|
||||||
|
|||||||
@@ -147,6 +147,15 @@ func (a *App) Create(ctx context.Context, actorID string, req *protocol.GroupCre
|
|||||||
|
|
||||||
var inserted []string
|
var inserted []string
|
||||||
err := a.db.Queue.Do(ctx, func(tx *sql.Tx) error {
|
err := a.db.Queue.Do(ctx, func(tx *sql.Tx) error {
|
||||||
|
if e := endpointCheckTx(tx, actorID); e != nil {
|
||||||
|
if protoCode(e) == protocol.CodeInvalidTarget {
|
||||||
|
return errCode(protocol.CodeInvalidTarget, "owner not found")
|
||||||
|
}
|
||||||
|
if protoCode(e) == protocol.CodeEndpointDisabled {
|
||||||
|
return errCode(protocol.CodeEndpointDisabled, "owner disabled")
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
var exists int
|
var exists int
|
||||||
qErr := tx.QueryRow(`SELECT 1 FROM groups WHERE id = ?`, gid).Scan(&exists)
|
qErr := tx.QueryRow(`SELECT 1 FROM groups WHERE id = ?`, gid).Scan(&exists)
|
||||||
if qErr == nil {
|
if qErr == nil {
|
||||||
|
|||||||
@@ -604,6 +604,106 @@ func TestU02CreateDedupesMembers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// raceTalk 在对话密码校验成功后执行一次 mutate,模拟事务外密码窗口里创建者被停用/删除。
|
||||||
|
type raceTalk struct {
|
||||||
|
inner group.TalkGate
|
||||||
|
mutate func()
|
||||||
|
once sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *raceTalk) CheckTalkPasswordForJoin(ctx context.Context, actorID, targetID, talkPassword, remoteIP string) error {
|
||||||
|
err := r.inner.CheckTalkPasswordForJoin(ctx, actorID, targetID, talkPassword, remoteIP)
|
||||||
|
if err == nil && r.mutate != nil {
|
||||||
|
r.once.Do(r.mutate)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestR307CreateRejectsActorDisabledOrDeletedBeforeWrite(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
code string
|
||||||
|
mutate func(ctx context.Context, t *testing.T, idApp *identity.App, db *store.DB)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "disabled",
|
||||||
|
code: protocol.CodeEndpointDisabled,
|
||||||
|
mutate: func(ctx context.Context, t *testing.T, idApp *identity.App, _ *store.DB) {
|
||||||
|
t.Helper()
|
||||||
|
if err := idApp.Disable(ctx, "alice"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deleted",
|
||||||
|
code: protocol.CodeInvalidTarget,
|
||||||
|
mutate: func(ctx context.Context, t *testing.T, idApp *identity.App, _ *store.DB) {
|
||||||
|
t.Helper()
|
||||||
|
if err := idApp.Delete(ctx, "alice"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
db, err := store.Open(filepath.Join(t.TempDir(), "data"), "FULL")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = db.Close() })
|
||||||
|
fixed := time.UnixMilli(1_700_000_000_000)
|
||||||
|
locks := auth.NewLoginLocks()
|
||||||
|
idApp := identity.New(identity.Config{
|
||||||
|
DB: db, Hash: auth.NewStubHashPool(), Locks: locks,
|
||||||
|
Sessions: auth.NewSessionTokens(),
|
||||||
|
Now: func() time.Time { return fixed },
|
||||||
|
})
|
||||||
|
gate := &raceTalk{inner: idApp}
|
||||||
|
gApp := group.New(group.Config{
|
||||||
|
DB: db, Talk: gate, MaxGroupMembers: 1000,
|
||||||
|
Now: func() time.Time { return fixed }, DefaultRemoteIP: "1.1.1.1",
|
||||||
|
})
|
||||||
|
ctx := context.Background()
|
||||||
|
insertEP(t, db, "alice", 1)
|
||||||
|
insertEP(t, db, "bob", 1)
|
||||||
|
if err := idApp.SelfSetTalkPassword(ctx, "bob", "secret"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
gate.mutate = func() { tc.mutate(ctx, t, idApp, db) }
|
||||||
|
|
||||||
|
var before int
|
||||||
|
if qErr := db.Read.QueryRow(`SELECT COUNT(*) FROM groups`).Scan(&before); qErr != nil {
|
||||||
|
t.Fatal(qErr)
|
||||||
|
}
|
||||||
|
_, err = gApp.Create(ctx, "alice", &protocol.GroupCreate{
|
||||||
|
V: protocol.Version, Type: protocol.TypeGroupCreate, RID: "1",
|
||||||
|
ID: "g_r307_" + tc.name, Name: "竞态群",
|
||||||
|
Members: []protocol.GroupMemberIn{{ID: "bob", TalkPassword: "secret"}},
|
||||||
|
})
|
||||||
|
if protoCode(err) != tc.code {
|
||||||
|
t.Fatalf("want %s got %v", tc.code, err)
|
||||||
|
}
|
||||||
|
var after int
|
||||||
|
if qErr := db.Read.QueryRow(`SELECT COUNT(*) FROM groups`).Scan(&after); qErr != nil {
|
||||||
|
t.Fatal(qErr)
|
||||||
|
}
|
||||||
|
if after != before {
|
||||||
|
t.Fatalf("groups leaked: before=%d after=%d", before, after)
|
||||||
|
}
|
||||||
|
var exists int
|
||||||
|
qErr := db.Read.QueryRow(`SELECT 1 FROM groups WHERE id = ?`, "g_r307_"+tc.name).Scan(&exists)
|
||||||
|
if !errors.Is(qErr, sql.ErrNoRows) {
|
||||||
|
t.Fatalf("expected no group row, got exists=%d err=%v", exists, qErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestU02AdminCreateOwnerMustExistAndEnabled(t *testing.T) {
|
func TestU02AdminCreateOwnerMustExistAndEnabled(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
gApp, _, _, db, down := setup(t)
|
gApp, _, _, db, down := setup(t)
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ func (a *App) Recall(ctx context.Context, senderID string, req *protocol.Recall)
|
|||||||
nowMs := a.now().UnixMilli()
|
nowMs := a.now().UnixMilli()
|
||||||
var data protocol.RecallData
|
var data protocol.RecallData
|
||||||
var revokes []revokeJob
|
var revokes []revokeJob
|
||||||
var wakeReceivers []string
|
|
||||||
err := a.db.Queue.Do(ctx, func(tx *sql.Tx) error {
|
err := a.db.Queue.Do(ctx, func(tx *sql.Tx) error {
|
||||||
var seq int64
|
var seq int64
|
||||||
var state string
|
var state string
|
||||||
@@ -157,7 +156,6 @@ WHERE seq = ? AND endpoint_id = ? AND state = 'pending'`,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
recalled++
|
recalled++
|
||||||
wakeReceivers = append(wakeReceivers, p.ep)
|
|
||||||
if p.pushed.Valid && p.pushed.String != "" {
|
if p.pushed.Valid && p.pushed.String != "" {
|
||||||
revokes = append(revokes, revokeJob{
|
revokes = append(revokes, revokeJob{
|
||||||
endpointID: p.ep,
|
endpointID: p.ep,
|
||||||
@@ -192,9 +190,6 @@ SELECT COUNT(*) FROM deliveries WHERE seq = ? AND state IN ('expired','dropped',
|
|||||||
a.pendingRevoke = append(a.pendingRevoke, revokes...)
|
a.pendingRevoke = append(a.pendingRevoke, revokes...)
|
||||||
a.mu.Unlock()
|
a.mu.Unlock()
|
||||||
a.flushRevokes(ctx)
|
a.flushRevokes(ctx)
|
||||||
for _, ep := range wakeReceivers {
|
|
||||||
a.WakePush(ep)
|
|
||||||
}
|
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,6 @@ LIMIT ?`, nowMs, limit)
|
|||||||
}
|
}
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
n++
|
n++
|
||||||
wake[d.senderID] = struct{}{}
|
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
rows2, qErr := a.db.Read.QueryContext(ctx, `
|
rows2, qErr := a.db.Read.QueryContext(ctx, `
|
||||||
SELECT DISTINCT endpoint_id FROM deliveries WHERE seq = ? AND state = 'pending'`, d.seq)
|
SELECT DISTINCT endpoint_id FROM deliveries WHERE seq = ? AND state = 'pending'`, d.seq)
|
||||||
@@ -204,10 +203,8 @@ LIMIT ?`, endpointID, room)
|
|||||||
}
|
}
|
||||||
|
|
||||||
var toClaim []pushItem
|
var toClaim []pushItem
|
||||||
skipped := false
|
|
||||||
for _, it := range items {
|
for _, it := range items {
|
||||||
if it.body == nil {
|
if it.body == nil {
|
||||||
skipped = true
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
msg := protocol.Msg{
|
msg := protocol.Msg{
|
||||||
@@ -230,7 +227,6 @@ LIMIT ?`, endpointID, room)
|
|||||||
return rejErr
|
return rejErr
|
||||||
}
|
}
|
||||||
a.WakePush(it.senderID)
|
a.WakePush(it.senderID)
|
||||||
skipped = true
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
it.payload = payload
|
it.payload = payload
|
||||||
@@ -256,10 +252,6 @@ LIMIT ?`, endpointID, room)
|
|||||||
a.observeDispatchToPush(it.sendAt, nowMs)
|
a.observeDispatchToPush(it.sendAt, nowMs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 超限等跳过未占满窗口时再唤醒本端,让 worker 下一轮取后续 pending(不在本轮递归扫表)。
|
|
||||||
if skipped && len(claimed) < room {
|
|
||||||
a.WakePush(endpointID)
|
|
||||||
}
|
|
||||||
return a.pushReceipts(ctx, endpointID, connID, nowMs)
|
return a.pushReceipts(ctx, endpointID, connID, nowMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
package message
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.asio.asia/nixevol/NixMsg/internal/app/port"
|
|
||||||
"git.asio.asia/nixevol/NixMsg/internal/protocol"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 定时到点且接收方离线成终态时,在线发送方经 WakePush 拿到回执。
|
|
||||||
func TestR365DispatchDueWakesSenderForReceipt(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
e := openDeliveryEnv(t, nil)
|
|
||||||
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
||||||
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
||||||
_ = e.db.Queue.Do(context.Background(), func(tx *sql.Tx) error {
|
|
||||||
_, err := tx.Exec(`UPDATE endpoints SET offline_since = ? WHERE id=?`, e.nowMs-120_000, "bob")
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
ctx := context.Background()
|
|
||||||
aliceConn := LiveConn{ConnID: "c-alice", Ready: true}
|
|
||||||
e.conns.Set("alice", aliceConn)
|
|
||||||
if err := e.app.OnHandshakeComplete(ctx, "alice", aliceConn); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() { e.app.stopPushWorker("alice", "") })
|
|
||||||
|
|
||||||
delay := int64(5000)
|
|
||||||
req := baseSend("due-rcpt", "bob")
|
|
||||||
req.DelayMs = &delay
|
|
||||||
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, req); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
st, _ := e.msgState("alice", "due-rcpt")
|
|
||||||
if st != StateScheduled {
|
|
||||||
t.Fatalf("want scheduled got %s", st)
|
|
||||||
}
|
|
||||||
if e.down.FilterType(protocol.TypeReceipt) != 0 {
|
|
||||||
t.Fatal("receipt before due")
|
|
||||||
}
|
|
||||||
|
|
||||||
e.setNow(e.nowMs + 5000)
|
|
||||||
if _, err := e.app.DispatchDue(ctx, e.nowMs, 10); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
deadline := time.Now().Add(2 * time.Second)
|
|
||||||
for time.Now().Before(deadline) {
|
|
||||||
if e.down.FilterType(protocol.TypeReceipt) > 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
time.Sleep(20 * time.Millisecond)
|
|
||||||
}
|
|
||||||
t.Fatalf("sender got no receipt after scheduled dispatch, receipts=%d", e.down.FilterType(protocol.TypeReceipt))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 窗口内首条超限拒收后,同连接后续小消息仍被 worker 推送。
|
|
||||||
func TestR365TooLargeThenSmallContinuesPush(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
e := openDeliveryEnv(t, func(l *Limits) { l.DeliveryWindow = 1 })
|
|
||||||
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
||||||
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
||||||
ctx := context.Background()
|
|
||||||
// 整帧上限:小消息能过,大正文整帧超限被拒。
|
|
||||||
bobConn := LiveConn{ConnID: "c-bob", Ready: true, MaxReceiveBytes: 200}
|
|
||||||
e.conns.Set("bob", bobConn)
|
|
||||||
if err := e.app.OnHandshakeComplete(ctx, "bob", bobConn); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() { e.app.stopPushWorker("bob", "") })
|
|
||||||
|
|
||||||
big := baseSend("big-skip", "bob")
|
|
||||||
b := make([]byte, 400)
|
|
||||||
for i := range b {
|
|
||||||
b[i] = 'A'
|
|
||||||
}
|
|
||||||
big.Body.Data = string(b)
|
|
||||||
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, big); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
small := baseSend("small-ok", "bob")
|
|
||||||
small.Body.Data = "hi"
|
|
||||||
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, small); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
deadline := time.Now().Add(2 * time.Second)
|
|
||||||
for time.Now().Before(deadline) {
|
|
||||||
foundSmall := false
|
|
||||||
for _, p := range e.down.Snapshots() {
|
|
||||||
if payloadType(p.Payload) != protocol.TypeMsg {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var m struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
}
|
|
||||||
_ = json.Unmarshal(p.Payload, &m)
|
|
||||||
if m.ID == "small-ok" {
|
|
||||||
foundSmall = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if foundSmall {
|
|
||||||
seqBig := e.seqOf("alice", "big-skip")
|
|
||||||
st, reason := e.deliveryState(seqBig, "bob")
|
|
||||||
if st != DeliveryRejected || reason != ReasonTooLarge {
|
|
||||||
t.Fatalf("big: %s/%s", st, reason)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
time.Sleep(20 * time.Millisecond)
|
|
||||||
}
|
|
||||||
t.Fatalf("small msg not pushed after too_large; msgs=%d snapshots=%d",
|
|
||||||
e.down.FilterType(protocol.TypeMsg), len(e.down.Snapshots()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 撤回已推在途消息后,同连接更晚的 pending 继续推。
|
|
||||||
func TestR365RecallFreesWindowForLaterPending(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
e := openDeliveryEnv(t, func(l *Limits) { l.DeliveryWindow = 1 })
|
|
||||||
insertEndpoint(t, e.db, "alice", "", 1, 0)
|
|
||||||
insertEndpoint(t, e.db, "bob", "", 1, 0)
|
|
||||||
ctx := context.Background()
|
|
||||||
bobConn := LiveConn{ConnID: "c-bob", Ready: true}
|
|
||||||
e.conns.Set("bob", bobConn)
|
|
||||||
if err := e.app.OnHandshakeComplete(ctx, "bob", bobConn); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() { e.app.stopPushWorker("bob", "") })
|
|
||||||
|
|
||||||
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, baseSend("first-in-flight", "bob")); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := e.app.Submit(ctx, "alice", port.ConnInfo{}, baseSend("second-wait", "bob")); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
deadline := time.Now().Add(2 * time.Second)
|
|
||||||
for time.Now().Before(deadline) {
|
|
||||||
if e.down.FilterType(protocol.TypeMsg) >= 1 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
}
|
|
||||||
if e.down.FilterType(protocol.TypeMsg) < 1 {
|
|
||||||
t.Fatal("first msg not pushed")
|
|
||||||
}
|
|
||||||
seq1 := e.seqOf("alice", "first-in-flight")
|
|
||||||
var pushed sql.NullString
|
|
||||||
_ = e.db.Read.QueryRow(`SELECT pushed_conn FROM deliveries WHERE seq=? AND endpoint_id='bob'`, seq1).Scan(&pushed)
|
|
||||||
if !pushed.Valid {
|
|
||||||
t.Fatal("first not in-flight")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := e.app.Recall(ctx, "alice", &protocol.Recall{
|
|
||||||
V: protocol.Version, Type: protocol.TypeRecall, RID: "r1", ID: "first-in-flight",
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
deadline = time.Now().Add(2 * time.Second)
|
|
||||||
for time.Now().Before(deadline) {
|
|
||||||
for _, p := range e.down.Snapshots() {
|
|
||||||
if payloadType(p.Payload) != protocol.TypeMsg {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var m struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
}
|
|
||||||
_ = json.Unmarshal(p.Payload, &m)
|
|
||||||
if m.ID == "second-wait" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
time.Sleep(20 * time.Millisecond)
|
|
||||||
}
|
|
||||||
t.Fatalf("second pending not pushed after recall; msgs=%d", e.down.FilterType(protocol.TypeMsg))
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user