perf: sync protocol and core hardening updates

This commit is contained in:
A 2026-07-11 19:48:26 +08:00
parent 152fed3b87
commit 4390ebf5a9
283 changed files with 29231 additions and 2295 deletions

View file

@ -84,6 +84,12 @@ WITH pm AS (
sender_user_id,
recipient_user_id,
random_id,
request_fingerprint,
recipient_delivered,
sender_box_id,
sender_pts,
recipient_box_id,
recipient_pts,
message_date,
body,
entities
@ -91,6 +97,12 @@ WITH pm AS (
$1,
$2,
0,
'\x'::bytea,
false,
0,
0,
0,
0,
$3,
$4,
$5::jsonb
@ -530,6 +542,12 @@ INSERT INTO private_messages (
sender_user_id,
recipient_user_id,
random_id,
request_fingerprint,
recipient_delivered,
sender_box_id,
sender_pts,
recipient_box_id,
recipient_pts,
message_date,
ttl_period,
expires_at,
@ -556,27 +574,29 @@ INSERT INTO private_messages (
grouped_id,
effect
) VALUES (
$1, $2, $3, $4, $6::int, $7::int, $5, $8::jsonb,
$9::boolean,
$10::boolean,
$11::int,
$12::text,
$13::bigint,
$14::int,
$15::int,
$16::text,
$17::jsonb,
$18::int,
$19::text,
$20::bigint,
$1, $2, $3, $6::bytea, $7::boolean,
0, 0, 0, 0,
$4, $8::int, $9::int, $5, $10::jsonb,
$11::boolean,
$12::boolean,
$13::int,
$14::text,
$15::bigint,
$16::int,
$17::int,
$18::text,
$19::jsonb,
$20::int,
$21::text,
$22::int,
$23::jsonb,
$24::jsonb,
$22::bigint,
$23::text,
$24::int,
$25::jsonb,
$26::bigint,
$27::bigint,
$28::bigint
$26::jsonb,
$27::jsonb,
$28::bigint,
$29::bigint,
$30::bigint
)
ON CONFLICT (sender_user_id, random_id) WHERE random_id <> 0 DO NOTHING
RETURNING
@ -593,34 +613,36 @@ RETURNING
`
type CreatePrivateMessageParams struct {
SenderUserID int64
RecipientUserID int64
RandomID int64
MessageDate int32
Body string
TtlPeriod int32
ExpiresAt int32
EntitiesJson []byte
Silent bool
Noforwards bool
ReplyToMsgID int32
ReplyToPeerType string
ReplyToPeerID int64
ReplyToTopID int32
ReplyToStoryID int32
QuoteText string
QuoteEntitiesJson []byte
QuoteOffset int32
FwdFromPeerType string
FwdFromPeerID int64
FwdFromName string
FwdDate int32
MediaJson []byte
ReplyMarkupJson []byte
RichMessageJson []byte
ViaBotID int64
GroupedID int64
Effect int64
SenderUserID int64
RecipientUserID int64
RandomID int64
MessageDate int32
Body string
RequestFingerprint []byte
RecipientDelivered bool
TtlPeriod int32
ExpiresAt int32
EntitiesJson []byte
Silent bool
Noforwards bool
ReplyToMsgID int32
ReplyToPeerType string
ReplyToPeerID int64
ReplyToTopID int32
ReplyToStoryID int32
QuoteText string
QuoteEntitiesJson []byte
QuoteOffset int32
FwdFromPeerType string
FwdFromPeerID int64
FwdFromName string
FwdDate int32
MediaJson []byte
ReplyMarkupJson []byte
RichMessageJson []byte
ViaBotID int64
GroupedID int64
Effect int64
}
type CreatePrivateMessageRow struct {
@ -643,6 +665,8 @@ func (q *Queries) CreatePrivateMessage(ctx context.Context, arg CreatePrivateMes
arg.RandomID,
arg.MessageDate,
arg.Body,
arg.RequestFingerprint,
arg.RecipientDelivered,
arg.TtlPeriod,
arg.ExpiresAt,
arg.EntitiesJson,
@ -1969,6 +1993,17 @@ SELECT
sender_user_id,
recipient_user_id,
random_id,
request_fingerprint,
recipient_delivered,
sender_box_id,
sender_pts,
recipient_box_id,
recipient_pts,
sender_snapshot::text AS sender_snapshot_json,
sender_delete_pts,
sender_delete_pts_count,
sender_delete_date,
sender_delete_message_ids::text AS sender_delete_message_ids_json,
message_date,
ttl_period,
expires_at,
@ -1987,16 +2022,27 @@ type GetPrivateMessageByRandomIDParams struct {
}
type GetPrivateMessageByRandomIDRow struct {
ID int64
SenderUserID int64
RecipientUserID int64
RandomID int64
MessageDate int32
TtlPeriod int32
ExpiresAt int32
EditDate int32
Body string
EntitiesJson string
ID int64
SenderUserID int64
RecipientUserID int64
RandomID int64
RequestFingerprint []byte
RecipientDelivered bool
SenderBoxID int32
SenderPts int32
RecipientBoxID int32
RecipientPts int32
SenderSnapshotJson string
SenderDeletePts int32
SenderDeletePtsCount int32
SenderDeleteDate int32
SenderDeleteMessageIdsJson string
MessageDate int32
TtlPeriod int32
ExpiresAt int32
EditDate int32
Body string
EntitiesJson string
}
func (q *Queries) GetPrivateMessageByRandomID(ctx context.Context, arg GetPrivateMessageByRandomIDParams) (GetPrivateMessageByRandomIDRow, error) {
@ -2007,6 +2053,17 @@ func (q *Queries) GetPrivateMessageByRandomID(ctx context.Context, arg GetPrivat
&i.SenderUserID,
&i.RecipientUserID,
&i.RandomID,
&i.RequestFingerprint,
&i.RecipientDelivered,
&i.SenderBoxID,
&i.SenderPts,
&i.RecipientBoxID,
&i.RecipientPts,
&i.SenderSnapshotJson,
&i.SenderDeletePts,
&i.SenderDeletePtsCount,
&i.SenderDeleteDate,
&i.SenderDeleteMessageIdsJson,
&i.MessageDate,
&i.TtlPeriod,
&i.ExpiresAt,

View file

@ -174,6 +174,7 @@ type AuthKey struct {
SystemVersion string
ApiID int32
AppVersion string
LastUsedAt pgtype.Timestamptz
}
type Authorization struct {
@ -621,6 +622,11 @@ type ChannelMessage struct {
GroupedID int64
SavedPeerType string
SavedPeerID int64
SendSnapshot []byte
DeletePts int32
DeletePtsCount int32
DeleteDate int32
DeleteMessageIds []byte
}
type ChannelMessageMedium struct {
@ -689,6 +695,14 @@ type ChannelUnreadMentionIndex struct {
CreatedAt pgtype.Timestamptz
}
type ChannelUpdateCheckpoint struct {
ChannelID int64
RetainedThroughPts int32
LatestEventDate int32
LatestPts int32
UpdatedAt pgtype.Timestamptz
}
type ChannelUpdateEvent struct {
ChannelID int64
Pts int32
@ -835,6 +849,16 @@ type DispatchOutbox struct {
UpdatedAt pgtype.Timestamptz
}
type DispatchOutboxUserHead struct {
TargetUserID int64
HeadID int64
HeadPts int32
LogicalShard *int16
Status string
NextAttemptAt pgtype.Timestamptz
UpdatedAt pgtype.Timestamptz
}
type Document struct {
ID int64
AccessHash int64
@ -1160,38 +1184,49 @@ type PrivateMediaCategoryCount struct {
}
type PrivateMessage struct {
ID int64
SenderUserID int64
RecipientUserID int64
RandomID int64
MessageDate int32
Body string
Entities []byte
CreatedAt pgtype.Timestamptz
EditDate int32
Silent bool
Noforwards bool
ReplyToMsgID int32
ReplyToPeerType string
ReplyToPeerID int64
ReplyToTopID int32
QuoteText string
QuoteEntities []byte
QuoteOffset int32
FwdFromPeerType string
FwdFromPeerID int64
FwdFromName string
FwdDate int32
Media []byte
TtlPeriod int32
ExpiresAt int32
ReplyMarkup []byte
ViaBotID int64
RichMessage []byte
GroupedID int64
ReplyToStoryID int32
Effect int64
HideEdited bool
ID int64
SenderUserID int64
RecipientUserID int64
RandomID int64
MessageDate int32
Body string
Entities []byte
CreatedAt pgtype.Timestamptz
EditDate int32
Silent bool
Noforwards bool
ReplyToMsgID int32
ReplyToPeerType string
ReplyToPeerID int64
ReplyToTopID int32
QuoteText string
QuoteEntities []byte
QuoteOffset int32
FwdFromPeerType string
FwdFromPeerID int64
FwdFromName string
FwdDate int32
Media []byte
TtlPeriod int32
ExpiresAt int32
ReplyMarkup []byte
ViaBotID int64
RichMessage []byte
GroupedID int64
ReplyToStoryID int32
Effect int64
HideEdited bool
RequestFingerprint []byte
RecipientDelivered bool
SenderBoxID int32
SenderPts int32
RecipientBoxID int32
RecipientPts int32
SenderSnapshot []byte
SenderDeletePts int32
SenderDeletePtsCount int32
SenderDeleteDate int32
SenderDeleteMessageIds []byte
}
type PrivateMessageReaction struct {
@ -1482,13 +1517,14 @@ type ThemeUserInstall struct {
}
type UpdateState struct {
AuthKeyID int64
Pts int32
Qts int32
Date int32
Seq int32
UpdatedAt pgtype.Timestamptz
UserID int64
AuthKeyID int64
Pts int32
Qts int32
Date int32
Seq int32
UpdatedAt pgtype.Timestamptz
UserID int64
ObservedPts int32
}
type UploadPart struct {
@ -1504,6 +1540,15 @@ type UploadPart struct {
Sha256 []byte
}
type UploadedMediaReceipt struct {
OwnerUserID int64
FileID int64
IntentHash []byte
MediaKind string
MediaID int64
CreatedAt pgtype.Timestamptz
}
type User struct {
ID int64
AccessHash int64
@ -1637,6 +1682,13 @@ type UserUpdateEvent struct {
EventPhone string
}
type UserUpdateRetention struct {
UserID int64
RetainedThroughPts int32
RetainedThroughDate int32
UpdatedAt pgtype.Timestamptz
}
type UserUpdateWatermark struct {
UserID int64
ContiguousPts int32

View file

@ -527,29 +527,29 @@ func (q *Queries) BatchListDispatchEvents(ctx context.Context, arg BatchListDisp
}
const claimDispatchOutbox = `-- name: ClaimDispatchOutbox :many
WITH picked AS (
SELECT d.target_user_id, d.id
FROM dispatch_outbox d
WITH picked_heads AS (
SELECT h.target_user_id, h.head_id
FROM dispatch_outbox_user_heads h
WHERE (
d.status = 'pending'
AND d.next_attempt_at <= now()
)
OR (
d.status = 'dispatching'
AND d.updated_at < now() - make_interval(secs => $1::int)
)
ORDER BY d.next_attempt_at ASC, d.target_user_id ASC, d.pts ASC, d.id ASC
h.status = 'pending'
AND h.next_attempt_at <= now()
)
OR (
h.status = 'dispatching'
AND h.updated_at < now() - make_interval(secs => $1::int)
)
ORDER BY h.next_attempt_at ASC, h.target_user_id ASC, h.head_pts ASC, h.head_id ASC
LIMIT $2
FOR UPDATE SKIP LOCKED
FOR UPDATE OF h SKIP LOCKED
)
UPDATE dispatch_outbox d
SET
status = 'dispatching',
attempts = d.attempts + 1,
updated_at = now()
FROM picked p
FROM picked_heads p
WHERE d.target_user_id = p.target_user_id
AND d.id = p.id
AND d.id = p.head_id
RETURNING
d.id,
d.target_user_id,
@ -575,6 +575,8 @@ type ClaimDispatchOutboxRow struct {
Attempts int32
}
// durable head 表只保留每用户一行,并同步 head 的 readiness。claim 先锁
// lane head 再更新对应 outbox 行,既不会扫描 backlog也不会并发领取同一用户。
func (q *Queries) ClaimDispatchOutbox(ctx context.Context, arg ClaimDispatchOutboxParams) ([]ClaimDispatchOutboxRow, error) {
rows, err := q.db.Query(ctx, claimDispatchOutbox, arg.LeaseSeconds, arg.LimitCount)
if err != nil {
@ -603,14 +605,100 @@ func (q *Queries) ClaimDispatchOutbox(ctx context.Context, arg ClaimDispatchOutb
return items, nil
}
const claimDispatchOutboxShards = `-- name: ClaimDispatchOutboxShards :many
WITH picked_heads AS (
SELECT h.target_user_id, h.head_id
FROM dispatch_outbox_user_heads h
-- 256 store.DispatchOutboxLogicalShards0069 generated column 是同一
-- schema 常量不得随 worker 数变化
WHERE h.logical_shard = ANY($1::smallint[])
AND (
(
h.status = 'pending'
AND h.next_attempt_at <= now()
)
OR (
h.status = 'dispatching'
AND h.updated_at < now() - make_interval(secs => $2::int)
)
)
ORDER BY h.next_attempt_at ASC, h.target_user_id ASC, h.head_pts ASC, h.head_id ASC
LIMIT $3
FOR UPDATE OF h SKIP LOCKED
)
UPDATE dispatch_outbox d
SET
status = 'dispatching',
attempts = d.attempts + 1,
updated_at = now()
FROM picked_heads p
WHERE d.target_user_id = p.target_user_id
AND d.id = p.head_id
RETURNING
d.id,
d.target_user_id,
d.pts,
d.event_type,
d.exclude_auth_key_id,
d.exclude_session_id,
d.attempts
`
type ClaimDispatchOutboxShardsParams struct {
ShardIds []int16
LeaseSeconds int32
LimitCount int32
}
type ClaimDispatchOutboxShardsRow struct {
ID int64
TargetUserID int64
Pts int32
EventType string
ExcludeAuthKeyID int64
ExcludeSessionID int64
Attempts int32
}
// 固定 logical shard 由 target_user_id 决定;运行时 worker 只领取分配给自己的
// shard 集合,因此同一用户永远只有一条串行 lane而不同用户可并行。
func (q *Queries) ClaimDispatchOutboxShards(ctx context.Context, arg ClaimDispatchOutboxShardsParams) ([]ClaimDispatchOutboxShardsRow, error) {
rows, err := q.db.Query(ctx, claimDispatchOutboxShards, arg.ShardIds, arg.LeaseSeconds, arg.LimitCount)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ClaimDispatchOutboxShardsRow
for rows.Next() {
var i ClaimDispatchOutboxShardsRow
if err := rows.Scan(
&i.ID,
&i.TargetUserID,
&i.Pts,
&i.EventType,
&i.ExcludeAuthKeyID,
&i.ExcludeSessionID,
&i.Attempts,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteFailedDispatchOutbox = `-- name: DeleteFailedDispatchOutbox :one
WITH doomed AS (
SELECT target_user_id, id
FROM dispatch_outbox
WHERE status = 'failed'
AND updated_at < now() - make_interval(secs => $1::int)
ORDER BY updated_at ASC, target_user_id ASC, id ASC
WITH doomed AS MATERIALIZED (
SELECT h.target_user_id, h.head_id AS id
FROM dispatch_outbox_user_heads h
WHERE h.status = 'failed'
AND h.updated_at < now() - make_interval(secs => $1::int)
ORDER BY h.updated_at ASC, h.target_user_id ASC, h.head_id ASC
LIMIT $2
FOR UPDATE OF h SKIP LOCKED
),
deleted AS (
DELETE FROM dispatch_outbox d
@ -628,6 +716,9 @@ type DeleteFailedDispatchOutboxParams struct {
LimitCount int32
}
// failed 只能成为 lane head从 head 表开始并先锁 head既走 0074 的小索引,也与
// claim/completion 保持同一 user_heads→outbox 锁序。删除的只是在线任务durable
// user_update_events 不动,故客户端仍可经 difference 恢复。
func (q *Queries) DeleteFailedDispatchOutbox(ctx context.Context, arg DeleteFailedDispatchOutboxParams) (int32, error) {
row := q.db.QueryRow(ctx, deleteFailedDispatchOutbox, arg.OlderThanSeconds, arg.LimitCount)
var deleted_count int32
@ -1087,66 +1178,121 @@ func (q *Queries) ListUserUpdateEventsAfter(ctx context.Context, arg ListUserUpd
return items, nil
}
const markDispatchDelivered = `-- name: MarkDispatchDelivered :exec
DELETE FROM dispatch_outbox
WHERE target_user_id = $1
AND id = $2
const markDispatchDelivered = `-- name: MarkDispatchDelivered :execrows
WITH locked_head AS MATERIALIZED (
SELECT h.target_user_id
FROM dispatch_outbox_user_heads h
WHERE h.target_user_id = $3::bigint
FOR UPDATE
)
DELETE FROM dispatch_outbox d
USING locked_head h
WHERE d.target_user_id = h.target_user_id
AND d.id = $1::bigint
AND d.status = 'dispatching'
AND d.attempts = $2::int
`
type MarkDispatchDeliveredParams struct {
TargetUserID int64
ID int64
ID int64
ExpectedAttempts int32
TargetUserID int64
}
// 方案 A投递成功即删除。outbox 是任务队列delivered 行无保留价值
// (消息在 message_boxes、离线补偿在 user_update_events删除让表维持「未完成任务」小稳态。
func (q *Queries) MarkDispatchDelivered(ctx context.Context, arg MarkDispatchDeliveredParams) error {
_, err := q.db.Exec(ctx, markDispatchDelivered, arg.TargetUserID, arg.ID)
return err
// claim 的锁序是 user_heads→outboxcompletion 必须先显式锁同一 head 再删 outbox
// 否则租约过期 claim 与完成恰好竞争时会形成 outbox→head / head→outbox 环路。
func (q *Queries) MarkDispatchDelivered(ctx context.Context, arg MarkDispatchDeliveredParams) (int64, error) {
result, err := q.db.Exec(ctx, markDispatchDelivered, arg.ID, arg.ExpectedAttempts, arg.TargetUserID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const markDispatchDeliveredBatch = `-- name: MarkDispatchDeliveredBatch :exec
const markDispatchDeliveredBatch = `-- name: MarkDispatchDeliveredBatch :execrows
WITH input AS MATERIALIZED (
SELECT tu.target_user_id, di.id, ea.attempts
FROM unnest($1::bigint[]) WITH ORDINALITY AS tu(target_user_id, ord)
JOIN unnest($2::bigint[]) WITH ORDINALITY AS di(id, ord) USING (ord)
JOIN unnest($3::int[]) WITH ORDINALITY AS ea(attempts, ord) USING (ord)
),
locked_heads AS MATERIALIZED (
SELECT h.target_user_id
FROM dispatch_outbox_user_heads h
JOIN (SELECT DISTINCT target_user_id FROM input) i USING (target_user_id)
-- Match ClaimDispatchOutbox[Shards] exactly. A stale-lease claim may lock several
-- dispatching heads while this completion batch locks the same set; a different
-- multi-row order would merely move the deadlock one level up.
ORDER BY h.next_attempt_at, h.target_user_id, h.head_pts, h.head_id
FOR UPDATE OF h
)
DELETE FROM dispatch_outbox d
USING unnest($1::bigint[]) WITH ORDINALITY AS tu(target_user_id, ord)
JOIN unnest($2::bigint[]) WITH ORDINALITY AS di(id, ord) USING (ord)
WHERE d.target_user_id = tu.target_user_id
AND d.id = di.id
USING input i, locked_heads h
WHERE d.target_user_id = h.target_user_id
AND d.target_user_id = i.target_user_id
AND d.id = i.id
AND d.status = 'dispatching'
AND d.attempts = i.attempts
`
type MarkDispatchDeliveredBatchParams struct {
TargetUserIds []int64
Ids []int64
TargetUserIds []int64
Ids []int64
ExpectedAttempts []int32
}
// 批量删除一批已投递的 (target_user_id, id)target_user_id 入 WHERE 命中唯一索引并避免串删。
func (q *Queries) MarkDispatchDeliveredBatch(ctx context.Context, arg MarkDispatchDeliveredBatchParams) error {
_, err := q.db.Exec(ctx, markDispatchDeliveredBatch, arg.TargetUserIds, arg.Ids)
return err
func (q *Queries) MarkDispatchDeliveredBatch(ctx context.Context, arg MarkDispatchDeliveredBatchParams) (int64, error) {
result, err := q.db.Exec(ctx, markDispatchDeliveredBatch, arg.TargetUserIds, arg.Ids, arg.ExpectedAttempts)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const markDispatchFailed = `-- name: MarkDispatchFailed :exec
UPDATE dispatch_outbox
const markDispatchFailed = `-- name: MarkDispatchFailed :execrows
WITH locked_head AS MATERIALIZED (
SELECT h.target_user_id
FROM dispatch_outbox_user_heads h
WHERE h.target_user_id = $4::bigint
FOR UPDATE
)
UPDATE dispatch_outbox d
SET
status = CASE WHEN attempts >= 5 THEN 'failed' ELSE 'pending' END,
status = CASE WHEN d.attempts >= 5 THEN 'failed' ELSE 'pending' END,
next_attempt_at = CASE
WHEN attempts >= 5 THEN next_attempt_at
ELSE now() + make_interval(secs => LEAST(60, attempts * attempts))
WHEN d.attempts >= 5 THEN d.next_attempt_at
ELSE now() + make_interval(secs => LEAST(60, d.attempts * d.attempts))
END,
last_error = $3,
last_error = $1::text,
updated_at = now()
WHERE target_user_id = $1
AND id = $2
FROM locked_head h
WHERE d.target_user_id = h.target_user_id
AND d.id = $2::bigint
AND d.status = 'dispatching'
AND d.attempts = $3::int
`
type MarkDispatchFailedParams struct {
TargetUserID int64
ID int64
LastError string
LastError string
ID int64
ExpectedAttempts int32
TargetUserID int64
}
func (q *Queries) MarkDispatchFailed(ctx context.Context, arg MarkDispatchFailedParams) error {
_, err := q.db.Exec(ctx, markDispatchFailed, arg.TargetUserID, arg.ID, arg.LastError)
return err
func (q *Queries) MarkDispatchFailed(ctx context.Context, arg MarkDispatchFailedParams) (int64, error) {
result, err := q.db.Exec(ctx, markDispatchFailed,
arg.LastError,
arg.ID,
arg.ExpectedAttempts,
arg.TargetUserID,
)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const maxUserPts = `-- name: MaxUserPts :one