fix: 作废投递走统一终态函数并写回执收尾

This commit is contained in:
Nixevol
2026-09-30 16:21:05 +08:00
parent 91e887ba46
commit f139b9ed9b
10 changed files with 365 additions and 166 deletions
+37 -122
View File
@@ -7,6 +7,7 @@ import (
"errors"
"time"
"git.asio.asia/nixevol/NixMsg/internal/app/message"
"git.asio.asia/nixevol/NixMsg/internal/app/port"
"git.asio.asia/nixevol/NixMsg/internal/protocol"
)
@@ -140,9 +141,10 @@ WHERE id = ?`, endpointID); e != nil {
}
func voidEndpointMessagesTx(tx *sql.Tx, endpointID, recvReason, sendReason string, nowMs int64, revokes *[]revokeItem) error {
days := message.DefaultVoidRetentionDays
// 发给 X 的 pending → rejected
rows, err := tx.Query(`
SELECT d.seq, d.pushed_at, m.id, m.sender_id, m.receipt
SELECT d.seq, m.id, m.sender_id
FROM deliveries d
JOIN messages m ON m.seq = d.seq
WHERE d.endpoint_id = ? AND d.state = 'pending'`, endpointID)
@@ -151,15 +153,13 @@ WHERE d.endpoint_id = ? AND d.state = 'pending'`, endpointID)
}
type pendRow struct {
seq int64
pushed sql.NullInt64
msgID string
senderID string
receipt int
}
var pending []pendRow
for rows.Next() {
var r pendRow
if scanErr := rows.Scan(&r.seq, &r.pushed, &r.msgID, &r.senderID, &r.receipt); scanErr != nil {
if scanErr := rows.Scan(&r.seq, &r.msgID, &r.senderID); scanErr != nil {
_ = rows.Close()
return scanErr
}
@@ -171,18 +171,11 @@ WHERE d.endpoint_id = ? AND d.state = 'pending'`, endpointID)
}
finalSeqs := map[int64]struct{}{}
for _, r := range pending {
if _, execErr := tx.Exec(`
UPDATE deliveries SET state = 'rejected', reason = ?, updated_at = ?
WHERE seq = ? AND endpoint_id = ? AND state = 'pending'`,
recvReason, nowMs, r.seq, endpointID); execErr != nil {
pushed, execErr := message.RejectPendingTx(tx, r.seq, endpointID, recvReason, nowMs)
if execErr != nil {
return execErr
}
if r.receipt != 0 {
if e := insertReceiptIfWantedTx(tx, r.senderID, r.msgID, endpointID, "rejected", recvReason, nowMs, true); e != nil {
return e
}
}
if r.pushed.Valid && revokes != nil {
if pushed && revokes != nil {
*revokes = append(*revokes, revokeItem{
endpointID: endpointID, msgID: r.msgID, fromID: r.senderID, reason: recvReason,
})
@@ -192,21 +185,20 @@ WHERE seq = ? AND endpoint_id = ? AND state = 'pending'`,
// 发给 X 的 scheduled 单聊 → completed,要回执则写
srows, err := tx.Query(`
SELECT seq, id, sender_id, receipt FROM messages
SELECT seq, sender_id, receipt FROM messages
WHERE dest_kind = 'endpoint' AND dest_id = ? AND state = 'scheduled'`, endpointID)
if err != nil {
return err
}
type schedRow struct {
seq int64
msgID string
senderID string
receipt int
}
var scheduledTo []schedRow
for srows.Next() {
var r schedRow
if scanErr := srows.Scan(&r.seq, &r.msgID, &r.senderID, &r.receipt); scanErr != nil {
if scanErr := srows.Scan(&r.seq, &r.senderID, &r.receipt); scanErr != nil {
_ = srows.Close()
return scanErr
}
@@ -217,19 +209,8 @@ WHERE dest_kind = 'endpoint' AND dest_id = ? AND state = 'scheduled'`, endpointI
return err
}
for _, r := range scheduledTo {
if _, execErr := tx.Exec(`
UPDATE messages SET state = 'completed', reason = ? WHERE seq = ? AND state = 'scheduled'`,
recvReason, r.seq); execErr != nil {
return execErr
}
if _, execErr := tx.Exec(`DELETE FROM message_bodies WHERE seq = ?`, r.seq); execErr != nil {
return execErr
}
if r.receipt != 0 {
// 消息级作废:endpoint_id 空,state=rejected(DEVELOPMENT 6.4)
if e := insertReceiptIfWantedTx(tx, r.senderID, r.msgID, "", "rejected", recvReason, nowMs, true); e != nil {
return e
}
if e := message.FinalizeMessageTx(tx, r.seq, r.receipt != 0, r.senderID, "", recvReason, nowMs, days); e != nil {
return e
}
}
@@ -252,19 +233,14 @@ UPDATE messages SET state = 'completed', reason = ? WHERE seq = ? AND state = 's
return err
}
for _, seq := range outSeqs {
if _, execErr := tx.Exec(`
UPDATE messages SET state = 'completed', reason = ? WHERE seq = ? AND state = 'scheduled'`,
sendReason, seq); execErr != nil {
return execErr
}
if _, execErr := tx.Exec(`DELETE FROM message_bodies WHERE seq = ?`, seq); execErr != nil {
return execErr
if e := message.FinalizeMessageTx(tx, seq, false, endpointID, "", sendReason, nowMs, days); e != nil {
return e
}
}
// X 发出的消息的 pending 投递 → rejected(sender_*),不写回执
drows, err := tx.Query(`
SELECT d.seq, d.endpoint_id, d.pushed_at, m.id, m.sender_id
SELECT d.seq, d.endpoint_id, m.id, m.sender_id
FROM deliveries d
JOIN messages m ON m.seq = d.seq
WHERE m.sender_id = ? AND d.state = 'pending'`, endpointID)
@@ -274,14 +250,13 @@ WHERE m.sender_id = ? AND d.state = 'pending'`, endpointID)
type outPend struct {
seq int64
endpointID string
pushed sql.NullInt64
msgID string
senderID string
}
var outPending []outPend
for drows.Next() {
var r outPend
if scanErr := drows.Scan(&r.seq, &r.endpointID, &r.pushed, &r.msgID, &r.senderID); scanErr != nil {
if scanErr := drows.Scan(&r.seq, &r.endpointID, &r.msgID, &r.senderID); scanErr != nil {
_ = drows.Close()
return scanErr
}
@@ -292,13 +267,11 @@ WHERE m.sender_id = ? AND d.state = 'pending'`, endpointID)
return err
}
for _, r := range outPending {
if _, execErr := tx.Exec(`
UPDATE deliveries SET state = 'rejected', reason = ?, updated_at = ?
WHERE seq = ? AND endpoint_id = ? AND state = 'pending'`,
sendReason, nowMs, r.seq, r.endpointID); execErr != nil {
pushed, execErr := message.RejectPendingTx(tx, r.seq, r.endpointID, sendReason, nowMs)
if execErr != nil {
return execErr
}
if r.pushed.Valid && revokes != nil {
if pushed && revokes != nil {
*revokes = append(*revokes, revokeItem{
endpointID: r.endpointID, msgID: r.msgID, fromID: r.senderID, reason: sendReason,
})
@@ -307,7 +280,7 @@ WHERE seq = ? AND endpoint_id = ? AND state = 'pending'`,
}
for seq := range finalSeqs {
if e := tryFinalizeTx(tx, seq); e != nil {
if e := message.TryFinalizeTx(tx, seq, nowMs, days); e != nil {
return e
}
}
@@ -445,7 +418,7 @@ func withoutMember(ids []string, drop string) []string {
// voidMemberDeliveriesTx 与 group 包同语义:退群成员的 pending 群投递改 rejected。
func voidMemberDeliveriesTx(tx *sql.Tx, groupID, endpointID, reason string, nowMs int64, revokes *[]revokeItem) error {
rows, err := tx.Query(`
SELECT d.seq, d.pushed_at, m.id, m.sender_id
SELECT d.seq, m.id, m.sender_id
FROM deliveries d
JOIN messages m ON m.seq = d.seq
WHERE d.endpoint_id = ? AND d.state = 'pending'
@@ -455,14 +428,13 @@ WHERE d.endpoint_id = ? AND d.state = 'pending'
}
type row struct {
seq int64
pushed sql.NullInt64
msgID string
senderID string
}
var list []row
for rows.Next() {
var r row
if scanErr := rows.Scan(&r.seq, &r.pushed, &r.msgID, &r.senderID); scanErr != nil {
if scanErr := rows.Scan(&r.seq, &r.msgID, &r.senderID); scanErr != nil {
_ = rows.Close()
return scanErr
}
@@ -472,19 +444,18 @@ WHERE d.endpoint_id = ? AND d.state = 'pending'
if err = rows.Err(); err != nil {
return err
}
days := message.DefaultVoidRetentionDays
for _, r := range list {
if _, execErr := tx.Exec(`
UPDATE deliveries SET state = 'rejected', reason = ?, updated_at = ?
WHERE seq = ? AND endpoint_id = ? AND state = 'pending'`,
reason, nowMs, r.seq, endpointID); execErr != nil {
pushed, execErr := message.RejectPendingTx(tx, r.seq, endpointID, reason, nowMs)
if execErr != nil {
return execErr
}
if r.pushed.Valid && revokes != nil {
if pushed && revokes != nil {
*revokes = append(*revokes, revokeItem{
endpointID: endpointID, msgID: r.msgID, fromID: r.senderID, reason: reason,
})
}
if e := tryFinalizeTx(tx, r.seq); e != nil {
if e := message.TryFinalizeTx(tx, r.seq, nowMs, days); e != nil {
return e
}
}
@@ -492,8 +463,9 @@ WHERE seq = ? AND endpoint_id = ? AND state = 'pending'`,
}
func voidGroupAllTx(tx *sql.Tx, groupID string, nowMs int64, revokes *[]revokeItem) error {
days := message.DefaultVoidRetentionDays
rows, err := tx.Query(`
SELECT d.seq, d.endpoint_id, d.pushed_at, m.id, m.sender_id
SELECT d.seq, d.endpoint_id, m.id, m.sender_id
FROM deliveries d
JOIN messages m ON m.seq = d.seq
WHERE d.state = 'pending' AND m.dest_kind = 'group' AND m.dest_id = ?`, groupID)
@@ -503,14 +475,13 @@ WHERE d.state = 'pending' AND m.dest_kind = 'group' AND m.dest_id = ?`, groupID)
type drow struct {
seq int64
endpointID string
pushed sql.NullInt64
msgID string
senderID string
}
var dlist []drow
for rows.Next() {
var r drow
if scanErr := rows.Scan(&r.seq, &r.endpointID, &r.pushed, &r.msgID, &r.senderID); scanErr != nil {
if scanErr := rows.Scan(&r.seq, &r.endpointID, &r.msgID, &r.senderID); scanErr != nil {
_ = rows.Close()
return scanErr
}
@@ -521,38 +492,35 @@ WHERE d.state = 'pending' AND m.dest_kind = 'group' AND m.dest_id = ?`, groupID)
return err
}
for _, r := range dlist {
if _, execErr := tx.Exec(`
UPDATE deliveries SET state = 'rejected', reason = ?, updated_at = ?
WHERE seq = ? AND endpoint_id = ? AND state = 'pending'`,
reasonGroupDissolved, nowMs, r.seq, r.endpointID); execErr != nil {
pushed, execErr := message.RejectPendingTx(tx, r.seq, r.endpointID, reasonGroupDissolved, nowMs)
if execErr != nil {
return execErr
}
if r.pushed.Valid && revokes != nil {
if pushed && revokes != nil {
*revokes = append(*revokes, revokeItem{
endpointID: r.endpointID, msgID: r.msgID, fromID: r.senderID, reason: reasonGroupDissolved,
})
}
if e := tryFinalizeTx(tx, r.seq); e != nil {
if e := message.TryFinalizeTx(tx, r.seq, nowMs, days); e != nil {
return e
}
}
srows, err := tx.Query(`
SELECT seq, id, sender_id, receipt FROM messages
SELECT seq, sender_id, receipt FROM messages
WHERE dest_kind = 'group' AND dest_id = ? AND state = 'scheduled'`, groupID)
if err != nil {
return err
}
type srow struct {
seq int64
msgID string
senderID string
receipt int
}
var slist []srow
for srows.Next() {
var r srow
if scanErr := srows.Scan(&r.seq, &r.msgID, &r.senderID, &r.receipt); scanErr != nil {
if scanErr := srows.Scan(&r.seq, &r.senderID, &r.receipt); scanErr != nil {
_ = srows.Close()
return scanErr
}
@@ -563,66 +531,13 @@ WHERE dest_kind = 'group' AND dest_id = ? AND state = 'scheduled'`, groupID)
return err
}
for _, r := range slist {
if _, execErr := tx.Exec(`
UPDATE messages SET state = 'completed', reason = ? WHERE seq = ? AND state = 'scheduled'`,
reasonGroupDissolved, r.seq); execErr != nil {
return execErr
}
if _, execErr := tx.Exec(`DELETE FROM message_bodies WHERE seq = ?`, r.seq); execErr != nil {
return execErr
}
if r.receipt != 0 {
if e := insertReceiptIfWantedTx(tx, r.senderID, r.msgID, "", "rejected", reasonGroupDissolved, nowMs, true); e != nil {
return e
}
if e := message.FinalizeMessageTx(tx, r.seq, r.receipt != 0, r.senderID, "", reasonGroupDissolved, nowMs, days); e != nil {
return e
}
}
return nil
}
func insertReceiptIfWantedTx(tx *sql.Tx, senderID, msgID, endpointID, state, reason string, nowMs int64, alreadyWanted bool) error {
if !alreadyWanted {
return nil
}
var one int
err := tx.QueryRow(`SELECT 1 FROM endpoints WHERE id = ?`, senderID).Scan(&one)
if errors.Is(err, sql.ErrNoRows) {
return nil
}
if err != nil {
return err
}
_, err = tx.Exec(`
INSERT INTO receipts(sender_id, msg_id, endpoint_id, state, reason, created_at, acked)
VALUES(?,?,?,?,?,?,0)`, senderID, msgID, endpointID, state, reason, nowMs)
return err
}
func tryFinalizeTx(tx *sql.Tx, seq int64) error {
var n int
if err := tx.QueryRow(`SELECT COUNT(*) FROM deliveries WHERE seq = ? AND state = 'pending'`, seq).Scan(&n); err != nil {
return err
}
if n > 0 {
return nil
}
var state string
if err := tx.QueryRow(`SELECT state FROM messages WHERE seq = ?`, seq).Scan(&state); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil
}
return err
}
if state == "completed" {
return nil
}
if _, err := tx.Exec(`UPDATE messages SET state = 'completed' WHERE seq = ?`, seq); err != nil {
return err
}
_, err := tx.Exec(`DELETE FROM message_bodies WHERE seq = ?`, seq)
return err
}
func (a *App) publishRevokes(ctx context.Context, items []revokeItem) {
if a.down == nil || len(items) == 0 {
return
+48
View File
@@ -228,6 +228,54 @@ func TestF01DisableVoidsScheduledAndRejectsNew(t *testing.T) {
}
}
func TestDisableLastPendingFinalizesAndReceipt(t *testing.T) {
t.Parallel()
idApp, msgApp, db := openLifecycle(t)
ctx := context.Background()
insertEPFull(t, db, "alice")
insertEPFull(t, db, "bob")
ttl := int64(3600)
if _, err := msgApp.Submit(ctx, "alice", port.ConnInfo{EndpointID: "alice"}, &protocol.Send{
V: protocol.Version, Type: protocol.TypeSend, RID: "1", ID: "keep-bob",
To: protocol.Target{Kind: protocol.TargetEndpoint, ID: "bob"},
Body: protocol.Body{Enc: protocol.EncUTF8, Data: "hi"},
Offline: &protocol.OfflineOpts{Keep: true, TTLSeconds: &ttl},
}); err != nil {
t.Fatal(err)
}
if err := idApp.Disable(ctx, "bob"); err != nil {
t.Fatal(err)
}
var state, reason string
if err := db.Read.QueryRow(`SELECT state, reason FROM messages WHERE id='keep-bob'`).Scan(&state, &reason); err != nil {
t.Fatal(err)
}
if state != "completed" {
t.Fatalf("state=%s want completed", state)
}
var bodies int
if err := db.Read.QueryRow(`SELECT COUNT(*) FROM message_bodies b JOIN messages m ON m.seq=b.seq WHERE m.id='keep-bob'`).Scan(&bodies); err != nil {
t.Fatal(err)
}
if bodies != 0 {
t.Fatalf("body still present: %d", bodies)
}
var rState, rReason string
if err := db.Read.QueryRow(`SELECT state, reason FROM receipts WHERE sender_id='alice' AND msg_id='keep-bob'`).Scan(&rState, &rReason); err != nil {
t.Fatalf("receipt: %v", err)
}
if rState != "rejected" || rReason != "endpoint_disabled" {
t.Fatalf("receipt state=%q reason=%q", rState, rReason)
}
var pending int
if err := db.Read.QueryRow(`SELECT COUNT(*) FROM messages WHERE sender_id='alice' AND state IN ('scheduled','dispatched')`).Scan(&pending); err != nil {
t.Fatal(err)
}
if pending != 0 {
t.Fatalf("sender pending=%d", pending)
}
}
func TestF01DeleteOwnerTransfersEarliest(t *testing.T) {
t.Parallel()
idApp, _, db := openLifecycle(t)