diff --git a/flink-core/src/main/java/org/apache/flink/core/fs/Abortable.java b/flink-core/src/main/java/org/apache/flink/core/fs/Abortable.java new file mode 100644 index 00000000000000..6cc949ac4d7de9 --- /dev/null +++ b/flink-core/src/main/java/org/apache/flink/core/fs/Abortable.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.core.fs; + +import org.apache.flink.annotation.Experimental; + +/** + * Optional capability for streams that support immediate resource release from an external thread. + * + *

Implemented by SDK streams that can unblock a concurrent {@code read()} or {@code write()}. + * Discovered via {@code instanceof}. + * + *

Contract: + * + *

+ */ +@Experimental +public interface Abortable { + + /** + * Releases the stream's resources immediately, unblocking any blocked {@code read()} or {@code + * write()}. + */ + void abort(); +} diff --git a/flink-core/src/main/java/org/apache/flink/core/fs/BufferingInputStreamExtension.java b/flink-core/src/main/java/org/apache/flink/core/fs/BufferingInputStreamExtension.java new file mode 100644 index 00000000000000..63a0979921d37f --- /dev/null +++ b/flink-core/src/main/java/org/apache/flink/core/fs/BufferingInputStreamExtension.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.core.fs; + +import org.apache.flink.annotation.Internal; +import org.apache.flink.util.Preconditions; + +import javax.annotation.concurrent.Immutable; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Default {@link InputStreamExtension} that opens the raw stream via an {@link InputStreamOpener} + * and wraps it with a {@link BufferedInputStream}. + */ +@Internal +@Immutable +final class BufferingInputStreamExtension implements InputStreamExtension { + + private final InputStreamOpener opener; + private final int readBufferSize; + + BufferingInputStreamExtension(final InputStreamOpener opener, final int readBufferSize) { + this.opener = Preconditions.checkNotNull(opener, "opener"); + Preconditions.checkArgument(readBufferSize > 0, "readBufferSize must be positive"); + this.readBufferSize = readBufferSize; + } + + @Override + public RawAndWrappedInputStreams openStream(final InputStreamExtension.StreamContext ctx) + throws IOException { + final InputStream raw = opener.open(ReadContext.of(ctx.getPos())); + return new RawAndWrappedInputStreams(raw, new BufferedInputStream(raw, readBufferSize)); + } +} diff --git a/flink-core/src/main/java/org/apache/flink/core/fs/InputStreamExtension.java b/flink-core/src/main/java/org/apache/flink/core/fs/InputStreamExtension.java new file mode 100644 index 00000000000000..59257d24fe9a34 --- /dev/null +++ b/flink-core/src/main/java/org/apache/flink/core/fs/InputStreamExtension.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.core.fs; + +import org.apache.flink.annotation.Internal; + +import java.io.IOException; + +/** + * Extension point for object-storage input stream implementations. + * + *

All methods are called with the stream's internal lock held. Implementations do not need + * their own synchronisation. + */ +@Internal +public interface InputStreamExtension { + + /** Read-only view of stream state passed to extension callbacks. */ + @Internal + interface StreamContext { + + /** Returns the current byte position. */ + long getPos(); + + /** Returns the total content length in bytes. */ + long getContentLength(); + } + + /** + * Opens streams at the current {@linkplain StreamContext#getPos() read position}. + * + *

Called during lazy initialization (first read) and stream recovery (seek beyond + * threshold). The previous streams are closed by the base class before this call. The base + * class manages the lifecycle of the returned streams (reads, closes, reopens on seek). + * + *

If the underlying source cannot do range reads (e.g., encrypted streams opened at offset + * 0), open at offset 0 and skip forward to {@link StreamContext#getPos()} after wrapping. + * + *

On failure, close any streams opened during this call before rethrowing. + * + * @param ctx read-only view of the stream state at the time of opening + * @return the raw and wrapped stream pair + * @throws IOException if opening fails + */ + RawAndWrappedInputStreams openStream(StreamContext ctx) throws IOException; + + /** + * Returns the default extension that opens the raw stream via {@code opener} and wraps it with + * a {@link java.io.BufferedInputStream}. + * + * @param opener opens a raw stream at a given byte position + * @param readBufferSize the buffer size for the {@link java.io.BufferedInputStream} + */ + static InputStreamExtension buffering( + final InputStreamOpener opener, final int readBufferSize) { + return new BufferingInputStreamExtension(opener, readBufferSize); + } +} diff --git a/flink-core/src/main/java/org/apache/flink/core/fs/InputStreamOpener.java b/flink-core/src/main/java/org/apache/flink/core/fs/InputStreamOpener.java new file mode 100644 index 00000000000000..d8705b3d04f448 --- /dev/null +++ b/flink-core/src/main/java/org/apache/flink/core/fs/InputStreamOpener.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.core.fs; + +import org.apache.flink.annotation.Experimental; +import org.apache.flink.annotation.Internal; + +import javax.annotation.concurrent.NotThreadSafe; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Cloud-agnostic opener that returns an input stream starting at the position described by a {@link + * ReadContext}. + * + *

