hubcio commented on code in PR #4219:
URL: https://github.com/apache/iggy/pull/4219#discussion_r4091162861


##########
core/connectors/sinks/delta_sink/README.md:
##########
@@ -10,29 +10,149 @@ The Delta Lake Sink Connector allows you to consume 
messages from Iggy topics an
 
 The table must already exist. The connector appends each successful nonempty 
batch in one Delta transaction and keeps its schema snapshot until restart. The 
plugin has no failed-batch retry loop; the Delta library can retry eligible 
commit conflicts and storage requests. Write or commit errors clear the writer 
buffers and return an error. The runtime uses consumer auto-commit and does not 
replay failed sink batches, so end-to-end at-least-once delivery is not 
guaranteed.
 
-## Configuration example
-
-### Local filesystem
-
-```toml
-[plugin_config]
-table_uri = "file:///tmp/iggy_delta_table"
+## How to configure a Delta Sink connector
+
+First, make sure that the Delta table already exists in the location you're 
providing. You can use this script for an example workload:
+
+```python
+import pyarrow as pa
+from deltalake import DeltaTable
+
+table_uri = "s3://test_location/tables/test"
+
+schema = pa.schema([
+    pa.field("user_id", pa.string(), nullable=True),
+    pa.field("user_type", pa.uint8(), nullable=True),
+    pa.field("email", pa.string(), nullable=True),
+    pa.field("source", pa.string(), nullable=True),
+    pa.field("state", pa.string(), nullable=True),
+    pa.field("message", pa.string(), nullable=True),
+    pa.field("created_at", pa.timestamp("us"), nullable=True),
+])
+
+DeltaTable.create(
+    table_uri,
+    schema,
+    name="test",
+    storage_options={"AWS_REGION": "us-east-1"},
+)
+
+print(f"Created table at {table_uri}")
 ```
 
-### AWS S3
-
-```toml
-[plugin_config]
-table_uri = "s3://my-bucket/delta-tables/users"
-storage_backend_type = "s3"
-aws_s3_access_key = "your-access-key"
-aws_s3_secret_key = "your-secret-key"
-aws_s3_region = "us-east-1"
-aws_s3_endpoint_url = "https://s3.amazonaws.com";
-aws_s3_allow_http = false
-```
+The configuration is usually written individually for every connector and 
consists of two parts: the runtime settings which are registering the sink and 
telling which streams should plug into it, and the plugin's settings 
themselves. Here's an example of a working configuration:
+
+  ```toml
+  type = "sink"
+  key = "delta"
+  enabled = true
+  version = 0
+  name = "Delta Lake sink"
+  path = "target/release/libiggy_connector_delta_sink" # make sure you have a 
compiled plugin binary in this path
+  verbose = true
+
+  # these settings are common between all sinks
+  [[streams]]
+  stream = "your_stream"
+  topics = ["topic_inside_of_your_stream"]
+  schema = "json"
+  # important: read about batch and poll interval settings below
+  batch_length = 10000
+  poll_interval = "3s"
+  consumer_group = "delta_sink_connector"
+
+  # these settings are specific to each plugin, and in case of Delta sink, to 
the type of storage used
+  [plugin_config]
+  # the table must exist in the given location
+  table_uri = "s3://iggy-sandbox/tables/test"
+  storage_backend_type = "s3"
+  aws_s3_region = "eu-central-1"
+  aws_s3_allow_http = false
+  ```
+
+### `[[streams]]` section
+
+- Find a topic that you need and the corresponding stream that this topic is 
in and add it into the configuration.
+- `batch_length` and `poll_interval` have to be carefully set for Delta 
tables. `poll_interval` sets the minimum gap between polls. Each poll returns 
immediately with whatever messages are currently available, up to 
`batch_length`. The Delta sink writes and commits exactly what one poll 
returns, so each Delta commit holds at most `batch_length` rows, and topic 
intake is capped at roughly `batch_length` messages per `poll_interval` per 
topic.
+  `batch_length` is a ceiling: it only produces large files when messages 
arrive fast enough between polls to fill it, so a low-throughput topic produces 
small, frequent commits regardless of how high `batch_length` is set. For 
Delta, writing means creating a log entry and a separate Parquet file, so a low 
`batch_length` (roughly < 1000) or a low-throughput topic results in lots of 
small files.
+  This is highly undesirable for the reader as it will have to read from many 
small files instead of a few larger ones. For query performance optimization, 
the system consuming these files will have to apply `OPTIMIZE` query in order 
to consolidate the files. We recommend setting `batch_length` pretty high based 
on your workload so that ideally the files are already optimized for reading.
+  The guideline recommended by the developers of Delta is to keep individual 
file sizes between 128 MB and 1 GB. In the context of this document, it means 
that ideally `batch_length` should be tuned so that each commit's file size 
falls within the suggested range, assuming your topic has enough throughput to 
fill it. The given range is only a rough guideline and you need to find a good 
setting based on the patterns of reading and writing in your systems.

Review Comment:
   warning: this sizes `batch_length` for 128 MB files but skips two costs: the 
poll sits in memory in several copies until commit, and offsets commit at poll 
time, so a failed write loses the whole batch. name both here.



##########
core/connectors/sinks/delta_sink/README.md:
##########
@@ -10,29 +10,149 @@ The Delta Lake Sink Connector allows you to consume 
messages from Iggy topics an
 
 The table must already exist. The connector appends each successful nonempty 
batch in one Delta transaction and keeps its schema snapshot until restart. The 
plugin has no failed-batch retry loop; the Delta library can retry eligible 
commit conflicts and storage requests. Write or commit errors clear the writer 
buffers and return an error. The runtime uses consumer auto-commit and does not 
replay failed sink batches, so end-to-end at-least-once delivery is not 
guaranteed.
 
-## Configuration example
-
-### Local filesystem
-
-```toml
-[plugin_config]
-table_uri = "file:///tmp/iggy_delta_table"
+## How to configure a Delta Sink connector
+
+First, make sure that the Delta table already exists in the location you're 
providing. You can use this script for an example workload:
+
+```python
+import pyarrow as pa
+from deltalake import DeltaTable
+
+table_uri = "s3://test_location/tables/test"
+
+schema = pa.schema([
+    pa.field("user_id", pa.string(), nullable=True),
+    pa.field("user_type", pa.uint8(), nullable=True),
+    pa.field("email", pa.string(), nullable=True),
+    pa.field("source", pa.string(), nullable=True),
+    pa.field("state", pa.string(), nullable=True),
+    pa.field("message", pa.string(), nullable=True),
+    pa.field("created_at", pa.timestamp("us"), nullable=True),
+])
+
+DeltaTable.create(
+    table_uri,
+    schema,
+    name="test",
+    storage_options={"AWS_REGION": "us-east-1"},
+)
+
+print(f"Created table at {table_uri}")
 ```
 
