Skip to content

HPCC-XXXXX Add S3 direct API storage plane support - #21199

Open
giftyedwin1 wants to merge 1 commit into
hpcc-systems:masterfrom
giftyedwin1:feature/s3-direct-api-production
Open

HPCC-XXXXX Add S3 direct API storage plane support#21199
giftyedwin1 wants to merge 1 commit into
hpcc-systems:masterfrom
giftyedwin1:feature/s3-direct-api-production

Conversation

@giftyedwin1

@giftyedwin1 giftyedwin1 commented Apr 13, 2026

Copy link
Copy Markdown

Add S3 storage plane support via the file hook mechanism, enabling Thor and Roxie to read/write S3 objects transparently through the IFile/IFileIO interfaces.

Core implementation:

  • S3FileReadIO: read via HTTP GET with Range headers
  • S3FileWriteIO: buffered writes with automatic PutObject (<8MB) or multipart upload (>=8MB) selection
  • S3MultipartUpload: manages CreateMultipartUpload/UploadPart/Complete lifecycle with abort-on-destroy safety
  • S3File: IFile implementation with metadata caching, directory listing via ListObjectsV2
  • S3APICopyClient: server-side copy for files <=5GB, multipart copy for larger files
  • Retry with exponential backoff and jitter for all S3 operations
  • S3 client cache keyed by storage plane name and device number

Platform changes:

  • Add queryPlaneName() to IStorageApiInfo interface
  • Add S3 bucket/region/endpoint config to storage plane schema
  • Add jplane_compat.hpp shim for cross-version jlib compatibility
  • Update Helm template validation to accept buckets (S3) or containers (Azure)

Deployment:

  • Dockerfile overlay for building S3 hook against stock platform-core
  • Helm values examples for S3 on EKS with IRSA authentication
  • Build and deployment guide (helm/examples/s3/README.md)

Testing:

  • ECL integration test verifying putObject vs multipart upload paths
  • Unit tests for S3 URL validation

Type of change:

  • This change is a bug fix (non-breaking change which fixes an issue).
  • This change is a new feature (non-breaking change which adds functionality).
  • This change improves the code (refactor or other change that does not change the functionality)
  • This change fixes warnings (the fix does not alter the functionality or the generated code)
  • This change is a breaking change (fix or feature that will cause existing behavior to change).
  • This change alters the query API (existing queries will have to be recompiled)

Checklist:

  • My code follows the code style of this project.
    • My code does not create any new warnings from compiler, build system, or lint.
  • The commit message is properly formatted and free of typos.
    • The commit message title makes sense in a changelog, by itself.
    • The commit is signed.
  • My change requires a change to the documentation.
    • I have updated the documentation accordingly, or...
    • I have created a JIRA ticket to update the documentation.
    • Any new interfaces or exported functions are appropriately commented.
  • I have read the CONTRIBUTORS document.
  • The change has been fully tested:
    • I have added tests to cover my changes.
    • All new and existing tests passed.
    • I have checked that this change does not introduce memory leaks.
    • I have used Valgrind or similar tools to check for potential issues.
  • I have given due consideration to all of the following potential concerns:
    • Scalability
    • Performance
    • Security
    • Thread-safety
    • Cloud-compatibility
    • Premature optimization
    • Existing deployed queries will not be broken
    • This change fixes the problem, not just the symptom
    • The target branch of this pull request is appropriate for such a change.
  • There are no similar instances of the same problem that should be addressed
    • I have addressed them here
    • I have raised JIRA issues to address them separately
  • This is a user interface / front-end modification
    • I have tested my changes in multiple modern browsers
    • The component(s) render as expected

Smoketest:

  • Send notifications about my Pull Request position in Smoketest queue.
  • Test my draft Pull Request.

Testing:

@GordonSmith
GordonSmith force-pushed the master branch 3 times, most recently from e724e28 to cc22aa6 Compare May 23, 2026 07:40
@giftyedwin1
giftyedwin1 force-pushed the feature/s3-direct-api-production branch from 896b057 to 24803cc Compare June 18, 2026 22:07