The implementation captures all cloud-specific state (client, path, bucket) in its closure, + * exposing only the read context to the caller. + * + * @see ReadContext + */ +@Internal +@Experimental +@NotThreadSafe +@FunctionalInterface +public interface InputStreamOpener { + + /** + * Opens an input stream starting at the position indicated by {@code ctx}. + * + * @param ctx context for this read, including the byte offset to start from + * @return an input stream positioned at {@code ctx.getPos()} + * @throws IOException if the stream cannot be opened + */ + InputStream open(ReadContext ctx) throws IOException; +} diff --git a/flink-core/src/main/java/org/apache/flink/core/fs/ObjectStorageFileSystem.java b/flink-core/src/main/java/org/apache/flink/core/fs/ObjectStorageFileSystem.java new file mode 100644 index 00000000000000..530b2c1940b7c9 --- /dev/null +++ b/flink-core/src/main/java/org/apache/flink/core/fs/ObjectStorageFileSystem.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.core.fs; + +import org.apache.flink.annotation.Experimental; +import org.apache.flink.annotation.Internal; +import org.apache.flink.util.CloseableIterator; + +import java.io.IOException; + +/** + * Optional capability for object storage filesystems that support rich metadata access and + * ETag-guarded atomic renames. + * + *

Implementations expose per-object metadata (e.g., encryption headers) and a conditional + * rename primitive used by re-encryption workflows. Discovered via {@code instanceof}. + * + * @see RichFileStatus + */ +@Internal +@Experimental +public interface ObjectStorageFileSystem { + + /** + * Lazily enumerates all file paths under {@code prefix}, recursively. + * + *

The returned iterator must be closed to release any paging cursors or SDK connections. + * + * @param prefix the path prefix to enumerate; must not be {@code null} + * @return a closeable, lazy iterator over all file paths under {@code prefix} + * @throws IOException if enumeration cannot be started + */ + CloseableIterator pathsList(Path prefix) throws IOException; + + /** + * Fetches file status with user metadata and ETag populated. + * + *

Performs one HEAD-equivalent request per call. + * + * @param path the path to query; must not be {@code null} + * @return file status enriched with user metadata and ETag + * @throws IOException if the status cannot be fetched + */ + RichFileStatus getRichFileStatus(Path path) throws IOException; + + /** + * ETag-guarded atomic rename: moves {@code src} to {@code dst} only if the ETags match. + * + *

The move succeeds only when the source ETag matches {@code srcETag} and, if {@code + * dstETag} is non-empty, the destination ETag matches {@code dstETag}. An empty {@code dstETag} + * asserts that the destination does not exist. + * + * @param src source path; must not be {@code null} + * @param dst destination path; must not be {@code null} + * @param srcETag expected ETag of the source object; must not be {@code null} + * @param dstETag expected ETag of the destination object, or {@code ""} to assert non-existence + * @return {@code true} if the move succeeded; {@code false} if any ETag guard fired + * @throws IOException if the storage operation itself failed and the source state is unknown + */ + boolean moveVerified(Path src, Path dst, String srcETag, String dstETag) throws IOException; +} diff --git a/flink-core/src/main/java/org/apache/flink/core/fs/OutputStreamOpener.java b/flink-core/src/main/java/org/apache/flink/core/fs/OutputStreamOpener.java new file mode 100644 index 00000000000000..69b047a97b1358 --- /dev/null +++ b/flink-core/src/main/java/org/apache/flink/core/fs/OutputStreamOpener.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.core.fs; + +import org.apache.flink.annotation.Experimental; +import org.apache.flink.annotation.Internal; + +import javax.annotation.concurrent.NotThreadSafe; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Cloud-agnostic opener that creates an output stream for a write operation. + * + *

