[ 
https://issues.apache.org/jira/browse/CASSANDRA-21480?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Gil Ganz updated CASSANDRA-21480:
---------------------------------
    Description: 
`CompactionTask.buildCompactionCandidatesForAvailableDiskSpace()` decides 
whether a compaction fits by comparing, *{*}per FileStore{*}*:

```
needed = expectedNewWriteSize(this task)               // ON-DISK (compressed) 
bytes
       + Σ estimatedRemainingWriteBytes(running comps)  // UNCOMPRESSED bytes   
<-- mismatch
against
available = getUsableSpace()                            // ON-DISK (compressed) 
bytes
```

The second term — the reservation for already-running compactions — is computed 
in *{*}uncompressed{*}* bytes, but it is summed with a compressed estimate and 
compared against *{*}compressed{*}* free space. For a table with compression 
ratio `r` (on-disk/uncompressed), the in-flight reservation is inflated by 
`1/r`.
 # 
 ## Observed

Cassandra 5.0.8, UnifiedCompactionStrategy, single data dir, table `SSTable 
Compression Ratio: 0.41839` (≈2.39× inflation). One in-flight tier compaction 
shows in `compactionstats`:

```
951b9c30-... Compaction  ...MY_TABLE_NAME 684.33 GiB / 4.76 TiB bytes  14.05%
```

That 4.76 TiB is the *{*}uncompressed{*}* input (~1.99 TiB on disk). Disk:

```
/dev/mapper/data_vg-cass_lv  14T  9.5T  4.6T  68%
```

Every new compaction is rejected:

```
WARN  Directories.java:553 - FileStore ... has only 4.32 TiB available, but 
4.79 TiB is needed
WARN  CompactionTask.java:104 - insufficient space to compact all requested 
files. 2784.6711MiB required ... removing largest SSTable ...
...   (scope reduced repeatedly down to a single 122 MB sstable, "needed" stays 
pinned ~4.78 TiB)
WARN  CompactionTask.java:434 - Not enough space for compaction ... estimated 
sstables = 1, expected write size = 122176278
ERROR JVMStabilityInspector.java:70 - Exception in thread CompactionExecutor...
java.lang.RuntimeException: Not enough space for compaction ... estimated 
sstables = 1, expected write size = 122176278
    at 
CompactionTask.buildCompactionCandidatesForAvailableDiskSpace(CompactionTask.java:436)
```

The "needed" is dominated by the running compaction's remaining uncompressed 
bytes (~4.08 TiB). Its true on-disk write is ~1.71 TiB. Reducing scope of the 
*candidate* never helps because the binding term is the *other* compaction's 
(mis-unit'd) reservation, so the task strips to one sstable and aborts.
 # 
 ## 
 ### Numbers (r = 0.41839)

|Quantity|Uncompressed (used by guard)|On disk (reality)|
|—|—|—|
|Running comp. remaining write|~4.08 TiB|~1.71 TiB|
|Guard "needed" total|4.79 TiB|~2.0 TiB|
|Available|—|4.32 TiB|

Real on-disk requirement (~2.0 TiB) fits in 4.32 TiB free with ~2.3 TiB to 
spare; the guard rejects it purely due to the unit mismatch.
 # 
 ## Root cause (code references, 5.0.8)

1. `CompactionTask.buildCompactionCandidatesForAvailableDiskSpace` builds the 
two maps and calls the check:
   - `src/java/org/apache/cassandra/db/compaction/CompactionTask.java:402` — 
new-task write size:
     `writeSize = cfs.getExpectedCompactedFileSize(nonExpiredSSTables, 
compactionType)`
   - `:409` — running-compactions reservation:
     `Map<File,Long> expectedWriteSize = 
CompactionManager.instance.active.estimatedRemainingWriteBytes()`
   - `:412` — 
`cfs.getDirectories().hasDiskSpaceForCompactionsAndStreams(expectedNewWriteSize,
 expectedWriteSize)`

2. New-task term is *{*}on-disk (compressed){*}* — correct basis:
   - `ColumnFamilyStore.getExpectedCompactedFileSize` 
(`ColumnFamilyStore.java:1695`) → `SSTableReader.getTotalBytes` 
(`SSTableReader.java:541`) → `sstable.onDiskLength()`.

3. Running-compactions term is *{*}uncompressed{*}* — the bug:
   - `ActiveCompactions.estimatedRemainingWriteBytes` 
(`ActiveCompactions.java:58`) sums per-dir
     `CompactionInfo.estimatedRemainingWriteBytes()`.
   - `CompactionInfo.estimatedRemainingWriteBytes` (`CompactionInfo.java:188`) 
returns `getTotal() - getCompleted()`.
   - For a running compaction the `CompactionInfo` comes from 
`CompactionIterator.getCompactionInfo()` (`CompactionIterator.java:167`) with:
     - `total = totalBytes = Σ scanner.getLengthInBytes()` 
(`CompactionIterator.java:137-140`)
     - `SSTableScanner.getLengthInBytes()` returns 
*{*}`sstable.uncompressedLength()`{*}* (`SSTableScanner.java:153-156`).
       Note the sibling `getCompressedLengthInBytes()` → `onDiskLength()` 
(`:159-162`) exists but is unused here.
     - `completed = bytesRead = Σ scanner.getBytesScanned()` 
(`CompactionIterator.java:289-294`), and
       `bytesScanned` accumulates the *{*}uncompressed{*}* data-file pointer 
(`SSTableScanner.java:217`).
   - So both `total` and `completed` are uncompressed; their difference (the 
"remaining write") is uncompressed.

4. Available side is *{*}on-disk (compressed){*}*:
   - `Directories.getAvailableSpaceForCompactions` (`Directories.java:563`) =
     `(FileStore.getUsableSpace() - min_free_space_per_drive) * 
max_space_usable_for_compactions_in_percentage` (default 0.95).
   - `hasDiskSpaceForCompactionsAndStreams` (`Directories.java:517-561`) sums 
the two maps and compares to that.

Result: a compressed-bytes free-space budget is charged an uncompressed-bytes 
reservation for every in-flight compaction → over-reservation by 
`1/compressionRatio` (here 2.39×).

The same uncompressed reservation also feeds the streaming disk check via
`StreamSession.java:927` 
(`Directories.perFileStore(CompactionManager.instance.active.estimatedRemainingWriteBytes(),
 ...)`), so streams are over-throttled on compressed tables too.

 
 # 
 ## Why this is a unit bug and not a deliberate worst-case policy

A reasonable objection would be : compaction *output* is not guaranteed to 
compress like existing data (incompressible output, or inputs written with a 
stronger compressor), so reserving the uncompressed size could be intended as a 
safe worst-case bound. Two facts refute that for compaction:

1. *{*}Compaction output is a recompression of data already on disk at ratio r, 
not new ingest.{*}* The merged rows are the same column values already stored 
compressed; running the same compressor over them yields ~the same ratio. 
Unlike a flush (genuinely new data, no prior on-disk evidence), compaction has 
direct evidence — the inputs' own realized on-disk sizes.

2. *{*}The new-task side of the *same check{*} already assumes output-on-disk ≈ 
input-on-disk (compressed).** `expectedNewWriteSize` comes from 
`getExpectedCompactedFileSize` → `getTotalBytes` → `onDiskLength()` 
(compressed). So the code already predicts compaction output in compressed 
terms; only the *in-flight* reservation for running compactions uses a 
different (uncompressed) measure for the identical quantity. That is an 
internal inconsistency — had uncompressed been a considered worst-case policy, 
the new-task side would use it too, and it does not.
 # 
 ## Proposed fix

Make the running-compactions reservation use the inputs' realized *{*}on-disk 
(compressed){*}* size so both terms and the free-space comparison share units. 
Preferred:
 - Base `estimatedRemainingWriteBytes()` on `onDiskLength()` rather than 
`uncompressedLength()` — e.g. have `CompactionIterator` track remaining via the 
already-exposed `ISSTableScanner.getCompressedLengthInBytes()` → 
`onDiskLength()` (`SSTableScanner.java:159-162`) and report `CompactionInfo` 
total/completed in compressed terms.

Lower-effort alternative (slightly less precise, extrapolates from the table 
average): multiply each holder's `estimatedRemainingWriteBytes()` by its CFS 
`metric.compressionRatio` when > 0 — mirroring what 
`ColumnFamilyStore.getExpectedCompactedFileSize` already does for the CLEANUP 
path (`ColumnFamilyStore.java:1712-1714`).

Either way, residual recompression risk (output marginally larger than input on 
disk) is already covered by the existing margins 
`max_space_usable_for_compactions_in_percentage` (0.95) and 
`min_free_space_per_drive`; a small explicit fudge factor could be added if 
desired. Neither approach requires assuming *new* data compresses like existing 
data — both measure the data actually being recompacted.
 # 
 ## Repro

1. Table with compression (ratio ~0.4) and several GB+ of data under UCS (or 
STCS with large sstables).
2. Fill the FileStore so that `free_on_disk < Σ 
uncompressed_remaining_of_running_compactions` but `free_on_disk > Σ 
on-disk_remaining`.
3. Observe new compactions abort with `Not enough space for compaction` even 
though the real on-disk writes fit. `compactionstats` "total" (uncompressed) vs 
`SSTable Compression Ratio` from `tablestats` shows the gap.
 # 
 ## Workaround

Per-table `ColumnFamilyStoreMBean.compactionDiskSpaceCheck(false)` 
(`ColumnFamilyStoreMBean.java:317`) disables the guard; keep 
`concurrent_compactors` low while disabled. Safe in practice because real 
on-disk write << the uncompressed reservation and `SSTableRewriter` releases 
inputs progressively.

  was:
`CompactionTask.buildCompactionCandidatesForAvailableDiskSpace()` decides 
whether a compaction fits by comparing, **per FileStore**:

```
needed = expectedNewWriteSize(this task)               // ON-DISK (compressed) 
bytes
       + Σ estimatedRemainingWriteBytes(running comps)  // UNCOMPRESSED bytes   
<-- mismatch
against
available = getUsableSpace()                            // ON-DISK (compressed) 
bytes
```

The second term — the reservation for already-running compactions — is computed 
in **uncompressed** bytes, but it is summed with a compressed estimate and 
compared against **compressed** free space. For a table with compression ratio 
`r` (on-disk/uncompressed), the in-flight reservation is inflated by `1/r`.

## Observed

Cassandra 5.0.8, UnifiedCompactionStrategy, single data dir, table `SSTable 
Compression Ratio: 0.41839` (≈2.39× inflation). One in-flight tier compaction 
shows in `compactionstats`:

```
951b9c30-... Compaction  ...MY_TABLE_NAME 684.33 GiB / 4.76 TiB bytes  14.05%
```

That 4.76 TiB is the **uncompressed** input (~1.99 TiB on disk). Disk:

```
/dev/mapper/data_vg-cass_lv  14T  9.5T  4.6T  68%
```

Every new compaction is rejected:

```
WARN  Directories.java:553 - FileStore ... has only 4.32 TiB available, but 
4.79 TiB is needed
WARN  CompactionTask.java:104 - insufficient space to compact all requested 
files. 2784.6711MiB required ... removing largest SSTable ...
...   (scope reduced repeatedly down to a single 122 MB sstable, "needed" stays 
pinned ~4.78 TiB)
WARN  CompactionTask.java:434 - Not enough space for compaction ... estimated 
sstables = 1, expected write size = 122176278
ERROR JVMStabilityInspector.java:70 - Exception in thread CompactionExecutor...
java.lang.RuntimeException: Not enough space for compaction ... estimated 
sstables = 1, expected write size = 122176278
    at 
CompactionTask.buildCompactionCandidatesForAvailableDiskSpace(CompactionTask.java:436)
```

The "needed" is dominated by the running compaction's remaining uncompressed 
bytes (~4.08 TiB). Its true on-disk write is ~1.71 TiB. Reducing scope of the 
*candidate* never helps because the binding term is the *other* compaction's 
(mis-unit'd) reservation, so the task strips to one sstable and aborts.

### Numbers (r = 0.41839)

| Quantity | Uncompressed (used by guard) | On disk (reality) |
|---|---|---|
| Running comp. remaining write | ~4.08 TiB | ~1.71 TiB |
| Guard "needed" total | 4.79 TiB | ~2.0 TiB |
| Available | — | 4.32 TiB |

Real on-disk requirement (~2.0 TiB) fits in 4.32 TiB free with ~2.3 TiB to 
spare; the guard rejects it purely due to the unit mismatch.

## Root cause (code references, 5.0.8)

1. `CompactionTask.buildCompactionCandidatesForAvailableDiskSpace` builds the 
two maps and calls the check:
   - `src/java/org/apache/cassandra/db/compaction/CompactionTask.java:402` — 
new-task write size:
     `writeSize = cfs.getExpectedCompactedFileSize(nonExpiredSSTables, 
compactionType)`
   - `:409` — running-compactions reservation:
     `Map<File,Long> expectedWriteSize = 
CompactionManager.instance.active.estimatedRemainingWriteBytes()`
   - `:412` — 
`cfs.getDirectories().hasDiskSpaceForCompactionsAndStreams(expectedNewWriteSize,
 expectedWriteSize)`

2. New-task term is **on-disk (compressed)** — correct basis:
   - `ColumnFamilyStore.getExpectedCompactedFileSize` 
(`ColumnFamilyStore.java:1695`) → `SSTableReader.getTotalBytes` 
(`SSTableReader.java:541`) → `sstable.onDiskLength()`.

3. Running-compactions term is **uncompressed** — the bug:
   - `ActiveCompactions.estimatedRemainingWriteBytes` 
(`ActiveCompactions.java:58`) sums per-dir
     `CompactionInfo.estimatedRemainingWriteBytes()`.
   - `CompactionInfo.estimatedRemainingWriteBytes` (`CompactionInfo.java:188`) 
returns `getTotal() - getCompleted()`.
   - For a running compaction the `CompactionInfo` comes from 
`CompactionIterator.getCompactionInfo()` (`CompactionIterator.java:167`) with:
     - `total = totalBytes = Σ scanner.getLengthInBytes()` 
(`CompactionIterator.java:137-140`)
     - `SSTableScanner.getLengthInBytes()` returns 
**`sstable.uncompressedLength()`** (`SSTableScanner.java:153-156`).
       Note the sibling `getCompressedLengthInBytes()` → `onDiskLength()` 
(`:159-162`) exists but is unused here.
     - `completed = bytesRead = Σ scanner.getBytesScanned()` 
(`CompactionIterator.java:289-294`), and
       `bytesScanned` accumulates the **uncompressed** data-file pointer 
(`SSTableScanner.java:217`).
   - So both `total` and `completed` are uncompressed; their difference (the 
"remaining write") is uncompressed.

4. Available side is **on-disk (compressed)**:
   - `Directories.getAvailableSpaceForCompactions` (`Directories.java:563`) =
     `(FileStore.getUsableSpace() - min_free_space_per_drive) * 
max_space_usable_for_compactions_in_percentage` (default 0.95).
   - `hasDiskSpaceForCompactionsAndStreams` (`Directories.java:517-561`) sums 
the two maps and compares to that.

Result: a compressed-bytes free-space budget is charged an uncompressed-bytes 
reservation for every in-flight compaction → over-reservation by 
`1/compressionRatio` (here 2.39×).

The same uncompressed reservation also feeds the streaming disk check via
`StreamSession.java:927` 
(`Directories.perFileStore(CompactionManager.instance.active.estimatedRemainingWriteBytes(),
 ...)`), so streams are over-throttled on compressed tables too.

 

## Why this is a unit bug and not a deliberate worst-case policy

A reasonable objection would be : compaction *output* is not guaranteed to 
compress like existing data (incompressible output, or inputs written with a 
stronger compressor), so reserving the uncompressed size could be intended as a 
safe worst-case bound. Two facts refute that for compaction:

1. **Compaction output is a recompression of data already on disk at ratio r, 
not new ingest.** The merged rows are the same column values already stored 
compressed; running the same compressor over them yields ~the same ratio. 
Unlike a flush (genuinely new data, no prior on-disk evidence), compaction has 
direct evidence — the inputs' own realized on-disk sizes.

2. **The new-task side of the *same check* already assumes output-on-disk ≈ 
input-on-disk (compressed).** `expectedNewWriteSize` comes from 
`getExpectedCompactedFileSize` → `getTotalBytes` → `onDiskLength()` 
(compressed). So the code already predicts compaction output in compressed 
terms; only the *in-flight* reservation for running compactions uses a 
different (uncompressed) measure for the identical quantity. That is an 
internal inconsistency — had uncompressed been a considered worst-case policy, 
the new-task side would use it too, and it does not.

## Proposed fix

Make the running-compactions reservation use the inputs' realized **on-disk 
(compressed)** size so both terms and the free-space comparison share units. 
Preferred:

- Base `estimatedRemainingWriteBytes()` on `onDiskLength()` rather than 
`uncompressedLength()` — e.g. have `CompactionIterator` track remaining via the 
already-exposed `ISSTableScanner.getCompressedLengthInBytes()` → 
`onDiskLength()` (`SSTableScanner.java:159-162`) and report `CompactionInfo` 
total/completed in compressed terms.

Lower-effort alternative (slightly less precise, extrapolates from the table 
average): multiply each holder's `estimatedRemainingWriteBytes()` by its CFS 
`metric.compressionRatio` when > 0 — mirroring what 
`ColumnFamilyStore.getExpectedCompactedFileSize` already does for the CLEANUP 
path (`ColumnFamilyStore.java:1712-1714`).

Either way, residual recompression risk (output marginally larger than input on 
disk) is already covered by the existing margins 
`max_space_usable_for_compactions_in_percentage` (0.95) and 
`min_free_space_per_drive`; a small explicit fudge factor could be added if 
desired. Neither approach requires assuming *new* data compresses like existing 
data — both measure the data actually being recompacted.

## Repro

1. Table with compression (ratio ~0.4) and several GB+ of data under UCS (or 
STCS with large sstables).
2. Fill the FileStore so that `free_on_disk < Σ 
uncompressed_remaining_of_running_compactions` but `free_on_disk > Σ 
on-disk_remaining`.
3. Observe new compactions abort with `Not enough space for compaction` even 
though the real on-disk writes fit. `compactionstats` "total" (uncompressed) vs 
`SSTable Compression Ratio` from `tablestats` shows the gap.

## Workaround

Per-table `ColumnFamilyStoreMBean.compactionDiskSpaceCheck(false)` 
(`ColumnFamilyStoreMBean.java:317`) disables the guard; keep 
`concurrent_compactors` low while disabled. Safe in practice because real 
on-disk write << the uncompressed reservation and `SSTableRewriter` releases 
inputs progressively.


> Compaction disk-space check over-reserves by the compression ratio 
> (uncompressed vs on-disk units)
> --------------------------------------------------------------------------------------------------
>
>                 Key: CASSANDRA-21480
>                 URL: https://issues.apache.org/jira/browse/CASSANDRA-21480
>             Project: Apache Cassandra
>          Issue Type: Bug
>          Components: Local/Compaction
>            Reporter: Gil Ganz
>            Priority: Normal
>
> `CompactionTask.buildCompactionCandidatesForAvailableDiskSpace()` decides 
> whether a compaction fits by comparing, *{*}per FileStore{*}*:
> ```
> needed = expectedNewWriteSize(this task)               // ON-DISK 
> (compressed) bytes
>        + Σ estimatedRemainingWriteBytes(running comps)  // UNCOMPRESSED bytes 
>   <-- mismatch
> against
> available = getUsableSpace()                            // ON-DISK 
> (compressed) bytes
> ```
> The second term — the reservation for already-running compactions — is 
> computed in *{*}uncompressed{*}* bytes, but it is summed with a compressed 
> estimate and compared against *{*}compressed{*}* free space. For a table with 
> compression ratio `r` (on-disk/uncompressed), the in-flight reservation is 
> inflated by `1/r`.
>  # 
>  ## Observed
> Cassandra 5.0.8, UnifiedCompactionStrategy, single data dir, table `SSTable 
> Compression Ratio: 0.41839` (≈2.39× inflation). One in-flight tier compaction 
> shows in `compactionstats`:
> ```
> 951b9c30-... Compaction  ...MY_TABLE_NAME 684.33 GiB / 4.76 TiB bytes  14.05%
> ```
> That 4.76 TiB is the *{*}uncompressed{*}* input (~1.99 TiB on disk). Disk:
> ```
> /dev/mapper/data_vg-cass_lv  14T  9.5T  4.6T  68%
> ```
> Every new compaction is rejected:
> ```
> WARN  Directories.java:553 - FileStore ... has only 4.32 TiB available, but 
> 4.79 TiB is needed
> WARN  CompactionTask.java:104 - insufficient space to compact all requested 
> files. 2784.6711MiB required ... removing largest SSTable ...
> ...   (scope reduced repeatedly down to a single 122 MB sstable, "needed" 
> stays pinned ~4.78 TiB)
> WARN  CompactionTask.java:434 - Not enough space for compaction ... estimated 
> sstables = 1, expected write size = 122176278
> ERROR JVMStabilityInspector.java:70 - Exception in thread 
> CompactionExecutor...
> java.lang.RuntimeException: Not enough space for compaction ... estimated 
> sstables = 1, expected write size = 122176278
>     at 
> CompactionTask.buildCompactionCandidatesForAvailableDiskSpace(CompactionTask.java:436)
> ```
> The "needed" is dominated by the running compaction's remaining uncompressed 
> bytes (~4.08 TiB). Its true on-disk write is ~1.71 TiB. Reducing scope of the 
> *candidate* never helps because the binding term is the *other* compaction's 
> (mis-unit'd) reservation, so the task strips to one sstable and aborts.
>  # 
>  ## 
>  ### Numbers (r = 0.41839)
> |Quantity|Uncompressed (used by guard)|On disk (reality)|
> |—|—|—|
> |Running comp. remaining write|~4.08 TiB|~1.71 TiB|
> |Guard "needed" total|4.79 TiB|~2.0 TiB|
> |Available|—|4.32 TiB|
> Real on-disk requirement (~2.0 TiB) fits in 4.32 TiB free with ~2.3 TiB to 
> spare; the guard rejects it purely due to the unit mismatch.
>  # 
>  ## Root cause (code references, 5.0.8)
> 1. `CompactionTask.buildCompactionCandidatesForAvailableDiskSpace` builds the 
> two maps and calls the check:
>    - `src/java/org/apache/cassandra/db/compaction/CompactionTask.java:402` — 
> new-task write size:
>      `writeSize = cfs.getExpectedCompactedFileSize(nonExpiredSSTables, 
> compactionType)`
>    - `:409` — running-compactions reservation:
>      `Map<File,Long> expectedWriteSize = 
> CompactionManager.instance.active.estimatedRemainingWriteBytes()`
>    - `:412` — 
> `cfs.getDirectories().hasDiskSpaceForCompactionsAndStreams(expectedNewWriteSize,
>  expectedWriteSize)`
> 2. New-task term is *{*}on-disk (compressed){*}* — correct basis:
>    - `ColumnFamilyStore.getExpectedCompactedFileSize` 
> (`ColumnFamilyStore.java:1695`) → `SSTableReader.getTotalBytes` 
> (`SSTableReader.java:541`) → `sstable.onDiskLength()`.
> 3. Running-compactions term is *{*}uncompressed{*}* — the bug:
>    - `ActiveCompactions.estimatedRemainingWriteBytes` 
> (`ActiveCompactions.java:58`) sums per-dir
>      `CompactionInfo.estimatedRemainingWriteBytes()`.
>    - `CompactionInfo.estimatedRemainingWriteBytes` 
> (`CompactionInfo.java:188`) returns `getTotal() - getCompleted()`.
>    - For a running compaction the `CompactionInfo` comes from 
> `CompactionIterator.getCompactionInfo()` (`CompactionIterator.java:167`) with:
>      - `total = totalBytes = Σ scanner.getLengthInBytes()` 
> (`CompactionIterator.java:137-140`)
>      - `SSTableScanner.getLengthInBytes()` returns 
> *{*}`sstable.uncompressedLength()`{*}* (`SSTableScanner.java:153-156`).
>        Note the sibling `getCompressedLengthInBytes()` → `onDiskLength()` 
> (`:159-162`) exists but is unused here.
>      - `completed = bytesRead = Σ scanner.getBytesScanned()` 
> (`CompactionIterator.java:289-294`), and
>        `bytesScanned` accumulates the *{*}uncompressed{*}* data-file pointer 
> (`SSTableScanner.java:217`).
>    - So both `total` and `completed` are uncompressed; their difference (the 
> "remaining write") is uncompressed.
> 4. Available side is *{*}on-disk (compressed){*}*:
>    - `Directories.getAvailableSpaceForCompactions` (`Directories.java:563`) =
>      `(FileStore.getUsableSpace() - min_free_space_per_drive) * 
> max_space_usable_for_compactions_in_percentage` (default 0.95).
>    - `hasDiskSpaceForCompactionsAndStreams` (`Directories.java:517-561`) sums 
> the two maps and compares to that.
> Result: a compressed-bytes free-space budget is charged an uncompressed-bytes 
> reservation for every in-flight compaction → over-reservation by 
> `1/compressionRatio` (here 2.39×).
> The same uncompressed reservation also feeds the streaming disk check via
> `StreamSession.java:927` 
> (`Directories.perFileStore(CompactionManager.instance.active.estimatedRemainingWriteBytes(),
>  ...)`), so streams are over-throttled on compressed tables too.
>  
>  # 
>  ## Why this is a unit bug and not a deliberate worst-case policy
> A reasonable objection would be : compaction *output* is not guaranteed to 
> compress like existing data (incompressible output, or inputs written with a 
> stronger compressor), so reserving the uncompressed size could be intended as 
> a safe worst-case bound. Two facts refute that for compaction:
> 1. *{*}Compaction output is a recompression of data already on disk at ratio 
> r, not new ingest.{*}* The merged rows are the same column values already 
> stored compressed; running the same compressor over them yields ~the same 
> ratio. Unlike a flush (genuinely new data, no prior on-disk evidence), 
> compaction has direct evidence — the inputs' own realized on-disk sizes.
> 2. *{*}The new-task side of the *same check{*} already assumes output-on-disk 
> ≈ input-on-disk (compressed).** `expectedNewWriteSize` comes from 
> `getExpectedCompactedFileSize` → `getTotalBytes` → `onDiskLength()` 
> (compressed). So the code already predicts compaction output in compressed 
> terms; only the *in-flight* reservation for running compactions uses a 
> different (uncompressed) measure for the identical quantity. That is an 
> internal inconsistency — had uncompressed been a considered worst-case 
> policy, the new-task side would use it too, and it does not.
>  # 
>  ## Proposed fix
> Make the running-compactions reservation use the inputs' realized *{*}on-disk 
> (compressed){*}* size so both terms and the free-space comparison share 
> units. Preferred:
>  - Base `estimatedRemainingWriteBytes()` on `onDiskLength()` rather than 
> `uncompressedLength()` — e.g. have `CompactionIterator` track remaining via 
> the already-exposed `ISSTableScanner.getCompressedLengthInBytes()` → 
> `onDiskLength()` (`SSTableScanner.java:159-162`) and report `CompactionInfo` 
> total/completed in compressed terms.
> Lower-effort alternative (slightly less precise, extrapolates from the table 
> average): multiply each holder's `estimatedRemainingWriteBytes()` by its CFS 
> `metric.compressionRatio` when > 0 — mirroring what 
> `ColumnFamilyStore.getExpectedCompactedFileSize` already does for the CLEANUP 
> path (`ColumnFamilyStore.java:1712-1714`).
> Either way, residual recompression risk (output marginally larger than input 
> on disk) is already covered by the existing margins 
> `max_space_usable_for_compactions_in_percentage` (0.95) and 
> `min_free_space_per_drive`; a small explicit fudge factor could be added if 
> desired. Neither approach requires assuming *new* data compresses like 
> existing data — both measure the data actually being recompacted.
>  # 
>  ## Repro
> 1. Table with compression (ratio ~0.4) and several GB+ of data under UCS (or 
> STCS with large sstables).
> 2. Fill the FileStore so that `free_on_disk < Σ 
> uncompressed_remaining_of_running_compactions` but `free_on_disk > Σ 
> on-disk_remaining`.
> 3. Observe new compactions abort with `Not enough space for compaction` even 
> though the real on-disk writes fit. `compactionstats` "total" (uncompressed) 
> vs `SSTable Compression Ratio` from `tablestats` shows the gap.
>  # 
>  ## Workaround
> Per-table `ColumnFamilyStoreMBean.compactionDiskSpaceCheck(false)` 
> (`ColumnFamilyStoreMBean.java:317`) disables the guard; keep 
> `concurrent_compactors` low while disabled. Safe in practice because real 
> on-disk write << the uncompressed reservation and `SSTableRewriter` releases 
> inputs progressively.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to