Merge pull request 'bug-prisma 3' (#65) from bug-prisma/3-mar-26 into staging
Reviewed-on: #65
This commit is contained in:
@@ -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.
|
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.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.3](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.2...v1.6.3) (2026-03-03)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hipmi",
|
"name": "hipmi",
|
||||||
"version": "1.6.4",
|
"version": "1.6.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"seed": "bun prisma/seed.ts"
|
"seed": "bun prisma/seed.ts"
|
||||||
|
|||||||
@@ -57,8 +57,4 @@ if (fs.existsSync(envLocalSrc)) {
|
|||||||
console.log('✓ .env-local file copied to standalone output');
|
console.log('✓ .env-local file copied to standalone output');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ Postbuild script completed!');
|
console.log('✅ Build 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');
|
|
||||||
@@ -76,15 +76,27 @@ export async function GET(req: Request) {
|
|||||||
data: user,
|
data: user,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
message: "Terjadi kesalahan pada server",
|
message: "Terjadi kesalahan pada server",
|
||||||
|
error: process.env.NODE_ENV === 'development' ? errorMsg : 'Internal server error',
|
||||||
},
|
},
|
||||||
{ status: 500 }
|
{ 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 {
|
} else {
|
||||||
if (!global.prisma) {
|
if (!global.prisma) {
|
||||||
global.prisma = new PrismaClient({
|
global.prisma = new PrismaClient({
|
||||||
@@ -65,6 +99,14 @@ if (!global.prismaListenersAdded) {
|
|||||||
process.exit(0);
|
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
|
// Tandai bahwa listener sudah ditambahkan
|
||||||
global.prismaListenersAdded = true;
|
global.prismaListenersAdded = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,10 +66,6 @@ export const middleware = async (req: NextRequest) => {
|
|||||||
const { pathname } = req.nextUrl;
|
const { pathname } = req.nextUrl;
|
||||||
|
|
||||||
const apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || new URL(req.url).origin;
|
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
|
// Handle CORS preflight
|
||||||
const corsResponse = handleCors(req);
|
const corsResponse = handleCors(req);
|
||||||
|
|||||||
Reference in New Issue
Block a user