From 6dba07baac6a3ef7d264c13e378a905002401e1b Mon Sep 17 00:00:00 2001 From: bipproduction Date: Tue, 3 Mar 2026 16:26:48 +0800 Subject: [PATCH] fix: prisma connection exhaustion & firebase lazy init - prisma/schema.prisma: tambah binaryTargets debian & linux-musl untuk Docker - src/lib/prisma.ts: pakai global singleton di dev & prod, hapus eager $connect() - src/lib/firebase-admin.ts: lazy initialization agar tidak crash saat build time - .env.example: lengkap dengan semua env variable + connection_limit & pool_timeout Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 53 ++++++++++++++++++ prisma/schema.prisma | 2 +- src/lib/firebase-admin.ts | 37 ++++++------- src/lib/prisma.ts | 110 +++----------------------------------- 4 files changed, 81 insertions(+), 121 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..ec0cd612 --- /dev/null +++ b/.env.example @@ -0,0 +1,53 @@ +# ============================== +# Database +# ============================== +# Tambahkan connection_limit dan pool_timeout untuk mencegah connection exhaustion +DATABASE_URL="postgresql://user:password@localhost:5432/dbname?connection_limit=10&pool_timeout=20" + +# ============================== +# Auth / Session +# ============================== +WIBU_PWD="your_wibu_password" +NEXT_PUBLIC_BASE_TOKEN_KEY="your_token_key" +NEXT_PUBLIC_BASE_SESSION_KEY="your_session_key" + +# ============================== +# Payment Gateway (Midtrans) +# ============================== +Client_KEY="your_midtrans_client_key" +Server_KEY="your_midtrans_server_key" + +# ============================== +# Maps +# ============================== +MAPBOX_TOKEN="your_mapbox_token" + +# ============================== +# Realtime (WebSocket) +# ============================== +WS_APIKEY="your_ws_api_key" +NEXT_PUBLIC_WIBU_REALTIME_TOKEN="your_realtime_token" + +# ============================== +# Email (Resend) +# ============================== +RESEND_APIKEY="your_resend_api_key" + +# ============================== +# WhatsApp +# ============================== +WA_SERVER_TOKEN="your_wa_server_token" + +# ============================== +# Firebase Admin (Push Notification) +# ============================== +FIREBASE_ADMIN_PROJECT_ID="your_firebase_project_id" +FIREBASE_ADMIN_CLIENT_EMAIL="your_firebase_client_email@project.iam.gserviceaccount.com" +# Private key: salin dari service account JSON, ganti newline dengan \n +FIREBASE_ADMIN_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY_HERE\n-----END PRIVATE KEY-----\n" + +# ============================== +# App +# ============================== +NEXT_PUBLIC_API_URL="http://localhost:3000" +LOG_LEVEL="info" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 81c3273d..23d70f4c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -4,7 +4,7 @@ generator client { provider = "prisma-client-js" engineType = "binary" - binaryTargets = ["native"] + binaryTargets = ["native", "debian-openssl-3.0.x", "linux-musl-openssl-3.0.x"] } datasource db { diff --git a/src/lib/firebase-admin.ts b/src/lib/firebase-admin.ts index 4811ff27..784a9f4b 100644 --- a/src/lib/firebase-admin.ts +++ b/src/lib/firebase-admin.ts @@ -1,24 +1,25 @@ // lib/firebase-admin.ts import { cert, getApp, getApps, initializeApp } from 'firebase-admin/app'; -import { getMessaging } from 'firebase-admin/messaging'; +import { getMessaging, Messaging } from 'firebase-admin/messaging'; -// Ambil dari environment -const serviceAccount = { - projectId: process.env.FIREBASE_ADMIN_PROJECT_ID, - clientEmail: process.env.FIREBASE_ADMIN_CLIENT_EMAIL, - privateKey: process.env.FIREBASE_ADMIN_PRIVATE_KEY?.replace(/\\n/g, '\n'), -}; +function getAdminApp() { + if (getApps().length > 0) return getApp(); -if (!serviceAccount.projectId || !serviceAccount.clientEmail || !serviceAccount.privateKey) { - throw new Error('Firebase Admin credentials are missing in environment variables'); + const privateKey = process.env.FIREBASE_ADMIN_PRIVATE_KEY?.replace(/\\n/g, '\n'); + const projectId = process.env.FIREBASE_ADMIN_PROJECT_ID; + const clientEmail = process.env.FIREBASE_ADMIN_CLIENT_EMAIL; + + if (!projectId || !clientEmail || !privateKey) { + throw new Error('Firebase Admin credentials are missing in environment variables'); + } + + return initializeApp({ + credential: cert({ projectId, clientEmail, privateKey }), + projectId, + }); } -// Inisialisasi hanya sekali -const app = !getApps().length - ? initializeApp({ - credential: cert(serviceAccount), - projectId: serviceAccount.projectId, - }) - : getApp(); - -export const adminMessaging = getMessaging(app); \ No newline at end of file +export const adminMessaging: Pick = { + send: (message) => getMessaging(getAdminApp()).send(message), + sendEachForMulticast: (message) => getMessaging(getAdminApp()).sendEachForMulticast(message), +}; diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 77d281ac..536b7e14 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -1,115 +1,21 @@ import { PrismaClient } from "@prisma/client"; -// Deklarasikan variabel global untuk menandai apakah listener sudah ditambahkan declare global { - var prisma: PrismaClient; - var prismaListenersAdded: boolean; // Flag untuk menandai listener + var prisma: PrismaClient | undefined; } -let prisma: PrismaClient; - -// Validasi DATABASE_URL sebelum inisialisasi if (!process.env.DATABASE_URL) { - console.error('❌ ERROR: DATABASE_URL tidak ditemukan di environment variables!'); - console.error(''); - console.error('Current working directory:', process.cwd()); - console.error('Available environment variables:', Object.keys(process.env).sort().join(', ')); - console.error(''); - console.error('Solusi:'); - console.error(' 1. Pastikan file .env ada dan berisi DATABASE_URL, ATAU'); - console.error(' 2. Set DATABASE_URL di system environment:'); - console.error(' export DATABASE_URL="postgresql://user:pass@host:port/dbname"'); - console.error(' 3. Jika menggunakan systemd service, tambahkan di file service:'); - console.error(' [Service]'); - console.error(' Environment=DATABASE_URL=postgresql://...'); - console.error(''); - throw new Error('DATABASE_URL is required but not found in environment variables'); + throw new Error("DATABASE_URL is required but not found in environment variables"); } -if (process.env.NODE_ENV === "production") { - prisma = new PrismaClient({ - // Reduce logging in production to improve performance - log: ['error', 'warn'], - datasources: { - db: { - url: process.env.DATABASE_URL, - }, - }, +const prisma = + global.prisma ?? + new PrismaClient({ + log: process.env.NODE_ENV === "development" ? ["error", "warn", "query"] : ["error", "warn"], }); - // Explicitly connect to database dengan retry - const maxRetries = 3; - let retryCount = 0; - - const connectWithRetry = async () => { - while (retryCount < maxRetries) { - try { - await prisma.$connect(); - console.log('✅ PostgreSQL connected successfully'); - return; - } catch (error) { - retryCount++; - const errorMsg = error instanceof Error ? error.message : 'Unknown error'; - console.error(`❌ PostgreSQL connection attempt ${retryCount}/${maxRetries} failed:`, errorMsg); - - if (retryCount >= maxRetries) { - console.error('❌ All database connection attempts failed. Application will continue but database operations will fail.'); - throw error; - } - - // Wait before retry (exponential backoff) - const waitTime = Math.min(1000 * Math.pow(2, retryCount), 10000); - console.log(`⏳ Retrying in ${waitTime}ms...`); - await new Promise(resolve => setTimeout(resolve, waitTime)); - } - } - }; - - // Initialize connection (non-blocking) - connectWithRetry().catch(err => { - console.error('Failed to initialize database connection:', err); - }); - -} else { - if (!global.prisma) { - global.prisma = new PrismaClient({ - log: ['error', 'warn', 'info', 'query'], // More verbose logging in development - datasources: { - db: { - url: process.env.DATABASE_URL, - }, - }, - }); - } - prisma = global.prisma; -} - -// Tambahkan listener hanya jika belum ditambahkan sebelumnya -if (!global.prismaListenersAdded) { - // Handle graceful shutdown - process.on("SIGINT", async () => { - console.log("Received SIGINT signal. Closing database connections..."); - await prisma.$disconnect(); - process.exit(0); - }); - - process.on("SIGTERM", async () => { - console.log("Received SIGTERM signal. Closing database connections..."); - await prisma.$disconnect(); - process.exit(0); - }); - - // Handle uncaught errors - process.on("uncaughtException", async (error) => { - if (error.message.includes("Prisma") || error.message.includes("database")) { - console.error("Uncaught database error:", error); - await prisma.$disconnect(); - } - }); - - // Tandai bahwa listener sudah ditambahkan - global.prismaListenersAdded = true; -} +// Selalu assign ke global agar hanya ada 1 instance (dev: cegah hot-reload, prod: cegah multiple instances) +global.prisma = prisma; export default prisma; export { prisma };