Skip to content

Commit 4b904d9

Browse files
authored
Add compact CLI command logic (#557)
* Add compact CLI command logic * Add root dataset validation * Add --max-size-flag | add --hard flag * Add possibility to commit blocks without changing head * Move random name generation to separate crate * Add supporting of all events between dataslices * Tests
1 parent c1577f9 commit 4b904d9

40 files changed

Lines changed: 2266 additions & 134 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## Unreleased
88
### Added
99
- Support `ArrowJson` schema output format in QGL API and CLI commands
10+
- New `kamu system compact <dataset>` command that compacts dataslices for the given dataset
1011

1112
## [0.170.0] - 2024-03-29
1213
### Added

Cargo.lock

Lines changed: 10 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ members = [
1010
"src/utils/event-sourcing-macros",
1111
"src/utils/internal-error",
1212
"src/utils/multiformats",
13+
"src/utils/random-names",
1314
"src/utils/repo-tools",
1415
"src/utils/tracing-perfetto",
1516
# Domain
@@ -46,6 +47,7 @@ internal-error = { version = "0.170.0", path = "src/utils/internal-error", defau
4647
multiformats = { version = "0.170.0", path = "src/utils/multiformats", default-features = false }
4748
kamu-data-utils = { version = "0.170.0", path = "src/utils/data-utils", default-features = false }
4849
kamu-datafusion-cli = { version = "0.170.0", path = "src/utils/datafusion-cli", default-features = false }
50+
random-names = { version = "0.170.0", path = "src/utils/random-names", default-features = false }
4951
tracing-perfetto = { version = "0.170.0", path = "src/utils/tracing-perfetto", default-features = false }
5052
# Domain
5153
opendatafabric = { version = "0.170.0", path = "src/domain/opendatafabric", default-features = false }

resources/cli-reference.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,7 @@ Command group for system-level functionality
10211021
* `ipfs` — IPFS helpers
10221022
* `check-token` — Validate a Kamu token
10231023
* `generate-token` — Generate a platform token from a known secret for debugging
1024+
* `compact` — Compact a dataset
10241025

10251026

10261027

@@ -1173,6 +1174,44 @@ Generate a platform token from a known secret for debugging
11731174

11741175

11751176

1177+
## `kamu system compact`
1178+
1179+
Compact a dataset
1180+
1181+
**Usage:** `kamu system compact [OPTIONS] <dataset>...`
1182+
1183+
**Arguments:**
1184+
1185+
* `<DATASET>` — Local dataset reference(s)
1186+
1187+
**Options:**
1188+
1189+
* `--max-slice-size <SIZE>` — Maximum size of a single data slice file in bytes
1190+
1191+
Default value: `1073741824`
1192+
* `--max-slice-records <RECORDS>` — Maximum amount of records in a single data slice file
1193+
1194+
Default value: `10000`
1195+
* `--hard` — Perform 'hard' compaction that rewrites the history of a dataset
1196+
* `--verify` — Perform verification of the dataset before running a compaction
1197+
1198+
For datasets that get frequent small appends the number of data slices can grow over time and affect the performance of querying. This command allows to merge multiple small data slices into a few large files, which can be beneficial in terms of size from more compact encoding, and in query performance, as data engines will have to scan through far fewer file headers.
1199+
1200+
There are two types of compactions: soft and hard.
1201+
1202+
Soft compactions produce new files while leaving the old blocks intact. This allows for faster queries, while still preserving the accurate history of how dataset evolved over time.
1203+
1204+
Hard compactions rewrite the history of the dataset as if data was originally written in big batches. They allow to shrink the history of a dataset to just a few blocks, reclaim the space used by old data files, but at the expense of history loss. Hard compactions will rewrite the metadata chain, changing block hashes. Therefore they will **break all downstream datasets** that depend on them.
1205+
1206+
**Examples:**
1207+
1208+
Perform a history-altering hard compaction:
1209+
1210+
kamu system compact --hard my.dataset
1211+
1212+
1213+
1214+
11761215
## `kamu tail`
11771216

11781217
Displays a sample of most recent records in a dataset

src/app/cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ kamu-adapter-http = { workspace = true }
5252
kamu-adapter-oauth = { workspace = true }
5353
kamu-adapter-odata = { workspace = true }
5454
kamu-datafusion-cli = { workspace = true }
55+
random-names = { workspace = true }
5556

5657
# CLI
5758
chrono-humanize = "0.2" # Human readable durations
@@ -114,7 +115,6 @@ glob = "0.3" # Used for path completions
114115
indoc = "2"
115116
itertools = "0.11"
116117
libc = "0.2" # Signal names
117-
rand = "0.8"
118118
regex = "1"
119119
shlex = "1" # Parsing partial input for custom completions
120120
signal-hook = "0.3" # Signal handling

src/app/cli/src/app.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use std::sync::Arc;
1212

1313
use container_runtime::{ContainerRuntime, ContainerRuntimeConfig};
1414
use dill::*;
15+
use kamu::domain::compact_service::CompactService;
1516
use kamu::domain::*;
1617
use kamu::*;
1718

@@ -236,6 +237,11 @@ pub fn configure_base_catalog(
236237

237238
b.add::<VerificationServiceImpl>();
238239

240+
b.add_builder(
241+
CompactServiceImpl::builder().with_run_info_dir(workspace_layout.run_info_dir.clone()),
242+
);
243+
b.bind::<dyn CompactService, CompactServiceImpl>();
244+
239245
b.add::<SearchServiceImpl>();
240246

241247
b.add::<SyncServiceImpl>();

src/app/cli/src/cli_commands.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,19 @@ pub fn get_command(
477477
)),
478478
_ => return Err(CommandInterpretationFailed.into()),
479479
},
480+
Some(("compact", submatches)) => Box::new(CompactCommand::new(
481+
cli_catalog.get_one()?,
482+
cli_catalog.get_one()?,
483+
cli_catalog.get_one()?,
484+
validate_dataset_ref(
485+
cli_catalog,
486+
submatches.get_one::<DatasetRef>("dataset").unwrap().clone(),
487+
)?,
488+
*(submatches.get_one("max-slice-size").unwrap()),
489+
*(submatches.get_one("max-slice-records").unwrap()),
490+
submatches.get_flag("hard"),
491+
submatches.get_flag("verify"),
492+
)),
480493
_ => return Err(CommandInterpretationFailed.into()),
481494
},
482495
Some(("tail", submatches)) => Box::new(TailCommand::new(

src/app/cli/src/cli_parser.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,6 +1253,53 @@ pub fn cli() -> Command {
12531253
.default_value("3600")
12541254
.help("Token expiration time in seconds"),
12551255
]),
1256+
Command::new("compact")
1257+
.about("Compact a dataset")
1258+
.args([
1259+
Arg::new("dataset")
1260+
.action(ArgAction::Append)
1261+
.index(1)
1262+
.required(true)
1263+
.value_parser(value_parse_dataset_ref_local)
1264+
.help("Local dataset reference(s)"),
1265+
Arg::new("max-slice-size")
1266+
.long("max-slice-size")
1267+
.value_parser(value_parser!(u64))
1268+
.value_name("SIZE")
1269+
.default_value("1073741824")
1270+
.help("Maximum size of a single data slice file in bytes"),
1271+
Arg::new("max-slice-records")
1272+
.long("max-slice-records")
1273+
.value_parser(value_parser!(u64))
1274+
.value_name("RECORDS")
1275+
.default_value("10000")
1276+
.help("Maximum amount of records in a single data slice file"),
1277+
Arg::new("hard")
1278+
.long("hard")
1279+
.action(ArgAction::SetTrue)
1280+
.help("Perform 'hard' compaction that rewrites the history of a dataset"),
1281+
Arg::new("verify")
1282+
.long("verify")
1283+
.action(ArgAction::SetTrue)
1284+
.help("Perform verification of the dataset before running a compaction"),
1285+
])
1286+
.after_help(indoc::indoc!(
1287+
r#"
1288+
For datasets that get frequent small appends the number of data slices can grow over time and affect the performance of querying. This command allows to merge multiple small data slices into a few large files, which can be beneficial in terms of size from more compact encoding, and in query performance, as data engines will have to scan through far fewer file headers.
1289+
1290+
There are two types of compactions: soft and hard.
1291+
1292+
Soft compactions produce new files while leaving the old blocks intact. This allows for faster queries, while still preserving the accurate history of how dataset evolved over time.
1293+
1294+
Hard compactions rewrite the history of the dataset as if data was originally written in big batches. They allow to shrink the history of a dataset to just a few blocks, reclaim the space used by old data files, but at the expense of history loss. Hard compactions will rewrite the metadata chain, changing block hashes. Therefore they will **break all downstream datasets** that depend on them.
1295+
1296+
**Examples:**
1297+
1298+
Perform a history-altering hard compaction:
1299+
1300+
kamu system compact --hard my.dataset
1301+
"#
1302+
)),
12561303
]),
12571304
tabular_output_params(
12581305
Command::new("tail")
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright Kamu Data, Inc. and contributors. All rights reserved.
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the LICENSE file.
5+
//
6+
// As of the Change Date specified in that file, in accordance with
7+
// the Business Source License, use of this software will be governed
8+
// by the Apache License, Version 2.0.
9+
10+
use std::sync::Arc;
11+
12+
use kamu::domain::compact_service::CompactService;
13+
use kamu::domain::{
14+
DatasetRepository,
15+
VerificationMultiListener,
16+
VerificationOptions,
17+
VerificationService,
18+
};
19+
use opendatafabric::{DatasetHandle, DatasetRef};
20+
21+
use crate::{CLIError, Command, CompactionMultiProgress, VerificationMultiProgress};
22+
23+
pub struct CompactCommand {
24+
dataset_repo: Arc<dyn DatasetRepository>,
25+
verification_svc: Arc<dyn VerificationService>,
26+
compact_svc: Arc<dyn CompactService>,
27+
dataset_ref: DatasetRef,
28+
max_slice_size: u64,
29+
max_slice_records: u64,
30+
is_hard: bool,
31+
is_verify: bool,
32+
}
33+
34+
impl CompactCommand {
35+
pub fn new(
36+
dataset_repo: Arc<dyn DatasetRepository>,
37+
verification_svc: Arc<dyn VerificationService>,
38+
compact_svc: Arc<dyn CompactService>,
39+
dataset_ref: DatasetRef,
40+
max_slice_size: u64,
41+
max_slice_records: u64,
42+
is_hard: bool,
43+
is_verify: bool,
44+
) -> Self {
45+
Self {
46+
dataset_repo,
47+
verification_svc,
48+
compact_svc,
49+
dataset_ref,
50+
max_slice_size,
51+
max_slice_records,
52+
is_hard,
53+
is_verify,
54+
}
55+
}
56+
57+
async fn verify_dataset(&self, dataset_handle: &DatasetHandle) -> Result<(), CLIError> {
58+
let progress = VerificationMultiProgress::new();
59+
let listener = Arc::new(progress.clone());
60+
let draw_thread = std::thread::spawn(move || {
61+
progress.draw();
62+
});
63+
64+
let result = self
65+
.verification_svc
66+
.verify(
67+
&dataset_handle.as_local_ref(),
68+
(None, None),
69+
VerificationOptions::default(),
70+
listener.begin_verify(dataset_handle),
71+
)
72+
.await;
73+
74+
listener.finish();
75+
draw_thread.join().unwrap();
76+
77+
result.outcome.map_err(CLIError::failure)
78+
}
79+
}
80+
81+
#[async_trait::async_trait(?Send)]
82+
impl Command for CompactCommand {
83+
async fn run(&mut self) -> Result<(), CLIError> {
84+
if !self.is_hard {
85+
return Err(CLIError::usage_error(
86+
"Soft compactions are not yet supported",
87+
));
88+
}
89+
let dataset_handle = self
90+
.dataset_repo
91+
.resolve_dataset_ref(&self.dataset_ref)
92+
.await
93+
.map_err(CLIError::failure)?;
94+
95+
if self.is_verify {
96+
if let Err(err) = self.verify_dataset(&dataset_handle).await {
97+
eprintln!(
98+
"{}",
99+
console::style("Cannot perform compacting, dataset is invalid".to_string())
100+
.red()
101+
);
102+
return Err(err);
103+
}
104+
}
105+
106+
let progress = CompactionMultiProgress::new();
107+
let listener = Arc::new(progress.clone());
108+
109+
let draw_thread = std::thread::spawn(move || {
110+
progress.draw();
111+
});
112+
113+
self.compact_svc
114+
.compact_dataset(
115+
&dataset_handle,
116+
self.max_slice_size,
117+
self.max_slice_records,
118+
Some(listener.clone()),
119+
)
120+
.await
121+
.map_err(CLIError::failure)?;
122+
123+
listener.finish();
124+
draw_thread.join().unwrap();
125+
126+
Ok(())
127+
}
128+
}

src/app/cli/src/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ mod alias_add_command;
1212
mod alias_delete_command;
1313
mod alias_list_command;
1414
mod common;
15+
mod compact_command;
1516
mod complete_command;
1617
mod completions_command;
1718
mod config_command;
@@ -59,6 +60,7 @@ pub use add_command::*;
5960
pub use alias_add_command::*;
6061
pub use alias_delete_command::*;
6162
pub use alias_list_command::*;
63+
pub use compact_command::*;
6264
pub use complete_command::*;
6365
pub use completions_command::*;
6466
pub use config_command::*;

0 commit comments

Comments
 (0)