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 <noreply@anthropic.com>
This commit is contained in:
53
.env.example
Normal file
53
.env.example
Normal file
@@ -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"
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
engineType = "binary"
|
engineType = "binary"
|
||||||
binaryTargets = ["native"]
|
binaryTargets = ["native", "debian-openssl-3.0.x", "linux-musl-openssl-3.0.x"]
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
// lib/firebase-admin.ts
|
// lib/firebase-admin.ts
|
||||||
import { cert, getApp, getApps, initializeApp } from 'firebase-admin/app';
|
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
|
function getAdminApp() {
|
||||||
const serviceAccount = {
|
if (getApps().length > 0) return getApp();
|
||||||
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'),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!serviceAccount.projectId || !serviceAccount.clientEmail || !serviceAccount.privateKey) {
|
const privateKey = process.env.FIREBASE_ADMIN_PRIVATE_KEY?.replace(/\\n/g, '\n');
|
||||||
throw new Error('Firebase Admin credentials are missing in environment variables');
|
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
|
export const adminMessaging: Pick<Messaging, 'send' | 'sendEachForMulticast'> = {
|
||||||
const app = !getApps().length
|
send: (message) => getMessaging(getAdminApp()).send(message),
|
||||||
? initializeApp({
|
sendEachForMulticast: (message) => getMessaging(getAdminApp()).sendEachForMulticast(message),
|
||||||
credential: cert(serviceAccount),
|
};
|
||||||
projectId: serviceAccount.projectId,
|
|
||||||
})
|
|
||||||
: getApp();
|
|
||||||
|
|
||||||
export const adminMessaging = getMessaging(app);
|
|
||||||
|
|||||||
@@ -1,115 +1,21 @@
|
|||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
// Deklarasikan variabel global untuk menandai apakah listener sudah ditambahkan
|
|
||||||
declare global {
|
declare global {
|
||||||
var prisma: PrismaClient;
|
var prisma: PrismaClient | undefined;
|
||||||
var prismaListenersAdded: boolean; // Flag untuk menandai listener
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let prisma: PrismaClient;
|
|
||||||
|
|
||||||
// Validasi DATABASE_URL sebelum inisialisasi
|
|
||||||
if (!process.env.DATABASE_URL) {
|
if (!process.env.DATABASE_URL) {
|
||||||
console.error('❌ ERROR: DATABASE_URL tidak ditemukan di environment variables!');
|
throw new Error("DATABASE_URL is required but not found in 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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "production") {
|
const prisma =
|
||||||
prisma = new PrismaClient({
|
global.prisma ??
|
||||||
// Reduce logging in production to improve performance
|
new PrismaClient({
|
||||||
log: ['error', 'warn'],
|
log: process.env.NODE_ENV === "development" ? ["error", "warn", "query"] : ["error", "warn"],
|
||||||
datasources: {
|
|
||||||
db: {
|
|
||||||
url: process.env.DATABASE_URL,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Explicitly connect to database dengan retry
|
// Selalu assign ke global agar hanya ada 1 instance (dev: cegah hot-reload, prod: cegah multiple instances)
|
||||||
const maxRetries = 3;
|
global.prisma = prisma;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default prisma;
|
export default prisma;
|
||||||
export { prisma };
|
export { prisma };
|
||||||
|
|||||||
Reference in New Issue
Block a user