merged from gramsrv upstream
This commit is contained in:
parent
79c64ee916
commit
21a0856587
651 changed files with 54774 additions and 4590 deletions
|
|
@ -64,7 +64,7 @@ func run() error {
|
|||
}
|
||||
defer pool.Close()
|
||||
|
||||
hs := hoststats.NewPoller(cfg.BlobDir)
|
||||
hs := hoststats.NewPoller(cfg.DiskStatsPath)
|
||||
go hs.Run(ctx, hostStatsPollInterval)
|
||||
|
||||
srv, err := newServer(cfg, newReadStore(pool), hs)
|
||||
|
|
@ -97,10 +97,10 @@ type uiConfig struct {
|
|||
Password string
|
||||
Token string
|
||||
SessionKey []byte
|
||||
// BlobDir is the local blob-storage root, reused only to pick which
|
||||
// filesystem the dashboard's disk-free reading statfs's -- irrelevant when
|
||||
// TELESRV_BLOB_BACKEND=s3, where disk space isn't the storage constraint.
|
||||
BlobDir string
|
||||
// DiskStatsPath points the dashboard host-disk sampler at the local path
|
||||
// that matters for the selected blob backend: permanent localfs storage or
|
||||
// the S3 upload spool.
|
||||
DiskStatsPath string
|
||||
// Permissions is the right set a panel session is issued with, from
|
||||
// TELESRV_ADMIN_UI_PERMISSIONS. The shipped default is the single wildcard
|
||||
// entry, so introducing the permission model never locks an operator out of a
|
||||
|
|
@ -163,14 +163,21 @@ func loadConfig() (uiConfig, error) {
|
|||
Password: appCfg.AdminUIPassword,
|
||||
Token: appCfg.AdminUIToken,
|
||||
SessionKey: sum[:],
|
||||
DiskStatsPath: dashboardDiskPath(appCfg),
|
||||
Permissions: appCfg.AdminUIPermissions,
|
||||
HideThirdPartyVerification: appCfg.HideThirdPartyVerification,
|
||||
BlobDir: appCfg.BlobDir,
|
||||
IdentityDir: appCfg.IdentityDir,
|
||||
RepoRoot: repoRoot,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func dashboardDiskPath(cfg config.Config) string {
|
||||
if strings.EqualFold(strings.TrimSpace(cfg.BlobBackendKind), "s3") && strings.TrimSpace(cfg.BlobStagingDir) != "" {
|
||||
return cfg.BlobStagingDir
|
||||
}
|
||||
return cfg.BlobDir
|
||||
}
|
||||
|
||||
func adminAPIURL(addr string) string {
|
||||
addr = strings.TrimSpace(addr)
|
||||
if addr == "" {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ import (
|
|||
// TELESRV_ADMIN_UI_PERMISSIONS and the ones the admin API enforces.
|
||||
const (
|
||||
permissionAll = "*"
|
||||
permissionPremiumManage = "premium.manage"
|
||||
permissionBotTokenRead = "bots.token.read"
|
||||
permissionVerificationReview = "verification.review"
|
||||
permissionVerificationRevoke = "verification.revoke"
|
||||
// Third-party bot verification. Deliberately not implied by the official
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ func (s *server) routes() http.Handler {
|
|||
mux.Handle("POST /api/actions/create-bot", s.requireAuthAPI(http.HandlerFunc(s.handleCreateBotAPI)))
|
||||
mux.Handle("POST /api/actions/create-broadcast", s.requireAuthAPI(http.HandlerFunc(s.handleCreateBroadcastAPI)))
|
||||
mux.Handle("POST /api/actions/delete-bot", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteBotAPI)))
|
||||
mux.Handle("POST /api/actions/export-bot-token", s.requireAuthAPI(http.HandlerFunc(s.handleExportBotTokenAPI)))
|
||||
mux.Handle("POST /api/actions/export-bot-token", s.requireAuthAPI(s.requirePermission(permissionBotTokenRead, http.HandlerFunc(s.handleExportBotTokenAPI))))
|
||||
mux.Handle("POST /api/actions/set-channel-verified", s.requireAuthAPI(http.HandlerFunc(s.handleSetChannelVerifiedAPI)))
|
||||
mux.Handle("POST /api/actions/revoke-sessions", s.requireAuthAPI(http.HandlerFunc(s.handleRevokeSessionsAPI)))
|
||||
mux.Handle("POST /api/actions/delete-messages", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteMessagesAPI)))
|
||||
|
|
|
|||
|
|
@ -144,6 +144,53 @@ func TestModerationReadAPIDisablesBrowserCaching(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAuthorizationRowJSONPreservesInt64AsDecimalStrings(t *testing.T) {
|
||||
const maxInt64 = int64(9223372036854775807)
|
||||
raw, err := json.Marshal(AuthorizationRow{AuthKeyID: maxInt64, Hash: maxInt64})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal authorization row: %v", err)
|
||||
}
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(raw, &got); err != nil {
|
||||
t.Fatalf("unmarshal authorization row: %v", err)
|
||||
}
|
||||
for _, field := range []string{"AuthKeyID", "Hash"} {
|
||||
if got[field] != "9223372036854775807" {
|
||||
t.Fatalf("authorization %s = %#v, want exact decimal string", field, got[field])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevokeSessionsBFFForwardsExactAuthorizationHash(t *testing.T) {
|
||||
const authorizationHash = int64(2361577175213625973)
|
||||
var got admin.RevokeSessionsRequest
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/accounts/revoke-sessions" || r.Header.Get("Authorization") != "Bearer secret" {
|
||||
t.Fatalf("upstream request path=%q authorization=%q", r.URL.Path, r.Header.Get("Authorization"))
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(admin.CommandResult{CommandID: got.CommandID, Status: "completed", DryRun: got.DryRun})
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
srv := &server{cfg: uiConfig{AdminAPIURL: upstream.URL, AdminAPIToken: "secret"}}
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/actions/revoke-sessions", strings.NewReader(`{
|
||||
"reason":"precision regression","confirm":false,"user_id":1001,
|
||||
"hash":"2361577175213625973"
|
||||
}`))
|
||||
req = req.WithContext(context.WithValue(req.Context(), actorKey{}, "operator"))
|
||||
rec := httptest.NewRecorder()
|
||||
srv.handleRevokeSessionsAPI(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if got.Hash != authorizationHash || got.Actor != "operator" || !got.DryRun {
|
||||
t.Fatalf("forwarded revoke request = %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMintCollectibleUsernameBFFForwardsActorAndTolerantScalars(t *testing.T) {
|
||||
const maxInt64 = int64(9223372036854775807)
|
||||
var got admin.MintCollectibleUsernameRequest
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
cmd/telesrv-admin/web/dist/index.html
vendored
4
cmd/telesrv-admin/web/dist/index.html
vendored
|
|
@ -23,8 +23,8 @@
|
|||
})();
|
||||
</script>
|
||||
|
||||
<script type="module" crossorigin src="/assets/index-D8u51wND.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-hA2EpjuH.css">
|
||||
<script type="module" crossorigin src="/assets/index-CwTwvGWj.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-0MvM-hpw.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
6
cmd/telesrv-admin/web/package-lock.json
generated
6
cmd/telesrv-admin/web/package-lock.json
generated
|
|
@ -758,9 +758,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -193,3 +193,20 @@ export function parseIDs(value: string, invalidMessage = "msg ids invalid"): num
|
|||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
// toUnixSeconds reads a datetime-local input. Such an input carries no zone, so
|
||||
// the value parses as the operator's local time — which is the time they picked.
|
||||
// 0 means "empty or unparseable", which every caller treats as "not scheduled".
|
||||
export function toUnixSeconds(value: string): number {
|
||||
if (!value.trim()) return 0;
|
||||
const ms = new Date(value).getTime();
|
||||
return Number.isFinite(ms) ? Math.floor(ms / 1000) : 0;
|
||||
}
|
||||
|
||||
// localInputValue formats a datetime-local default some seconds out, so a
|
||||
// scheduling form never opens on a value the server would reject as past.
|
||||
export function localInputValue(offsetSeconds: number): string {
|
||||
const at = new Date(Date.now() + offsetSeconds * 1000);
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
return `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}T${pad(at.getHours())}:${pad(at.getMinutes())}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ export function ownerLabel(row: CollectibleUsernameRow, vaultLabel: string): str
|
|||
export function priceLabel(row: CollectibleUsernameRow): string {
|
||||
const base = formatCurrency(row.Amount, row.Currency);
|
||||
if (row.CryptoCurrency && row.CryptoAmount && row.CryptoAmount !== "0") {
|
||||
return `${base} (${formatCurrency(row.CryptoAmount, row.CryptoCurrency)})`;
|
||||
return `${formatCurrency(row.CryptoAmount, row.CryptoCurrency)} (${base})`;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { Alert, PageFrame } from "./components/ui";
|
|||
// (cmd/telesrv-admin/security.go). "*" is the wildcard an operator configures for
|
||||
// a full-access session.
|
||||
export const permissionAll = "*";
|
||||
export const permissionPremiumManage = "premium.manage";
|
||||
export const permissionBotTokenRead = "bots.token.read";
|
||||
export const permissionVerificationReview = "verification.review";
|
||||
export const permissionVerificationRevoke = "verification.revoke";
|
||||
// Third-party verification is a separate mechanism and therefore a separate pair of
|
||||
|
|
|
|||
|
|
@ -425,3 +425,34 @@
|
|||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.secret-reveal {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 10px;
|
||||
background: var(--warn-tint);
|
||||
border: 1px solid var(--warn-border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.secret-reveal-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--warn);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.secret-reveal-row { display: flex; align-items: center; gap: 10px; }
|
||||
.secret-reveal-value {
|
||||
overflow: hidden;
|
||||
flex: 1 1 auto;
|
||||
padding: 6px 10px;
|
||||
color: var(--text-soft);
|
||||
letter-spacing: .12em;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,16 @@ func run(ctx context.Context, args []string) error {
|
|||
return runKeygen(args[1:])
|
||||
case "provision":
|
||||
return runProvision(ctx, args[1:])
|
||||
case "plan-dataset":
|
||||
return runPlanDataset(args[1:])
|
||||
case "seed":
|
||||
return runSeed(ctx, args[1:])
|
||||
case "snapshot":
|
||||
return runSnapshot(ctx, args[1:])
|
||||
case "mutate-offline":
|
||||
return runMutateOffline(ctx, args[1:])
|
||||
case "startup-run":
|
||||
return runStartup(ctx, args[1:])
|
||||
case "run":
|
||||
return runLoad(ctx, args[1:])
|
||||
case "summarize":
|
||||
|
|
@ -50,6 +60,231 @@ func run(ctx context.Context, args []string) error {
|
|||
}
|
||||
}
|
||||
|
||||
func runPlanDataset(args []string) error {
|
||||
flags := flag.NewFlagSet("plan-dataset", flag.ContinueOnError)
|
||||
out := flags.String("out", filepath.FromSlash("data/loadtest/dataset.json"), "owner-only immutable dataset plan")
|
||||
accounts := flags.Int("accounts", 1000, "logical primary accounts in the provisioned manifest")
|
||||
seed := flags.Int64("seed", 20260827, "deterministic topology and idempotency seed")
|
||||
privateFanout := flags.Int("private-fanout", -1, "outgoing private messages per account; -1 uses min(10, accounts-1)")
|
||||
hotGroups := flags.Int("hot-groups", 10, "hot supergroup count")
|
||||
hotMembers := flags.Int("hot-members", 0, "members per hot supergroup; 0 uses all accounts")
|
||||
hotHistory := flags.Int("hot-history", 100, "messages per hot supergroup")
|
||||
mediumGroups := flags.Int("medium-groups", 100, "medium supergroup count")
|
||||
mediumMembers := flags.Int("medium-members", 100, "members per medium supergroup")
|
||||
mediumHistory := flags.Int("medium-history", 30, "messages per medium supergroup")
|
||||
smallGroups := flags.Int("small-groups", 200, "small supergroup count")
|
||||
smallMembers := flags.Int("small-members", 20, "members per small supergroup")
|
||||
smallHistory := flags.Int("small-history", 10, "messages per small supergroup")
|
||||
heavyGroups := flags.Int("heavy-groups", 200, "heavy-user supergroup count")
|
||||
heavyAccounts := flags.Int("heavy-accounts", 100, "accounts included in every heavy supergroup")
|
||||
heavyHistory := flags.Int("heavy-history", 30, "messages per heavy supergroup")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if flags.NArg() != 0 {
|
||||
return errors.New("plan-dataset accepts no positional arguments")
|
||||
}
|
||||
if *hotMembers == 0 {
|
||||
*hotMembers = *accounts
|
||||
}
|
||||
if *privateFanout == -1 {
|
||||
*privateFanout = min(10, max(*accounts-1, 0))
|
||||
}
|
||||
cfg := loadharness.DatasetConfig{
|
||||
Accounts: *accounts, Seed: *seed, PrivateFanout: *privateFanout,
|
||||
HotGroups: *hotGroups, HotMembers: *hotMembers, HotHistory: *hotHistory,
|
||||
MediumGroups: *mediumGroups, MediumMembers: min(*mediumMembers, *accounts), MediumHistory: *mediumHistory,
|
||||
SmallGroups: *smallGroups, SmallMembers: min(*smallMembers, *accounts), SmallHistory: *smallHistory,
|
||||
HeavyGroups: *heavyGroups, HeavyAccounts: min(*heavyAccounts, *accounts), HeavyHistory: *heavyHistory,
|
||||
}
|
||||
if _, err := os.Stat(*out); err == nil {
|
||||
existing, loadErr := loadharness.LoadDataset(*out)
|
||||
if loadErr != nil {
|
||||
return loadErr
|
||||
}
|
||||
if existing.Config != cfg {
|
||||
return fmt.Errorf("refusing to replace existing dataset plan %s with different config", *out)
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "dataset plan already exists at %s hash=%s groups=%d private_messages=%d\n",
|
||||
*out, existing.PlanSHA256, len(existing.Groups), len(existing.PrivateEdges))
|
||||
return nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
dataset, err := loadharness.PlanDataset(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := loadharness.WriteDataset(*out, dataset); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "dataset plan written to %s hash=%s groups=%d private_messages=%d\n",
|
||||
*out, dataset.PlanSHA256, len(dataset.Groups), len(dataset.PrivateEdges))
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSeed(ctx context.Context, args []string) error {
|
||||
flags := flag.NewFlagSet("seed", flag.ContinueOnError)
|
||||
manifest := flags.String("manifest", filepath.FromSlash("data/loadtest/manifest.json"), "provisioned manifest")
|
||||
sessionKey := flags.String("session-key", filepath.FromSlash("data/loadtest/session.key"), "session encryption key")
|
||||
rsaOverride := flags.String("rsa-key", "", "optional RSA public/private PEM override")
|
||||
dataset := flags.String("dataset", filepath.FromSlash("data/loadtest/dataset.json"), "immutable dataset plan")
|
||||
state := flags.String("state", filepath.FromSlash("data/loadtest/dataset-seed-state.json"), "resumable seed journal")
|
||||
concurrency := flags.Int("concurrency", 8, "parallel account workers (max 64)")
|
||||
operationTimeout := flags.Duration("operation-timeout", 30*time.Second, "maximum duration of one seed RPC")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if flags.NArg() != 0 {
|
||||
return errors.New("seed accepts no positional arguments")
|
||||
}
|
||||
result, err := loadharness.Seed(ctx, loadharness.SeedConfig{
|
||||
ManifestPath: *manifest, SessionKeyPath: *sessionKey, RSAKeyOverride: *rsaOverride,
|
||||
DatasetPath: *dataset, SeedStatePath: *state, Concurrency: *concurrency, OperationTimeout: *operationTimeout,
|
||||
}, func(event loadharness.SeedEvent) {
|
||||
status := "ok"
|
||||
if event.Err != nil {
|
||||
status = "error"
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "seed phase=%s %d/%d account=%d status=%s\n",
|
||||
event.Phase, event.Completed, event.Total, event.Account, status)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "seed complete private_messages=%d supergroups=%d invited_members=%d group_messages=%d rich_state_accounts=%d state=%s\n",
|
||||
result.PrivateMessages, result.Groups, result.InvitedMembers, result.GroupMessages, result.RichStateAccounts, *state)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSnapshot(ctx context.Context, args []string) error {
|
||||
flags := flag.NewFlagSet("snapshot", flag.ContinueOnError)
|
||||
manifest := flags.String("manifest", filepath.FromSlash("data/loadtest/manifest.json"), "provisioned manifest")
|
||||
sessionKey := flags.String("session-key", filepath.FromSlash("data/loadtest/session.key"), "session encryption key")
|
||||
rsaOverride := flags.String("rsa-key", "", "optional RSA public/private PEM override")
|
||||
dataset := flags.String("dataset", filepath.FromSlash("data/loadtest/dataset.json"), "immutable dataset plan")
|
||||
seedState := flags.String("seed-state", filepath.FromSlash("data/loadtest/dataset-seed-state.json"), "completed seed journal")
|
||||
clientState := flags.String("client-state", filepath.FromSlash("data/loadtest/client-state.json"), "baseline account/dialog/PTS snapshot")
|
||||
concurrency := flags.Int("concurrency", 8, "parallel account workers (max 64)")
|
||||
operationTimeout := flags.Duration("operation-timeout", 30*time.Second, "maximum duration of one snapshot RPC")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if flags.NArg() != 0 {
|
||||
return errors.New("snapshot accepts no positional arguments")
|
||||
}
|
||||
result, err := loadharness.SnapshotClientState(ctx, loadharness.SnapshotConfig{
|
||||
ManifestPath: *manifest, SessionKeyPath: *sessionKey, RSAKeyOverride: *rsaOverride,
|
||||
DatasetPath: *dataset, SeedStatePath: *seedState, ClientStatePath: *clientState,
|
||||
Concurrency: *concurrency, OperationTimeout: *operationTimeout,
|
||||
}, func(event loadharness.SnapshotEvent) {
|
||||
status := "ok"
|
||||
if event.Resumed {
|
||||
status = "resumed"
|
||||
}
|
||||
if event.Err != nil {
|
||||
status = "error"
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "snapshot %d/%d account=%d status=%s\n", event.Completed, event.Total, event.Account, status)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "snapshot complete accounts=%d dialogs=%d channel_dialogs=%d client_state=%s\n",
|
||||
result.Accounts, result.Dialogs, result.Channels, *clientState)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMutateOffline(ctx context.Context, args []string) error {
|
||||
flags := flag.NewFlagSet("mutate-offline", flag.ContinueOnError)
|
||||
manifest := flags.String("manifest", filepath.FromSlash("data/loadtest/manifest.json"), "provisioned manifest")
|
||||
sessionKey := flags.String("session-key", filepath.FromSlash("data/loadtest/session.key"), "session encryption key")
|
||||
rsaOverride := flags.String("rsa-key", "", "optional RSA public/private PEM override")
|
||||
dataset := flags.String("dataset", filepath.FromSlash("data/loadtest/dataset.json"), "immutable dataset plan")
|
||||
seedState := flags.String("seed-state", filepath.FromSlash("data/loadtest/dataset-seed-state.json"), "completed seed journal")
|
||||
clientState := flags.String("client-state", filepath.FromSlash("data/loadtest/client-state.json"), "immutable old account/channel PTS snapshot")
|
||||
mutationState := flags.String("mutation-state", filepath.FromSlash("data/loadtest/offline-mutation-state.json"), "resumable offline mutation journal")
|
||||
concurrency := flags.Int("concurrency", 8, "parallel writer accounts (max 64)")
|
||||
operationTimeout := flags.Duration("operation-timeout", 30*time.Second, "maximum duration of one mutation RPC")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if flags.NArg() != 0 {
|
||||
return errors.New("mutate-offline accepts no positional arguments")
|
||||
}
|
||||
result, err := loadharness.MutateOffline(ctx, loadharness.MutateOfflineConfig{
|
||||
ManifestPath: *manifest, SessionKeyPath: *sessionKey, RSAKeyOverride: *rsaOverride,
|
||||
DatasetPath: *dataset, SeedStatePath: *seedState, ClientStatePath: *clientState,
|
||||
MutationStatePath: *mutationState, Concurrency: *concurrency, OperationTimeout: *operationTimeout,
|
||||
}, func(event loadharness.MutationEvent) {
|
||||
status := "ok"
|
||||
if event.Err != nil {
|
||||
status = "error"
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "mutate phase=%s %d/%d account=%d status=%s\n",
|
||||
event.Phase, event.Completed, event.Total, event.Account, status)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(os.Stdout, "offline mutation complete private_messages=%d dirty_channels=%d channel_messages=%d edited=%d deleted=%d pinned=%d state=%s\n",
|
||||
result.PrivateMessages, result.DirtyChannels, result.ChannelMessages, result.Edited, result.Deleted, result.Pinned, *mutationState)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runStartup(ctx context.Context, args []string) error {
|
||||
flags := flag.NewFlagSet("startup-run", flag.ContinueOnError)
|
||||
manifest := flags.String("manifest", filepath.FromSlash("data/loadtest/manifest.json"), "provisioned manifest")
|
||||
sessionKey := flags.String("session-key", filepath.FromSlash("data/loadtest/session.key"), "session encryption key")
|
||||
rsaOverride := flags.String("rsa-key", "", "optional RSA public/private PEM override")
|
||||
dataset := flags.String("dataset", filepath.FromSlash("data/loadtest/dataset.json"), "immutable dataset plan")
|
||||
seedState := flags.String("seed-state", filepath.FromSlash("data/loadtest/dataset-seed-state.json"), "completed seed journal")
|
||||
clientState := flags.String("client-state", filepath.FromSlash("data/loadtest/client-state.json"), "immutable old account/channel PTS snapshot")
|
||||
mutationState := flags.String("mutation-state", filepath.FromSlash("data/loadtest/offline-mutation-state.json"), "completed offline mutation journal")
|
||||
report := flags.String("report", filepath.FromSlash("data/loadtest/startup-report.json"), "startup correctness and latency report")
|
||||
events := flags.String("events", filepath.FromSlash("data/loadtest/startup-events.ndjson"), "periodic owner-only startup and server metric evidence")
|
||||
serverMetrics := flags.String("server-metrics", "http://127.0.0.1:6060/metrics", "server metrics URL; empty disables")
|
||||
profile := flags.String("profile", loadharness.StartupProfileTDesktopReturningV1, "startup workload: tdesktop-cold-returning-v1 or tdlib-returning-v1")
|
||||
startOrder := flags.String("start-order", loadharness.StartupOrderShuffled, "account launch order: shuffled or account-index")
|
||||
startOrderSeed := flags.Int64("start-order-seed", 0, "deterministic shuffled launch seed; 0 uses the dataset seed")
|
||||
accounts := flags.Int("accounts", 0, "limit first N accounts; 0 uses the complete dataset")
|
||||
ramp := flags.Duration("ramp", 30*time.Second, "connection start ramp duration")
|
||||
operationTimeout := flags.Duration("operation-timeout", 30*time.Second, "maximum duration of one startup RPC")
|
||||
sampleInterval := flags.Duration("sample-interval", 2*time.Second, "server resource sampling interval")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if flags.NArg() != 0 {
|
||||
return errors.New("startup-run accepts no positional arguments")
|
||||
}
|
||||
result, err := loadharness.StartupRun(ctx, loadharness.StartupRunConfig{
|
||||
ManifestPath: *manifest, SessionKeyPath: *sessionKey, RSAKeyOverride: *rsaOverride,
|
||||
DatasetPath: *dataset, SeedStatePath: *seedState, ClientStatePath: *clientState,
|
||||
MutationStatePath: *mutationState, ReportPath: *report, EventsPath: *events, ServerMetricsURL: *serverMetrics,
|
||||
Profile: *profile, StartOrder: *startOrder, StartOrderSeed: *startOrderSeed,
|
||||
AccountLimit: *accounts, RampDuration: *ramp, OperationTimeout: *operationTimeout,
|
||||
SampleInterval: *sampleInterval,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printStartupSummary(result)
|
||||
if !result.Pass {
|
||||
return fmt.Errorf("startup acceptance failed; see %s", *report)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printStartupSummary(report *loadharness.StartupRunReport) {
|
||||
fmt.Fprintf(os.Stdout, "pass=%v business_ready=%d/%d dialogs=%d channel_dialogs=%d account_diff_calls=%d channel_diff_calls=%d channel_full=%d channel_too_long=%d channel_empty=%d\n",
|
||||
report.Pass, report.BusinessReady, report.ExpectedAccounts, report.DialogsObserved, report.ChannelDialogs,
|
||||
report.AccountDifference.Calls, report.ChannelDifference.Calls, report.ChannelDifference.Full,
|
||||
report.ChannelDifference.TooLong, report.ChannelDifference.Empty)
|
||||
for _, failure := range report.Failures {
|
||||
fmt.Fprintln(os.Stdout, "failure:", failure)
|
||||
}
|
||||
}
|
||||
|
||||
func runKeygen(args []string) error {
|
||||
flags := flag.NewFlagSet("keygen", flag.ContinueOnError)
|
||||
path := flags.String("out", filepath.FromSlash("data/loadtest/session.key"), "owner-only session encryption key file")
|
||||
|
|
@ -78,7 +313,7 @@ func runProvision(ctx context.Context, args []string) error {
|
|||
accounts := flags.Int("accounts", 450, "unique accounts")
|
||||
extraDevices := flags.Int("extra-devices", 50, "accounts receiving a second independent session")
|
||||
concurrency := flags.Int("concurrency", 8, "parallel provisioning workers (max 64)")
|
||||
phonePrefix := flags.String("phone-prefix", "+155500", "E.164 prefix followed by a six-digit account index")
|
||||
phonePrefix := flags.String("phone-prefix", loadharness.DefaultPhonePrefix, "possible reserved NANP prefix followed by a six-digit account index")
|
||||
firstName := flags.String("first-name-prefix", "Load", "generated first-name prefix")
|
||||
obfuscated := flags.Bool("obfuscated", true, "use TDesktop-like Obfuscated2 + abridged transport")
|
||||
pfs := flags.Bool("pfs", true, "bind temporary auth keys using PFS")
|
||||
|
|
@ -129,12 +364,17 @@ func runLoad(ctx context.Context, args []string) error {
|
|||
events := flags.String("events", filepath.FromSlash("data/loadtest/events.ndjson"), "periodic NDJSON evidence")
|
||||
fileFixture := flags.String("file-fixture", "", "reusable fixture JSON; empty stores beside manifest")
|
||||
serverMetrics := flags.String("server-metrics", "http://127.0.0.1:6060/metrics", "server metrics URL; empty disables")
|
||||
startOrder := flags.String("start-order", loadharness.StartupOrderShuffled, "session launch order: shuffled or account-index")
|
||||
startOrderSeed := flags.Int64("start-order-seed", 20260827, "deterministic shuffled launch seed")
|
||||
sessions := flags.Int("sessions", 0, "limit selected sessions; 0 uses all")
|
||||
duration := flags.Duration("duration", 30*time.Minute, "sustained load duration")
|
||||
recovery := flags.Duration("recovery", 7*time.Minute, "post-disconnect reclamation observation")
|
||||
ramp := flags.Duration("ramp", 2*time.Minute, "connection ramp duration")
|
||||
rpcInterval := flags.Duration("rpc-interval", 5*time.Second, "per-session background RPC interval")
|
||||
messageInterval := flags.Duration("message-interval", 30*time.Second, "per-primary-session message interval; negative disables")
|
||||
messageRate := flags.Float64("message-rate", 0, "aggregate fixed arrival rate in messages/second; use with message-interval=-1")
|
||||
messageQueue := flags.Int("message-queue", 8, "bounded pending sends per primary session for fixed-rate workload")
|
||||
deliverySettle := flags.Duration("delivery-settle", 10*time.Second, "maximum live-delivery settle time before final updates.getDifference reconciliation")
|
||||
fileInterval := flags.Duration("file-interval", time.Minute, "per-session upload.getFile interval")
|
||||
fileSize := flags.Int("file-size", 4<<20, "generated shared download fixture bytes; 0 disables")
|
||||
fileChunk := flags.Int("file-chunk", 1<<20, "upload.getFile bytes per request (max 1MiB)")
|
||||
|
|
@ -155,8 +395,10 @@ func runLoad(ctx context.Context, args []string) error {
|
|||
result, err := loadharness.Run(ctx, loadharness.RunConfig{
|
||||
ManifestPath: *manifest, SessionKeyPath: *sessionKey, RSAKeyOverride: *rsaOverride,
|
||||
ReportPath: *report, EventsPath: *events, FileFixturePath: *fileFixture, ServerMetricsURL: *serverMetrics,
|
||||
StartOrder: *startOrder, StartOrderSeed: *startOrderSeed,
|
||||
SessionLimit: *sessions, Duration: *duration, RecoveryDuration: *recovery, RampDuration: *ramp,
|
||||
RPCInterval: *rpcInterval, MessageInterval: *messageInterval, SampleInterval: *sampleInterval,
|
||||
RPCInterval: *rpcInterval, MessageInterval: *messageInterval, MessageRate: *messageRate,
|
||||
MessageQueueDepth: *messageQueue, DeliverySettle: *deliverySettle, SampleInterval: *sampleInterval,
|
||||
FileInterval: *fileInterval, FileSizeBytes: *fileSize, FileChunkBytes: *fileChunk, SetupTimeout: *setupTimeout,
|
||||
OperationTimeout: *operationTimeout,
|
||||
OfflineFraction: *offlineFraction, OfflineAt: *offlineAt, OfflineFor: *offlineFor,
|
||||
|
|
@ -183,6 +425,25 @@ func runSummarize(args []string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var shape struct {
|
||||
BusinessReady *int `json:"business_ready"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &shape); err != nil {
|
||||
return err
|
||||
}
|
||||
if shape.BusinessReady != nil {
|
||||
var report loadharness.StartupRunReport
|
||||
decoder := json.NewDecoder(strings.NewReader(string(data)))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&report); err != nil {
|
||||
return err
|
||||
}
|
||||
printStartupSummary(&report)
|
||||
if !report.Pass {
|
||||
return errors.New("startup report did not pass")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var report loadharness.RunReport
|
||||
decoder := json.NewDecoder(strings.NewReader(string(data)))
|
||||
decoder.DisallowUnknownFields()
|
||||
|
|
@ -197,9 +458,9 @@ func runSummarize(args []string) error {
|
|||
}
|
||||
|
||||
func printSummary(report *loadharness.RunReport) {
|
||||
fmt.Fprintf(os.Stdout, "pass=%v sessions=%d peak_ready=%d reconnects=%d disconnects=%d flood_waits=%d fatal_errors=%d\n",
|
||||
fmt.Fprintf(os.Stdout, "pass=%v sessions=%d peak_ready=%d reconnects=%d disconnects=%d flood_waits=%d fatal_errors=%d scheduled=%d delivered=%d missing=%d\n",
|
||||
report.Pass, report.ExpectedSessions, report.PeakReadySessions, report.Reconnects, report.Disconnects,
|
||||
totalFloodWaits(report), report.WorkerFatalErrors)
|
||||
totalFloodWaits(report), report.WorkerFatalErrors, report.MessageScheduled, report.Delivery.Delivered, report.Delivery.Missing)
|
||||
for _, failure := range report.Failures {
|
||||
fmt.Fprintln(os.Stdout, "failure:", failure)
|
||||
}
|
||||
|
|
@ -214,12 +475,17 @@ func totalFloodWaits(report *loadharness.RunReport) uint64 {
|
|||
}
|
||||
|
||||
func usageError() error {
|
||||
return errors.New("expected one of: keygen, provision, run, summarize, help")
|
||||
return errors.New("expected one of: keygen, provision, plan-dataset, seed, snapshot, mutate-offline, startup-run, run, summarize, help")
|
||||
}
|
||||
|
||||
const usageText = `telesrv-load commands:
|
||||
keygen generate an owner-only AES-256 session key
|
||||
provision create accounts and encrypted sessions through real MTProto auth
|
||||
plan-dataset create an immutable real-data topology with stable RPC identities
|
||||
seed materialize private dialogs, supergroups and messages via real RPCs
|
||||
snapshot save paginated real dialogs and old account/channel PTS cursors
|
||||
mutate-offline create account/channel gaps while preserving the old cursors
|
||||
startup-run restore old cursors and measure dialogs/difference business readiness
|
||||
run execute sustained real-client load, offline recovery and reclamation
|
||||
summarize print the acceptance summary from a JSON report
|
||||
|
||||
|
|
|
|||
109
cmd/telesrv-update/main.go
Normal file
109
cmd/telesrv-update/main.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// Command telesrv-update serves native Telegram client update metadata and
|
||||
// immutable, range-enabled desktop update packages.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"telesrv/internal/updatecdn"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "telesrv-update:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
listenDefault := envOr("TELESRV_UPDATE_LISTEN", "127.0.0.1:2402")
|
||||
manifestDefault := envOr("TELESRV_UPDATE_MANIFEST", "data/updates/manifest.json")
|
||||
filesDefault := envOr("TELESRV_UPDATE_FILES_DIR", "data/updates/files")
|
||||
|
||||
listenAddr := flag.String("listen", listenDefault, "HTTP listen address")
|
||||
manifestPath := flag.String("manifest", manifestDefault, "release manifest path")
|
||||
filesDir := flag.String("files", filesDefault, "desktop update package directory")
|
||||
check := flag.Bool("check", false, "validate the catalog and exit")
|
||||
flag.Parse()
|
||||
|
||||
store, err := updatecdn.NewStore(*manifestPath, *filesDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load update catalog: %w", err)
|
||||
}
|
||||
if *check {
|
||||
fmt.Println("update catalog is valid")
|
||||
return nil
|
||||
}
|
||||
handler, err := updatecdn.NewHandler(store)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listener, err := net.Listen("tcp", *listenAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen on %s: %w", *listenAddr, err)
|
||||
}
|
||||
|
||||
logger, err := zap.NewProduction()
|
||||
if err != nil {
|
||||
_ = listener.Close()
|
||||
return fmt.Errorf("initialize logger: %w", err)
|
||||
}
|
||||
defer logger.Sync() //nolint:errcheck
|
||||
|
||||
server := &http.Server{
|
||||
Handler: handler,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 5 * time.Minute,
|
||||
IdleTimeout: 2 * time.Minute,
|
||||
MaxHeaderBytes: 32 << 10,
|
||||
}
|
||||
serveErr := make(chan error, 1)
|
||||
go func() {
|
||||
serveErr <- server.Serve(listener)
|
||||
}()
|
||||
logger.Info("update service started",
|
||||
zap.String("listen", listener.Addr().String()),
|
||||
zap.String("manifest", *manifestPath),
|
||||
zap.String("files", *filesDir))
|
||||
|
||||
stopCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
select {
|
||||
case err := <-serveErr:
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("serve HTTP: %w", err)
|
||||
case <-stopCtx.Done():
|
||||
}
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(shutdownCtx); err != nil {
|
||||
return fmt.Errorf("shutdown HTTP server: %w", err)
|
||||
}
|
||||
if err := <-serveErr; err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return fmt.Errorf("serve HTTP: %w", err)
|
||||
}
|
||||
logger.Info("update service stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
func envOr(key, fallback string) string {
|
||||
if value, ok := os.LookupEnv(key); ok && value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
runtimemetrics "runtime/metrics"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
|
@ -60,6 +61,7 @@ import (
|
|||
"telesrv/internal/app/userprojection"
|
||||
"telesrv/internal/app/users"
|
||||
verificationapp "telesrv/internal/app/verification"
|
||||
welcomemessagesapp "telesrv/internal/app/welcomemessages"
|
||||
"telesrv/internal/botapi"
|
||||
"telesrv/internal/config"
|
||||
"telesrv/internal/domain"
|
||||
|
|
@ -79,6 +81,7 @@ import (
|
|||
"telesrv/internal/store/redisstore"
|
||||
"telesrv/internal/telegramloginhttp"
|
||||
"telesrv/internal/turnsrv"
|
||||
"telesrv/internal/updatecdn"
|
||||
"telesrv/internal/web"
|
||||
)
|
||||
|
||||
|
|
@ -273,8 +276,9 @@ func startDebugServer(ctx context.Context, addr string, metricsHandler http.Hand
|
|||
func goRuntimeGaugeSamples() []obsmetrics.GaugeSample {
|
||||
var mem runtime.MemStats
|
||||
runtime.ReadMemStats(&mem)
|
||||
return []obsmetrics.GaugeSample{
|
||||
samples := []obsmetrics.GaugeSample{
|
||||
{Name: "telesrv_go_goroutines", Value: float64(runtime.NumGoroutine())},
|
||||
{Name: "telesrv_go_scheduler_busy_seconds", Value: goSchedulerBusySeconds()},
|
||||
{Name: "telesrv_go_heap_alloc_bytes", Value: float64(mem.HeapAlloc)},
|
||||
{Name: "telesrv_go_heap_inuse_bytes", Value: float64(mem.HeapInuse)},
|
||||
{Name: "telesrv_go_heap_objects", Value: float64(mem.HeapObjects)},
|
||||
|
|
@ -283,6 +287,28 @@ func goRuntimeGaugeSamples() []obsmetrics.GaugeSample {
|
|||
{Name: "telesrv_go_gc_cycles", Value: float64(mem.NumGC)},
|
||||
{Name: "telesrv_go_gc_pause_seconds", Value: time.Duration(mem.PauseTotalNs).Seconds()},
|
||||
}
|
||||
if value, ok := processCPUSeconds(); ok {
|
||||
samples = append(samples, obsmetrics.GaugeSample{Name: "telesrv_process_cpu_seconds", Value: value})
|
||||
}
|
||||
return samples
|
||||
}
|
||||
|
||||
// goSchedulerBusySeconds is a Go scheduler-class estimate. The runtime
|
||||
// documentation explicitly warns that CPU-class values are overestimates and
|
||||
// are not comparable to operating-system process CPU time, so capacity reports
|
||||
// use telesrv_process_cpu_seconds instead.
|
||||
func goSchedulerBusySeconds() float64 {
|
||||
samples := []runtimemetrics.Sample{
|
||||
{Name: "/cpu/classes/total:cpu-seconds"},
|
||||
{Name: "/cpu/classes/idle:cpu-seconds"},
|
||||
}
|
||||
runtimemetrics.Read(samples)
|
||||
total := samples[0].Value.Float64()
|
||||
idle := samples[1].Value.Float64()
|
||||
if total <= idle {
|
||||
return 0
|
||||
}
|
||||
return total - idle
|
||||
}
|
||||
|
||||
func mtprotoRuntimeGaugeSamples(snapshot mtprotoedge.RuntimeSnapshot) []obsmetrics.GaugeSample {
|
||||
|
|
@ -303,6 +329,15 @@ func mtprotoRuntimeGaugeSamples(snapshot mtprotoedge.RuntimeSnapshot) []obsmetri
|
|||
{Name: "telesrv_mtproto_inbound_rpc_ready_connections", Value: float64(snapshot.InboundRPCReadyConnections)},
|
||||
{Name: "telesrv_mtproto_inbound_rpc_task_limit", Value: float64(snapshot.InboundRPCMaxTasks)},
|
||||
{Name: "telesrv_mtproto_inbound_rpc_byte_limit", Value: float64(snapshot.InboundRPCMaxBytes)},
|
||||
{Name: "telesrv_mtproto_rpc_delivery_hook_workers", Value: float64(snapshot.RPCDeliveryHookWorkers)},
|
||||
{Name: "telesrv_mtproto_rpc_delivery_hook_capacity", Value: float64(snapshot.RPCDeliveryHookCapacity)},
|
||||
{Name: "telesrv_mtproto_rpc_delivery_hook_reserved", Value: float64(snapshot.RPCDeliveryHookReserved)},
|
||||
{Name: "telesrv_mtproto_rpc_delivery_hook_queued", Value: float64(snapshot.RPCDeliveryHookQueued)},
|
||||
{Name: "telesrv_mtproto_rpc_delivery_hook_running", Value: float64(snapshot.RPCDeliveryHookRunning)},
|
||||
{Name: "telesrv_mtproto_rpc_delivery_hook_completed_total", Value: float64(snapshot.RPCDeliveryHookCompleted)},
|
||||
{Name: "telesrv_mtproto_rpc_delivery_hook_rejected_total", Value: float64(snapshot.RPCDeliveryHookRejected)},
|
||||
{Name: "telesrv_mtproto_rpc_delivery_hook_panics_total", Value: float64(snapshot.RPCDeliveryHookPanics)},
|
||||
{Name: "telesrv_mtproto_rpc_delivery_hook_duration_seconds_total", Value: snapshot.RPCDeliveryHookDurationSeconds},
|
||||
{Name: "telesrv_mtproto_inbound_frame_bytes", Value: float64(snapshot.InboundFrameBytes)},
|
||||
{Name: "telesrv_mtproto_inbound_frame_byte_limit", Value: float64(snapshot.InboundFrameMaxBytes)},
|
||||
{Name: "telesrv_mtproto_outbound_tracked_bytes", Labels: []obsmetrics.Label{{Name: "kind", Value: "body"}}, Value: float64(snapshot.OutboundTrackedBytes)},
|
||||
|
|
@ -445,15 +480,20 @@ type rpcProjectionVerificationNotifier struct {
|
|||
invalidator interface {
|
||||
InvalidateRPCProjectionReadModelForUser(userID int64)
|
||||
InvalidateRPCProjectionReadModelForChannel(channelID int64)
|
||||
InvalidatePeerIdentityReadModel(domain.Peer)
|
||||
}
|
||||
users storepkg.UserCache
|
||||
log *zap.Logger
|
||||
users storepkg.UserCache
|
||||
peerIdentity bool
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
func (n rpcProjectionVerificationNotifier) NotifyPeerVerified(ctx context.Context, peer domain.Peer) error {
|
||||
if n.invalidator == nil {
|
||||
return nil
|
||||
}
|
||||
if n.peerIdentity {
|
||||
n.invalidator.InvalidatePeerIdentityReadModel(peer)
|
||||
}
|
||||
switch peer.Type {
|
||||
case domain.PeerTypeUser:
|
||||
n.invalidator.InvalidateRPCProjectionReadModelForUser(peer.ID)
|
||||
|
|
@ -570,6 +610,15 @@ func run(logger *zap.Logger) error {
|
|||
zap.Bool("schema_dirty", migrationStatus.Dirty),
|
||||
zap.Bool("schema_empty", migrationStatus.Empty),
|
||||
)
|
||||
blobRuntimeLock, err := postgres.AcquireBlobRuntimeLock(ctx, cfg.PostgresDSN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("acquire blob runtime lock: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := blobRuntimeLock.Close(); err != nil {
|
||||
logger.Error("release blob runtime lock", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
pool, err := postgres.Open(ctx, cfg.PostgresDSN,
|
||||
postgres.WithMaxConns(cfg.PostgresMaxConns),
|
||||
postgres.WithMinConns(cfg.PostgresMinConns),
|
||||
|
|
@ -649,7 +698,8 @@ func run(logger *zap.Logger) error {
|
|||
if cfg.TelegramLoginEnabled {
|
||||
telegramLoginHTTPHandler, err = telegramloginhttp.NewHandler(telegramloginhttp.Config{
|
||||
Service: telegramLoginService, Tokens: telegramLoginIDTokens,
|
||||
Limiter: redisstore.NewRateLimiter(rdb), AppName: cfg.PublicAppName,
|
||||
BotUsernames: postgres.NewUserStore(pool),
|
||||
Limiter: redisstore.NewRateLimiter(rdb), AppName: cfg.PublicAppName,
|
||||
Logger: logger.Named("telegram-login-http"), TrustedProxyCIDRs: cfg.TelegramLoginTrustedProxyCIDRs,
|
||||
AllowHTTP: cfg.TelegramLoginAllowHTTP,
|
||||
})
|
||||
|
|
@ -662,51 +712,157 @@ func run(logger *zap.Logger) error {
|
|||
}
|
||||
|
||||
authKeyStore := postgres.NewAuthKeyStore(pool)
|
||||
authKeyGetBatchStore, err := postgres.NewBatchedAuthKeyStore(
|
||||
authKeyStore,
|
||||
postgres.AuthKeyGetBatchConfig{
|
||||
MaxSize: cfg.AuthKeyGetBatchMax, MaxWait: cfg.AuthKeyGetBatchWait,
|
||||
QueueSize: cfg.AuthKeyGetBatchQueue, QueryTimeout: cfg.AuthKeyGetBatchTimeout,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer authKeyGetBatchStore.Close()
|
||||
authKeySessionLayerStore, err := postgres.NewBatchedAuthKeySessionLayerStore(
|
||||
authKeyStore,
|
||||
postgres.AuthKeySessionLayerBatchConfig{
|
||||
MaxSize: cfg.LayerAdvanceBatchMax, MaxWait: cfg.LayerAdvanceBatchWait,
|
||||
QueueSize: cfg.LayerAdvanceBatchQueue, QueryTimeout: cfg.LayerAdvanceBatchTimeout,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer authKeySessionLayerStore.Close()
|
||||
userStore := postgres.NewUserStore(pool)
|
||||
authzStore := postgres.NewAuthorizationStore(pool)
|
||||
adminStore := postgres.NewAdminStore(pool)
|
||||
updateStateStore := postgres.NewUpdateStateStore(pool)
|
||||
updateEventStore := postgres.NewUpdateEventStore(pool, postgres.WithUpdateEventLogger(logger.Named("store").Named("updates")))
|
||||
phoneChangeStore := postgres.NewPhoneChangeStore(pool)
|
||||
readModelVersionStore := storepkg.NewCachedReadModelVersionStore(postgres.NewReadModelVersionStore(pool), 0, 0)
|
||||
readModelVersionBatchStore, err := storepkg.NewBatchedReadModelVersionStore(
|
||||
postgres.NewReadModelVersionStore(pool),
|
||||
storepkg.ReadModelVersionBatchConfig{
|
||||
MaxKeys: cfg.ReadModelVersionBatchMaxKeys, MaxWait: cfg.ReadModelVersionBatchWait,
|
||||
QueueSize: cfg.ReadModelVersionBatchQueue, QueryTimeout: cfg.ReadModelVersionBatchTimeout,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer readModelVersionBatchStore.Close()
|
||||
readModelVersionStore := storepkg.NewCachedReadModelVersionStore(
|
||||
readModelVersionBatchStore,
|
||||
0,
|
||||
cfg.ReadModelVersionCacheMaxEntries,
|
||||
)
|
||||
dialogListSnapshotCache := redisstore.NewDialogListSnapshotCache(rdb, cfg.DialogListSnapshotRedisTTL)
|
||||
activeChannelIDsPageCache := redisstore.NewActiveChannelIDsPageCache(rdb, cfg.ActiveChannelIDsRedisTTL)
|
||||
dispatchOutboxStore := postgres.NewDispatchOutboxStore(pool, postgres.WithLeaseTimeout(cfg.OutboxLeaseTimeout))
|
||||
bootstrapUpdateStore := postgres.NewBootstrapUpdateJobStore(pool)
|
||||
bootstrapUpdateStore, err := postgres.NewBatchedBootstrapUpdateJobStore(
|
||||
postgres.NewBootstrapUpdateJobStore(pool),
|
||||
postgres.BootstrapReadyBatchConfig{
|
||||
MaxSize: cfg.BootstrapReadyBatchMax, MaxWait: cfg.BootstrapReadyBatchWait,
|
||||
QueueSize: cfg.BootstrapReadyBatchQueue, QueryTimeout: cfg.BootstrapReadyBatchTimeout,
|
||||
Metrics: metricRegistry,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer bootstrapUpdateStore.Close()
|
||||
botAPIUpdateStore := postgres.NewBotAPIUpdateStore(pool)
|
||||
botCallbackStore := redisstore.NewBotCallbackRegistryStore(rdb)
|
||||
ephemeralStore := redisstore.NewEphemeralMessageStore(rdb)
|
||||
ephemeralReportStore := postgres.NewEphemeralReportStore(pool)
|
||||
welcomeMessageStore := postgres.NewWelcomeMessageStore(pool)
|
||||
moderationReportStore := postgres.NewModerationReportStore(pool)
|
||||
authDeliveryReportStore := postgres.NewAuthDeliveryReportStore(pool)
|
||||
clientTelemetryStore := postgres.NewClientTelemetryStore(pool)
|
||||
boxIDAllocator := redisstore.NewBoxIDAllocator(rdb, postgres.NewMessageBoxCounterSource(pool))
|
||||
channelIDAllocator := redisstore.NewChannelIDAllocator(rdb, postgres.NewChannelIDCounterSource(pool))
|
||||
channelMessageIDAllocator := redisstore.NewChannelMessageIDAllocator(rdb, postgres.NewChannelMessageIDCounterSource(pool))
|
||||
secretChatIDAllocator := redisstore.NewSecretChatIDAllocator(rdb, postgres.NewSecretChatIDCounterSource(pool))
|
||||
contactStore := userprojection.NewCachedContactStore(postgres.NewContactStore(pool), 0)
|
||||
reverseContactStore, err := storepkg.NewBatchedReverseContactStore(
|
||||
postgres.NewContactStore(pool),
|
||||
storepkg.ReverseContactBatchConfig{
|
||||
MaxPairs: cfg.ContactReverseBatchMaxPairs, MaxWait: cfg.ContactReverseBatchWait,
|
||||
QueueSize: cfg.ContactReverseBatchQueue, QueryTimeout: cfg.ContactReverseBatchTimeout,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer reverseContactStore.Close()
|
||||
contactStore := userprojection.NewCachedContactStoreWithMaxViewers(
|
||||
reverseContactStore,
|
||||
0,
|
||||
cfg.ContactSnapshotCacheMaxViewers,
|
||||
)
|
||||
dialogStore := postgres.NewDialogStore(pool)
|
||||
chatlistStore := postgres.NewChatlistStore(pool)
|
||||
messageStore := postgres.NewMessageStore(pool,
|
||||
postgres.WithMessageAllocators(boxIDAllocator),
|
||||
postgres.WithMessageLogger(logger.Named("store").Named("messages")))
|
||||
broadcastStore := postgres.NewBroadcastStore(pool)
|
||||
broadcastService := broadcastapp.NewService(broadcastStore,
|
||||
broadcastapp.WithMessageSender(messageStore),
|
||||
broadcastapp.WithLogger(logger.Named("broadcast")))
|
||||
// 共享频道行/成员缓存 + 统一 read-model LISTEN/NOTIFY 实时失效:消除高频「逐 RPC
|
||||
// 解析频道/成员」在客户端重连同步突发里重复读同一行的放大。
|
||||
channelRowCache := postgres.NewChannelRowCache(cfg.ChannelRowCacheMaxEntries)
|
||||
channelTopMessageCache := postgres.NewChannelTopMessageCache(cfg.ChannelTopMessageCacheMaxEntries)
|
||||
channelMemberCache := postgres.NewChannelMemberCache(cfg.ChannelMemberCacheMaxEntries)
|
||||
channelDialogCache := postgres.NewChannelDialogCache(cfg.ChannelDialogCacheMaxEntries)
|
||||
channelDifferenceCache := postgres.NewChannelDifferenceBaseCache(
|
||||
cfg.ChannelDifferenceCacheMaxEntries,
|
||||
cfg.ChannelDifferenceCacheMaxBytes,
|
||||
cfg.ChannelDifferenceCacheTTL,
|
||||
)
|
||||
channelBoostCache := postgres.NewChannelBoostCache(cfg.ChannelBoostCacheMaxEntries, cfg.ChannelBoostCacheTTL)
|
||||
channelStore := postgres.NewChannelStore(pool,
|
||||
postgres.WithChannelAllocators(channelIDAllocator, channelMessageIDAllocator),
|
||||
postgres.WithChannelLogger(logger.Named("store").Named("channels")),
|
||||
postgres.WithChannelRowCache(channelRowCache),
|
||||
postgres.WithChannelTopMessageCache(channelTopMessageCache),
|
||||
postgres.WithChannelMemberCache(channelMemberCache),
|
||||
postgres.WithChannelDialogCache(channelDialogCache),
|
||||
postgres.WithChannelDifferenceBaseCache(channelDifferenceCache),
|
||||
postgres.WithChannelBoostCache(channelBoostCache))
|
||||
communityStore := postgres.NewCommunityStore(pool, channelIDAllocator, channelMessageIDAllocator)
|
||||
activeChannelIDsPageBatcher, err := postgres.NewActiveChannelIDsPageBatcher(
|
||||
channelStore,
|
||||
postgres.ActiveChannelIDsBatchConfig{
|
||||
MaxSize: cfg.ActiveChannelIDsBatchMax, MaxWait: cfg.ActiveChannelIDsBatchWait,
|
||||
QueueSize: cfg.ActiveChannelIDsBatchQueue, QueryTimeout: cfg.ActiveChannelIDsBatchTimeout,
|
||||
Metrics: metricRegistry,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer activeChannelIDsPageBatcher.Close()
|
||||
metricRegistry.AddGaugeProvider(func() []obsmetrics.GaugeSample {
|
||||
snapshot := channelDifferenceCache.Snapshot()
|
||||
return []obsmetrics.GaugeSample{
|
||||
{Name: "telesrv_channel_difference_cache_entries", Value: float64(snapshot.Entries)},
|
||||
{Name: "telesrv_channel_difference_cache_weight_bytes", Value: float64(snapshot.Weight)},
|
||||
{Name: "telesrv_channel_difference_cache_hits", Value: float64(snapshot.Hits)},
|
||||
{Name: "telesrv_channel_difference_cache_misses", Value: float64(snapshot.Misses)},
|
||||
{Name: "telesrv_channel_difference_cache_loads", Value: float64(snapshot.Loads)},
|
||||
{Name: "telesrv_channel_difference_cache_load_errors", Value: float64(snapshot.LoadErrors)},
|
||||
}
|
||||
})
|
||||
communityCatalogCache := postgres.NewCommunityCatalogCache()
|
||||
communityStore := postgres.NewCommunityStore(pool, channelIDAllocator, channelMessageIDAllocator,
|
||||
postgres.WithCommunityCatalogCache(communityCatalogCache))
|
||||
pollStore := postgres.NewPollStore(pool)
|
||||
mediaStore := postgres.NewMediaStore(pool)
|
||||
// 头像投影缓存:所有 projector 共用一层短 TTL owner→头像缓存,消除高频「返回用户」RPC
|
||||
// 每次投影对每批 owner 固定 2 次的 CurrentProfilePhotosKind PG 查询。
|
||||
cachedPhotos := userprojection.NewCachedPhotoProvider(mediaStore, userprojection.DefaultPhotoCacheTTL)
|
||||
// 头像投影缓存:所有 projector 共用 owner→头像正/负 LRU。profile_photo NOTIFY
|
||||
// 精确失效负责正常新鲜度,长 TTL 只覆盖漏通知,避免登录 ramp 周期性重查稳定负值。
|
||||
cachedPhotos := userprojection.NewCachedPhotoProviderWithMaxEntries(
|
||||
mediaStore,
|
||||
cfg.ProfilePhotoCacheTTL,
|
||||
cfg.ProfilePhotoCacheMaxEntries,
|
||||
)
|
||||
privacyStore := privacyapp.NewCachedPrivacyStore(postgres.NewPrivacyStore(pool), 0)
|
||||
storyStore := postgres.NewStoryStore(pool)
|
||||
// Transient upload-part scratch storage always stays on local disk
|
||||
|
|
@ -902,6 +1058,11 @@ func run(logger *zap.Logger) error {
|
|||
Commands: adminStore,
|
||||
Restrictions: adminStore,
|
||||
})
|
||||
userProjectionFacts := userprojection.NewDurableUserProjectionFacts(
|
||||
adminService,
|
||||
readModelVersionStore,
|
||||
cfg.UserProjectionFactCacheMaxEntries,
|
||||
)
|
||||
storageRetentionMaxAge := cfg.StorageRetentionMaxAge
|
||||
if !cfg.StorageRetentionEnable {
|
||||
storageRetentionMaxAge = 0
|
||||
|
|
@ -932,7 +1093,7 @@ func run(logger *zap.Logger) error {
|
|||
contactsService := contacts.NewService(contactStore, userStore).Configure(
|
||||
contacts.WithPhotoProvider(cachedPhotos),
|
||||
contacts.WithPrivacyEvaluator(privacyService),
|
||||
contacts.WithAccountFreezeProvider(adminService),
|
||||
contacts.WithAccountFreezeProvider(userProjectionFacts),
|
||||
contacts.WithReadModelVersions(readModelVersionStore),
|
||||
contacts.WithHideThirdPartyVerification(cfg.HideThirdPartyVerification),
|
||||
)
|
||||
|
|
@ -1036,6 +1197,33 @@ func run(logger *zap.Logger) error {
|
|||
botsapp.WithDialogRateLimiter(rateLimiter, cfg.VerificationBotRateLimit, cfg.VerificationBotRateWindow),
|
||||
botsapp.WithPublicBaseURL(cfg.PublicBaseURL),
|
||||
botsapp.WithHideThirdPartyVerification(cfg.HideThirdPartyVerification))
|
||||
// The built-in ChatBot and StickersBot are seeded with the default product
|
||||
// name in their bio (users.about) and description (bots.description). Align
|
||||
// them with the active branding on startup so the seeded "telesrv" text is
|
||||
// replaced. SetBotInfo writes both fields; the sync is a no-op when the text
|
||||
// already matches.
|
||||
for _, botID := range []int64{domain.ChatBotUserID, domain.StickersBotUserID} {
|
||||
var wantAbout, wantDesc string
|
||||
switch botID {
|
||||
case domain.ChatBotUserID:
|
||||
wantAbout = domain.ChatBotDescription()
|
||||
wantDesc = wantAbout
|
||||
case domain.StickersBotUserID:
|
||||
wantAbout = domain.StickersBotDescription()
|
||||
wantDesc = wantAbout
|
||||
}
|
||||
if _, curAbout, curDesc, err := botsService.GetBotInfo(ctx, botID); err == nil && curAbout == wantAbout && curDesc == wantDesc {
|
||||
continue
|
||||
}
|
||||
if _, err := botsService.SetBotInfo(ctx, botID, domain.BotInfoUpdate{
|
||||
SetAbout: true,
|
||||
About: wantAbout,
|
||||
SetDescription: true,
|
||||
Description: wantDesc,
|
||||
}); err != nil {
|
||||
logger.Warn("sync bot branding", zap.Int64("bot", botID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
groupCallStore := postgres.NewGroupCallStore(pool)
|
||||
groupCallsService := groupcallsapp.NewService(groupCallStore, groupcallsapp.WithPublicBaseURL(cfg.PublicBaseURL))
|
||||
// 群通话媒体面:内嵌 pion SFU(M1+)。SFU 的 liveness reporter 把媒体面存活
|
||||
|
|
@ -1120,7 +1308,7 @@ func run(logger *zap.Logger) error {
|
|||
// 私聊端对端加密(Secret Chat)握手状态机 + qts 投递队列(盲中继)。
|
||||
secretChatStore := postgres.NewSecretChatStore(pool)
|
||||
encryptedQueueStore := postgres.NewEncryptedQueueStore(pool)
|
||||
secretChatService := secretchatapp.NewService(secretChatStore, encryptedQueueStore, secretChatIDAllocator)
|
||||
secretChatService := secretchatapp.NewService(secretChatStore, encryptedQueueStore)
|
||||
// Passkey:凭据持久化走 postgres;一次性挑战走进程内内存(短 TTL,与 QR 登录 token
|
||||
// 同属进程内一次性凭据,不跨实例)。
|
||||
passkeyStore := postgres.NewPasskeyStore(pool)
|
||||
|
|
@ -1129,7 +1317,7 @@ func run(logger *zap.Logger) error {
|
|||
passkeyapp.WithAllowedOrigins(cfg.PasskeyAllowedOrigins))
|
||||
// 自定义云主题(Create a New Theme):主题目录与每用户已安装列表均持久化到 postgres。
|
||||
themeService := themesapp.NewService(postgres.NewThemeStore(pool))
|
||||
usersService := users.NewService(userStore, users.WithBaseUserCache(userCache), users.WithContactStore(contactStore), users.WithPhotoProvider(cachedPhotos), users.WithPrivacyEvaluator(privacyService), users.WithAccountFreezeProvider(adminService), users.WithHideThirdPartyVerification(cfg.HideThirdPartyVerification), users.WithReservedUsernames(cfg.ReservedUsernames))
|
||||
usersService := users.NewService(userStore, users.WithBaseUserCache(userCache), users.WithContactStore(contactStore), users.WithPhotoProvider(cachedPhotos), users.WithPrivacyEvaluator(privacyService), users.WithAccountFreezeProvider(userProjectionFacts), users.WithHideThirdPartyVerification(cfg.HideThirdPartyVerification), users.WithReservedUsernames(cfg.ReservedUsernames))
|
||||
privacyService.ConfigureReadModels(usersService, channelStore)
|
||||
aiComposeService := aiapp.NewService(aiComposeStore, newAIComposeOptions(cfg, rateLimiter, usersService.PremiumActive, logger)...)
|
||||
botsService.SetAIChatGenerator(aiComposeService)
|
||||
|
|
@ -1137,9 +1325,21 @@ func run(logger *zap.Logger) error {
|
|||
dialogs.WithContactStore(contactStore),
|
||||
dialogs.WithPhotoProvider(cachedPhotos),
|
||||
dialogs.WithPrivacyEvaluator(privacyService),
|
||||
dialogs.WithAccountFreezeProvider(adminService),
|
||||
dialogs.WithAccountFreezeProvider(userProjectionFacts),
|
||||
dialogs.WithPremiumChecker(usersService.PremiumActive),
|
||||
dialogs.WithReadModelVersions(readModelVersionStore),
|
||||
dialogs.WithDialogHydrationCaches(
|
||||
cfg.DialogPrivatePeerCacheMaxEntries,
|
||||
cfg.DialogPrivatePeerCacheMaxBytes,
|
||||
cfg.DialogDraftCacheMaxEntries,
|
||||
cfg.DialogDraftCacheMaxBytes,
|
||||
),
|
||||
dialogs.WithDialogListSnapshotCache(
|
||||
cfg.DialogListSnapshotCacheMaxEntries,
|
||||
cfg.DialogListSnapshotCacheMaxHeaders,
|
||||
cfg.DialogListSnapshotCacheTTL,
|
||||
),
|
||||
dialogs.WithSharedDialogListSnapshotCache(dialogListSnapshotCache),
|
||||
)
|
||||
// 编译期保证 *users.Service 满足 channel fan-out 跨 viewer 投影预热的可选能力;签名漂移会在
|
||||
// 这里立刻断编译,而非在运行时静默退化回 O(viewer) 逐 viewer 投影。
|
||||
|
|
@ -1147,11 +1347,19 @@ func run(logger *zap.Logger) error {
|
|||
channelsService := channelapp.NewService(channelStore,
|
||||
channelapp.WithBotProfileResolver(botsService),
|
||||
channelapp.WithReadModelVersions(readModelVersionStore),
|
||||
channelapp.WithActiveChannelIDsReadModel(
|
||||
activeChannelIDsPageCache,
|
||||
activeChannelIDsPageBatcher,
|
||||
cfg.ActiveChannelIDsCacheMaxEntries,
|
||||
cfg.ActiveChannelIDsCacheTTL,
|
||||
metricRegistry,
|
||||
),
|
||||
channelapp.WithSendPermissionChecker(adminService),
|
||||
channelapp.WithReservedUsernames(cfg.ReservedUsernames),
|
||||
)
|
||||
communitiesService := communitiesapp.NewService(communityStore)
|
||||
ephemeralService := ephemeralapp.NewService(ephemeralStore, channelsService, usersService, botsService)
|
||||
welcomeMessageService := welcomemessagesapp.NewService(welcomeMessageStore, channelsService)
|
||||
storiesService := storiesapp.NewService(storyStore, storiesapp.WithChannelStoryAccess(channelsService))
|
||||
chatlistsService := chatlistsapp.NewService(
|
||||
chatlistStore,
|
||||
|
|
@ -1164,7 +1372,7 @@ func run(logger *zap.Logger) error {
|
|||
messageapp.WithContactStore(contactStore),
|
||||
messageapp.WithPhotoProvider(cachedPhotos),
|
||||
messageapp.WithPrivacyEvaluator(privacyService),
|
||||
messageapp.WithAccountFreezeProvider(adminService),
|
||||
messageapp.WithAccountFreezeProvider(userProjectionFacts),
|
||||
messageapp.WithReadModelVersions(readModelVersionStore),
|
||||
messageapp.WithBotResponder(botsService),
|
||||
messageapp.WithSendPermissionChecker(adminService),
|
||||
|
|
@ -1191,7 +1399,7 @@ func run(logger *zap.Logger) error {
|
|||
dialogStore,
|
||||
newTranslationOptions(cfg, rateLimiter, logger)...,
|
||||
)
|
||||
authService := auth.NewService(userStore, authzStore, codeStore, authKeyStore, tempAuthKeyStore, cfg.DevAuthCode,
|
||||
authService := auth.NewService(userStore, authzStore, codeStore, authKeyGetBatchStore, tempAuthKeyStore, cfg.DevAuthCode,
|
||||
auth.WithLoginMessages(messageStore, dialogStore),
|
||||
auth.WithLoginCodeDelivery(messageStore),
|
||||
auth.WithPasswords(passwordStore),
|
||||
|
|
@ -1285,6 +1493,14 @@ func run(logger *zap.Logger) error {
|
|||
logger.Info("default verifier seed complete", zap.Int64("bot_id", domain.VerifierBotUserID))
|
||||
}
|
||||
updatesService := updates.NewService(updateStateStore, updateEventStore, updates.WithLogger(logger.Named("app").Named("updates")))
|
||||
var appUpdateResolver updatecdn.Resolver
|
||||
if cfg.UpdateServiceURL != "" {
|
||||
client, err := updatecdn.NewClient(cfg.UpdateServiceURL, cfg.UpdateRequestTimeout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("initialize update service client: %w", err)
|
||||
}
|
||||
appUpdateResolver = client
|
||||
}
|
||||
router := rpc.New(rpc.Config{
|
||||
DC: cfg.DC,
|
||||
DefaultCountryCode: cfg.DefaultCountryCode,
|
||||
|
|
@ -1304,17 +1520,29 @@ func run(logger *zap.Logger) error {
|
|||
GroupCallMaxParticipants: cfg.GroupCallMaxParticipants,
|
||||
RtmpIngestURL: cfg.LiveStreamRtmpURL,
|
||||
PublicBaseURL: cfg.PublicBaseURL,
|
||||
UpdatePublicURL: cfg.UpdatePublicURL,
|
||||
PublicAppScheme: cfg.PublicAppScheme,
|
||||
PublicAppLinkBase: cfg.PublicAppLinkBase,
|
||||
// PFS temp→perm 解析缓存:显式撤销会清缓存并断开连接,re-bind 即时失效;
|
||||
// 配置 TTL 只承担跨进程/异常失效兜底,避免大连接数周期性打满 PG。
|
||||
TempKeyResolveCacheTTL: cfg.TempKeyResolveCacheTTL,
|
||||
TempKeyResolveCacheMaxEntries: cfg.TempKeyResolveCacheMaxEntries,
|
||||
TempKeyResolveCacheTTL: cfg.TempKeyResolveCacheTTL,
|
||||
TempKeyResolveCacheMaxEntries: cfg.TempKeyResolveCacheMaxEntries,
|
||||
PeerIdentityCacheMaxEntries: cfg.PeerIdentityCacheMaxEntries,
|
||||
StoryActivePeerCacheMaxEntries: cfg.StoryActivePeerCacheMaxEntries,
|
||||
StoryHiddenListCacheMaxEntries: cfg.StoryHiddenListCacheMaxEntries,
|
||||
StoryHiddenListCacheMaxBytes: cfg.StoryHiddenListCacheMaxBytes,
|
||||
PresenceLastSeenBatchMax: cfg.PresenceLastSeenBatchMax,
|
||||
PresenceLastSeenBatchWait: cfg.PresenceLastSeenBatchWait,
|
||||
PresenceLastSeenBatchQueue: cfg.PresenceLastSeenBatchQueue,
|
||||
PresenceLastSeenBatchTimeout: cfg.PresenceLastSeenBatchTimeout,
|
||||
PresenceLastSeenDrainTimeout: cfg.PresenceLastSeenDrainTimeout,
|
||||
}, rpc.Deps{
|
||||
Auth: authService,
|
||||
AuthDeliveryReports: authDeliveryReportService,
|
||||
ClientTelemetry: clientTelemetryService,
|
||||
AuthKeySessionLayers: authKeyStore,
|
||||
AuthKeySessionLayers: authKeySessionLayerStore,
|
||||
ReadModelVersions: readModelVersionStore,
|
||||
UserProjectionFacts: userProjectionFacts,
|
||||
Account: accountService,
|
||||
Privacy: privacyService,
|
||||
Help: help.NewService(helpStore, helpStore,
|
||||
|
|
@ -1323,73 +1551,77 @@ func run(logger *zap.Logger) error {
|
|||
help.WithEmailSignupPhonePrefixes(cfg.EmailSignupPhonePrefixes),
|
||||
help.WithAccountFreezeProvider(adminService),
|
||||
),
|
||||
AccountFreeze: adminService,
|
||||
AICompose: aiComposeService,
|
||||
Ephemeral: ephemeralService,
|
||||
EphemeralPush: ephemeralStore,
|
||||
Moderation: moderationService,
|
||||
Users: usersService,
|
||||
Usernames: usernamesService,
|
||||
BotVerifications: botVerificationService,
|
||||
TelegramLogin: telegramLoginRPCDependency(telegramLoginService),
|
||||
Updates: updatesService,
|
||||
BootstrapUpdates: bootstrapUpdateStore,
|
||||
BotAPIUpdates: botAPIUpdateStore,
|
||||
BotCallbacks: botCallbackStore,
|
||||
Contacts: contactsService,
|
||||
Dialogs: dialogsService,
|
||||
Chatlists: chatlistsService,
|
||||
Messages: messagesService,
|
||||
Translation: translationService,
|
||||
Channels: channelsService,
|
||||
Communities: communitiesService,
|
||||
Files: filesService,
|
||||
PremiumPromo: filesService,
|
||||
Bots: botsService,
|
||||
ServiceBotCallbacks: botsService,
|
||||
ServiceBotInlineResults: botsService,
|
||||
Polls: pollsapp.NewService(pollStore),
|
||||
Stories: storiesService,
|
||||
Phone: phoneService,
|
||||
SecretChats: secretChatService,
|
||||
Passkey: passkeyService,
|
||||
Themes: themeService,
|
||||
GroupCalls: groupCallsService,
|
||||
LiveStreams: liveStreamDep(liveStreamService),
|
||||
SFU: sfuService,
|
||||
TURN: turnService,
|
||||
LangPack: langPackService,
|
||||
Sessions: activeSessions,
|
||||
Metrics: metricRegistry,
|
||||
Inline: inlineRegistryStore,
|
||||
Limiter: rateLimiter,
|
||||
AppUpdates: appUpdateResolver,
|
||||
AccountFreeze: userProjectionFacts,
|
||||
AccountFreezeNotifications: adminService,
|
||||
AICompose: aiComposeService,
|
||||
Ephemeral: ephemeralService,
|
||||
EphemeralPush: ephemeralStore,
|
||||
WelcomeMessages: welcomeMessageService,
|
||||
Moderation: moderationService,
|
||||
Users: usersService,
|
||||
Usernames: usernamesService,
|
||||
BotVerifications: botVerificationService,
|
||||
TelegramLogin: telegramLoginRPCDependency(telegramLoginService),
|
||||
Updates: updatesService,
|
||||
BootstrapUpdates: bootstrapUpdateStore,
|
||||
BotAPIUpdates: botAPIUpdateStore,
|
||||
BotCallbacks: botCallbackStore,
|
||||
Contacts: contactsService,
|
||||
Dialogs: dialogsService,
|
||||
Chatlists: chatlistsService,
|
||||
Messages: messagesService,
|
||||
Translation: translationService,
|
||||
Channels: channelsService,
|
||||
Communities: communitiesService,
|
||||
Files: filesService,
|
||||
PremiumPromo: filesService,
|
||||
Bots: botsService,
|
||||
ServiceBotCallbacks: botsService,
|
||||
ServiceBotInlineResults: botsService,
|
||||
Polls: pollsapp.NewService(pollStore),
|
||||
Stories: storiesService,
|
||||
Phone: phoneService,
|
||||
SecretChats: secretChatService,
|
||||
Passkey: passkeyService,
|
||||
Themes: themeService,
|
||||
GroupCalls: groupCallsService,
|
||||
LiveStreams: liveStreamDep(liveStreamService),
|
||||
SFU: sfuService,
|
||||
TURN: turnService,
|
||||
LangPack: langPackService,
|
||||
Sessions: activeSessions,
|
||||
Metrics: metricRegistry,
|
||||
Inline: inlineRegistryStore,
|
||||
Limiter: rateLimiter,
|
||||
}, logger.Named("rpc"), clock.System)
|
||||
readModelListener := postgres.NewReadModelChangeListener(cfg.PostgresDSN, postgres.ReadModelCacheSet{
|
||||
ReadModelVersions: readModelVersionStore,
|
||||
ChannelRows: channelRowCache,
|
||||
ChannelMembers: channelMemberCache,
|
||||
ChannelDialogs: channelDialogCache,
|
||||
ChannelBoosts: channelBoostCache,
|
||||
Contacts: postgres.ContactReadModelCaches{contactStore, contactsService},
|
||||
Dialogs: dialogsService,
|
||||
Privacy: privacyService,
|
||||
ProfilePhotos: cachedPhotos,
|
||||
Stories: router,
|
||||
ChannelFullBots: router,
|
||||
ChannelBotMembers: channelsService,
|
||||
ChannelMediaCounts: channelsService,
|
||||
PrivateMediaCounts: messagesService,
|
||||
RPCProjections: router,
|
||||
BaseUsers: userCache,
|
||||
BotProfiles: botsService,
|
||||
AccountSettings: router,
|
||||
ReadModelVersions: readModelVersionStore,
|
||||
ChannelRows: channelRowCache,
|
||||
ChannelTopMessages: channelTopMessageCache,
|
||||
CommunityCatalog: communityCatalogCache,
|
||||
ChannelMembers: channelMemberCache,
|
||||
ChannelDialogs: channelDialogCache,
|
||||
ChannelDifferences: channelDifferenceCache,
|
||||
ChannelBoosts: channelBoostCache,
|
||||
Contacts: postgres.ContactReadModelCaches{contactStore, contactsService},
|
||||
Dialogs: dialogsService,
|
||||
Privacy: privacyService,
|
||||
ProfilePhotos: cachedPhotos,
|
||||
Stories: router,
|
||||
ChannelFullBots: router,
|
||||
ChannelBotMembers: channelsService,
|
||||
ChannelMediaCounts: channelsService,
|
||||
PrivateMediaCounts: messagesService,
|
||||
RPCProjections: router,
|
||||
PeerIdentities: router,
|
||||
BaseUsers: userCache,
|
||||
BotProfiles: botsService,
|
||||
AccountSettings: router,
|
||||
UserProjectionFacts: userProjectionFacts,
|
||||
}, logger.Named("store").Named("read-model-listener"))
|
||||
go readModelListener.Run(ctx)
|
||||
activeSessions.SetLifecycleObserver(router)
|
||||
broadcastStore := postgres.NewBroadcastStore(pool)
|
||||
broadcastService := broadcastapp.NewService(broadcastStore,
|
||||
broadcastapp.WithMessageSender(messageStore),
|
||||
broadcastapp.WithLogger(logger.Named("broadcast")))
|
||||
adminService.Configure(adminapp.Dependencies{
|
||||
Auth: authService,
|
||||
Revoker: router,
|
||||
|
|
@ -1453,9 +1685,10 @@ func run(logger *zap.Logger) error {
|
|||
if notifier, ok := any(router).(botverificationapp.PeerNotifier); ok {
|
||||
botVerificationService.SetPeerNotifier(compositeBotVerificationNotifier{
|
||||
cache: rpcProjectionVerificationNotifier{
|
||||
invalidator: router,
|
||||
users: userCache,
|
||||
log: verificationLogger,
|
||||
invalidator: router,
|
||||
users: userCache,
|
||||
peerIdentity: true,
|
||||
log: verificationLogger,
|
||||
},
|
||||
edge: notifier,
|
||||
})
|
||||
|
|
@ -1474,7 +1707,9 @@ func run(logger *zap.Logger) error {
|
|||
// not wait on however long sending to all of them takes.
|
||||
go broadcastapp.NewWorker(broadcastService, logger.Named("broadcast").Named("delivery"),
|
||||
cfg.BroadcastWorkerInterval, cfg.BroadcastWorkerBatch).Run(ctx)
|
||||
moderationActionOptions := []moderationapp.ActionExecutorOption{}
|
||||
moderationActionOptions := []moderationapp.ActionExecutorOption{
|
||||
moderationapp.WithAccountDeletionNotifier(router),
|
||||
}
|
||||
if cfg.PublicLinkWebAddr != "" {
|
||||
moderationActionOptions = append(
|
||||
moderationActionOptions,
|
||||
|
|
@ -1503,6 +1738,7 @@ func run(logger *zap.Logger) error {
|
|||
rpc.WithOutboxUpdateBuilder(router.BuildOutboxUpdates),
|
||||
).Run(ctx)
|
||||
go rpc.NewBootstrapUpdateDispatcher(router, logger.Named("rpc").Named("bootstrap")).Run(ctx)
|
||||
go rpc.NewWelcomeDeliveryDispatcher(router, welcomeMessageStore, logger.Named("rpc").Named("welcome-delivery")).Run(ctx)
|
||||
go rpc.NewScheduledDispatcher(router, logger.Named("rpc").Named("scheduled")).Run(ctx)
|
||||
go rpc.NewSuggestedPostDispatcher(router, logger.Named("rpc").Named("suggested-post")).Run(ctx)
|
||||
go rpc.NewExpiryDispatcher(router, logger.Named("rpc").Named("expiry")).Run(ctx)
|
||||
|
|
@ -1510,6 +1746,7 @@ func run(logger *zap.Logger) error {
|
|||
go rpc.NewGroupCallSweepDispatcher(router, logger.Named("rpc").Named("groupcall-sweep"), cfg.GroupCallSweepInterval, cfg.GroupCallCheckTTL).Run(ctx)
|
||||
go router.RunChannelFanout(ctx)
|
||||
go router.RunBotAPIEnqueue(ctx)
|
||||
go router.RunPresenceLastSeenBatch(ctx)
|
||||
go router.RunPresenceSweeper(ctx, time.Minute)
|
||||
go activeSessions.RunPendingSweeper(ctx, time.Minute)
|
||||
go router.RunPremiumSweeper(ctx, cfg.PremiumSweepInterval, cfg.PremiumSweepBatch)
|
||||
|
|
@ -1567,7 +1804,7 @@ func run(logger *zap.Logger) error {
|
|||
RSAKey: rsaKey,
|
||||
IdentityDir: cfg.IdentityDir,
|
||||
LayerRPC: router,
|
||||
AuthKeys: authKeyStore,
|
||||
AuthKeys: authKeyGetBatchStore,
|
||||
ActiveSessions: activeSessions,
|
||||
Metrics: metricRegistry,
|
||||
ObfuscatedTCP: true,
|
||||
|
|
@ -1582,6 +1819,8 @@ func run(logger *zap.Logger) error {
|
|||
RPCGlobalWorkers: cfg.MTProtoRPCGlobalWorkers,
|
||||
RPCGlobalMaxTasks: cfg.MTProtoRPCGlobalMaxTasks,
|
||||
RPCGlobalMaxBytes: cfg.MTProtoRPCGlobalMaxBytes,
|
||||
RPCDeliveryHookWorkers: cfg.MTProtoRPCDeliveryHookWorkers,
|
||||
RPCDeliveryHookMaxPending: cfg.MTProtoRPCDeliveryHookMaxPending,
|
||||
RPCExecutionMaxEntries: cfg.MTProtoRPCExecutionMaxEntries,
|
||||
RPCExecutionAuthMaxEntries: cfg.MTProtoRPCExecutionAuthMaxEntries,
|
||||
RPCExecutionSessionMaxEntries: cfg.MTProtoRPCExecutionSessionMaxEntries,
|
||||
|
|
|
|||
7
cmd/telesrv/process_cpu_fallback.go
Normal file
7
cmd/telesrv/process_cpu_fallback.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
//go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows
|
||||
|
||||
package main
|
||||
|
||||
func processCPUSeconds() (float64, bool) {
|
||||
return 0, false
|
||||
}
|
||||
12
cmd/telesrv/process_cpu_test.go
Normal file
12
cmd/telesrv/process_cpu_test.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || windows
|
||||
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestProcessCPUSecondsAvailable(t *testing.T) {
|
||||
seconds, ok := processCPUSeconds()
|
||||
if !ok || seconds < 0 {
|
||||
t.Fatalf("process CPU seconds = %v, available=%v", seconds, ok)
|
||||
}
|
||||
}
|
||||
17
cmd/telesrv/process_cpu_unix.go
Normal file
17
cmd/telesrv/process_cpu_unix.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
|
||||
|
||||
package main
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
func processCPUSeconds() (float64, bool) {
|
||||
var usage unix.Rusage
|
||||
if err := unix.Getrusage(unix.RUSAGE_SELF, &usage); err != nil {
|
||||
return 0, false
|
||||
}
|
||||
ns := unix.TimevalToNsec(usage.Utime) + unix.TimevalToNsec(usage.Stime)
|
||||
if ns < 0 {
|
||||
return 0, false
|
||||
}
|
||||
return float64(ns) / 1e9, true
|
||||
}
|
||||
21
cmd/telesrv/process_cpu_windows.go
Normal file
21
cmd/telesrv/process_cpu_windows.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import "golang.org/x/sys/windows"
|
||||
|
||||
func processCPUSeconds() (float64, bool) {
|
||||
handle, err := windows.GetCurrentProcess()
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
var creation, exit, kernel, user windows.Filetime
|
||||
if err := windows.GetProcessTimes(handle, &creation, &exit, &kernel, &user); err != nil {
|
||||
return 0, false
|
||||
}
|
||||
ns := kernel.Nanoseconds() + user.Nanoseconds()
|
||||
if ns < 0 {
|
||||
return 0, false
|
||||
}
|
||||
return float64(ns) / 1e9, true
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue