Skip to content

Commit 2efd323

Browse files
authored
Merge pull request #766 from sipcapture/fix/drain-inlined-data-without-compaction
fix(ducklake): drain inlined data even when compaction is disabled
2 parents 924227c + c48ce22 commit 2efd323

4 files changed

Lines changed: 88 additions & 22 deletions

File tree

src/config/config.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -652,9 +652,12 @@ type VolumeConfig struct {
652652
// CompactionConfig configures automatic compaction and retention
653653
type CompactionConfig struct {
654654
// Enable turns on periodic compaction on the writer DuckLake catalog.
655-
// When storage_policy has multiple volumes and at least one local volume,
656-
// the writer forces compaction on regardless of this flag (hot parquet).
657-
Enable bool `json:"enable" mapstructure:"enable" default:"false"`
655+
// On by default: small per-flush Parquet files and DuckLake snapshots
656+
// accumulate quickly, so without periodic merge/expire/cleanup the catalog
657+
// and file count grow unbounded. When storage_policy has multiple volumes
658+
// and at least one local volume, the writer forces compaction on regardless
659+
// of this flag (hot parquet). Set to false only to opt out explicitly.
660+
Enable bool `json:"enable" mapstructure:"enable" default:"true"`
658661
CheckIntervalSec int `json:"check_interval_sec" mapstructure:"check_interval_sec" default:"3600"` // 1 hour
659662
RetentionDays int `json:"retention_days" mapstructure:"retention_days" default:"0"` // 0 = disabled
660663
// SnapshotExpireIntervalSec controls how long to keep DuckLake snapshots (seconds).

src/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import (
2424
// Version information for homer-core
2525
var (
2626
// VERSION_APPLICATION is the application version
27-
VERSION_APPLICATION = "11.0.232"
27+
VERSION_APPLICATION = "11.0.234"
2828

2929
// BuildDate is the build date
3030
BuildDate = ""

src/writer/compaction.go

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,16 @@ func (c *CompactionService) warnMaintenanceS3Failure(op string, err error) {
204204
// Start begins the compaction service
205205
func (c *CompactionService) Start() error {
206206
if !c.config.Enable {
207-
logger.Info("CompactionService disabled")
207+
// Compaction (merge/expire/cleanup) is off, but inlined data must
208+
// still be drained. Disabling data inlining only stops NEW inlining;
209+
// rows inlined earlier (e.g. a catalog created before inlining was
210+
// disabled) stay in the catalog and resident in the DuckLake
211+
// extension's memory until flushed. Without this, an upgraded node
212+
// with compaction off would never release its legacy inline backlog.
213+
logger.Info("CompactionService: compaction disabled; starting inline-flush-only maintenance",
214+
"interval", fmt.Sprintf("%ds", c.config.CheckIntervalSec))
215+
c.wg.Add(1)
216+
go c.inlineFlushOnlyLoop()
208217
return nil
209218
}
210219

@@ -301,6 +310,67 @@ func (c *CompactionService) withCatalogLock(fn func()) {
301310
fn()
302311
}
303312

313+
// flushInlinedData drains DuckLake inlined rows into Parquet and drops the
314+
// backing ducklake_inlined_data_* tables. Disabling data inlining only stops
315+
// NEW inlining; rows inlined earlier stay in the catalog and resident in the
316+
// DuckLake extension's memory until flushed. Cheap no-op once nothing is
317+
// inlined. Shared by the full compaction cycle (step 0) and the flush-only
318+
// loop used when compaction is disabled.
319+
func (c *CompactionService) flushInlinedData() {
320+
c.withCatalogLock(func() {
321+
c.ensureS3ClientSettings()
322+
logger.Info("CompactionService: Flush inlined data", "lake", c.lakeName)
323+
flushSQL := fmt.Sprintf("CALL ducklake_flush_inlined_data('%s')", c.lakeName)
324+
if _, err := c.execWithRetry(flushSQL); err != nil {
325+
logger.Warn("CompactionService: flush_inlined_data failed", "error", err)
326+
}
327+
})
328+
}
329+
330+
// inlineFlushOnlyLoop periodically drains inlined data when full compaction is
331+
// disabled, so an upgraded node with compaction off still releases its legacy
332+
// inline backlog from the DuckLake extension's memory. Paced by
333+
// CheckIntervalSec; the flush is a no-op once nothing is inlined.
334+
func (c *CompactionService) inlineFlushOnlyLoop() {
335+
defer c.wg.Done()
336+
defer func() {
337+
if r := recover(); r != nil {
338+
logger.Error("CompactionService: inline-flush loop panic", "panic", r)
339+
}
340+
}()
341+
342+
interval := time.Duration(c.config.CheckIntervalSec) * time.Second
343+
if interval <= 0 {
344+
interval = time.Hour
345+
}
346+
347+
// First run shortly after startup so a legacy backlog drains promptly
348+
// instead of waiting a full interval.
349+
firstDelay := 1 * time.Minute
350+
if interval < firstDelay {
351+
firstDelay = interval
352+
}
353+
timer := time.NewTimer(firstDelay)
354+
select {
355+
case <-c.ctx.Done():
356+
timer.Stop()
357+
return
358+
case <-timer.C:
359+
c.flushInlinedData()
360+
}
361+
362+
ticker := time.NewTicker(interval)
363+
defer ticker.Stop()
364+
for {
365+
select {
366+
case <-c.ctx.Done():
367+
return
368+
case <-ticker.C:
369+
c.flushInlinedData()
370+
}
371+
}
372+
}
373+
304374
// runCompaction performs a single compaction cycle.
305375
// Instead of holding the catalog lock for the entire cycle (which can take
306376
// minutes and block flush + queries), the lock is acquired and released
@@ -424,21 +494,10 @@ func (c *CompactionService) runMerge(tables []string) error {
424494
logger.Info("CompactionService: Active snapshots before merge", "count", snapshotCount)
425495
}
426496

427-
// 0. Flush inlined data to Parquet FIRST. DuckLake inlines small writes
428-
// (DATA_INLINING_ROW_LIMIT) directly into the catalog DB; with inlining
429-
// left enabled and no periodic flush, those rows accumulate inside the
430-
// catalog forever — an 800 MB catalog backing only a few dozen Parquet
431-
// files is the classic symptom, and DuckLake mirrors the catalog in
432-
// memory (multi-GB RSS). Flushing first also lets the subsequent merge /
433-
// expire act on freshly written Parquet instead of catalog-resident rows.
434-
// Harmless no-op when inlining is disabled (the recommended default).
435-
c.withCatalogLock(func() {
436-
logger.Info("CompactionService: Flush inlined data", "lake", c.lakeName)
437-
flushSQL := fmt.Sprintf("CALL ducklake_flush_inlined_data('%s')", c.lakeName)
438-
if _, err := c.execWithRetry(flushSQL); err != nil {
439-
logger.Warn("CompactionService: flush_inlined_data failed", "error", err)
440-
}
441-
})
497+
// 0. Flush inlined data to Parquet FIRST, so the subsequent merge/expire
498+
// act on freshly written Parquet rather than catalog-resident rows. See
499+
// flushInlinedData for why this matters even when inlining is disabled.
500+
c.flushInlinedData()
442501

443502
// 1. Merge adjacent small files FIRST — lock per table
444503
for _, table := range tables {

src/writer/writer.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,10 +312,14 @@ func (w *Writer) Start() error {
312312
// When multi-volume storage_policy is active with a local volume (hot parquet),
313313
// compaction is always enabled on the writer DuckLake manager — hot data
314314
// accumulates small files and cannot be safely turned off for that mode.
315+
// The compaction service is started unconditionally: when compaction is
316+
// enabled it runs the full merge/expire/cleanup cycle, otherwise it runs a
317+
// lightweight inline-flush-only loop so inlined-data backlog still drains
318+
// (disabling inlining alone does not flush rows inlined earlier).
315319
compactionEnable := w.storageConfig.DuckLake.Compaction.Enable || w.shouldAutoEnableCompactionForTieredHot()
316-
if compactionEnable {
320+
{
317321
compactionCfg := CompactionConfig{
318-
Enable: true,
322+
Enable: compactionEnable,
319323
CheckIntervalSec: w.storageConfig.DuckLake.Compaction.CheckIntervalSec,
320324
RetentionDays: w.storageConfig.DuckLake.Compaction.RetentionDays,
321325
SnapshotExpireIntervalSec: w.storageConfig.DuckLake.Compaction.SnapshotExpireIntervalSec,

0 commit comments

Comments
 (0)