@ghalliday ghalliday left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@giftyedwin1 a few comments. The api copying all looks very useful. It may be worth comparing with the existing version since it may have changed since you first looked at it.

// Use dlsym to find whichever exists
typedef IPropertyTree * (*fn2_t)(const char *, bool);
typedef IPropertyTree * (*fn1_t)(const char *);
static fn2_t fn2 = (fn2_t)dlsym(RTLD_DEFAULT, "_Z21getStoragePlaneConfigPKcb");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably better as a direct dependency to jlib. See https://github.com/hpcc-systems/HPCC-Platform/blob/master/common/remote/hooks/azure/CMakeLists.txt for a similar dependency in the azure api code.
Everything is versioned in sync, so not abi problems. getStoragePlaneConfig is preferred.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread common/remote/hooks/s3/s3api.cpp Outdated
auto & error = outcome.GetError();
VStringBuffer msg("S3 copy: cannot stat %s/%s: %s - %s", srcBucket.str(), srcKey.str(),
error.GetExceptionName().c_str(), error.GetMessage().c_str());
throw std::runtime_error(msg.str());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't throw exceptions derived from std::exception in this code base (it predates the std library).
Instead we throw pointers to objects derived from IException. e.g.

   throw makeStringException(code, text);

See handleRequestException in azureapiutils.cpp and the call from azurefile.cpp for similar translation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment thread common/remote/hooks/s3/s3file.cpp Outdated
if (!multipartUpload->uploadPart(data, len))
throw makeStringException(-1, "Failed to upload part to S3");

pending.append(len, data);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to involve cloning a significant amount of memory. How does it compare with the implementation in the current master?
I think that logic (which may have been rewritten since you first looked at it requires that the plane is configured with a block size >=5MB, so that each call to write can be sent as-is.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Send-as-is did not work here: the engine writes ~1MB blocks (DEFAULT_BUFFER_SIZE, via createBufferedIOStream), so each multipart part is ~1MB and CompleteMultipartUpload fails with EntityTooSmall (S3 requires non-final parts ≥5MiB).

blockedSequentialIO doesn't help. Hence the write coalesces into ≥8MB parts, bounded to ~one part per writer rather than
the whole stream. thorbench1 passes at the default block size.

Is there a way to make the OUTPUT path emit ≥5MB blocks that I've missed?

@giftyedwin1
giftyedwin1 force-pushed the feature/s3-direct-api-production branch 4 times, most recently from 237dc60 to 0cfa350 Compare June 22, 2026 15:19
…3 storage plane hook

Build on the existing S3 hook in master, adding the capabilities it lacks
and fixing multipart upload sizing:

- getCopyApiClient: server-side copy via CopyObject, with multipart
  UploadPartCopy for objects larger than 5GB (master returned nullptr).
- directoryFiles: directory listing via ListObjectsV2 (was UNIMPLEMENTED).
- Coalesce S3 multipart writes into >=5MB parts. S3FileWriteIO previously
  uploaded each engine write() (~1MB blocks) as its own multipart part, so
  non-final parts fell below S3's 5MB minimum and CompleteMultipartUpload
  failed with EntityTooSmall. Buffer writes and upload >=8MB parts,
  flushing the remainder as the final part on close() (last part may be
  <5MB, which S3 allows). Memory use is bounded to ~one part per writer.
- Route the read, write and multipart upload paths through the same
  retryS3Op helper used by the new copy/listing clients, so retry/backoff
  handling is consistent across the hook (gatherMetadata stays inline as it
  treats NO_SUCH_KEY as a normal not-found result rather than a failure).

Both API clients reuse the hook's existing S3 client cache and its
handleRequestException / IException error handling. Adds queryPlaneName()
to IStorageApiInfo so the copy client can resolve the source/target planes.

Also adds storage-plane schema/Helm support for s3 buckets, S3 EKS Helm
value examples, and an ECL regression test for the write paths.
@giftyedwin1
giftyedwin1 force-pushed the feature/s3-direct-api-production branch from 0cfa350 to d043b57 Compare June 22, 2026 15:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants