mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
perf(message): drop two reads and a write from message create (#2123)
This commit is contained in:
1 parent
36dcf51024
commit
8aa39bc7db
3 files changed
+153
-14
No files matched your search
@@ -0,0 +1,128 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import * as BucketUtils from '@fluxer/snowflake/src/SnowflakeBuckets';
|
||||
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
||||
import {type ChannelID, createChannelID, createMessageID, createUserID, type MessageID} from '../../../BrandedTypes';
|
||||
import {fetchOne, setCassandraQueryExecutorForTesting} from '../../../database/CassandraQueryExecution';
|
||||
import type {CassandraParams, KvQueryMeta, PreparedQuery} from '../../../database/CassandraTypes';
|
||||
import type {ChannelStateRow, MessageRow} from '../../../database/types/MessageTypes';
|
||||
import {ChannelState} from '../../../Tables';
|
||||
import {InMemoryCassandraQueryExecutor} from '../../../test/InMemoryCassandraQueryExecutor';
|
||||
import {MessageDataRepository} from './MessageDataRepository';
|
||||
|
||||
const FETCH_CHANNEL_STATE = ChannelState.select({where: ChannelState.where.eq('channel_id'), limit: 1});
|
||||
|
||||
class RecordingCassandraQueryExecutor {
|
||||
readonly statements: Array<string> = [];
|
||||
readonly batchAtomicity: Array<boolean> = [];
|
||||
private readonly inner = new InMemoryCassandraQueryExecutor();
|
||||
|
||||
async executeQuery<T = Record<string, unknown>, P extends CassandraParams = CassandraParams>(
|
||||
query: PreparedQuery<P>,
|
||||
): Promise<Array<T>> {
|
||||
this.record(query.kvMeta);
|
||||
return this.inner.executeQuery<T>(query);
|
||||
}
|
||||
|
||||
async executeBatch(
|
||||
queries: Array<{query: string; params: object; meta?: KvQueryMeta}>,
|
||||
atomic = true,
|
||||
): Promise<void> {
|
||||
this.batchAtomicity.push(atomic);
|
||||
for (const entry of queries) {
|
||||
this.record(entry.meta);
|
||||
}
|
||||
await this.inner.executeBatch(queries);
|
||||
}
|
||||
|
||||
countStatements(statement: string): number {
|
||||
return this.statements.filter((entry) => entry === statement).length;
|
||||
}
|
||||
|
||||
private record(meta: KvQueryMeta | undefined): void {
|
||||
if (!meta) return;
|
||||
this.statements.push(`${meta.action}:${meta.table.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
function makeMessageRow(channelId: ChannelID, messageId: MessageID): MessageRow {
|
||||
return {
|
||||
channel_id: channelId,
|
||||
bucket: BucketUtils.makeBucket(messageId),
|
||||
message_id: messageId,
|
||||
author_id: createUserID(7n),
|
||||
type: 0,
|
||||
webhook_id: null,
|
||||
webhook_name: null,
|
||||
webhook_avatar_hash: null,
|
||||
content: 'hello',
|
||||
edited_timestamp: null,
|
||||
pinned_timestamp: null,
|
||||
flags: 0,
|
||||
mention_everyone: false,
|
||||
mention_users: null,
|
||||
mention_roles: null,
|
||||
mention_channels: null,
|
||||
attachments: null,
|
||||
embeds: null,
|
||||
sticker_items: null,
|
||||
message_reference: null,
|
||||
message_snapshots: null,
|
||||
call: null,
|
||||
nsfw_emojis: null,
|
||||
has_reaction: false,
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
let executor: RecordingCassandraQueryExecutor;
|
||||
|
||||
describe('MessageDataRepository.upsertMessage round trips', () => {
|
||||
beforeEach(() => {
|
||||
executor = new RecordingCassandraQueryExecutor();
|
||||
setCassandraQueryExecutorForTesting(executor);
|
||||
});
|
||||
afterEach(() => {
|
||||
setCassandraQueryExecutorForTesting(new InMemoryCassandraQueryExecutor());
|
||||
});
|
||||
it('writes channel_state once and skips the read-before-write when the caller passes no previous row', async () => {
|
||||
const channelId = createChannelID(1710000000000000000n);
|
||||
const messageId = createMessageID(1720000000000000000n);
|
||||
const repository = new MessageDataRepository();
|
||||
|
||||
await repository.upsertMessage(makeMessageRow(channelId, messageId), null);
|
||||
|
||||
expect(executor.countStatements('select:messages')).toBe(0);
|
||||
expect(executor.countStatements('patch:channel_state')).toBe(1);
|
||||
expect(executor.batchAtomicity).toEqual([false]);
|
||||
expect(await loadChannelState(channelId)).toMatchObject({
|
||||
created_bucket: BucketUtils.makeBucket(channelId),
|
||||
has_messages: true,
|
||||
last_message_id: messageId,
|
||||
last_message_bucket: BucketUtils.makeBucket(messageId),
|
||||
});
|
||||
});
|
||||
it('keeps the newer last_message_id while still recording channel state for an older message', async () => {
|
||||
const channelId = createChannelID(1710000000000000000n);
|
||||
const newerMessageId = createMessageID(1730000000000000000n);
|
||||
const olderMessageId = createMessageID(1720000000000000000n);
|
||||
const repository = new MessageDataRepository();
|
||||
await repository.upsertMessage(makeMessageRow(channelId, newerMessageId), null);
|
||||
executor.statements.length = 0;
|
||||
executor.batchAtomicity.length = 0;
|
||||
|
||||
await repository.upsertMessage(makeMessageRow(channelId, olderMessageId), null);
|
||||
|
||||
expect(executor.countStatements('patch:channel_state')).toBe(1);
|
||||
expect(await loadChannelState(channelId)).toMatchObject({
|
||||
created_bucket: BucketUtils.makeBucket(channelId),
|
||||
has_messages: true,
|
||||
last_message_id: newerMessageId,
|
||||
last_message_bucket: BucketUtils.makeBucket(newerMessageId),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function loadChannelState(channelId: ChannelID): Promise<ChannelStateRow | null> {
|
||||
return fetchOne<ChannelStateRow>(FETCH_CHANNEL_STATE.bind({channel_id: channelId}));
|
||||
}
|
||||
@@ -538,6 +538,28 @@ export class MessageDataRepository {
|
||||
);
|
||||
}
|
||||
|
||||
private async recordChannelStateForMessage(
|
||||
channelId: ChannelID,
|
||||
messageId: MessageID,
|
||||
messageBucket: number,
|
||||
): Promise<void> {
|
||||
const state = await this.getChannelState(channelId);
|
||||
const prev = state?.last_message_id ?? null;
|
||||
const patch = {
|
||||
created_bucket: Db.set(BucketUtils.makeBucket(channelId)),
|
||||
has_messages: Db.set(true),
|
||||
updated_at: Db.set(new Date()),
|
||||
};
|
||||
await upsertOne(
|
||||
ChannelState.patchByPk(
|
||||
{channel_id: channelId},
|
||||
prev !== null && messageId <= prev
|
||||
? patch
|
||||
: {...patch, last_message_id: Db.set(messageId), last_message_bucket: Db.set(messageBucket)},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async getChannelState(channelId: ChannelID): Promise<ChannelStateRow | null> {
|
||||
return fetchOne<ChannelStateRow>(FETCH_CHANNEL_STATE.bind({channel_id: channelId}));
|
||||
}
|
||||
@@ -718,19 +740,8 @@ export class MessageDataRepository {
|
||||
updated_at: new Date(),
|
||||
}),
|
||||
);
|
||||
const createdBucket = BucketUtils.makeBucket(data.channel_id);
|
||||
batch.addPrepared(
|
||||
ChannelState.patchByPk(
|
||||
{channel_id: data.channel_id},
|
||||
{
|
||||
created_bucket: Db.set(createdBucket),
|
||||
has_messages: Db.set(true),
|
||||
updated_at: Db.set(new Date()),
|
||||
},
|
||||
),
|
||||
);
|
||||
await batch.execute();
|
||||
await this.advanceChannelStateLastMessageIfNewer(data.channel_id, data.message_id, data.bucket);
|
||||
await batch.execute(false);
|
||||
await this.recordChannelStateForMessage(data.channel_id, data.message_id, data.bucket);
|
||||
return new Message({...data, version: finalVersion});
|
||||
}
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ export class MessagePersistenceService {
|
||||
has_reaction: false,
|
||||
version: 1,
|
||||
};
|
||||
const message = await this.channelRepository.messages.upsertMessage(messageRowData);
|
||||
const message = await this.channelRepository.messages.upsertMessage(messageRowData, null);
|
||||
const enqueueDeferredEmbeds = await this.runPostPersistenceOperations({
|
||||
message,
|
||||
params,
|
||||
|
||||
Reference in new issue
Block a user