feat: make sure the partition table exsits.
All checks were successful
Deploy GeoPulse / Build And Deploy (push) Successful in 4s

This commit is contained in:
saingchildren 2026-05-09 13:04:44 +08:00
parent e91cc88ef2
commit a05d5a9050
3 changed files with 86 additions and 2 deletions

View File

@ -10,6 +10,7 @@ public class KafkaConsumer : BackgroundService
{
private readonly ILogger<KafkaConsumer> _logger;
private readonly NpgsqlDataSource _dataSource;
private readonly PartitionManager _partitionManager;
private readonly string _kafkaHost;
private readonly ElasticsearchClient _esClient;
@ -21,11 +22,13 @@ public class KafkaConsumer : BackgroundService
ILogger<KafkaConsumer> logger,
IConfiguration configuration,
NpgsqlDataSource dataSource,
PartitionManager partitionManager,
ElasticsearchClient esClient)
{
_logger = logger;
_kafkaHost = configuration["KafkaHost"];
_dataSource = dataSource;
_partitionManager = partitionManager;
_esClient = esClient;
}
@ -100,13 +103,28 @@ public class KafkaConsumer : BackgroundService
{
var jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
await using var batch = _dataSource.CreateBatch();
// 先收集所有不重複的日期並確保分區表存在
var uniqueDates = new HashSet<DateTimeOffset>();
var processedMessages = new List<(TelemetryRequest Data, ConsumeResult<Ignore, string> Msg)>();
foreach (var msg in buffer)
{
var data = JsonSerializer.Deserialize<TelemetryRequest>(msg.Message.Value, jsonOptions);
if (data == null) continue;
uniqueDates.Add(data.Timestamp);
processedMessages.Add((data, msg));
}
foreach (var timestamp in uniqueDates)
{
await _partitionManager.EnsurePartitionExistsAsync(timestamp);
}
await using var batch = _dataSource.CreateBatch();
foreach (var (data, msg) in processedMessages)
{
var cmd = batch.CreateBatchCommand();
cmd.CommandText =
@"INSERT INTO telemetry_history (id, device_id, geom, timestamp) VALUES (gen_random_uuid(), $1, ST_SetSRID(ST_MakePoint($2, $3), 4326), $4)";

View File

@ -0,0 +1,63 @@
using System.Collections.Concurrent;
using Npgsql;
namespace GeoPulse_Pipeline;
public class PartitionManager
{
private readonly NpgsqlDataSource _dataSource;
private readonly ConcurrentDictionary<string, bool> _knownPartitions = new();
private readonly ILogger<PartitionManager> _logger;
public PartitionManager(NpgsqlDataSource dataSource, ILogger<PartitionManager> logger)
{
_dataSource = dataSource;
_logger = logger;
}
public async Task EnsurePartitionExistsAsync(DateTimeOffset timestamp)
{
// According to user example, partitions are daily based on UTC+8
var localTime = timestamp.ToOffset(TimeSpan.FromHours(8));
var date = localTime.Date;
var partitionName = $"telemetry_history_y{date:yyyy}m{date:MM}d{date:dd}";
if (_knownPartitions.ContainsKey(partitionName))
{
return;
}
var startTime = new DateTimeOffset(date, TimeSpan.FromHours(8));
var endTime = startTime.AddDays(1);
// SQL to create partition if it doesn't exist
// We use a DO block to ensure atomicity and handle the OWNER change
var sql = $@"
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE tablename = '{partitionName}') THEN
CREATE TABLE public.{partitionName} PARTITION OF public.telemetry_history
FOR VALUES FROM ('{startTime:yyyy-MM-dd HH:mm:sszzz}') TO ('{endTime:yyyy-MM-dd HH:mm:sszzz}');
ALTER TABLE public.{partitionName} OWNER TO postgres;
END IF;
END $$;";
try
{
await using var conn = await _dataSource.OpenConnectionAsync();
await using var cmd = new NpgsqlCommand(sql, conn);
await cmd.ExecuteNonQueryAsync();
_knownPartitions.TryAdd(partitionName, true);
_logger.LogInformation("已確保分區表存在: {PartitionName}", partitionName);
}
catch (Exception ex)
{
_logger.LogError(ex, "建立分區表 {PartitionName} 時發生錯誤", partitionName);
// We don't throw here to avoid blocking the whole batch if one partition fails,
// but the subsequent insert will fail anyway if it's needed.
// Actually, it's better to let it throw so the caller knows.
throw;
}
}
}

View File

@ -44,6 +44,7 @@ builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHostedService<KafkaConsumer>();
builder.Services.AddNpgsqlDataSource(connString);
builder.Services.AddSingleton<PartitionManager>();
builder.Services.AddScoped<IDbConnection>(sp => sp.GetRequiredService<NpgsqlDataSource>().CreateConnection());
var app = builder.Build();
@ -61,6 +62,7 @@ app.MapPost("/api/telemetry", async (
[FromQuery] bool useKafka,
IProducer<Null, string> producer,
IDbConnection dbConnection,
PartitionManager partitionManager,
ILogger<Program> logger) =>
{
if (string.IsNullOrWhiteSpace(request.DeviceId))
@ -87,8 +89,9 @@ app.MapPost("/api/telemetry", async (
{
try
{
await partitionManager.EnsurePartitionExistsAsync(request.Timestamp);
string sql =
@"INSERT INTO telemetry_history (id, device_id, geom, timestamp) VALUES (gen_random_uuid(), @DeviceId, @Lng, @Lat, @Timestamp";
@"INSERT INTO telemetry_history (id, device_id, geom, timestamp) VALUES (gen_random_uuid(), @DeviceId, @Lng, @Lat, @Timestamp)";
await dbConnection.ExecuteAsync(sql, request);
return Results.Ok(new { Status = "Success", Route = "Direct-DB", Message = "已直接寫入資料庫" });