Skip to content

Commit 3cd0638

Browse files
authored
Merge pull request #24 from emeraldpay/feature/target-json
2 parents 0a4c681 + 0c810d5 commit 3cd0638

17 files changed

Lines changed: 1520 additions & 150 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@ shadow-rs = "1.4"
4848
[dev-dependencies]
4949
testcontainers = "0.27"
5050
rand = "0.10"
51+
tempfile = "3"

README.adoc

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,61 +47,103 @@ Arguments:
4747
Options:
4848
-b, --blockchain <BLOCKCHAIN>
4949
Blockchain
50+
5051
--dry-run
5152
Do not modify the storage
53+
5254
-c, --connection <HOST:PORT>
5355
Connection (host:port)
56+
5457
--connection.notls
5558
Disable TLS
59+
5660
--parallel <PARALLEL>
5761
How many API requests to make in parallel. Range: 1..512. Default: 16
62+
5863
--notify.dir <NOTIFY_DIR>
5964
Write notifications as JSON line to the specified dir in a file <dshackle-archive-%STARTTIME.jsonl>
65+
6066
--notify.pulsar.topic <PULSAR_TOPIC>
6167
Send notifications as JSON to the Pulsar to the specified topic (notify.pulsar.url must be specified)
68+
6269
--notify.pulsar.url <PULSAR_URL>
6370
Send notifications as JSON to the Pulsar with specified URL (notify.pulsar.topic must be specified)
71+
6472
--auth.aws.access-key <ACCESS_KEY>
6573
AWS / S3 Access Key
74+
6675
--auth.aws.secret-key <SECRET_KEY>
6776
AWS / S3 Secret Key
77+
6878
--aws.endpoint <ENDPOINT>
6979
AWS / S3 endpoint url instead of the default one
80+
7081
--aws.region <REGION>
7182
AWS / S3 region ID to use for requests
83+
7284
--aws.s3.path-style
7385
Enable S3 Path Style access (default is false). Use this flag for a no-AWS service
86+
7487
--aws.trust-tls
7588
Trust any TLS certificate for AWS / S3 (default is false)
89+
7690
-d, --dir <DIR>
7791
Target directory
92+
7893
--continue
7994
[Stream Command] Continue from the last archived block; i.e., not the latest in blockchain
95+
8096
--tail <TAIL>
8197
[Verify/Fix Commands] Use the latest T blocks instead of a range
98+
8299
-r, --range <RANGE>
83100
Blocks Range (`N...M`)
101+
84102
--range.chunk <RANGE_CHUNK>
85103
Range chunk size (default 1000)
104+
86105
-t, --tables <TABLES>
87106
Types of tables to archive (comma-separated list of `blocks`, `txes`, `traces`). Default: blocks,txes
107+
88108
--fields.trace <FIELDS_TRACE>
89109
List of data to include into tracing archive table (comma-separated list of `calls`, `stateDiff`). Default: calls,stateDiff; Used only if `traces` are included into the archived tables (see `--tables` option); Details: `calls` - debug_traceTransaction with `callTracer` tracing; `stateDiff` - debug_traceTransaction with `prestateTracer` tracing
110+
90111
--fix.clean
91112
[Fix Command] Set to remove any existing data in whole chunk if any of tables is missing a block in the chunk or has broken values. Default is `false`, which deleted only tables with missing / corrupted data
113+
92114
--compression <COMPRESSION>
93-
Compression algorithm to use when writing new Avro files. Default is `zstd` [possible values: snappy, zstd]
115+
Compression algorithm to use when writing new Avro files. Default is `zstd`
116+
117+
[possible values: snappy, zstd]
118+
94119
--follow <FOLLOW>
95-
[Stream Command] Follow mode for new blocks: `latest` - follow the latest blocks (default); `finalized` - follow only finalized blocks [default: latest] [possible values: latest, finalized]
120+
[Stream Command] Follow mode for new blocks: `latest` - follow the latest blocks (default); `finalized` - follow only finalized blocks
121+
122+
[default: latest]
123+
[possible values: latest, finalized]
124+
125+
--format <FORMAT>
126+
Output format. `avro` (default) writes one row-batched Avro file per (kind, range) under the historical layout. `json` writes one JSON file per field (block.json, tx-<HASH>.json, receipt-<HASH>.json, raw-<HASH>.hex, …) inside a directory per height. `compact` and `verify` are not supported with `json`
127+
128+
Possible values:
129+
- avro: One Avro file per (kind, range). Historical layout — supports all commands
130+
- json: One JSON file per field under a per-height directory. `archive`, `stream`, and `fix` are supported; `compact` and `verify` are rejected at startup
131+
132+
[default: avro]
133+
96134
--metrics <HOST:PORT>
97135
Start a Prometheus-compatible metrics server on the given address (e.g., 127.0.0.1:8080). Metrics are served at http://HOST:PORT/metrics
136+
98137
--metrics.await
99138
After the main command finishes, keep the metrics server running until one final scrape completes (or 60 seconds elapse). Useful for short-lived commands (fix, verify, compact) to ensure Prometheus collects the final metrics before the process exits
139+
100140
-h, --help
101-
Print help
141+
Print help (see a summary with '-h')
142+
102143
-V, --version
103144
Print version
104145
146+
105147
----
106148

107149
=== Commands

src/archiver/filenames.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,19 @@ impl Filenames {
168168
self.full_path(self.relative_path(kind, range))
169169
}
170170

171+
/// Directory that holds the per-field files for a single block height.
172+
/// Used by the JSON layout (one directory per height, each containing
173+
/// `block.json`, `tx-<HASH>.json`, etc.) — reuses the same two-level
174+
/// height bucketing as [`Self::path`].
175+
pub fn height_dir(&self, height: u64) -> String {
176+
self.full_path(format!(
177+
"{}/{}/{}",
178+
self.level_1(height),
179+
self.level_2(height),
180+
self.range_padded(height)
181+
))
182+
}
183+
171184
fn level_1(&self, value: u64) -> String {
172185
let number = value / self.dir_block_size_l1 * self.dir_block_size_l1;
173186
self.range_padded(number)

src/args.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,14 @@ pub struct Args {
108108
#[arg(long = "follow", default_value = "latest")]
109109
pub follow: Follow,
110110

111+
/// Output format. `avro` (default) writes one row-batched Avro file per
112+
/// (kind, range) under the historical layout. `json` writes one JSON file
113+
/// per field (block.json, tx-<HASH>.json, receipt-<HASH>.json,
114+
/// raw-<HASH>.hex, …) inside a directory per height. `compact` and `verify`
115+
/// are not supported with `json`.
116+
#[arg(long = "format", default_value = "avro")]
117+
pub format: Format,
118+
111119
/// Start a Prometheus-compatible metrics server on the given address (e.g., 127.0.0.1:8080).
112120
/// Metrics are served at http://HOST:PORT/metrics
113121
#[arg(long = "metrics", value_name = "HOST:PORT")]
@@ -139,6 +147,7 @@ impl Default for Args {
139147
fix_clean: false,
140148
compression: None,
141149
follow: Follow::Latest,
150+
format: Format::Avro,
142151
metrics: None,
143152
metrics_await: false,
144153
}
@@ -269,6 +278,16 @@ pub enum Compression {
269278
Zstd,
270279
}
271280

281+
/// Output format selected via `--format`.
282+
#[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
283+
pub enum Format {
284+
/// One Avro file per (kind, range). Historical layout — supports all commands.
285+
Avro,
286+
/// One JSON file per field under a per-height directory. `archive`, `stream`,
287+
/// and `fix` are supported; `compact` and `verify` are rejected at startup.
288+
Json,
289+
}
290+
272291
#[derive(clap::ValueEnum, Debug, Clone, PartialEq)]
273292
pub enum Follow {
274293
Latest,

src/blockchain/ethereum.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ impl EthereumData {
9494
if data_as_json == b"null" {
9595
return Err(anyhow!("Transaction not found: 0x{:x}", hash));
9696
}
97+
// Wire format is a JSON string like "0xabcdef…"; strip the surrounding
98+
// quotes and the `0x` prefix, then hex-decode to bytes.
9799
let data_as_hex = String::from_utf8(
98100
data_as_json[3..(data_as_json.len() - 1)].to_vec()
99101
).map_err(|_| anyhow!("Invalid hex"))?;
@@ -136,7 +138,7 @@ impl EthereumData {
136138
.take(10);
137139
Retry::spawn(retry_strategy, async || {
138140
self.get_tx_raw(hash).await
139-
.and_then(|value| if value == b"null" {
141+
.and_then(|value| if value.is_empty() {
140142
Err(anyhow!("Transaction Raw not found: 0x{:x}", hash))
141143
} else {
142144
Ok(value)

src/command/fix.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::marker::PhantomData;
22
use async_trait::async_trait;
3-
use crate::{archiver::{ArchiveAll, Archiver}, args::Args, blockchain::BlockchainTypes, command::CommandExecutor, global, notify::RunMode, storage::ReadTarget};
3+
use crate::{archiver::{ArchiveAll, Archiver}, args::Args, blockchain::BlockchainTypes, command::CommandExecutor, global, notify::RunMode, storage::ScanTarget};
44
use crate::archiver::blocks_config::Blocks;
55
use crate::archiver::datakind::DataOptions;
66

@@ -9,15 +9,15 @@ use crate::archiver::datakind::DataOptions;
99
/// It checks the archive for the specified range and add missing data
1010
///
1111
#[derive(Clone)]
12-
pub struct FixCommand<B: BlockchainTypes, TS: ReadTarget> {
12+
pub struct FixCommand<B: BlockchainTypes, TS: ScanTarget> {
1313
b: PhantomData<B>,
1414
blocks: Blocks,
1515
chunk_size: usize,
1616
archiver: Archiver<B, TS>,
1717
tx_options: DataOptions,
1818
}
1919

20-
impl<B: BlockchainTypes, TS: ReadTarget> FixCommand<B, TS> {
20+
impl<B: BlockchainTypes, TS: ScanTarget> FixCommand<B, TS> {
2121
pub fn new(config: &Args,
2222
archiver: Archiver<B, TS>) -> anyhow::Result<Self> {
2323

@@ -34,7 +34,7 @@ impl<B: BlockchainTypes, TS: ReadTarget> FixCommand<B, TS> {
3434
}
3535

3636
#[async_trait]
37-
impl<B: BlockchainTypes, TS: ReadTarget> CommandExecutor for FixCommand<B, TS> {
37+
impl<B: BlockchainTypes, TS: ScanTarget> CommandExecutor for FixCommand<B, TS> {
3838

3939
async fn execute(&self) -> anyhow::Result<()> {
4040
let shutdown = global::get_shutdown();

src/command/stream.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::{
1111
command::CommandExecutor,
1212
global,
1313
notify::RunMode,
14-
storage::ReadTarget
14+
storage::ScanTarget
1515
};
1616
use anyhow::Result;
1717
use crate::archiver::datakind::DataOptions;
@@ -25,7 +25,7 @@ use crate::notify::Maturity;
2525
/// It appends fresh blocks one by one to the archive
2626
///
2727
#[derive(Clone)]
28-
pub struct StreamCommand<B: BlockchainTypes, TS: ReadTarget> {
28+
pub struct StreamCommand<B: BlockchainTypes, TS: ScanTarget> {
2929
b: PhantomData<B>,
3030
blockchain: Arc<Blockchain>,
3131
continue_blocks: Option<u64>,
@@ -34,7 +34,7 @@ pub struct StreamCommand<B: BlockchainTypes, TS: ReadTarget> {
3434
follow: Follow,
3535
}
3636

37-
impl<B: BlockchainTypes, TS: ReadTarget> StreamCommand<B, TS> {
37+
impl<B: BlockchainTypes, TS: ScanTarget> StreamCommand<B, TS> {
3838
pub async fn new(config: &Args,
3939
archiver: Archiver<B, TS>
4040
) -> Result<Self> {
@@ -87,7 +87,7 @@ impl<B: BlockchainTypes, TS: ReadTarget> StreamCommand<B, TS> {
8787
}
8888

8989
#[async_trait]
90-
impl<B: BlockchainTypes, TS: ReadTarget> CommandExecutor for StreamCommand<B, TS> {
90+
impl<B: BlockchainTypes, TS: ScanTarget> CommandExecutor for StreamCommand<B, TS> {
9191

9292
async fn execute(&self) -> Result<()> {
9393

0 commit comments

Comments
 (0)