fix(group): bound push pressure and synchronize load agents
This commit is contained in:
+164
-21
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user