-### AWS S3
-
-```toml
-[plugin_config]
-table_uri = "s3://my-bucket/delta-tables/users"
-storage_backend_type = "s3"
-aws_s3_access_key = "your-access-key"
-aws_s3_secret_key = "your-secret-key"
-aws_s3_region = "us-east-1"
-aws_s3_endpoint_url = "https://s3.amazonaws.com";
-aws_s3_allow_http = false
-```
+The configuration is usually written individually for every connector and 
consists of two parts: the runtime settings which are registering the sink and 
telling which streams should plug into it, and the plugin's settings 
themselves. Here's an example of a working configuration:
+
+  ```toml
+  type = "sink"
+  key = "delta"
+  enabled = true
+  version = 0
+  name = "Delta Lake sink"
+  path = "target/release/libiggy_connector_delta_sink" # make sure you have a 
compiled plugin binary in this path
+  verbose = true
+
+  # these settings are common between all sinks
+  [[streams]]
+  stream = "your_stream"
+  topics = ["topic_inside_of_your_stream"]
+  schema = "json"
+  # important: read about batch and poll interval settings below
+  batch_length = 10000
+  poll_interval = "3s"
+  consumer_group = "delta_sink_connector"
+
+  # these settings are specific to each plugin, and in case of Delta sink, to 
the type of storage used
+  [plugin_config]
+  # the table must exist in the given location
+  table_uri = "s3://iggy-sandbox/tables/test"
+  storage_backend_type = "s3"
+  aws_s3_region = "eu-central-1"
+  aws_s3_allow_http = false
+  ```
+
+### `[[streams]]` section
+
+- Find a topic that you need and the corresponding stream that this topic is 
in and add it into the configuration.
+- `batch_length` and `poll_interval` have to be carefully set for Delta 
tables. `poll_interval` sets the minimum gap between polls. Each poll returns 
immediately with whatever messages are currently available, up to 
`batch_length`. The Delta sink writes and commits exactly what one poll 
returns, so each Delta commit holds at most `batch_length` rows, and topic 
intake is capped at roughly `batch_length` messages per `poll_interval` per 
topic.
+  `batch_length` is a ceiling: it only produces large files when messages 
arrive fast enough between polls to fill it, so a low-throughput topic produces 
small, frequent commits regardless of how high `batch_length` is set. For 
Delta, writing means creating a log entry and a separate Parquet file, so a low 
`batch_length` (roughly < 1000) or a low-throughput topic results in lots of 
small files.
+  This is highly undesirable for the reader as it will have to read from many 
small files instead of a few larger ones. For query performance optimization, 
the system consuming these files will have to apply `OPTIMIZE` query in order 
to consolidate the files. We recommend setting `batch_length` pretty high based 
on your workload so that ideally the files are already optimized for reading.
+  The guideline recommended by the developers of Delta is to keep individual 
file sizes between 128 MB and 1 GB. In the context of this document, it means 
that ideally `batch_length` should be tuned so that each commit's file size 
falls within the suggested range, assuming your topic has enough throughput to 
fill it. The given range is only a rough guideline and you need to find a good 
setting based on the patterns of reading and writing in your systems.
+
+### Plugin configuration
+
+#### Attributes common to all types of storage
+
+- **table_uri** (required): Absolute URI to the Delta table. Supported 
schemes: `file://`, `s3://`, `az://`, `gs://`. Bare filesystem paths are not 
accepted; local tables must use the `file://` scheme.
+- **storage_backend_type** (optional): The cloud storage backend to use. One 
of `"s3"`, `"azure"`, or `"gcs"`. Omit for local filesystem tables.
 
