fix(group): bound push pressure and synchronize load agents

This commit is contained in:
richjohnrambo
2026-10-04 00:49:27 +08:00
parent 746f75ab0f
commit 7b25365ec3
14 changed files with 476 additions and 47 deletions
+164 -21
View File
@@ -12,6 +12,7 @@ import (
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/tinode/chat/server/logs"
@@ -21,16 +22,39 @@ import (
)
const (
defaultBufferSize = 1024
defaultAPIDomain = "https://api.jpush.cn"
defaultPushPath = "/v3/push"
defaultTTL = 86400
defaultCallTTL = 30
callCategory = "im_call"
defaultBufferSize = 1024
defaultWorkers = 8
defaultAPIDomain = "https://api.jpush.cn"
defaultPushPath = "/v3/push"
defaultTTL = 86400
defaultCallTTL = 30
defaultDeviceCacheTTL = 30
callCategory = "im_call"
)
var handler Handler
type deviceCacheKey struct {
tenantID t.TenantID
provider string
uid t.Uid
}
type deviceCacheEntry struct {
devices []t.DeviceDef
expiresAt time.Time
staleUntil time.Time
}
type providerDeviceCache struct {
mu sync.Mutex
entries map[deviceCacheKey]deviceCacheEntry
retryAfter time.Time
nextCleanup time.Time
}
var jpushDeviceCache = providerDeviceCache{entries: make(map[deviceCacheKey]deviceCacheEntry)}
type Handler struct {
input chan *push.Receipt
channel chan *push.ChannelReq
@@ -40,19 +64,21 @@ type Handler struct {
}
type configType struct {
Enabled bool `json:"enabled"`
DryRun bool `json:"dry_run"`
AppKey string `json:"app_key"`
MasterSecret string `json:"master_secret"`
APIDomain string `json:"api_domain"`
PushPath string `json:"push_path"`
Buffer int `json:"buffer"`
DefaultTTLSeconds int `json:"default_ttl_seconds"`
CallTTLSeconds int `json:"call_ttl_seconds"`
APNsProduction bool `json:"apns_production"`
UseAlias *bool `json:"use_alias"`
Classification int `json:"classification"`
Title string `json:"title"`
Enabled bool `json:"enabled"`
DryRun bool `json:"dry_run"`
AppKey string `json:"app_key"`
MasterSecret string `json:"master_secret"`
APIDomain string `json:"api_domain"`
PushPath string `json:"push_path"`
Buffer int `json:"buffer"`
Workers int `json:"workers"`
DefaultTTLSeconds int `json:"default_ttl_seconds"`
CallTTLSeconds int `json:"call_ttl_seconds"`
DeviceCacheTTLSeconds int `json:"device_cache_ttl_seconds"`
APNsProduction bool `json:"apns_production"`
UseAlias *bool `json:"use_alias"`
Classification int `json:"classification"`
Title string `json:"title"`
}
type jpushPayload struct {
@@ -154,10 +180,19 @@ func (Handler) Init(jsonconf json.RawMessage) (bool, error) {
handler.config = normalized
go func() {
workers := make(chan struct{}, normalized.Workers)
for {
select {
case rcpt := <-handler.input:
go sendReceipt(rcpt, &handler.config, handler.client)
select {
case workers <- struct{}{}:
go func(receipt *push.Receipt) {
defer func() { <-workers }()
sendReceipt(receipt, &handler.config, handler.client)
}(rcpt)
case <-handler.stop:
return
}
case req := <-handler.channel:
logs.Info.Println("jpush: channel subscription ignored", req.Uid.UserId(), req.Channel)
case <-handler.stop:
@@ -186,12 +221,18 @@ func normalizeConfig(config configType) (configType, error) {
if config.Buffer <= 0 {
config.Buffer = defaultBufferSize
}
if config.Workers <= 0 {
config.Workers = defaultWorkers
}
if config.DefaultTTLSeconds <= 0 {
config.DefaultTTLSeconds = defaultTTL
}
if config.CallTTLSeconds <= 0 {
config.CallTTLSeconds = defaultCallTTL
}
if config.DeviceCacheTTLSeconds <= 0 {
config.DeviceCacheTTLSeconds = defaultDeviceCacheTTL
}
if config.Title == "" {
config.Title = "MeetingIM"
}
@@ -260,6 +301,8 @@ func loadThirdPartyConfig(config *configType) {
"jpush_master_secret",
"jpush_api_domain",
"jpush_push_path",
"jpush_workers",
"jpush_device_cache_ttl_seconds",
"jpush_default_ttl_seconds",
"jpush_call_ttl_seconds",
"jpush_apns_production",
@@ -285,6 +328,12 @@ func loadThirdPartyConfig(config *configType) {
if v := strings.TrimSpace(values["jpush_push_path"]); v != "" {
config.PushPath = v
}
if v, ok := parseConfigInt(values["jpush_workers"]); ok {
config.Workers = v
}
if v, ok := parseConfigInt(values["jpush_device_cache_ttl_seconds"]); ok {
config.DeviceCacheTTLSeconds = v
}
if v, ok := parseConfigInt(values["jpush_default_ttl_seconds"]); ok {
config.DefaultTTLSeconds = v
}
@@ -344,7 +393,8 @@ func preparePayloads(rcpt *push.Receipt, config *configType) ([]*jpushPayload, [
}
var devices map[t.Uid][]t.DeviceDef
if !useAlias {
devices, _, err = providerDevicesForUsers(rcpt.TenantID, "jpush", uids...)
devices, _, err = cachedProviderDevicesForUsers(rcpt.TenantID, "jpush",
time.Duration(config.DeviceCacheTTLSeconds)*time.Second, uids...)
if err != nil {
logs.Warn.Println("jpush: db error", err)
return nil, nil
@@ -699,6 +749,99 @@ func providerDevicesForUsers(tenantID t.TenantID, provider string, uids ...t.Uid
return map[t.Uid][]t.DeviceDef{}, 0, nil
}
func cachedProviderDevicesForUsers(tenantID t.TenantID, provider string, ttl time.Duration,
uids ...t.Uid) (map[t.Uid][]t.DeviceDef, int, error) {
if ttl <= 0 {
ttl = defaultDeviceCacheTTL * time.Second
}
provider = strings.ToLower(strings.TrimSpace(provider))
now := time.Now()
cache := &jpushDeviceCache
cache.mu.Lock()
defer cache.mu.Unlock()
result := make(map[t.Uid][]t.DeviceDef, len(uids))
misses := make([]t.Uid, 0, len(uids))
for _, uid := range uids {
key := deviceCacheKey{tenantID: tenantID, provider: provider, uid: uid}
entry, ok := cache.entries[key]
if ok && now.Before(entry.expiresAt) {
result[uid] = append([]t.DeviceDef(nil), entry.devices...)
continue
}
misses = append(misses, uid)
}
if len(misses) == 0 {
return result, countDevices(result), nil
}
if cache.nextCleanup.IsZero() || !now.Before(cache.nextCleanup) {
for key, entry := range cache.entries {
if !now.Before(entry.staleUntil) {
delete(cache.entries, key)
}
}
cache.nextCleanup = now.Add(time.Minute)
}
// A failed lookup used to trigger one identical database query per group
// message. Back off briefly and use stale entries where possible instead.
if now.Before(cache.retryAfter) {
cache.addStale(result, tenantID, provider, now, misses)
return result, countDevices(result), nil
}
loaded, _, err := providerDevicesForUsers(tenantID, provider, misses...)
if err != nil {
cache.retryAfter = now.Add(time.Second)
if cache.addStale(result, tenantID, provider, now, misses) == len(misses) {
return result, countDevices(result), nil
}
return result, countDevices(result), err
}
cache.retryAfter = time.Time{}
staleTTL := 10 * ttl
if staleTTL < 5*time.Minute {
staleTTL = 5 * time.Minute
}
for _, uid := range misses {
devices := append([]t.DeviceDef(nil), loaded[uid]...)
cache.entries[deviceCacheKey{tenantID: tenantID, provider: provider, uid: uid}] = deviceCacheEntry{
devices: devices, expiresAt: now.Add(ttl), staleUntil: now.Add(staleTTL),
}
result[uid] = append([]t.DeviceDef(nil), devices...)
}
return result, countDevices(result), nil
}
func (c *providerDeviceCache) addStale(result map[t.Uid][]t.DeviceDef, tenantID t.TenantID,
provider string, now time.Time, uids []t.Uid) int {
covered := 0
for _, uid := range uids {
entry, ok := c.entries[deviceCacheKey{tenantID: tenantID, provider: provider, uid: uid}]
if ok && now.Before(entry.staleUntil) {
result[uid] = append([]t.DeviceDef(nil), entry.devices...)
covered++
}
}
return covered
}
func countDevices(devices map[t.Uid][]t.DeviceDef) int {
count := 0
for _, entries := range devices {
count += len(entries)
}
return count
}
func resetDeviceCache() {
jpushDeviceCache.mu.Lock()
jpushDeviceCache.entries = make(map[deviceCacheKey]deviceCacheEntry)
jpushDeviceCache.retryAfter = time.Time{}
jpushDeviceCache.nextCleanup = time.Time{}
jpushDeviceCache.mu.Unlock()
}
func (Handler) IsReady() bool {
return handler.input != nil
}