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 {
|
||||
provider = "prisma-client-js"
|
||||
engineType = "binary"
|
||||
binaryTargets = ["native"]
|
||||
binaryTargets = ["native", "debian-openssl-3.0.x", "linux-musl-openssl-3.0.x"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
|
||||
@@ -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);
|
||||
export const adminMessaging: Pick<Messaging, 'send' | 'sendEachForMulticast'> = {
|
||||
send: (message) => getMessaging(getAdminApp()).send(message),
|
||||
sendEachForMulticast: (message) => getMessaging(getAdminApp()).sendEachForMulticast(message),
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user