bug-prisma 3 #65

Merged
bagasbanuna merged 3 commits from bug-prisma/3-mar-26 into staging 2026-03-03 15:36:44 +08:00
7 changed files with 254 additions and 13 deletions

View File

@@ -2,6 +2,13 @@
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [1.6.5](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.4...v1.6.5) (2026-03-03)
### Bug Fixes
* error koneksi Prisma - DATABASE_URL tidak loaded di ([a658881](https://wibugit.wibudev.com/wibu/hipmi/commit/a6588818b5d8018b3a634e0ae0846e309569d370))
## [1.6.4](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.3...v1.6.4) (2026-03-03)
## [1.6.3](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.2...v1.6.3) (2026-03-03)

View File

@@ -1,6 +1,6 @@
{
"name": "hipmi",
"version": "1.6.4",
"version": "1.6.5",
"private": true,
"prisma": {
"seed": "bun prisma/seed.ts"

View File

@@ -57,8 +57,4 @@ if (fs.existsSync(envLocalSrc)) {
console.log('✓ .env-local file copied to standalone output');
}
console.log('✅ Postbuild script completed!');
console.log('');
console.log('📋 Penting untuk Production:');
console.log(' - Pastikan DATABASE_URL tersedia di environment server, ATAU');
console.log(' - File .env sudah ter-copy ke /app/.env di server');
console.log('✅ Build script completed!');

View File

@@ -76,15 +76,27 @@ export async function GET(req: Request) {
data: user,
});
} catch (error) {
console.error("Error in user validation:", error);
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
const errorStack = error instanceof Error ? error.stack : 'No stack';
// Log detailed error for debugging
console.error("❌ [USER-VALIDATE] Error:", errorMsg);
console.error("❌ [USER-VALIDATE] Stack:", errorStack);
console.error("❌ [USER-VALIDATE] Time:", new Date().toISOString());
// Check if it's a database connection error
if (errorMsg.includes("Prisma") || errorMsg.includes("database") || errorMsg.includes("connection")) {
console.error("❌ [USER-VALIDATE] Database connection error detected!");
console.error("❌ [USER-VALIDATE] DATABASE_URL exists:", !!process.env.DATABASE_URL);
}
return NextResponse.json(
{
success: false,
message: "Terjadi kesalahan pada server",
error: process.env.NODE_ENV === 'development' ? errorMsg : 'Internal server error',
},
{ status: 500 }
);
}
// Removed prisma.$disconnect() from here to prevent connection pool exhaustion
// Prisma connections are handled globally and shouldn't be disconnected on each request
}

188
src/lib/prisma-retry.ts Normal file
View File

@@ -0,0 +1,188 @@
import { prisma } from './prisma';
/**
* Retry configuration for database operations
*/
interface RetryConfig {
maxRetries: number;
initialDelay: number;
maxDelay: number;
factor: number;
}
const DEFAULT_RETRY_CONFIG: RetryConfig = {
maxRetries: 3,
initialDelay: 100,
maxDelay: 5000,
factor: 2,
};
/**
* Check if error is retryable (transient error)
*/
function isRetryableError(error: any): boolean {
const errorMsg = error instanceof Error ? error.message : '';
// Retry on connection-related errors
const retryablePatterns = [
'ECONNRESET',
'ECONNREFUSED',
'ETIMEDOUT',
'ENOTFOUND',
'connection closed',
'connection terminated',
'connection timeout',
'socket hang up',
'too many connections',
'pool is full',
'server login has been failing',
'FATAL:',
'PrismaClientUnknownRequestError',
];
return retryablePatterns.some(pattern =>
errorMsg.toLowerCase().includes(pattern.toLowerCase())
);
}
/**
* Execute database operation with retry mechanism
*
* @param operation - The database operation to execute
* @param config - Retry configuration (optional)
* @param operationName - Name of the operation for logging
*
* @example
* const user = await withRetry(
* () => prisma.user.findUnique({ where: { id: '123' } }),
* undefined,
* 'findUser'
* );
*/
export async function withRetry<T>(
operation: () => Promise<T>,
config?: Partial<RetryConfig>,
operationName?: string
): Promise<T> {
const retryConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
let lastError: any;
for (let attempt = 1; attempt <= retryConfig.maxRetries; attempt++) {
try {
const result = await operation();
// Log success if it was a retry
if (attempt > 1 && operationName) {
console.log(`✅ [DB-RETRY] ${operationName} succeeded after ${attempt} attempts`);
}
return result;
} catch (error) {
lastError = error;
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
// Check if we should retry
if (attempt < retryConfig.maxRetries && isRetryableError(error)) {
// Calculate delay with exponential backoff + jitter
const delay = Math.min(
retryConfig.initialDelay * Math.pow(retryConfig.factor, attempt - 1),
retryConfig.maxDelay
);
const jitter = Math.random() * 0.3 * delay; // Add 30% jitter
if (operationName) {
console.warn(
`⚠️ [DB-RETRY] ${operationName} failed (attempt ${attempt}/${retryConfig.maxRetries}): ${errorMsg}`
);
console.log(`⏳ [DB-RETRY] Retrying in ${Math.round(delay + jitter)}ms...`);
}
await new Promise(resolve => setTimeout(resolve, delay + jitter));
} else {
// Don't retry - either max retries reached or not a retryable error
if (operationName) {
console.error(
`❌ [DB-RETRY] ${operationName} failed after ${attempt} attempts: ${errorMsg}`
);
}
break;
}
}
}
// All retries exhausted, throw the last error
throw lastError;
}
/**
* Execute database operation with timeout
*
* @param operation - The database operation to execute
* @param timeout - Timeout in milliseconds (default: 30000)
* @param operationName - Name of the operation for logging
*/
export async function withTimeout<T>(
operation: () => Promise<T>,
timeout: number = 30000,
operationName?: string
): Promise<T> {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error(`Operation timed out after ${timeout}ms`));
}, timeout);
});
try {
return await Promise.race([operation(), timeoutPromise]);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
if (errorMsg.includes('timed out')) {
if (operationName) {
console.error(`⏱️ [DB-TIMEOUT] ${operationName} timed out after ${timeout}ms`);
}
}
throw error;
}
}
/**
* Combine retry and timeout for robust database operations
*
* @param operation - The database operation to execute
* @param options - Retry and timeout options
* @param operationName - Name of the operation for logging
*/
export async function withRetryAndTimeout<T>(
operation: () => Promise<T>,
options?: {
retry?: Partial<RetryConfig>;
timeout?: number;
},
operationName?: string
): Promise<T> {
return withRetry(
() => withTimeout(operation, options?.timeout, operationName),
options?.retry,
operationName
);
}
/**
* Health check for database connection
*/
export async function checkDatabaseConnection(): Promise<boolean> {
try {
await withTimeout(
() => prisma.$queryRaw`SELECT 1`,
5000,
'healthCheck'
);
return true;
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
console.error('❌ [DB-HEALTH] Database connection check failed:', errorMsg);
return false;
}
}
export { prisma };

View File

@@ -36,6 +36,40 @@ if (process.env.NODE_ENV === "production") {
},
},
});
// 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({
@@ -65,6 +99,14 @@ if (!global.prismaListenersAdded) {
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;
}

View File

@@ -66,10 +66,6 @@ export const middleware = async (req: NextRequest) => {
const { pathname } = req.nextUrl;
const apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || new URL(req.url).origin;
// Removed excessive logging that was causing high CPU usage
// const dbUrl = process.env.DATABASE_URL;
// console.log("DATABASE_URL >>", dbUrl);
// console.log("URL Access >>", req.url);
// Handle CORS preflight
const corsResponse = handleCors(req);