The implementation captures all cloud-specific state (client, path, bucket) in its closure. + * The {@link WriteContext} provides encryption metadata to attach to the cloud object before the + * stream is opened. + */ +@Internal +@Experimental +@NotThreadSafe +@FunctionalInterface +public interface OutputStreamOpener { + + /** + * Opens an output stream for the write operation described by {@code ctx}. + * + * @param ctx context for this write, including metadata to attach to the cloud object + * @return an output stream for writing the file content + * @throws IOException if the stream cannot be opened + */ + OutputStream open(WriteContext ctx) throws IOException; +} diff --git a/flink-core/src/main/java/org/apache/flink/core/fs/RawAndWrappedInputStreams.java b/flink-core/src/main/java/org/apache/flink/core/fs/RawAndWrappedInputStreams.java new file mode 100644 index 00000000000000..d021801deb1b23 --- /dev/null +++ b/flink-core/src/main/java/org/apache/flink/core/fs/RawAndWrappedInputStreams.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.core.fs; + +import org.apache.flink.annotation.Internal; +import org.apache.flink.util.Preconditions; + +import java.io.InputStream; + +/** + * A pair of streams returned by {@link + * InputStreamExtension#openStream(InputStreamExtension.StreamContext)}. + */ +@Internal +public final class RawAndWrappedInputStreams { + + private final InputStream sdk; + private final InputStream wrapped; + + /** + * Creates a new stream pair. + * + * @param sdk the raw SDK stream + * @param wrapped the wrapped stream used for reads + */ + public RawAndWrappedInputStreams(final InputStream sdk, final InputStream wrapped) { + this.sdk = Preconditions.checkNotNull(sdk, "sdk"); + this.wrapped = Preconditions.checkNotNull(wrapped, "wrapped"); + } + + /** Returns the raw SDK stream. */ + public InputStream sdk() { + return sdk; + } + + /** Returns the wrapped stream used for reads. */ + public InputStream wrapped() { + return wrapped; + } +} diff --git a/flink-core/src/main/java/org/apache/flink/core/fs/ReadContext.java b/flink-core/src/main/java/org/apache/flink/core/fs/ReadContext.java new file mode 100644 index 00000000000000..3d38b950b83c32 --- /dev/null +++ b/flink-core/src/main/java/org/apache/flink/core/fs/ReadContext.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.core.fs; + +import org.apache.flink.annotation.Experimental; +import org.apache.flink.annotation.Internal; +import org.apache.flink.util.Preconditions; + +import javax.annotation.concurrent.Immutable; + +/** + * Context passed to {@link InputStreamOpener} describing the desired read position. + * + * @see InputStreamOpener + */ +@Internal +@Experimental +@Immutable +public interface ReadContext { + + /** + * Returns the byte offset at which the stream should start. + * + * @return byte offset; must be ≥ 0 + */ + long getPos(); + + /** + * Creates a {@link ReadContext} for the given byte position. + * + * @param pos byte offset; must be ≥ 0 + * @return a {@link ReadContext} that returns {@code pos} + * @throws IllegalArgumentException if {@code pos} is negative + */ + static ReadContext of(final long pos) { + Preconditions.checkArgument(pos >= 0, "pos must be >= 0"); + return () -> pos; + } +} diff --git a/flink-core/src/main/java/org/apache/flink/core/fs/RichFileStatus.java b/flink-core/src/main/java/org/apache/flink/core/fs/RichFileStatus.java new file mode 100644 index 00000000000000..8837f6303e7da8 --- /dev/null +++ b/flink-core/src/main/java/org/apache/flink/core/fs/RichFileStatus.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.core.fs; + +import org.apache.flink.annotation.Experimental; +import org.apache.flink.annotation.Internal; + +import java.util.Map; + +/** + * A {@link FileStatus} enriched with cloud-object user metadata and an ETag. + * + *

Returned by {@link ObjectStorageFileSystem#getRichFileStatus(Path)} and used in + * re-encryption workflows to read encryption headers and obtain a stable ETag before a conditional + * move. + * + * @see ObjectStorageFileSystem + */ +@Internal +@Experimental +public interface RichFileStatus extends FileStatus { + + /** + * Returns user-defined blob metadata (e.g., Azure {@code PathProperties.getMetadata()}). + * + * @return an unmodifiable map of metadata key-value pairs; never {@code null} + */ + Map getMetadata(); + + /** + * Returns the ETag of the object at fetch time. + * + *

Used for {@link ObjectStorageFileSystem#moveVerified} to guard against concurrent + * modifications. + * + * @return the ETag string; never {@code null} + */ + String getETag(); +} diff --git a/flink-core/src/main/java/org/apache/flink/core/fs/WriteContext.java b/flink-core/src/main/java/org/apache/flink/core/fs/WriteContext.java new file mode 100644 index 00000000000000..d07837bb4fc305 --- /dev/null +++ b/flink-core/src/main/java/org/apache/flink/core/fs/WriteContext.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.flink.core.fs; + +import org.apache.flink.annotation.Experimental; +import org.apache.flink.annotation.Internal; +import org.apache.flink.util.Preconditions; + +import javax.annotation.concurrent.Immutable; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Context passed to {@link OutputStreamOpener} describing the write operation, including encryption + * metadata to be persisted with the cloud object. + * + * @see OutputStreamOpener + */ +@Internal +@Experimental +@Immutable +public interface WriteContext { + + /** + * Returns the metadata to attach to the cloud object (e.g., encryption headers). + * + * @return key-value metadata map; never {@code null} + */ + Map getMetadata(); + + /** Shared empty context — avoids allocation for plain (non-encrypted) writes. */ + WriteContext EMPTY_WRITE_CONTEXT = Collections::emptyMap; + + /** + * Creates a {@link WriteContext} backed by a defensive copy of the given metadata map. + * + * @param metadata the metadata to expose; must not be {@code null} + * @return a {@link WriteContext} that returns an unmodifiable copy of {@code metadata} + * @throws NullPointerException if {@code metadata} is {@code null} + */ + static WriteContext of(final Map metadata) { + Preconditions.checkNotNull(metadata, "metadata"); + final Map copy = Collections.unmodifiableMap(new HashMap<>(metadata)); + return () -> copy; + } +}