fix: resolve 8 SonarQube reliability bugs
- ElkDiagramRenderer: guard against null containingNode before getElkRoot()
- OpenSearchAdminController: return 503/502 instead of 200 on errors
- DatabaseAdminController: return 503 instead of 200 on connection failure
- SpaForwardController: replace unbound {path} variables with /** wildcards
- WriteBuffer: check offer() return value and log on unexpected rejection
- ApiExceptionHandler: extract getReason() to local var for null safety
- Admin UI pages: handle isError state for disconnected service display
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,8 @@ public class ApiExceptionHandler {
|
|||||||
|
|
||||||
@ExceptionHandler(ResponseStatusException.class)
|
@ExceptionHandler(ResponseStatusException.class)
|
||||||
public ResponseEntity<ErrorResponse> handleResponseStatus(ResponseStatusException ex) {
|
public ResponseEntity<ErrorResponse> handleResponseStatus(ResponseStatusException ex) {
|
||||||
|
String reason = ex.getReason();
|
||||||
return ResponseEntity.status(ex.getStatusCode())
|
return ResponseEntity.status(ex.getStatusCode())
|
||||||
.body(new ErrorResponse(ex.getReason() != null ? ex.getReason() : "Unknown error"));
|
.body(new ErrorResponse(reason != null ? reason : "Unknown error"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ public class DatabaseAdminController {
|
|||||||
String host = extractHost(dataSource);
|
String host = extractHost(dataSource);
|
||||||
return ResponseEntity.ok(new DatabaseStatusResponse(true, version, host, schema, timescaleDb));
|
return ResponseEntity.ok(new DatabaseStatusResponse(true, version, host, schema, timescaleDb));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return ResponseEntity.ok(new DatabaseStatusResponse(false, null, null, null, false));
|
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||||
|
.body(new DatabaseStatusResponse(false, null, null, null, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ public class OpenSearchAdminController {
|
|||||||
health.numberOfNodes(),
|
health.numberOfNodes(),
|
||||||
opensearchUrl));
|
opensearchUrl));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return ResponseEntity.ok(new OpenSearchStatusResponse(
|
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||||
|
.body(new OpenSearchStatusResponse(
|
||||||
false, "UNREACHABLE", null, 0, opensearchUrl));
|
false, "UNREACHABLE", null, 0, opensearchUrl));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,7 +150,8 @@ public class OpenSearchAdminController {
|
|||||||
pageItems, totalIndices, totalDocs,
|
pageItems, totalIndices, totalDocs,
|
||||||
humanSize(totalBytes), page, size, totalPages));
|
humanSize(totalBytes), page, size, totalPages));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return ResponseEntity.ok(new IndicesPageResponse(
|
return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
|
||||||
|
.body(new IndicesPageResponse(
|
||||||
List.of(), 0, 0, "0 B", page, size, 0));
|
List.of(), 0, 0, "0 B", page, size, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,7 +236,8 @@ public class OpenSearchAdminController {
|
|||||||
searchLatency, indexingLatency,
|
searchLatency, indexingLatency,
|
||||||
heapUsed, heapMax));
|
heapUsed, heapMax));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return ResponseEntity.ok(new PerformanceResponse(0, 0, 0, 0, 0, 0));
|
return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
|
||||||
|
.body(new PerformanceResponse(0, 0, 0, 0, 0, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -415,12 +415,13 @@ public class ElkDiagramRenderer implements DiagramRenderer {
|
|||||||
for (ElkEdge elkEdge : allEdges) {
|
for (ElkEdge elkEdge : allEdges) {
|
||||||
String sourceId = elkEdge.getSources().isEmpty() ? "" : elkEdge.getSources().get(0).getIdentifier();
|
String sourceId = elkEdge.getSources().isEmpty() ? "" : elkEdge.getSources().get(0).getIdentifier();
|
||||||
String targetId = elkEdge.getTargets().isEmpty() ? "" : elkEdge.getTargets().get(0).getIdentifier();
|
String targetId = elkEdge.getTargets().isEmpty() ? "" : elkEdge.getTargets().get(0).getIdentifier();
|
||||||
ElkNode edgeRoot = getElkRoot(elkEdge.getContainingNode());
|
ElkNode containingNode = elkEdge.getContainingNode();
|
||||||
|
ElkNode edgeRoot = containingNode != null ? getElkRoot(containingNode) : null;
|
||||||
|
|
||||||
List<double[]> points = new ArrayList<>();
|
List<double[]> points = new ArrayList<>();
|
||||||
for (ElkEdgeSection section : elkEdge.getSections()) {
|
for (ElkEdgeSection section : elkEdge.getSections()) {
|
||||||
double cx = getAbsoluteX(elkEdge.getContainingNode(), edgeRoot);
|
double cx = containingNode != null ? getAbsoluteX(containingNode, edgeRoot) : 0;
|
||||||
double cy = getAbsoluteY(elkEdge.getContainingNode(), edgeRoot);
|
double cy = containingNode != null ? getAbsoluteY(containingNode, edgeRoot) : 0;
|
||||||
points.add(new double[]{section.getStartX() + cx, section.getStartY() + cy});
|
points.add(new double[]{section.getStartX() + cx, section.getStartY() + cy});
|
||||||
for (ElkBendPoint bp : section.getBendPoints()) {
|
for (ElkBendPoint bp : section.getBendPoints()) {
|
||||||
points.add(new double[]{bp.getX() + cx, bp.getY() + cy});
|
points.add(new double[]{bp.getX() + cx, bp.getY() + cy});
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ public class SpaForwardController {
|
|||||||
@GetMapping(value = {
|
@GetMapping(value = {
|
||||||
"/login",
|
"/login",
|
||||||
"/executions",
|
"/executions",
|
||||||
"/executions/{path:[^\\.]*}",
|
"/executions/**",
|
||||||
"/oidc/callback",
|
"/oidc/callback",
|
||||||
"/admin/{path:[^\\.]*}"
|
"/admin/**"
|
||||||
})
|
})
|
||||||
public String forward() {
|
public String forward() {
|
||||||
return "forward:/index.html";
|
return "forward:/index.html";
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.cameleer3.server.core.ingestion;
|
package com.cameleer3.server.core.ingestion;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ArrayBlockingQueue;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
@@ -16,6 +19,8 @@ import java.util.concurrent.BlockingQueue;
|
|||||||
*/
|
*/
|
||||||
public class WriteBuffer<T> {
|
public class WriteBuffer<T> {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(WriteBuffer.class);
|
||||||
|
|
||||||
private final BlockingQueue<T> queue;
|
private final BlockingQueue<T> queue;
|
||||||
private final int capacity;
|
private final int capacity;
|
||||||
|
|
||||||
@@ -45,7 +50,10 @@ public class WriteBuffer<T> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (T item : items) {
|
for (T item : items) {
|
||||||
queue.offer(item);
|
if (!queue.offer(item)) {
|
||||||
|
log.warn("WriteBuffer offer rejected despite capacity check — possible concurrent modification");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import type { Column } from '@cameleer/design-system';
|
|||||||
import { useDatabaseStatus, useConnectionPool, useDatabaseTables, useActiveQueries, useKillQuery } from '../../api/queries/admin/database';
|
import { useDatabaseStatus, useConnectionPool, useDatabaseTables, useActiveQueries, useKillQuery } from '../../api/queries/admin/database';
|
||||||
|
|
||||||
export default function DatabaseAdminPage() {
|
export default function DatabaseAdminPage() {
|
||||||
const { data: status } = useDatabaseStatus();
|
const { data: status, isError: statusError } = useDatabaseStatus();
|
||||||
|
const unreachable = statusError || (status && !status.connected);
|
||||||
const { data: pool } = useConnectionPool();
|
const { data: pool } = useConnectionPool();
|
||||||
const { data: tables } = useDatabaseTables();
|
const { data: tables } = useDatabaseTables();
|
||||||
const { data: queries } = useActiveQueries();
|
const { data: queries } = useActiveQueries();
|
||||||
@@ -34,7 +35,7 @@ export default function DatabaseAdminPage() {
|
|||||||
<h2 style={{ marginBottom: '1rem' }}>Database Administration</h2>
|
<h2 style={{ marginBottom: '1rem' }}>Database Administration</h2>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
|
||||||
<StatCard label="Status" value={status?.connected ? 'Connected' : 'Disconnected'} accent={status?.connected ? 'success' : 'error'} />
|
<StatCard label="Status" value={unreachable ? 'Disconnected' : status ? 'Connected' : '\u2014'} accent={unreachable ? 'error' : status ? 'success' : undefined} />
|
||||||
<StatCard label="Version" value={status?.version ?? '—'} />
|
<StatCard label="Version" value={status?.version ?? '—'} />
|
||||||
<StatCard label="TimescaleDB" value={status?.timescaleDb ? 'Enabled' : 'Disabled'} />
|
<StatCard label="TimescaleDB" value={status?.timescaleDb ? 'Enabled' : 'Disabled'} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import { useOpenSearchStatus, usePipelineStats, useOpenSearchIndices, useOpenSea
|
|||||||
import styles from './OpenSearchAdminPage.module.css';
|
import styles from './OpenSearchAdminPage.module.css';
|
||||||
|
|
||||||
export default function OpenSearchAdminPage() {
|
export default function OpenSearchAdminPage() {
|
||||||
const { data: status } = useOpenSearchStatus();
|
const { data: status, isError: statusError } = useOpenSearchStatus();
|
||||||
const { data: pipeline } = usePipelineStats();
|
const { data: pipeline } = usePipelineStats();
|
||||||
const { data: perf } = useOpenSearchPerformance();
|
const { data: perf } = useOpenSearchPerformance();
|
||||||
const { data: execIndices } = useOpenSearchIndices(0, 50, '', 'executions');
|
const { data: execIndices } = useOpenSearchIndices(0, 50, '', 'executions');
|
||||||
const { data: logIndices } = useOpenSearchIndices(0, 50, '', 'logs');
|
const { data: logIndices } = useOpenSearchIndices(0, 50, '', 'logs');
|
||||||
|
const unreachable = statusError || (status && !status.reachable);
|
||||||
const deleteIndex = useDeleteIndex();
|
const deleteIndex = useDeleteIndex();
|
||||||
|
|
||||||
const indexColumns: Column<any>[] = [
|
const indexColumns: Column<any>[] = [
|
||||||
@@ -22,7 +23,7 @@ export default function OpenSearchAdminPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.statStrip}>
|
<div className={styles.statStrip}>
|
||||||
<StatCard label="Status" value={status?.reachable ? 'Connected' : 'Disconnected'} accent={status?.reachable ? 'success' : 'error'} />
|
<StatCard label="Status" value={unreachable ? 'Disconnected' : status ? 'Connected' : '\u2014'} accent={unreachable ? 'error' : status ? 'success' : undefined} />
|
||||||
<StatCard label="Health" value={status?.clusterHealth ?? '\u2014'} accent={status?.clusterHealth === 'green' ? 'success' : 'warning'} />
|
<StatCard label="Health" value={status?.clusterHealth ?? '\u2014'} accent={status?.clusterHealth === 'green' ? 'success' : 'warning'} />
|
||||||
<StatCard label="Version" value={status?.version ?? '\u2014'} />
|
<StatCard label="Version" value={status?.version ?? '\u2014'} />
|
||||||
<StatCard label="Nodes" value={status?.nodeCount ?? 0} />
|
<StatCard label="Nodes" value={status?.nodeCount ?? 0} />
|
||||||
|
|||||||
Reference in New Issue
Block a user