@atproto/ozone 0.1.69 → 0.1.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/api/moderation/queryStatuses.d.ts.map +1 -1
- package/dist/api/moderation/queryStatuses.js +1 -33
- package/dist/api/moderation/queryStatuses.js.map +1 -1
- package/dist/background.d.ts +49 -6
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +149 -14
- package/dist/background.js.map +1 -1
- package/dist/config/config.d.ts +1 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +1 -0
- package/dist/config/config.js.map +1 -1
- package/dist/config/env.d.ts +1 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +1 -0
- package/dist/config/env.js.map +1 -1
- package/dist/daemon/context.d.ts +9 -3
- package/dist/daemon/context.d.ts.map +1 -1
- package/dist/daemon/context.js +33 -3
- package/dist/daemon/context.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +3 -6
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/materialized-view-refresher.d.ts +5 -0
- package/dist/daemon/materialized-view-refresher.d.ts.map +1 -0
- package/dist/daemon/materialized-view-refresher.js +29 -0
- package/dist/daemon/materialized-view-refresher.js.map +1 -0
- package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.d.ts +5 -0
- package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.d.ts.map +1 -0
- package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.js +158 -0
- package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.js.map +1 -0
- package/dist/db/migrations/index.d.ts +1 -0
- package/dist/db/migrations/index.d.ts.map +1 -1
- package/dist/db/migrations/index.js +2 -1
- package/dist/db/migrations/index.js.map +1 -1
- package/dist/db/schema/account_events_stats.d.ts +15 -0
- package/dist/db/schema/account_events_stats.d.ts.map +1 -0
- package/dist/db/schema/account_events_stats.js +5 -0
- package/dist/db/schema/account_events_stats.js.map +1 -0
- package/dist/db/schema/account_record_events_stats.d.ts +15 -0
- package/dist/db/schema/account_record_events_stats.d.ts.map +1 -0
- package/dist/db/schema/account_record_events_stats.js +5 -0
- package/dist/db/schema/account_record_events_stats.js.map +1 -0
- package/dist/db/schema/account_record_status_stats.d.ts +15 -0
- package/dist/db/schema/account_record_status_stats.d.ts.map +1 -0
- package/dist/db/schema/account_record_status_stats.js +5 -0
- package/dist/db/schema/account_record_status_stats.js.map +1 -0
- package/dist/db/schema/index.d.ts +5 -1
- package/dist/db/schema/index.d.ts.map +1 -1
- package/dist/db/schema/record_events_stats.d.ts +14 -0
- package/dist/db/schema/record_events_stats.d.ts.map +1 -0
- package/dist/db/schema/record_events_stats.js +5 -0
- package/dist/db/schema/record_events_stats.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -4
- package/dist/index.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +174 -2
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +92 -1
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +40 -0
- package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/defs.js +20 -0
- package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts +7 -1
- package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts.map +1 -1
- package/dist/mod-service/index.d.ts +4 -62
- package/dist/mod-service/index.d.ts.map +1 -1
- package/dist/mod-service/index.js +80 -74
- package/dist/mod-service/index.js.map +1 -1
- package/dist/mod-service/status.d.ts +115 -4
- package/dist/mod-service/status.d.ts.map +1 -1
- package/dist/mod-service/status.js +51 -34
- package/dist/mod-service/status.js.map +1 -1
- package/dist/mod-service/types.d.ts +16 -1
- package/dist/mod-service/types.d.ts.map +1 -1
- package/dist/mod-service/views.d.ts.map +1 -1
- package/dist/mod-service/views.js +49 -41
- package/dist/mod-service/views.js.map +1 -1
- package/dist/util.d.ts +34 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +132 -0
- package/dist/util.js.map +1 -1
- package/package.json +3 -3
- package/src/api/moderation/queryStatuses.ts +1 -63
- package/src/background.ts +140 -14
- package/src/config/config.ts +2 -0
- package/src/config/env.ts +4 -0
- package/src/daemon/context.ts +43 -5
- package/src/daemon/index.ts +3 -6
- package/src/daemon/materialized-view-refresher.ts +27 -0
- package/src/db/migrations/20241220T144630860Z-stats-materialized-views.ts +218 -0
- package/src/db/migrations/index.ts +1 -0
- package/src/db/schema/account_events_stats.ts +16 -0
- package/src/db/schema/account_record_events_stats.ts +15 -0
- package/src/db/schema/account_record_status_stats.ts +15 -0
- package/src/db/schema/index.ts +10 -1
- package/src/db/schema/record_events_stats.ts +15 -0
- package/src/index.ts +1 -7
- package/src/lexicon/lexicons.ts +100 -1
- package/src/lexicon/types/tools/ozone/moderation/defs.ts +62 -0
- package/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts +11 -1
- package/src/mod-service/index.ts +181 -118
- package/src/mod-service/status.ts +55 -28
- package/src/mod-service/types.ts +22 -1
- package/src/mod-service/views.ts +64 -50
- package/src/util.ts +145 -0
- package/tests/__snapshots__/get-record.test.ts.snap +28 -0
- package/tests/__snapshots__/get-records.test.ts.snap +14 -0
- package/tests/__snapshots__/get-repo.test.ts.snap +11 -0
- package/tests/__snapshots__/get-repos.test.ts.snap +11 -0
- package/tests/__snapshots__/moderation-events.test.ts.snap +19 -0
- package/tests/__snapshots__/moderation-statuses.test.ts.snap +114 -0
- package/tests/get-record.test.ts +4 -0
- package/tests/get-records.test.ts +4 -0
- package/tests/get-repo.test.ts +4 -0
- package/tests/get-repos.test.ts +4 -0
- package/tests/moderation-events.test.ts +4 -0
- package/tests/moderation-statuses.test.ts +4 -0
- package/tests/query-labels.test.ts +1 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/src/background.ts
CHANGED
|
@@ -1,35 +1,161 @@
|
|
|
1
1
|
import PQueue from 'p-queue'
|
|
2
2
|
import { Database } from './db'
|
|
3
3
|
import { dbLogger } from './logger'
|
|
4
|
+
import { boundAbortController, isCausedBySignal, startInterval } from './util'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
type Task = (db: Database, signal: AbortSignal) => Promise<void>
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* A simple queue for in-process, out-of-band/backgrounded work
|
|
10
|
+
*/
|
|
7
11
|
export class BackgroundQueue {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
constructor(public db: Database) {}
|
|
12
|
+
private abortController = new AbortController()
|
|
13
|
+
private queue = new PQueue({ concurrency: 20 })
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
public get signal() {
|
|
16
|
+
return this.abortController.signal
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public get destroyed() {
|
|
20
|
+
return this.signal.aborted
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
constructor(protected db: Database) {}
|
|
24
|
+
|
|
25
|
+
getStats() {
|
|
26
|
+
return {
|
|
27
|
+
runningCount: this.queue.pending,
|
|
28
|
+
waitingCount: this.queue.size,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Add a task that will be executed at some point in the future.
|
|
34
|
+
*
|
|
35
|
+
* The task will be executed even if the backgroundQueue is destroyed, unless
|
|
36
|
+
* the provided `signal` is aborted.
|
|
37
|
+
*
|
|
38
|
+
* The `signal` provided to the task will be aborted whenever either the
|
|
39
|
+
* backgroundQueue is destroyed or the provided `signal` is aborted.
|
|
40
|
+
*/
|
|
41
|
+
async add(task: Task, signal?: AbortSignal): Promise<void> {
|
|
13
42
|
if (this.destroyed) {
|
|
14
43
|
return
|
|
15
44
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
45
|
+
|
|
46
|
+
const abortController = boundAbortController(this.signal, signal)
|
|
47
|
+
|
|
48
|
+
return this.queue.add<void>(async () => {
|
|
49
|
+
try {
|
|
50
|
+
// Do not run the task if the signal provided to the task has become
|
|
51
|
+
// aborted. Do not use `abortController.signal` here since we do not
|
|
52
|
+
// want to abort the task if the backgroundQueue is being destroyed.
|
|
53
|
+
if (signal?.aborted) return
|
|
54
|
+
|
|
55
|
+
// The task will receive a "combined signal" allowing it to abort if
|
|
56
|
+
// either the backgroundQueue is destroyed or the provided signal is
|
|
57
|
+
// aborted.
|
|
58
|
+
await task(this.db, abortController.signal)
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (!isCausedBySignal(err, abortController.signal)) {
|
|
61
|
+
dbLogger.error(err, 'background queue task failed')
|
|
62
|
+
}
|
|
63
|
+
} finally {
|
|
64
|
+
abortController.abort()
|
|
65
|
+
}
|
|
66
|
+
})
|
|
21
67
|
}
|
|
22
68
|
|
|
23
69
|
async processAll() {
|
|
24
70
|
await this.queue.onIdle()
|
|
25
71
|
}
|
|
26
72
|
|
|
27
|
-
|
|
28
|
-
|
|
73
|
+
/**
|
|
74
|
+
* On destroy we stop accepting new tasks, but complete all
|
|
75
|
+
* pending/in-progress tasks. Tasks can decide to abort their current
|
|
76
|
+
* operation based on the signal they received. The application calls this
|
|
77
|
+
* only once http connections have drained (tasks no longer being added).
|
|
78
|
+
*/
|
|
29
79
|
async destroy() {
|
|
30
|
-
this.
|
|
80
|
+
this.abortController.abort()
|
|
31
81
|
await this.queue.onIdle()
|
|
32
82
|
}
|
|
33
83
|
}
|
|
34
84
|
|
|
35
|
-
|
|
85
|
+
/**
|
|
86
|
+
* A simple periodic background task runner. This class will schedule a task to
|
|
87
|
+
* run through a provided {@link BackgroundQueue} at a fixed interval. The task
|
|
88
|
+
* will never run more than once concurrently, and will wait at least `interval`
|
|
89
|
+
* milliseconds between the end of one run and the start of the next.
|
|
90
|
+
*/
|
|
91
|
+
export class PeriodicBackgroundTask {
|
|
92
|
+
private abortController: AbortController
|
|
93
|
+
|
|
94
|
+
private intervalPromise?: Promise<void>
|
|
95
|
+
private runningPromise?: Promise<void>
|
|
96
|
+
|
|
97
|
+
public get signal() {
|
|
98
|
+
return this.abortController.signal
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public get destroyed() {
|
|
102
|
+
return this.signal.aborted
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
constructor(
|
|
106
|
+
protected backgroundQueue: BackgroundQueue,
|
|
107
|
+
protected interval: number,
|
|
108
|
+
protected task: Task,
|
|
109
|
+
) {
|
|
110
|
+
if (!Number.isFinite(interval) || interval <= 0) {
|
|
111
|
+
throw new TypeError('interval must be a positive number')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Bind this class's signal to the backgroundQueue's signal (destroying this
|
|
115
|
+
// instance if the backgroundQueue is destroyed)
|
|
116
|
+
this.abortController = boundAbortController(backgroundQueue.signal)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public run(signal?: AbortSignal): Promise<void> {
|
|
120
|
+
// `startInterval` already ensures that only one run is in progress at a
|
|
121
|
+
// time. However, we want to be able to expose a `run()` method that can be
|
|
122
|
+
// used to force a run, which could cause concurrent executions. We prevent
|
|
123
|
+
// this using the `runningPromise` property.
|
|
124
|
+
|
|
125
|
+
if (this.runningPromise) return this.runningPromise
|
|
126
|
+
|
|
127
|
+
// Combine the `this.signal` with the provided `signal`, if any.
|
|
128
|
+
const abortController = boundAbortController(this.signal, signal)
|
|
129
|
+
|
|
130
|
+
const promise = this.backgroundQueue.add(this.task, abortController.signal)
|
|
131
|
+
|
|
132
|
+
return (this.runningPromise = promise).finally(() => {
|
|
133
|
+
if (this.runningPromise === promise) this.runningPromise = undefined
|
|
134
|
+
|
|
135
|
+
// Cleanup the listeners added by `boundAbortController`
|
|
136
|
+
abortController.abort()
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public start() {
|
|
141
|
+
// Noop if already started. Throws if this.signal is aborted (instance is
|
|
142
|
+
// destroyed).
|
|
143
|
+
this.intervalPromise ||= startInterval(
|
|
144
|
+
async (signal) => this.run(signal),
|
|
145
|
+
this.interval,
|
|
146
|
+
this.signal,
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public async destroy() {
|
|
151
|
+
// @NOTE This instance does not "own" the backgroundQueue, so we do not
|
|
152
|
+
// destroy it here.
|
|
153
|
+
|
|
154
|
+
this.abortController.abort()
|
|
155
|
+
console.error('ABOOOORT')
|
|
156
|
+
|
|
157
|
+
await this.intervalPromise
|
|
158
|
+
this.intervalPromise = undefined
|
|
159
|
+
console.error('DONE -_-')
|
|
160
|
+
}
|
|
161
|
+
}
|
package/src/config/config.ts
CHANGED
|
@@ -24,6 +24,7 @@ export const envToCfg = (env: OzoneEnvironment): OzoneConfig => {
|
|
|
24
24
|
poolSize: env.dbPoolSize,
|
|
25
25
|
poolMaxUses: env.dbPoolMaxUses,
|
|
26
26
|
poolIdleTimeoutMs: env.dbPoolIdleTimeoutMs,
|
|
27
|
+
materializedViewRefreshIntervalMs: env.dbMaterializedViewRefreshIntervalMs,
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
assert(env.appviewUrl, 'appviewUrl is required')
|
|
@@ -122,6 +123,7 @@ export type DatabaseConfig = {
|
|
|
122
123
|
poolSize?: number
|
|
123
124
|
poolMaxUses?: number
|
|
124
125
|
poolIdleTimeoutMs?: number
|
|
126
|
+
materializedViewRefreshIntervalMs?: number
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
export type AppviewConfig = {
|
package/src/config/env.ts
CHANGED
|
@@ -20,6 +20,9 @@ export const readEnv = (): OzoneEnvironment => {
|
|
|
20
20
|
dbPoolSize: envInt('OZONE_DB_POOL_SIZE'),
|
|
21
21
|
dbPoolMaxUses: envInt('OZONE_DB_POOL_MAX_USES'),
|
|
22
22
|
dbPoolIdleTimeoutMs: envInt('OZONE_DB_POOL_IDLE_TIMEOUT_MS'),
|
|
23
|
+
dbMaterializedViewRefreshIntervalMs: envInt(
|
|
24
|
+
'OZONE_DB_MATERIALIZED_VIEW_REFRESH_INTERVAL_MS',
|
|
25
|
+
),
|
|
23
26
|
didPlcUrl: envStr('OZONE_DID_PLC_URL'),
|
|
24
27
|
didCacheStaleTTL: envInt('OZONE_DID_CACHE_STALE_TTL'),
|
|
25
28
|
didCacheMaxTTL: envInt('OZONE_DID_CACHE_MAX_TTL'),
|
|
@@ -53,6 +56,7 @@ export type OzoneEnvironment = {
|
|
|
53
56
|
dbPoolSize?: number
|
|
54
57
|
dbPoolMaxUses?: number
|
|
55
58
|
dbPoolIdleTimeoutMs?: number
|
|
59
|
+
dbMaterializedViewRefreshIntervalMs?: number
|
|
56
60
|
didPlcUrl?: string
|
|
57
61
|
didCacheStaleTTL?: number
|
|
58
62
|
didCacheMaxTTL?: number
|
package/src/daemon/context.ts
CHANGED
|
@@ -6,17 +6,20 @@ import { OzoneConfig, OzoneSecrets } from '../config'
|
|
|
6
6
|
import { Database } from '../db'
|
|
7
7
|
import { EventPusher } from './event-pusher'
|
|
8
8
|
import { EventReverser } from './event-reverser'
|
|
9
|
-
import { ModerationService
|
|
9
|
+
import { ModerationService } from '../mod-service'
|
|
10
10
|
import { BackgroundQueue } from '../background'
|
|
11
11
|
import { getSigningKeyId } from '../util'
|
|
12
|
+
import { MaterializedViewRefresher } from './materialized-view-refresher'
|
|
13
|
+
import { allFulfilled } from '@atproto/common'
|
|
12
14
|
|
|
13
15
|
export type DaemonContextOptions = {
|
|
14
16
|
db: Database
|
|
15
17
|
cfg: OzoneConfig
|
|
16
|
-
|
|
18
|
+
backgroundQueue: BackgroundQueue
|
|
17
19
|
signingKey: Keypair
|
|
18
20
|
eventPusher: EventPusher
|
|
19
21
|
eventReverser: EventReverser
|
|
22
|
+
materializedViewRefresher: MaterializedViewRefresher
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export class DaemonContext {
|
|
@@ -67,13 +70,19 @@ export class DaemonContext {
|
|
|
67
70
|
|
|
68
71
|
const eventReverser = new EventReverser(db, modService)
|
|
69
72
|
|
|
73
|
+
const materializedViewRefresher = new MaterializedViewRefresher(
|
|
74
|
+
backgroundQueue,
|
|
75
|
+
cfg.db.materializedViewRefreshIntervalMs,
|
|
76
|
+
)
|
|
77
|
+
|
|
70
78
|
return new DaemonContext({
|
|
71
79
|
db,
|
|
72
80
|
cfg,
|
|
73
|
-
|
|
81
|
+
backgroundQueue,
|
|
74
82
|
signingKey,
|
|
75
83
|
eventPusher,
|
|
76
84
|
eventReverser,
|
|
85
|
+
materializedViewRefresher,
|
|
77
86
|
...(overrides ?? {}),
|
|
78
87
|
})
|
|
79
88
|
}
|
|
@@ -86,8 +95,8 @@ export class DaemonContext {
|
|
|
86
95
|
return this.opts.cfg
|
|
87
96
|
}
|
|
88
97
|
|
|
89
|
-
get
|
|
90
|
-
return this.opts.
|
|
98
|
+
get backgroundQueue(): BackgroundQueue {
|
|
99
|
+
return this.opts.backgroundQueue
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
get eventPusher(): EventPusher {
|
|
@@ -97,6 +106,35 @@ export class DaemonContext {
|
|
|
97
106
|
get eventReverser(): EventReverser {
|
|
98
107
|
return this.opts.eventReverser
|
|
99
108
|
}
|
|
109
|
+
|
|
110
|
+
get materializedViewRefresher(): MaterializedViewRefresher {
|
|
111
|
+
return this.opts.materializedViewRefresher
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async start() {
|
|
115
|
+
this.eventPusher.start()
|
|
116
|
+
this.eventReverser.start()
|
|
117
|
+
this.materializedViewRefresher.start()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async processAll() {
|
|
121
|
+
// Sequential because the materialized view values depend on the events.
|
|
122
|
+
await this.eventPusher.processAll()
|
|
123
|
+
await this.materializedViewRefresher.run()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async destroy() {
|
|
127
|
+
try {
|
|
128
|
+
await allFulfilled([
|
|
129
|
+
this.eventReverser.destroy(),
|
|
130
|
+
this.eventPusher.destroy(),
|
|
131
|
+
this.materializedViewRefresher.destroy(),
|
|
132
|
+
])
|
|
133
|
+
} finally {
|
|
134
|
+
await this.backgroundQueue.destroy()
|
|
135
|
+
await this.db.close()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
100
138
|
}
|
|
101
139
|
|
|
102
140
|
export default DaemonContext
|
package/src/daemon/index.ts
CHANGED
|
@@ -18,17 +18,14 @@ export class OzoneDaemon {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
async start() {
|
|
21
|
-
this.ctx.
|
|
22
|
-
this.ctx.eventReverser.start()
|
|
21
|
+
await this.ctx.start()
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
async processAll() {
|
|
26
|
-
await this.ctx.
|
|
25
|
+
await this.ctx.processAll()
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
async destroy() {
|
|
30
|
-
await this.ctx.
|
|
31
|
-
await this.ctx.eventPusher.destroy()
|
|
32
|
-
await this.ctx.db.close()
|
|
29
|
+
await this.ctx.destroy()
|
|
33
30
|
}
|
|
34
31
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MINUTE } from '@atproto/common'
|
|
2
|
+
import { sql } from 'kysely'
|
|
3
|
+
import { BackgroundQueue, PeriodicBackgroundTask } from '../background'
|
|
4
|
+
|
|
5
|
+
export class MaterializedViewRefresher extends PeriodicBackgroundTask {
|
|
6
|
+
constructor(backgroundQueue: BackgroundQueue, interval = 30 * MINUTE) {
|
|
7
|
+
super(backgroundQueue, interval, async ({ db }, signal) => {
|
|
8
|
+
for (const view of [
|
|
9
|
+
'account_events_stats',
|
|
10
|
+
'record_events_stats',
|
|
11
|
+
'account_record_events_stats',
|
|
12
|
+
'account_record_status_stats',
|
|
13
|
+
]) {
|
|
14
|
+
if (signal.aborted) break
|
|
15
|
+
|
|
16
|
+
// Kysely does not provide a way to cancel a running query. Because of
|
|
17
|
+
// this, killing the process during a refresh will cause the process to
|
|
18
|
+
// wait for the current refresh to finish before exiting. This is not
|
|
19
|
+
// ideal, but it is the best we can do until Kysely provides a way to
|
|
20
|
+
// cancel a query.
|
|
21
|
+
await sql`REFRESH MATERIALIZED VIEW CONCURRENTLY ${sql.id(view)}`.execute(
|
|
22
|
+
db,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { Kysely, sql } from 'kysely'
|
|
2
|
+
|
|
3
|
+
import { REASONAPPEAL } from '../../lexicon/types/com/atproto/moderation/defs'
|
|
4
|
+
import { DatabaseSchemaType } from '../schema'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
REVIEWESCALATED,
|
|
8
|
+
REVIEWOPEN,
|
|
9
|
+
} from '../../lexicon/types/tools/ozone/moderation/defs'
|
|
10
|
+
import * as modEvent from '../schema/moderation_event'
|
|
11
|
+
import * as modStatus from '../schema/moderation_subject_status'
|
|
12
|
+
import * as recordEventsStats from '../schema/record_events_stats'
|
|
13
|
+
|
|
14
|
+
export async function up(db: Kysely<any>): Promise<void> {
|
|
15
|
+
// Used by "tools.ozone.moderation.queryStatuses". Reduces query cost by two
|
|
16
|
+
// order of magnitudes when sorting using "reportedRecordsCount" or
|
|
17
|
+
// "takendownRecordsCount" and filtering by "reviewState".
|
|
18
|
+
await db.schema
|
|
19
|
+
.createIndex('moderation_subject_status_did_id_review_state_idx')
|
|
20
|
+
.on('moderation_subject_status')
|
|
21
|
+
.column('did')
|
|
22
|
+
.expression(sql`"id" ASC NULLS FIRST`)
|
|
23
|
+
.column('reviewState')
|
|
24
|
+
.execute()
|
|
25
|
+
|
|
26
|
+
// ~6sec for 16M events
|
|
27
|
+
await db.schema
|
|
28
|
+
.createView('account_events_stats')
|
|
29
|
+
.materialized()
|
|
30
|
+
.ifNotExists()
|
|
31
|
+
.as(
|
|
32
|
+
(db as Kysely<modEvent.PartialDB>)
|
|
33
|
+
.selectFrom('moderation_event')
|
|
34
|
+
.where('subjectType', '=', 'com.atproto.admin.defs#repoRef')
|
|
35
|
+
.where('subjectUri', 'is', null)
|
|
36
|
+
.select('subjectDid')
|
|
37
|
+
.select([
|
|
38
|
+
(eb) =>
|
|
39
|
+
sql<number>`COUNT(*) FILTER(
|
|
40
|
+
WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventTakedown'
|
|
41
|
+
AND ${eb.ref('durationInHours')} IS NULL
|
|
42
|
+
)`.as('takedownCount'),
|
|
43
|
+
(eb) =>
|
|
44
|
+
sql<number>`COUNT(*) FILTER(
|
|
45
|
+
WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventTakedown'
|
|
46
|
+
AND ${eb.ref('durationInHours')} IS NOT NULL
|
|
47
|
+
)`.as('suspendCount'),
|
|
48
|
+
(eb) =>
|
|
49
|
+
sql<number>`COUNT(*) FILTER(
|
|
50
|
+
WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventEscalate'
|
|
51
|
+
)`.as('escalateCount'),
|
|
52
|
+
(eb) =>
|
|
53
|
+
sql<number>`COUNT(*) FILTER(
|
|
54
|
+
WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventReport'
|
|
55
|
+
AND ${eb.ref('meta')} ->> 'reportType' != ${REASONAPPEAL}
|
|
56
|
+
)`.as('reportCount'),
|
|
57
|
+
(eb) =>
|
|
58
|
+
sql<number>`COUNT(*) FILTER(
|
|
59
|
+
WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventReport'
|
|
60
|
+
AND ${eb.ref('meta')} ->> 'reportType' = ${REASONAPPEAL}
|
|
61
|
+
)`.as('appealCount'),
|
|
62
|
+
])
|
|
63
|
+
.groupBy('subjectDid'),
|
|
64
|
+
)
|
|
65
|
+
.execute()
|
|
66
|
+
|
|
67
|
+
await db.schema
|
|
68
|
+
.createIndex('account_events_stats_did_idx')
|
|
69
|
+
.unique()
|
|
70
|
+
.on('account_events_stats')
|
|
71
|
+
.column('subjectDid')
|
|
72
|
+
.execute()
|
|
73
|
+
|
|
74
|
+
await db.schema
|
|
75
|
+
.createIndex('account_events_stats_suspend_count_idx')
|
|
76
|
+
.on('account_events_stats')
|
|
77
|
+
.expression(sql`"suspendCount" ASC NULLS FIRST`)
|
|
78
|
+
.column('subjectDid')
|
|
79
|
+
.execute()
|
|
80
|
+
|
|
81
|
+
// ~50sec for 16M events
|
|
82
|
+
await db.schema
|
|
83
|
+
.createView('record_events_stats')
|
|
84
|
+
.materialized()
|
|
85
|
+
.ifNotExists()
|
|
86
|
+
.as(
|
|
87
|
+
(db as Kysely<modEvent.PartialDB>)
|
|
88
|
+
.selectFrom('moderation_event')
|
|
89
|
+
.select([
|
|
90
|
+
'subjectDid',
|
|
91
|
+
'subjectUri',
|
|
92
|
+
(eb) =>
|
|
93
|
+
sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventEscalate')`.as(
|
|
94
|
+
'escalateCount',
|
|
95
|
+
),
|
|
96
|
+
(eb) =>
|
|
97
|
+
sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventReport' AND ${eb.ref('meta')} ->> 'reportType' != 'com.atproto.moderation.defs#reasonAppeal')`.as(
|
|
98
|
+
'reportCount',
|
|
99
|
+
),
|
|
100
|
+
(eb) =>
|
|
101
|
+
sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('action')} = 'tools.ozone.moderation.defs#modEventReport' AND ${eb.ref('meta')} ->> 'reportType' = 'com.atproto.moderation.defs#reasonAppeal')`.as(
|
|
102
|
+
'appealCount',
|
|
103
|
+
),
|
|
104
|
+
])
|
|
105
|
+
.where('subjectType', '=', 'com.atproto.repo.strongRef')
|
|
106
|
+
.where('subjectUri', 'is not', null)
|
|
107
|
+
.groupBy(['subjectDid', 'subjectUri']),
|
|
108
|
+
)
|
|
109
|
+
.execute()
|
|
110
|
+
|
|
111
|
+
await db.schema
|
|
112
|
+
.createIndex('record_events_stats_uri_idx')
|
|
113
|
+
.unique()
|
|
114
|
+
.on('record_events_stats')
|
|
115
|
+
.column('subjectUri')
|
|
116
|
+
.execute()
|
|
117
|
+
|
|
118
|
+
await db.schema
|
|
119
|
+
.createIndex('record_events_stats_did_idx')
|
|
120
|
+
.on('record_events_stats')
|
|
121
|
+
.column('subjectDid')
|
|
122
|
+
.execute()
|
|
123
|
+
|
|
124
|
+
await db.schema
|
|
125
|
+
.createView('account_record_events_stats')
|
|
126
|
+
.materialized()
|
|
127
|
+
.ifNotExists()
|
|
128
|
+
.as(
|
|
129
|
+
(db as Kysely<recordEventsStats.PartialDB>)
|
|
130
|
+
.selectFrom('record_events_stats')
|
|
131
|
+
.select([
|
|
132
|
+
'subjectDid',
|
|
133
|
+
(eb) =>
|
|
134
|
+
// Casting to "bigint" because "numeric" gets casted to a string
|
|
135
|
+
// by default by postgres-node.
|
|
136
|
+
sql<number>`SUM(${eb.ref('reportCount')})::bigint`.as(
|
|
137
|
+
'totalReports',
|
|
138
|
+
),
|
|
139
|
+
(eb) =>
|
|
140
|
+
sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('reportCount')} > 0)`.as(
|
|
141
|
+
'reportedCount',
|
|
142
|
+
),
|
|
143
|
+
(eb) =>
|
|
144
|
+
sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('escalateCount')} > 0)`.as(
|
|
145
|
+
'escalatedCount',
|
|
146
|
+
),
|
|
147
|
+
(eb) =>
|
|
148
|
+
sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('appealCount')} > 0)`.as(
|
|
149
|
+
'appealedCount',
|
|
150
|
+
),
|
|
151
|
+
])
|
|
152
|
+
.groupBy('subjectDid'),
|
|
153
|
+
)
|
|
154
|
+
.execute()
|
|
155
|
+
|
|
156
|
+
await db.schema
|
|
157
|
+
.createIndex('account_record_events_stats_did_idx')
|
|
158
|
+
.unique()
|
|
159
|
+
.on('account_record_events_stats')
|
|
160
|
+
.column('subjectDid')
|
|
161
|
+
.execute()
|
|
162
|
+
|
|
163
|
+
await db.schema
|
|
164
|
+
.createIndex('account_record_events_stats_reported_count_idx')
|
|
165
|
+
.on('account_record_events_stats')
|
|
166
|
+
.expression(sql`"reportedCount" ASC NULLS FIRST`)
|
|
167
|
+
.column('subjectDid')
|
|
168
|
+
.execute()
|
|
169
|
+
|
|
170
|
+
await db.schema
|
|
171
|
+
.createView('account_record_status_stats')
|
|
172
|
+
.materialized()
|
|
173
|
+
.ifNotExists()
|
|
174
|
+
.as(
|
|
175
|
+
(db as Kysely<modStatus.PartialDB>)
|
|
176
|
+
.selectFrom('moderation_subject_status')
|
|
177
|
+
.select('did')
|
|
178
|
+
.select([
|
|
179
|
+
sql<number>`COUNT(*)`.as('subjectCount'),
|
|
180
|
+
(eb) =>
|
|
181
|
+
sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('reviewState')} IN (${REVIEWOPEN}, ${REVIEWESCALATED}))`.as(
|
|
182
|
+
'pendingCount',
|
|
183
|
+
),
|
|
184
|
+
(eb) =>
|
|
185
|
+
sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('reviewState')} NOT IN (${REVIEWOPEN}, ${REVIEWESCALATED}))`.as(
|
|
186
|
+
'processedCount',
|
|
187
|
+
),
|
|
188
|
+
(eb) =>
|
|
189
|
+
sql<number>`COUNT(*) FILTER (WHERE ${eb.ref('takendown')})`.as(
|
|
190
|
+
'takendownCount',
|
|
191
|
+
),
|
|
192
|
+
])
|
|
193
|
+
.where('recordPath', '!=', '')
|
|
194
|
+
.groupBy('did'),
|
|
195
|
+
)
|
|
196
|
+
.execute()
|
|
197
|
+
|
|
198
|
+
await db.schema
|
|
199
|
+
.createIndex('account_record_status_stats_did_idx')
|
|
200
|
+
.unique()
|
|
201
|
+
.on('account_record_status_stats')
|
|
202
|
+
.column('did')
|
|
203
|
+
.execute()
|
|
204
|
+
|
|
205
|
+
await db.schema
|
|
206
|
+
.createIndex('account_record_status_stats_takendown_count_idx')
|
|
207
|
+
.on('account_record_status_stats')
|
|
208
|
+
.expression(sql`"takendownCount" ASC NULLS FIRST`)
|
|
209
|
+
.column('did')
|
|
210
|
+
.execute()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function down(db: Kysely<DatabaseSchemaType>): Promise<void> {
|
|
214
|
+
db.schema.dropView('account_record_status_stats').materialized().execute()
|
|
215
|
+
db.schema.dropView('account_record_events_stats').materialized().execute()
|
|
216
|
+
db.schema.dropView('record_events_stats').materialized().execute()
|
|
217
|
+
db.schema.dropView('account_events_stats').materialized().execute()
|
|
218
|
+
}
|
|
@@ -17,3 +17,4 @@ export * as _20241001T205730722Z from './20241001T205730722Z-subject-status-revi
|
|
|
17
17
|
export * as _20241008T205730722Z from './20241008T205730722Z-sets'
|
|
18
18
|
export * as _20241018T205730722Z from './20241018T205730722Z-setting'
|
|
19
19
|
export * as _20241026T205730722Z from './20241026T205730722Z-add-hosting-status-to-subject-status'
|
|
20
|
+
export * as _20241220T144630860Z from './20241220T144630860Z-stats-materialized-views'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { GeneratedAlways, Selectable } from 'kysely'
|
|
2
|
+
|
|
3
|
+
export const tableName = 'account_events_stats'
|
|
4
|
+
|
|
5
|
+
export type AccountEventsStats = {
|
|
6
|
+
subjectDid: GeneratedAlways<string>
|
|
7
|
+
takedownCount: GeneratedAlways<number>
|
|
8
|
+
suspendCount: GeneratedAlways<number>
|
|
9
|
+
escalateCount: GeneratedAlways<number>
|
|
10
|
+
reportCount: GeneratedAlways<number>
|
|
11
|
+
appealCount: GeneratedAlways<number>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type AccountEventsStatsRow = Selectable<AccountEventsStats>
|
|
15
|
+
|
|
16
|
+
export type PartialDB = { [tableName]: AccountEventsStats }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { GeneratedAlways, Selectable } from 'kysely'
|
|
2
|
+
|
|
3
|
+
export const tableName = 'account_record_events_stats'
|
|
4
|
+
|
|
5
|
+
type AccountRecordEventsStats = {
|
|
6
|
+
subjectDid: GeneratedAlways<string>
|
|
7
|
+
totalReports: GeneratedAlways<number>
|
|
8
|
+
reportedCount: GeneratedAlways<number>
|
|
9
|
+
escalatedCount: GeneratedAlways<number>
|
|
10
|
+
appealedCount: GeneratedAlways<number>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type AccountRecordEventsStatsRow = Selectable<AccountRecordEventsStats>
|
|
14
|
+
|
|
15
|
+
export type PartialDB = { [tableName]: AccountRecordEventsStats }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { GeneratedAlways, Selectable } from 'kysely'
|
|
2
|
+
|
|
3
|
+
export const tableName = 'account_record_status_stats'
|
|
4
|
+
|
|
5
|
+
type AccountRecordStatusStats = {
|
|
6
|
+
did: GeneratedAlways<string>
|
|
7
|
+
subjectCount: GeneratedAlways<number>
|
|
8
|
+
pendingCount: GeneratedAlways<number>
|
|
9
|
+
processedCount: GeneratedAlways<number>
|
|
10
|
+
takendownCount: GeneratedAlways<number>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type AccountRecordStatusStatsRow = Selectable<AccountRecordStatusStats>
|
|
14
|
+
|
|
15
|
+
export type PartialDB = { [tableName]: AccountRecordStatusStats }
|
package/src/db/schema/index.ts
CHANGED
|
@@ -11,6 +11,11 @@ import * as set from './ozone_set'
|
|
|
11
11
|
import * as member from './member'
|
|
12
12
|
import * as setting from './setting'
|
|
13
13
|
|
|
14
|
+
import * as recordEventsStats from './record_events_stats'
|
|
15
|
+
import * as accountEventsStats from './account_events_stats'
|
|
16
|
+
import * as accountRecordEventsStats from './account_record_events_stats'
|
|
17
|
+
import * as accountRecordStatusStats from './account_record_status_stats'
|
|
18
|
+
|
|
14
19
|
export type DatabaseSchemaType = modEvent.PartialDB &
|
|
15
20
|
modSubjectStatus.PartialDB &
|
|
16
21
|
label.PartialDB &
|
|
@@ -21,7 +26,11 @@ export type DatabaseSchemaType = modEvent.PartialDB &
|
|
|
21
26
|
communicationTemplate.PartialDB &
|
|
22
27
|
set.PartialDB &
|
|
23
28
|
member.PartialDB &
|
|
24
|
-
setting.PartialDB
|
|
29
|
+
setting.PartialDB &
|
|
30
|
+
accountEventsStats.PartialDB &
|
|
31
|
+
recordEventsStats.PartialDB &
|
|
32
|
+
accountRecordEventsStats.PartialDB &
|
|
33
|
+
accountRecordStatusStats.PartialDB
|
|
25
34
|
|
|
26
35
|
export type DatabaseSchema = Kysely<DatabaseSchemaType>
|
|
27
36
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { GeneratedAlways, Selectable } from 'kysely'
|
|
2
|
+
|
|
3
|
+
export const tableName = 'record_events_stats'
|
|
4
|
+
|
|
5
|
+
export type RecordEventsStats = {
|
|
6
|
+
subjectDid: GeneratedAlways<string>
|
|
7
|
+
subjectUri: GeneratedAlways<string>
|
|
8
|
+
escalateCount: GeneratedAlways<number>
|
|
9
|
+
reportCount: GeneratedAlways<number>
|
|
10
|
+
appealCount: GeneratedAlways<number>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type RecordEventsStatsRow = Selectable<RecordEventsStats>
|
|
14
|
+
|
|
15
|
+
export type PartialDB = { [tableName]: RecordEventsStats }
|
package/src/index.ts
CHANGED
|
@@ -114,13 +114,7 @@ export class OzoneService {
|
|
|
114
114
|
},
|
|
115
115
|
'db pool stats',
|
|
116
116
|
)
|
|
117
|
-
dbLogger.info(
|
|
118
|
-
{
|
|
119
|
-
runningCount: backgroundQueue.queue.pending,
|
|
120
|
-
waitingCount: backgroundQueue.queue.size,
|
|
121
|
-
},
|
|
122
|
-
'background queue stats',
|
|
123
|
-
)
|
|
117
|
+
dbLogger.info(backgroundQueue.getStats(), 'background queue stats')
|
|
124
118
|
}, 10000)
|
|
125
119
|
await this.ctx.sequencer.start()
|
|
126
120
|
const server = this.app.listen(this.ctx.cfg.service.port)
|