mirror of
https://github.com/codeflash-ai/codeflash-internal.git
synced 2026-05-04 18:25:18 +00:00
proof: PrismaClient singleton consolidation (16c5887a) (#2543)
## Proof of Correctness — Commit 3/22 **Optimization:** Replace 5 separate `new PrismaClient()` instances with a shared singleton at `@/lib/prisma`. **Claim:** Eliminates 5 independent connection pools → 1 shared pool with `connection_limit=10`, `pool_timeout=20`. Prevents connection pool exhaustion under concurrent requests. ### Evidence Each `new PrismaClient()` creates its own query engine and PostgreSQL connection pool (default 5 connections). With 5 instances, the app could hold 25 connections simultaneously — a real risk against PostgreSQL's hard limit (typically 100, often lower on Azure). Files that had their own `new PrismaClient()`: - `src/app/(dashboard)/apikeys/page.tsx` - `src/app/(dashboard)/apikeys/tokenfuncs.ts` - `src/app/api/traces/[trace_id]/save-modified-code/route.ts` - `src/app/trace/[trace_id]/page.tsx` - `src/lib/modified-code-utils.ts` The singleton pattern is [Prisma's official recommendation for Next.js](https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices). ### Reproducer ```bash cd js/cf-webapp bash proof/reproducers/03-prisma-singleton.sh ``` Verifies: 1. No `new PrismaClient()` outside `src/lib/prisma.ts` 2. All 5 affected files import from `@/lib/prisma` 3. Singleton has connection pooling + globalThis caching 4. TypeScript compiles cleanly ### Files - `js/cf-webapp/proof/03-prisma-singleton.md` — detailed proof - `js/cf-webapp/proof/reproducers/03-prisma-singleton.sh` — reproducer ### Reference - Source commit: 16c5887a from PR #2536
This commit is contained in:
parent
b4d89c5cd3
commit
fad39c934d
7 changed files with 244 additions and 20 deletions
79
js/cf-webapp/proof/03-prisma-singleton.md
Normal file
79
js/cf-webapp/proof/03-prisma-singleton.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# Proof: PrismaClient Singleton (16c5887a)
|
||||
|
||||
## Optimization
|
||||
|
||||
Replace 5 separate `new PrismaClient()` calls with the shared singleton at `@/lib/prisma`.
|
||||
|
||||
## Claim
|
||||
|
||||
Eliminates 5 independent connection pools in favor of 1 shared pool with `connection_limit=10` and `pool_timeout=20`. Prevents connection pool exhaustion under concurrent requests.
|
||||
|
||||
## Root Cause
|
||||
|
||||
Each `new PrismaClient()` creates its own:
|
||||
- Query engine instance (Rust binary via WASM/native)
|
||||
- Connection pool to PostgreSQL (default: 5 connections per pool)
|
||||
- Event listeners and logging infrastructure
|
||||
|
||||
With 5 independent instances across 5 files, the app could hold up to 25 connections to PostgreSQL simultaneously (5 pools × 5 default connections). PostgreSQL has a hard limit (typically 100 connections), and Azure-hosted instances often have lower limits.
|
||||
|
||||
### Before (5 files, each with their own instance)
|
||||
|
||||
```ts
|
||||
// apikeys/page.tsx
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
// apikeys/tokenfuncs.ts
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
// api/traces/[trace_id]/save-modified-code/route.ts
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
// trace/[trace_id]/page.tsx
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
// lib/modified-code-utils.ts
|
||||
const prisma = new PrismaClient()
|
||||
```
|
||||
|
||||
### After (all use shared singleton)
|
||||
|
||||
```ts
|
||||
import { prisma } from "@/lib/prisma"
|
||||
```
|
||||
|
||||
The singleton at `src/lib/prisma.ts`:
|
||||
- Creates one PrismaClient with `connection_limit=10`, `pool_timeout=20`
|
||||
- Caches in `globalThis` during development (survives Next.js HMR reloads)
|
||||
- Logs slow queries (>500ms) in development
|
||||
- Forwards Prisma errors to Sentry
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/app/(dashboard)/apikeys/page.tsx` | `new PrismaClient()` → `import { prisma } from "@/lib/prisma"` |
|
||||
| `src/app/(dashboard)/apikeys/tokenfuncs.ts` | Same |
|
||||
| `src/app/api/traces/[trace_id]/save-modified-code/route.ts` | Same |
|
||||
| `src/app/trace/[trace_id]/page.tsx` | Same |
|
||||
| `src/lib/modified-code-utils.ts` | Same |
|
||||
|
||||
## How to Verify
|
||||
|
||||
```bash
|
||||
cd js/cf-webapp
|
||||
bash proof/reproducers/03-prisma-singleton.sh
|
||||
```
|
||||
|
||||
The reproducer:
|
||||
1. Greps the codebase for `new PrismaClient()` — should only appear in `src/lib/prisma.ts`
|
||||
2. Verifies all 5 previously-affected files import from `@/lib/prisma`
|
||||
3. Confirms the singleton has connection pooling configured
|
||||
4. Runs `tsc --noEmit` to verify types check clean
|
||||
|
||||
## Why This Is Real
|
||||
|
||||
1. **Each `new PrismaClient()` is a new connection pool** — this is documented Prisma behavior, not speculation. The Prisma query engine is a separate process that maintains its own PostgreSQL connections.
|
||||
2. **Connection pool exhaustion is a production risk** — with 5 pools × default 5 connections = 25 connections from a single Next.js process. Under serverless/edge deployments with multiple instances, this multiplies further.
|
||||
3. **The singleton pattern is Prisma's official recommendation** for Next.js — [Prisma docs: Best practice for instantiating PrismaClient](https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices).
|
||||
4. **`globalThis` caching prevents HMR leaks** — without it, each hot reload in development creates a new instance, eventually exhausting connections.
|
||||
153
js/cf-webapp/proof/reproducers/03-prisma-singleton.sh
Executable file
153
js/cf-webapp/proof/reproducers/03-prisma-singleton.sh
Executable file
|
|
@ -0,0 +1,153 @@
|
|||
#!/usr/bin/env bash
|
||||
# Reproducer: PrismaClient singleton verification
|
||||
#
|
||||
# Verifies:
|
||||
# 1. No `new PrismaClient()` outside src/lib/prisma.ts
|
||||
# 2. All previously-affected files import from @/lib/prisma
|
||||
# 3. The singleton configures connection pooling
|
||||
# 4. TypeScript compiles cleanly
|
||||
#
|
||||
# Usage:
|
||||
# cd js/cf-webapp
|
||||
# bash proof/reproducers/03-prisma-singleton.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
WEBAPP_DIR="$REPO_ROOT/js/cf-webapp"
|
||||
cd "$WEBAPP_DIR"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
check() {
|
||||
local label="$1"
|
||||
local result="$2"
|
||||
if [ "$result" = "pass" ]; then
|
||||
echo " PASS: $label"
|
||||
((PASS++))
|
||||
else
|
||||
echo " FAIL: $label"
|
||||
((FAIL++))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "================================================================"
|
||||
echo " Reproducer: PrismaClient Singleton"
|
||||
echo "================================================================"
|
||||
echo ""
|
||||
|
||||
# ── Check 1: No new PrismaClient() outside singleton ────────────────────────
|
||||
|
||||
echo "── Check 1: No 'new PrismaClient()' outside src/lib/prisma.ts ──"
|
||||
# Find all new PrismaClient() calls, excluding the singleton file and node_modules
|
||||
EXTRA_INSTANCES=$(grep -rn 'new PrismaClient' src/ --include='*.ts' --include='*.tsx' \
|
||||
| grep -v 'src/lib/prisma.ts' \
|
||||
| grep -v 'node_modules' \
|
||||
| grep -v '__tests__' \
|
||||
| grep -v '.test.' \
|
||||
|| true)
|
||||
|
||||
if [ -z "$EXTRA_INSTANCES" ]; then
|
||||
check "No PrismaClient instantiation outside singleton" "pass"
|
||||
else
|
||||
echo " Found extra PrismaClient instances:"
|
||||
echo "$EXTRA_INSTANCES"
|
||||
check "No PrismaClient instantiation outside singleton" "fail"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ── Check 2: Previously-affected files import from @/lib/prisma ──────────────
|
||||
|
||||
echo "── Check 2: Affected files import from @/lib/prisma ──"
|
||||
|
||||
AFFECTED_FILES=(
|
||||
"src/app/(dashboard)/apikeys/page.tsx"
|
||||
"src/app/(dashboard)/apikeys/tokenfuncs.ts"
|
||||
"src/app/api/traces/[trace_id]/save-modified-code/route.ts"
|
||||
"src/app/trace/[trace_id]/page.tsx"
|
||||
"src/lib/modified-code-utils.ts"
|
||||
)
|
||||
|
||||
for file in "${AFFECTED_FILES[@]}"; do
|
||||
if [ -f "$file" ]; then
|
||||
if grep -q '@/lib/prisma' "$file"; then
|
||||
check "$file imports from @/lib/prisma" "pass"
|
||||
else
|
||||
check "$file imports from @/lib/prisma" "fail"
|
||||
fi
|
||||
else
|
||||
echo " SKIP: $file not found"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
# ── Check 3: Singleton configures connection pooling ─────────────────────────
|
||||
|
||||
echo "── Check 3: Singleton has connection pooling ──"
|
||||
SINGLETON="src/lib/prisma.ts"
|
||||
|
||||
if grep -q 'connection_limit' "$SINGLETON"; then
|
||||
LIMIT=$(grep -oP 'connection_limit=\d+' "$SINGLETON")
|
||||
echo " Found: $LIMIT"
|
||||
check "Connection limit configured" "pass"
|
||||
else
|
||||
check "Connection limit configured" "fail"
|
||||
fi
|
||||
|
||||
if grep -q 'pool_timeout' "$SINGLETON"; then
|
||||
TIMEOUT=$(grep -oP 'pool_timeout=\d+' "$SINGLETON")
|
||||
echo " Found: $TIMEOUT"
|
||||
check "Pool timeout configured" "pass"
|
||||
else
|
||||
check "Pool timeout configured" "fail"
|
||||
fi
|
||||
|
||||
if grep -q 'globalForPrisma\|globalThis' "$SINGLETON"; then
|
||||
check "globalThis caching for HMR" "pass"
|
||||
else
|
||||
check "globalThis caching for HMR" "fail"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ── Check 4: Count total PrismaClient import sources ────────────────────────
|
||||
|
||||
echo "── Check 4: All Prisma imports use singleton ──"
|
||||
DIRECT_PRISMA_IMPORTS=$(grep -rn "from ['\"]@prisma/client['\"]" src/ --include='*.ts' --include='*.tsx' \
|
||||
| grep -v 'src/lib/prisma.ts' \
|
||||
| grep -v 'node_modules' \
|
||||
| grep -v '__tests__' \
|
||||
| grep -v '.test.' \
|
||||
|| true)
|
||||
|
||||
# Filter to only lines that import PrismaClient (type-only imports are fine)
|
||||
CONSTRUCTOR_IMPORTS=$(echo "$DIRECT_PRISMA_IMPORTS" | grep 'PrismaClient' | grep -v 'type ' | grep -v 'import type' || true)
|
||||
|
||||
if [ -z "$CONSTRUCTOR_IMPORTS" ]; then
|
||||
check "No direct PrismaClient constructor imports outside singleton" "pass"
|
||||
else
|
||||
echo " Direct PrismaClient imports found (non-type):"
|
||||
echo "$CONSTRUCTOR_IMPORTS"
|
||||
check "No direct PrismaClient constructor imports outside singleton" "fail"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ── Check 5: TypeScript compiles ─────────────────────────────────────────────
|
||||
|
||||
echo "── Check 5: TypeScript type check ──"
|
||||
if npx tsc --noEmit 2>&1; then
|
||||
check "TypeScript compiles cleanly" "pass"
|
||||
else
|
||||
check "TypeScript compiles cleanly" "fail"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# ── Summary ──────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "================================================================"
|
||||
echo " Results: $PASS passed, $FAIL failed"
|
||||
echo "================================================================"
|
||||
|
||||
if [ "$FAIL" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -4,11 +4,10 @@ import { auth0 } from "@/lib/auth0"
|
|||
import { CreateApiKeyDialog } from "./dialog-create-api-key"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { ApiKeyTable } from "./api-key-table"
|
||||
import { type cf_api_keys, PrismaClient } from "@prisma/client"
|
||||
import { type cf_api_keys } from "@prisma/client"
|
||||
import PostHogClient from "@/lib/posthog"
|
||||
import { VS_CODE_KEY_NAME } from "@codeflash-ai/common"
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
interface ApiKeyWithOrg extends cf_api_keys {
|
||||
organization?: {
|
||||
|
|
@ -41,10 +40,7 @@ export default async function APIKeyGenerator(): Promise<JSX.Element> {
|
|||
// Fetch personal keys (no organization) and keys from user's organizations
|
||||
const apiKeys: ApiKeyWithOrg[] = await prisma.cf_api_keys.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ user_id: userId, organization_id: null },
|
||||
{ organization_id: { in: userOrgIds } },
|
||||
],
|
||||
OR: [{ user_id: userId, organization_id: null }, { organization_id: { in: userOrgIds } }],
|
||||
},
|
||||
include: {
|
||||
organization: {
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ import {
|
|||
VS_CODE_KEY_NAME,
|
||||
} from "@codeflash-ai/common"
|
||||
import { TokenLimitExceededError } from "./token-error"
|
||||
import { PrismaClient } from "@prisma/client"
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
export async function generateToken(
|
||||
keyName: string,
|
||||
|
|
@ -29,7 +27,10 @@ export async function generateToken(
|
|||
if (error instanceof Error && error.message === "Token limit exceeded") {
|
||||
return { success: false, err: new TokenLimitExceededError().message, token: undefined }
|
||||
}
|
||||
if (error instanceof Error && error.message === "User is not a member of the specified organization") {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message === "User is not a member of the specified organization"
|
||||
) {
|
||||
return { success: false, err: error.message, token: undefined }
|
||||
}
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { PrismaClient } from "@prisma/client"
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
export async function POST(request: NextRequest, props: { params: Promise<{ trace_id: string }> }) {
|
||||
const params = await props.params;
|
||||
const params = await props.params
|
||||
try {
|
||||
const { trace_id } = params
|
||||
const body = await request.json()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { PrismaClient } from "@prisma/client"
|
||||
import { notFound } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import { ExperimentMetadata } from "@/lib/types" // Your defined types
|
||||
|
|
@ -6,13 +5,13 @@ import MonacoDiffViewer from "@/components/trace/monaco-diff-viewer"
|
|||
import { Metadata } from "next" // For Next.js metadata API
|
||||
import { auth0 } from "@/lib/auth0"
|
||||
import { isTeamMember } from "@/app/utils/auth"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
interface TraceDetailsPageProps {
|
||||
params: Promise<{
|
||||
trace_id: string
|
||||
}>
|
||||
}
|
||||
const prisma = new PrismaClient()
|
||||
// Function to generate dynamic metadata (e.g., page title)
|
||||
export async function generateMetadata(props: TraceDetailsPageProps): Promise<Metadata> {
|
||||
const params = await props.params
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { PrismaClient } from "@prisma/client"
|
||||
import { ExperimentMetadata } from "./types"
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
/**
|
||||
* Get the modified code for a trace, falling back to original optimized code if no modifications exist
|
||||
|
|
|
|||
Loading…
Reference in a new issue