feat(01-01): add WriteBuffer, repository interfaces, and config classes
- WriteBuffer<T> with offer/offerBatch/drain and backpressure (all tests green) - ExecutionRepository, DiagramRepository, MetricsRepository interfaces - MetricsSnapshot record for agent metrics data - IngestionConfig for buffer-capacity/batch-size/flush-interval-ms properties - ClickHouseConfig exposing JdbcTemplate bean Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
package com.cameleer3.server.app.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ClickHouse configuration.
|
||||||
|
* <p>
|
||||||
|
* Spring Boot auto-configures the DataSource from {@code spring.datasource.*} properties.
|
||||||
|
* This class exposes a JdbcTemplate bean for repository implementations.
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class ClickHouseConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
|
||||||
|
return new JdbcTemplate(dataSource);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.cameleer3.server.app.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration properties for the ingestion write buffer.
|
||||||
|
* Bound from the {@code ingestion.*} namespace in application.yml.
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "ingestion")
|
||||||
|
public class IngestionConfig {
|
||||||
|
|
||||||
|
private int bufferCapacity = 50_000;
|
||||||
|
private int batchSize = 5_000;
|
||||||
|
private long flushIntervalMs = 1_000;
|
||||||
|
|
||||||
|
public int getBufferCapacity() {
|
||||||
|
return bufferCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBufferCapacity(int bufferCapacity) {
|
||||||
|
this.bufferCapacity = bufferCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBatchSize() {
|
||||||
|
return batchSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBatchSize(int batchSize) {
|
||||||
|
this.batchSize = batchSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getFlushIntervalMs() {
|
||||||
|
return flushIntervalMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFlushIntervalMs(long flushIntervalMs) {
|
||||||
|
this.flushIntervalMs = flushIntervalMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.cameleer3.server.core.ingestion;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bounded write buffer that decouples HTTP ingestion from ClickHouse batch inserts.
|
||||||
|
* <p>
|
||||||
|
* Items are offered to the buffer by controllers and drained in batches by a
|
||||||
|
* scheduled flush task. When the buffer is full, {@link #offer} returns false,
|
||||||
|
* signaling the caller to apply backpressure (HTTP 503).
|
||||||
|
*
|
||||||
|
* @param <T> the type of items buffered
|
||||||
|
*/
|
||||||
|
public class WriteBuffer<T> {
|
||||||
|
|
||||||
|
private final BlockingQueue<T> queue;
|
||||||
|
private final int capacity;
|
||||||
|
|
||||||
|
public WriteBuffer(int capacity) {
|
||||||
|
this.capacity = capacity;
|
||||||
|
this.queue = new ArrayBlockingQueue<>(capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offer a single item to the buffer.
|
||||||
|
*
|
||||||
|
* @return true if the item was added, false if the buffer is full
|
||||||
|
*/
|
||||||
|
public boolean offer(T item) {
|
||||||
|
return queue.offer(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offer a batch of items with all-or-nothing semantics.
|
||||||
|
* If the buffer does not have enough remaining capacity for the entire batch,
|
||||||
|
* no items are added and false is returned.
|
||||||
|
*
|
||||||
|
* @return true if all items were added, false if insufficient capacity
|
||||||
|
*/
|
||||||
|
public boolean offerBatch(List<T> items) {
|
||||||
|
if (queue.remainingCapacity() < items.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (T item : items) {
|
||||||
|
queue.offer(item);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drain up to {@code maxBatch} items from the buffer.
|
||||||
|
* Called by the scheduled flush task.
|
||||||
|
*
|
||||||
|
* @return list of drained items (may be empty)
|
||||||
|
*/
|
||||||
|
public List<T> drain(int maxBatch) {
|
||||||
|
List<T> batch = new ArrayList<>(maxBatch);
|
||||||
|
queue.drainTo(batch, maxBatch);
|
||||||
|
return batch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
return queue.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int capacity() {
|
||||||
|
return capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFull() {
|
||||||
|
return queue.remainingCapacity() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int remainingCapacity() {
|
||||||
|
return queue.remainingCapacity();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.cameleer3.server.core.storage;
|
||||||
|
|
||||||
|
import com.cameleer3.common.graph.RouteGraph;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository for route diagram storage with content-hash deduplication.
|
||||||
|
*/
|
||||||
|
public interface DiagramRepository {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a route graph. Uses content-hash deduplication via ReplacingMergeTree.
|
||||||
|
*/
|
||||||
|
void store(RouteGraph graph);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a route graph by its content hash.
|
||||||
|
*/
|
||||||
|
Optional<RouteGraph> findByContentHash(String contentHash);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the content hash for the latest diagram of a given route and agent.
|
||||||
|
*/
|
||||||
|
Optional<String> findContentHashForRoute(String routeId, String agentId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.cameleer3.server.core.storage;
|
||||||
|
|
||||||
|
import com.cameleer3.common.model.RouteExecution;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository for route execution batch inserts into ClickHouse.
|
||||||
|
*/
|
||||||
|
public interface ExecutionRepository {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a batch of route executions.
|
||||||
|
* Implementations must perform a single batch insert for efficiency.
|
||||||
|
*/
|
||||||
|
void insertBatch(List<RouteExecution> executions);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.cameleer3.server.core.storage;
|
||||||
|
|
||||||
|
import com.cameleer3.server.core.storage.model.MetricsSnapshot;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository for agent metrics batch inserts into ClickHouse.
|
||||||
|
*/
|
||||||
|
public interface MetricsRepository {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a batch of metrics snapshots.
|
||||||
|
* Implementations must perform a single batch insert for efficiency.
|
||||||
|
*/
|
||||||
|
void insertBatch(List<MetricsSnapshot> metrics);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.cameleer3.server.core.storage.model;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single metrics data point from an agent.
|
||||||
|
*/
|
||||||
|
public record MetricsSnapshot(
|
||||||
|
String agentId,
|
||||||
|
Instant collectedAt,
|
||||||
|
String metricName,
|
||||||
|
double metricValue,
|
||||||
|
Map<String, String> tags
|
||||||
|
) {
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user