-### Azure Blob Storage
+#### Local filesystem
+
+  ```toml
+  [plugin_config]
+  table_uri = "file:///tmp/iggy_delta_table"
+  ```
+
+#### AWS S3
+
+Currently the implementation offers two possible ways of accessing the bucket.
+
+1. No static keys — the AWS SDK discovers credentials on its own via its 
default credential chain (environment variables, shared config/profile 
including SSO, web identity federation, ECS/EKS container credentials, or EC2 
instance metadata). This is the best practice recommended by AWS. For example, 
if running with an attached IAM role, the role assumed by the writer should 
allow these actions on the bucket, here is how a working policy looks in HCL:

Review Comment:
   warning: a keyed delta sink in this runtime writes its keys into the process 
env as `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`, and a keyless sink then 
picks them up. say so here - init order decides which identity writes.



##########
core/connectors/sinks/delta_sink/src/storage.rs:
##########
@@ -27,25 +27,38 @@ pub(crate) fn build_storage_options(
 
     match config.storage_backend_type {
         Some(StorageBackendType::S3) => {
-            let access_key = config.aws_s3_access_key.as_ref().ok_or_else(|| {
-                Error::InitError("S3 backend requires 
'aws_s3_access_key'".into())
-            })?;
-            let secret_key = config.aws_s3_secret_key.as_ref().ok_or_else(|| {
-                Error::InitError("S3 backend requires 
'aws_s3_secret_key'".into())
+            match (
+                config.aws_s3_access_key.as_ref(),
+                config.aws_s3_secret_key.as_ref(),
+            ) {
+                (Some(access_key), Some(secret_key)) => {
+                    opts.insert(
+                        "AWS_ACCESS_KEY_ID".into(),
+                        access_key.expose_secret().to_owned(),
+                    );
+                    opts.insert(
+                        "AWS_SECRET_ACCESS_KEY".into(),
+                        secret_key.expose_secret().to_owned(),
+                    );
+                }
+                (None, None) => {

Review Comment:
   warning: with both key names misspelled this arm silently goes keyless and 
writes under the ambient AWS identity, where the old code errored out. log the 
credential mode at open in place of the "Successfully composed" info line. 
optionally add `#[serde(deny_unknown_fields)]` to `DeltaSinkConfig` so the typo 
fails fast.



##########
core/connectors/sinks/delta_sink/src/sink.rs:
##########
@@ -37,33 +37,66 @@ impl Sink for DeltaSink {
         );
 
         let table_url = url::Url::parse(&self.config.table_uri).map_err(|e| {
-            error!("Failed to parse table URI '{}': {e}", 
self.config.table_uri);
-            Error::InitError(format!("Invalid table URI: {e}"))
+            error!(
+                "Connector configuration: failed to parse table_uri = '{}': 
{e}.",
+                self.config.table_uri
+            );
+            Error::InvalidConfigValue(format!("table_uri: {e}"))
         })?;
+        let table_uri = &self.config.table_uri;
 
         info!("Parsed table URI: {}", table_url);
 
-        let storage_options = build_storage_options(&self.config).map_err(|e| {
-            error!("Invalid storage configuration: {e}");
-            Error::InitError(format!("Invalid storage configuration: {e}"))
+        let storage_options = 
build_storage_options(&self.config).inspect_err(|e| {
+            error!("Connector configuration: invalid storage configuration. 
Error message: {e}");
         })?;
 
-        let table =
-            match deltalake::open_table_with_storage_options(table_url, 
storage_options).await {
-                Ok(table) => table,
-                Err(e) => {
-                    error!("Failed to load Delta table: {e}");
-                    return Err(Error::InitError(format!("Failed to load Delta 
table: {e}")));
+        info!("Successfully composed the storage options for accessing the 
storage backend");
+
+        let builder = deltalake::DeltaTableBuilder::from_url(table_url)
+            .map_err(|e| {
+                error!("deltalake-rs interface: failed to configure with 
table_uri = '{table_uri}'. Check deltalake::DeltaTableBuilder::from_url docs 
and code to correct your table_uri. Error message: {e}");
+                Error::InvalidConfigValue(format!("table_uri = '{table_uri}' 
caused an error in deltalake-rs interface. Check 
deltalake::DeltaTableBuilder::from_url docs and code to correct your table_uri. 
Error message: {e}"))
+            })?
+            .with_storage_options(storage_options);
+        let mut table = builder.build().map_err(|e| {
+            error!("deltalake-rs interface: failed to configure with provided 
storage configuration. Error message: {e}");
+            Error::InitError(format!("deltalake-rs interface: failed to 
configure with provided storage configuration. Error message: {e}"))
+        })?;
+        let table_exists = table
+            .verify_deltatable_existence()
+            .await
+            .map_err(
+                |e| {
+                    error!("deltalake-rs interface: failed to list table_url 
'{table_uri}' directory to verify delta table existence. Make sure the 
destination exists and the access to the destination is set up correctly - read 
the Iggy delta connector docs for more information. Error message: {e}");

Review Comment:
   nit: the message says `table_url` but the config field is `table_uri`, so an 
operator grepping their config finds nothing. rename the label here and in the 
two tests that match it (delta_sink.rs:256 and :331).



##########
core/connectors/sinks/delta_sink/README.md:
##########
@@ -10,29 +10,149 @@ The Delta Lake Sink Connector allows you to consume 
messages from Iggy topics an
 
 The table must already exist. The connector appends each successful nonempty 
batch in one Delta transaction and keeps its schema snapshot until restart. The 
plugin has no failed-batch retry loop; the Delta library can retry eligible 
commit conflicts and storage requests. Write or commit errors clear the writer 
buffers and return an error. The runtime uses consumer auto-commit and does not 
replay failed sink batches, so end-to-end at-least-once delivery is not 
guaranteed.
 
-## Configuration example
-
-### Local filesystem
-
-```toml
-[plugin_config]
-table_uri = "file:///tmp/iggy_delta_table"
+## How to configure a Delta Sink connector
+
+First, make sure that the Delta table already exists in the location you're 
providing. You can use this script for an example workload:
+
+```python
+import pyarrow as pa
+from deltalake import DeltaTable
+
+table_uri = "s3://test_location/tables/test"

Review Comment:
   nit: `test_location` is not a valid bucket name (no underscores), and the 
examples use three bucket placeholders and two regions, `iggy-sandbox` looks 
like a real one. pick one valid placeholder and one region for all of them.
   
   also line 151: "the same regions that is set" wants "region".



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to