bug-prisma 3 #65
@@ -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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hipmi",
|
||||
"version": "1.6.4",
|
||||
"version": "1.6.5",
|
||||
"private": true,
|
||||
"prisma": {
|
||||
"seed": "bun prisma/seed.ts"
|
||||
|
||||
@@ -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!');
|
||||
@@ -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
188
src/lib/prisma-retry.ts
Normal 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 };
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user