logical rep setup

This commit is contained in:
Brett Woodward
2025-08-12 13:20:21 -04:00
parent 0d1c8af7fd
commit 8ab42f4981
14 changed files with 98 additions and 16 deletions
+2
View File
@@ -17,6 +17,7 @@ services:
depends_on:
- db
- notification-db
- logical-db
- redis
- redis-sys
environment:
@@ -26,6 +27,7 @@ services:
DARKPIT_SHADOW_URL: 'postgresql://modelshare:postgres@db:5432/postgres'
NOTIFICATION_DB_URL: 'postgresql://postgres:postgres@notification-db:5432/postgres'
NOTIFICATION_DB_REPLICA_URL: 'postgresql://postgres:postgres@notification-db:5432/postgres'
LOGICAL_REPLICA_DB_URL: 'postgresql://postgres:postgres@logical-db:5432/postgres'
REDIS_URL: 'redis://:redis@redis:6379'
REDIS_SYS_URL: 'redis://:redis@redis-sys:6379'
CLICKHOUSE_HOST: 'http://clickhouse:8123'
+2
View File
@@ -13,6 +13,8 @@ DATABASE_REPLICA_URL=postgresql://postgres:postgres@localhost:15432/civitai
NOTIFICATION_DB_URL=postgresql://postgres:postgres@localhost:15434/postgres
NOTIFICATION_DB_REPLICA_URL=postgresql://postgres:postgres@localhost:15434/postgres
LOGICAL_REPLICA_DB_URL=postgresql://postgres:postgres@localhost:15435/postgres
# Redis
REDIS_URL=redis://:redis@localhost:6379
REDIS_SYS_URL=redis://:redis@localhost:6378
+3
View File
@@ -0,0 +1,3 @@
FROM postgres:15-bookworm
COPY ./docker-init/*.sql /docker-entrypoint-initdb.d/
RUN chmod a+r /docker-entrypoint-initdb.d/*
@@ -0,0 +1,3 @@
CREATE TABLE public."Image" (
id integer NOT NULL
);
+13
View File
@@ -35,6 +35,18 @@ services:
volumes:
- notification-db:/var/lib/postgresql/data
logical-db:
build:
context: containers/logical-db
dockerfile: Dockerfile
ports:
- "15435:5432"
environment:
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
volumes:
- logical-db:/var/lib/postgresql/data
redis:
image: redis:7-bookworm
command: redis-server --requirepass "redis"
@@ -127,6 +139,7 @@ volumes:
prisma-pit:
db:
notification-db:
logical-db:
redis:
redis-sys:
minio:
@@ -23,5 +23,5 @@ JOIN "ModelVersion" mv ON mv.id = irn."modelVersionId"
JOIN "RestrictedBaseModels" rbm ON rbm."baseModel" = mv."baseModel"
WITH DATA;
CREATE UNIQUE INDEX CONCURRENTLY idx_restricted_images_by_base_model_imageid
CREATE UNIQUE INDEX idx_restricted_images_by_base_model_imageid
ON "RestrictedImagesByBaseModel" ("imageId");
+5 -3
View File
@@ -1,6 +1,7 @@
import { faker } from '@faker-js/faker';
import dayjs from 'dayjs';
import { capitalize, pull, range, without } from 'lodash-es';
import type { DatabaseError } from 'pg';
import format from 'pg-format';
// import type { DatabaseError } from 'pg-protocol/src/messages';
import { clickhouse } from '~/server/clickhouse/client';
@@ -12,6 +13,7 @@ import { pgDbWrite } from '~/server/db/pgDb';
import { notificationProcessors } from '~/server/notifications/utils.notifications';
import { REDIS_SYS_KEYS, sysRedis } from '~/server/redis/client';
import { getChatHash, getUsersFromHash } from '~/server/utils/chat';
import { baseModels } from '~/shared/constants/base-model.constants';
import { IMAGE_MIME_TYPE, VIDEO_MIME_TYPE } from '~/shared/constants/mime-types';
import {
ArticleEngagementType,
@@ -51,8 +53,6 @@ import {
insertRows,
randPrependBad,
} from './utils';
import type { DatabaseError } from 'pg';
import { baseModels } from '~/shared/constants/base-model.constants';
// import { fetchBlob } from '~/utils/file-utils';
// Usage: npx tsx ./scripts/local-dev/gen_seed.ts --rows=1000
@@ -76,6 +76,8 @@ const fbool = faker.datatype.boolean;
// TODO fix tables ownership from doadmin to civitai
// TODO seed logicalDb
const setSerialNotif = async (table: string) => {
// language=text
const query = `SELECT setval(pg_get_serial_sequence('"${table}"', 'id'), coalesce(max(id)+1, 1), false) FROM %I`;
@@ -439,7 +441,7 @@ const genUsers = (num: number, includeCiv = false) => {
for (let step = extraUsers.length + (includeCiv ? 0 : 1); step <= num; step++) {
const created = faker.date.past({ years: 3 }).toISOString();
const isMuted = fbool(0.01);
let username = randPrependBad(faker.internet.userName(), '.');
let username = randPrependBad(faker.internet.username(), '.');
if (seenUserNames.includes(username)) username = `${username}_${faker.number.int(1_000)}`;
seenUserNames.push(username);
+1
View File
@@ -19,6 +19,7 @@ export const serverSchema = z.object({
DATABASE_SSL: zc.booleanString.default(true),
NOTIFICATION_DB_URL: z.url(),
NOTIFICATION_DB_REPLICA_URL: z.url(),
LOGICAL_REPLICA_DB_URL: z.url(),
DATABASE_CONNECTION_TIMEOUT: z.coerce.number().default(0),
DATABASE_POOL_MAX: z.coerce.number().default(20),
DATABASE_POOL_IDLE_TIMEOUT: z.coerce.number().default(30000),
+7 -4
View File
@@ -12,6 +12,10 @@ import { dbRead, dbWrite } from '~/server/db/client';
import { reportAcceptedReward } from '~/server/rewards';
import type { GetByIdInput } from '~/server/schema/base.schema';
import { getUserCollectionPermissionsById } from '~/server/services/collection.service';
import {
isImageInQueue,
updatePendingImageRatings,
} from '~/server/services/games/new-order.service';
import {
addBlockedImage,
bulkRemoveBlockedImages,
@@ -63,10 +67,6 @@ import {
getTagNamesForImages,
moderateImages,
} from './../services/image.service';
import {
isImageInQueue,
updatePendingImageRatings,
} from '~/server/services/games/new-order.service';
export const moderateImageHandler = async ({
input,
@@ -257,6 +257,7 @@ export const getInfiniteImagesHandler = async ({
useCombinedNsfwLevel: !features.canViewNsfw,
headers: { src: 'getInfiniteImagesHandler' },
include: [...input.include, 'tagIds'],
useLogicalReplica: features.logicalReplica,
});
} catch (error) {
if (error instanceof TRPCError) throw error;
@@ -317,6 +318,7 @@ export const getImagesAsPostsInfiniteHandler = async ({
user,
headers: { src: 'getImagesAsPostsInfiniteHandler' },
include: [...input.include, 'tagIds', 'profilePictures'],
useLogicalReplica: features.logicalReplica,
});
for (const image of pinnedPostsImages) {
@@ -338,6 +340,7 @@ export const getImagesAsPostsInfiniteHandler = async ({
user,
headers: { src: 'getImagesAsPostsInfiniteHandler' },
include: [...input.include, 'tagIds', 'profilePictures'],
useLogicalReplica: features.logicalReplica,
});
// Merge images by postId
+8 -2
View File
@@ -24,13 +24,15 @@ type ClientInstanceType =
| 'primaryRead'
| 'primaryReadLong'
| 'notification'
| 'notificationRead';
| 'notificationRead'
| 'logicalReplica';
const instanceUrlMap: Record<ClientInstanceType, string> = {
notification: env.NOTIFICATION_DB_URL,
notificationRead: env.NOTIFICATION_DB_REPLICA_URL ?? env.NOTIFICATION_DB_URL,
primary: env.DATABASE_URL,
primaryRead: env.DATABASE_REPLICA_URL ?? env.DATABASE_URL,
primaryReadLong: env.DATABASE_REPLICA_LONG_URL ?? env.DATABASE_URL,
logicalReplica: env.LOGICAL_REPLICA_DB_URL ?? env.DATABASE_URL,
};
export function getClient(
@@ -46,7 +48,11 @@ export function getClient(
const connectionString = connectionStringUrl.toString();
const isNotification = instance === 'notification' || instance === 'notificationRead';
const appBaseName = isNotification ? 'notif-pg' : 'node-pg';
const appBaseName = isNotification
? 'notif-pg'
: instance === 'logicalReplica'
? 'logical-pg'
: 'node-pg';
const pool = new Pool({
connectionString,
+22
View File
@@ -0,0 +1,22 @@
import { types } from 'pg';
import { isProd } from '~/env/other';
import type { AugmentedPool } from '~/server/db/db-helpers';
import { getClient } from '~/server/db/db-helpers';
declare global {
// eslint-disable-next-line no-var, vars-on-top
var globalLogicalDb: AugmentedPool | undefined;
}
// Fix Dates
types.setTypeParser(types.builtins.TIMESTAMP, function (stringValue) {
return new Date(stringValue.replace(' ', 'T') + 'Z');
});
export let logicalDb: AugmentedPool;
if (isProd) {
logicalDb = getClient({ instance: 'logicalReplica' });
} else {
if (!global.globalLogicalDb) global.globalLogicalDb = getClient({ instance: 'logicalReplica' });
logicalDb = global.globalLogicalDb;
}
@@ -128,6 +128,7 @@ const featureFlags = createFeatureFlags({
zkp2pPayments: ['mod'],
thirtyDayEarlyAccess: ['granted'],
kontextAds: ['mod', 'granted'],
logicalReplica: ['public'],
});
export const featureFlagKeys = Object.keys(featureFlags) as FeatureFlagKey[];
+20 -5
View File
@@ -29,6 +29,7 @@ import {
import { getImageGenerationProcess } from '~/server/common/model-helpers';
import { dbRead, dbWrite } from '~/server/db/client';
import { getDbWithoutLag, preventReplicationLag } from '~/server/db/db-lag-helpers';
import { logicalDb } from '~/server/db/logicalDb';
import { pgDbRead } from '~/server/db/pgDb';
import { poolCounters } from '~/server/games/new-order/utils';
import { logToAxiom } from '~/server/logging/client';
@@ -767,6 +768,7 @@ type GetAllImagesInput = GetInfiniteImagesOutput & {
useCombinedNsfwLevel?: boolean;
user?: SessionUser;
headers?: Record<string, string>; // TODO needed?
useLogicalReplica: boolean;
};
export type ImagesInfiniteModel = AsyncReturnType<typeof getAllImages>['items'][0];
export const getAllImages = async (
@@ -1738,6 +1740,7 @@ export async function getImagesFromSearch(input: ImageSearchInput) {
poiOnly,
minorOnly,
blockedFor,
useLogicalReplica,
// TODO check the unused stuff in here
} = input;
let { browsingLevel, userId } = input;
@@ -2063,11 +2066,23 @@ export async function getImagesFromSearch(input: ImageSearchInput) {
});
const filteredHitIds = filteredHits.map((fh) => fh.id);
// we could pull in nsfwLevel/needsReview here too and overwrite the search index attributes (move above the hits filter)
const dbIdResp = await dbRead.image.findMany({
where: { id: { in: filteredHitIds } },
select: { id: true },
});
let dbIdResp: { id: number }[];
if (!useLogicalReplica) {
// we could pull in nsfwLevel/needsReview here too and overwrite the search index attributes (move above the hits filter)
dbIdResp = await dbRead.image.findMany({
where: { id: { in: filteredHitIds } },
select: { id: true },
});
} else {
const dbIdData = await logicalDb.query<{ id: number }>(
`select id from "Image" where "id" in ($1)`,
[filteredHitIds]
);
dbIdResp = dbIdData.rows;
}
const dbIds = dbIdResp.map((dbi) => dbi.id);
const filtered = filteredHits.filter((fh) => dbIds.includes(fh.id));
+10 -1
View File
@@ -7,6 +7,11 @@ test.describe('bidding', () => {
test('bid first', async ({ page }) => {
await page.goto('/auctions');
const skipTour = page.getByRole('button', { name: 'Skip' });
if (await skipTour.isVisible()) {
await skipTour.click();
}
await page.getByRole('button', { name: 'Select model' }).click();
const confirmBtn = page.getByRole('button', { name: 'Close' });
@@ -15,7 +20,11 @@ test.describe('bidding', () => {
}
await page.getByText('ALL', { exact: true }).click();
await page.locator('div').getByRole('button', { name: 'Select' }).first().click();
await page
.getByTestId('resource-select-items')
.getByRole('button', { name: 'Select' })
.first()
.click();
// TODO get name here...
await page.getByRole('button', { name: '1st' }).click();