diff --git a/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml b/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml index 1d2122db61b..9d6f32f2914 100644 --- a/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml +++ b/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml @@ -1220,7 +1220,7 @@ com.amazonaws.auth.AWSCredentialsProvider. When S3A delegation tokens are not enabled, this list will be used - to directly authenticate with S3 and DynamoDB services. + to directly authenticate with S3 and other AWS services. When S3A Delegation tokens are enabled, depending upon the delegation token binding it may be used to communicate wih the STS endpoint to request session/role @@ -1669,180 +1669,18 @@ - - fs.s3a.metadatastore.authoritative - false - - When true, allow MetadataStore implementations to act as source of - truth for getting file status and directory listings. Even if this - is set to true, MetadataStore implementations may choose not to - return authoritative results. If the configured MetadataStore does - not support being authoritative, this setting will have no effect. - - - - - fs.s3a.metadatastore.metadata.ttl - 15m - - This value sets how long an entry in a MetadataStore is valid. - - - - - fs.s3a.metadatastore.impl - org.apache.hadoop.fs.s3a.s3guard.NullMetadataStore - - Fully-qualified name of the class that implements the MetadataStore - to be used by s3a. The default class, NullMetadataStore, has no - effect: s3a will continue to treat the backing S3 service as the one - and only source of truth for file and directory metadata. - - - - - fs.s3a.metadatastore.fail.on.write.error - true - - When true (default), FileSystem write operations generate - org.apache.hadoop.fs.s3a.MetadataPersistenceException if the metadata - cannot be saved to the metadata store. When false, failures to save to - metadata store are logged at ERROR level, but the overall FileSystem - write operation succeeds. - - - - - fs.s3a.s3guard.cli.prune.age - 86400000 - - Default age (in milliseconds) after which to prune metadata from the - metadatastore when the prune command is run. Can be overridden on the - command-line. - - - - fs.s3a.impl org.apache.hadoop.fs.s3a.S3AFileSystem The implementation class of the S3A Filesystem - - fs.s3a.s3guard.ddb.region - - - AWS DynamoDB region to connect to. An up-to-date list is - provided in the AWS Documentation: regions and endpoints. Without this - property, the S3Guard will operate table in the associated S3 bucket region. - - - - - fs.s3a.s3guard.ddb.table - - - The DynamoDB table name to operate. Without this property, the respective - S3 bucket name will be used. - - - - - fs.s3a.s3guard.ddb.table.create - false - - If true, the S3A client will create the table if it does not already exist. - - - - - fs.s3a.s3guard.ddb.table.capacity.read - 0 - - Provisioned throughput requirements for read operations in terms of capacity - units for the DynamoDB table. This config value will only be used when - creating a new DynamoDB table. - If set to 0 (the default), new tables are created with "per-request" capacity. - If a positive integer is provided for this and the write capacity, then - a table with "provisioned capacity" will be created. - You can change the capacity of an existing provisioned-capacity table - through the "s3guard set-capacity" command. - - - - - fs.s3a.s3guard.ddb.table.capacity.write - 0 - - Provisioned throughput requirements for write operations in terms of - capacity units for the DynamoDB table. - If set to 0 (the default), new tables are created with "per-request" capacity. - Refer to related configuration option fs.s3a.s3guard.ddb.table.capacity.read - - - - - fs.s3a.s3guard.ddb.table.sse.enabled - false - - Whether server-side encryption (SSE) is enabled or disabled on the table. - By default it's disabled, meaning SSE is set to AWS owned CMK. - - - - - fs.s3a.s3guard.ddb.table.sse.cmk - - - The KMS Customer Master Key (CMK) used for the KMS encryption on the table. - To specify a CMK, this config value can be its key ID, Amazon Resource Name - (ARN), alias name, or alias ARN. Users only need to provide this config if - the key is different from the default DynamoDB KMS Master Key, which is - alias/aws/dynamodb. - - - - - fs.s3a.s3guard.ddb.max.retries - 9 - - Max retries on throttled/incompleted DynamoDB operations - before giving up and throwing an IOException. - Each retry is delayed with an exponential - backoff timer which starts at 100 milliseconds and approximately - doubles each time. The minimum wait before throwing an exception is - sum(100, 200, 400, 800, .. 100*2^N-1 ) == 100 * ((2^N)-1) - - - - - fs.s3a.s3guard.ddb.throttle.retry.interval - 100ms - - Initial interval to retry after a request is throttled events; - the back-off policy is exponential until the number of retries of - fs.s3a.s3guard.ddb.max.retries is reached. - - - - - fs.s3a.s3guard.ddb.background.sleep - 25ms - - Length (in milliseconds) of pause between each batch of deletes when - pruning metadata. Prevents prune operations (which can typically be low - priority background operations) from overly interfering with other I/O - operations. - - - fs.s3a.retry.limit 7 Number of times to retry any repeatable S3 client request on failure, - excluding throttling requests and S3Guard inconsistency resolution. + excluding throttling requests. @@ -1851,7 +1689,7 @@ 500ms Initial retry interval when retrying operations for any reason other - than S3 throttle errors and S3Guard inconsistency resolution. + than S3 throttle errors. @@ -1874,27 +1712,6 @@ - - fs.s3a.s3guard.consistency.retry.limit - 7 - - Number of times to retry attempts to read/open/copy files when - S3Guard believes a specific version of the file to be available, - but the S3 request does not find any version of a file, or a different - version. - - - - - fs.s3a.s3guard.consistency.retry.interval - 2s - - Initial interval between attempts to retry operations while waiting for S3 - to become consistent with the S3Guard data. - An exponential back-off is used here: every failure doubles the delay. - - - fs.s3a.committer.name file diff --git a/hadoop-common-project/hadoop-common/src/site/markdown/AdminCompatibilityGuide.md b/hadoop-common-project/hadoop-common/src/site/markdown/AdminCompatibilityGuide.md index 67f9c907ff9..5d2c38d4c56 100644 --- a/hadoop-common-project/hadoop-common/src/site/markdown/AdminCompatibilityGuide.md +++ b/hadoop-common-project/hadoop-common/src/site/markdown/AdminCompatibilityGuide.md @@ -137,7 +137,8 @@ internal state stores: * The internal MapReduce state data will remain compatible across minor releases within the same major version to facilitate rolling upgrades while MapReduce workloads execute. * HDFS maintains metadata about the data stored in HDFS in a private, internal format that is versioned. In the event of an incompatible change, the store's version number will be incremented. When upgrading an existing cluster, the metadata store will automatically be upgraded if possible. After the metadata store has been upgraded, it is always possible to reverse the upgrade process. -* The AWS S3A guard keeps a private, internal metadata store that is versioned. Incompatible changes will cause the version number to be incremented. If an upgrade requires reformatting the store, it will be indicated in the release notes. +* The AWS S3A guard kept a private, internal metadata store. + Now that the feature has been removed, the store is obsolete and can be deleted. * The YARN resource manager keeps a private, internal state store of application and scheduler information that is versioned. Incompatible changes will cause the version number to be incremented. If an upgrade requires reformatting the store, it will be indicated in the release notes. * The YARN node manager keeps a private, internal state store of application information that is versioned. Incompatible changes will cause the version number to be incremented. If an upgrade requires reformatting the store, it will be indicated in the release notes. * The YARN federation service keeps a private, internal state store of application and cluster information that is versioned. Incompatible changes will cause the version number to be incremented. If an upgrade requires reformatting the store, it will be indicated in the release notes. diff --git a/hadoop-common-project/hadoop-common/src/site/markdown/Compatibility.md b/hadoop-common-project/hadoop-common/src/site/markdown/Compatibility.md index 03d162a18ac..0ccb6a8b5c3 100644 --- a/hadoop-common-project/hadoop-common/src/site/markdown/Compatibility.md +++ b/hadoop-common-project/hadoop-common/src/site/markdown/Compatibility.md @@ -477,19 +477,12 @@ rolled back to the older layout. ##### AWS S3A Guard Metadata -For each operation in the Hadoop S3 client (s3a) that reads or modifies -file metadata, a shadow copy of that file metadata is stored in a separate -metadata store, which offers HDFS-like consistency for the metadata, and may -also provide faster lookups for things like file status or directory listings. -S3A guard tables are created with a version marker which indicates -compatibility. +The S3Guard metastore used to store metadata in DynamoDB tables; +as such it had to maintain a compatibility strategy. +Now that S3Guard is removed, the tables are not needed. -###### Policy - -The S3A guard metadata schema SHALL be considered -[Private](./InterfaceClassification.html#Private) and -[Unstable](./InterfaceClassification.html#Unstable). Any incompatible change -to the schema MUST result in the version number of the schema being incremented. +Applications configured to use an S3A metadata store other than +the "null" store will fail. ##### YARN Resource Manager State Store diff --git a/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/introduction.md b/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/introduction.md index 37191a5b2a6..903d2bb90ff 100644 --- a/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/introduction.md +++ b/hadoop-common-project/hadoop-common/src/site/markdown/filesystem/introduction.md @@ -343,7 +343,7 @@ stores pretend that they are a FileSystem, a FileSystem with the same features and operations as HDFS. This is —ultimately—a pretence: they have different characteristics and occasionally the illusion fails. -1. **Consistency**. Object stores are generally *Eventually Consistent*: it +1. **Consistency**. Object may be *Eventually Consistent*: it can take time for changes to objects —creation, deletion and updates— to become visible to all callers. Indeed, there is no guarantee a change is immediately visible to the client which just made the change. As an example, @@ -447,10 +447,6 @@ Object stores have an even vaguer view of time, which can be summarized as * The timestamp is likely to be in UTC or the TZ of the object store. If the client is in a different timezone, the timestamp of objects may be ahead or behind that of the client. - * Object stores with cached metadata databases (for example: AWS S3 with - an in-memory or a DynamoDB metadata store) may have timestamps generated - from the local system clock, rather than that of the service. - This is an optimization to avoid round-trip calls to the object stores. + A file's modification time is often the same as its creation time. + The `FileSystem.setTimes()` operation to set file timestamps *may* be ignored. * `FileSystem.chmod()` may update modification times (example: Azure `wasb://`). diff --git a/hadoop-project/src/site/markdown/index.md.vm b/hadoop-project/src/site/markdown/index.md.vm index 438145a3619..edc38a52861 100644 --- a/hadoop-project/src/site/markdown/index.md.vm +++ b/hadoop-project/src/site/markdown/index.md.vm @@ -203,16 +203,6 @@ in both the task configuration and as a Java option. Existing configs that already specify both are not affected by this change. See the full release notes of MAPREDUCE-5785 for more details. -S3Guard: Consistency and Metadata Caching for the S3A filesystem client ---------------------- - -[HADOOP-13345](https://issues.apache.org/jira/browse/HADOOP-13345) adds an -optional feature to the S3A client of Amazon S3 storage: the ability to use -a DynamoDB table as a fast and consistent store of file and directory -metadata. - -See [S3Guard](./hadoop-aws/tools/hadoop-aws/s3guard.html) for more details. - HDFS Router-Based Federation --------------------- HDFS Router-Based Federation adds a RPC routing layer that provides a federated diff --git a/hadoop-tools/hadoop-aws/dev-support/findbugs-exclude.xml b/hadoop-tools/hadoop-aws/dev-support/findbugs-exclude.xml index 9d598be856a..d0496de538e 100644 --- a/hadoop-tools/hadoop-aws/dev-support/findbugs-exclude.xml +++ b/hadoop-tools/hadoop-aws/dev-support/findbugs-exclude.xml @@ -29,20 +29,6 @@ - - - - - - - - - - 3600 - - false - false - local + 200000 @@ -165,10 +162,6 @@ ${fs.s3a.scale.test.huge.filesize} ${fs.s3a.scale.test.huge.partitionsize} ${fs.s3a.scale.test.timeout} - - ${fs.s3a.s3guard.test.enabled} - ${fs.s3a.s3guard.test.authoritative} - ${fs.s3a.s3guard.test.implementation} ${fs.s3a.directory.marker.retention} ${test.integration.timeout} @@ -192,14 +185,10 @@ **/ITestS3AFileContextStatistics.java **/ITestS3AEncryptionSSEC*.java **/ITestS3AHuge*.java - - **/ITestDynamoDBMetadataStoreScale.java **/ITestTerasort*.java **/ITestMarkerToolRootOperations.java - - **/ITestS3GuardDDBRootOperations.java **/ITestAggregateIOStatistics.java @@ -222,10 +211,6 @@ ${fs.s3a.scale.test.huge.filesize} ${fs.s3a.scale.test.huge.partitionsize} ${fs.s3a.scale.test.timeout} - - ${fs.s3a.s3guard.test.enabled} - ${fs.s3a.s3guard.test.implementation} - ${fs.s3a.s3guard.test.authoritative} ${fs.s3a.directory.marker.retention} ${fs.s3a.directory.marker.audit} @@ -238,8 +223,6 @@ **/ITestS3AHuge*.java **/ITestS3AEncryptionSSEC*.java - - **/ITestDynamoDBMetadataStoreScale.java @@ -248,9 +231,8 @@ **/ITestMarkerToolRootOperations.java - + **/ITestS3AContractRootDir.java - **/ITestS3GuardDDBRootOperations.java **/ITestAggregateIOStatistics.java @@ -285,10 +267,6 @@ ${fs.s3a.scale.test.enabled} ${fs.s3a.scale.test.huge.filesize} ${fs.s3a.scale.test.timeout} - - ${fs.s3a.s3guard.test.enabled} - ${fs.s3a.s3guard.test.implementation} - ${fs.s3a.s3guard.test.authoritative} ${fs.s3a.directory.marker.retention} ${fs.s3a.directory.marker.audit} @@ -315,46 +293,6 @@ - - - s3guard - - - s3guard - - - - true - - - - - - dynamo - - - dynamo - - - - dynamo - - - - - - auth - - - auth - - - - true - - - keep-markers diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Constants.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Constants.java index 461fe87d331..35387f0c21d 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Constants.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Constants.java @@ -25,11 +25,14 @@ import org.apache.hadoop.security.ssl.DelegatingSSLSocketFactory; import java.util.concurrent.TimeUnit; /** - * All the constants used with the {@link S3AFileSystem}. + * Constants used with the {@link S3AFileSystem}. * * Some of the strings are marked as {@code Unstable}. This means - * that they may be unsupported in future; at which point they will be marked + * that they may be Unsupported in future; at which point they will be marked * as deprecated and simply ignored. + * + * All S3Guard related constants are marked as Deprecated and either ignored (ddb config) + * or rejected (setting the metastore to anything other than the null store) */ @InterfaceAudience.Public @InterfaceStability.Evolving @@ -130,7 +133,7 @@ public final class Constants { /** * JSON policy containing the policy to apply to the role: {@value}. * This is not used for delegation tokens, which generate the policy - * automatically, and restrict it to the S3, KMS and S3Guard services + * automatically, and restrict it to the S3 and KMS services * needed. */ public static final String ASSUMED_ROLE_POLICY = @@ -488,20 +491,17 @@ public final class Constants { public static final String CUSTOM_SIGNERS = "fs.s3a.custom.signers"; /** - * There's 3 parameters that can be used to specify a non-default signing + * Multiple parameters can be used to specify a non-default signing * algorithm.
* fs.s3a.signing-algorithm - This property has existed for the longest time. - * If specified, without either of the other 2 properties being specified, - * this signing algorithm will be used for S3 and DDB (S3Guard).
- * The other 2 properties override this value for S3 or DDB.
+ * If specified, without other properties being specified, + * this signing algorithm will be used for all services.
+ * Another property overrides this value for S3.
* fs.s3a.s3.signing-algorithm - Allows overriding the S3 Signing algorithm. - * This does not affect DDB. Specifying this property without specifying + * Specifying this property without specifying * fs.s3a.signing-algorithm will only update the signing algorithm for S3 - * requests, and the default will be used for DDB.
- * fs.s3a.ddb.signing-algorithm - Allows overriding the DDB Signing algorithm. - * This does not affect S3. Specifying this property without specifying - * fs.s3a.signing-algorithm will only update the signing algorithm for - * DDB requests, and the default will be used for S3. + * requests. + * {@code fs.s3a.sts.signing-algorithm}: algorithm to use for STS interaction. */ public static final String SIGNING_ALGORITHM = "fs.s3a.signing-algorithm"; @@ -509,6 +509,7 @@ public final class Constants { "fs.s3a." + Constants.AWS_SERVICE_IDENTIFIER_S3.toLowerCase() + ".signing-algorithm"; + @Deprecated public static final String SIGNING_ALGORITHM_DDB = "fs.s3a." + Constants.AWS_SERVICE_IDENTIFIER_DDB.toLowerCase() + "signing-algorithm"; @@ -534,13 +535,23 @@ public final class Constants { public static final String USER_AGENT_PREFIX = "fs.s3a.user.agent.prefix"; - /** Whether or not to allow MetadataStore to be source of truth for a path prefix */ + /** + * Paths considered "authoritative". + * When S3guard was supported, this skipped checks to s3 on directory listings. + * It is also use to optionally disable marker retentation purely on these + * paths -a feature which is still retained/available. + * */ public static final String AUTHORITATIVE_PATH = "fs.s3a.authoritative.path"; public static final String[] DEFAULT_AUTHORITATIVE_PATH = {}; - /** Whether or not to allow MetadataStore to be source of truth. */ + /** + * Whether or not to allow MetadataStore to be source of truth. + * @deprecated no longer supported + */ + @Deprecated public static final String METADATASTORE_AUTHORITATIVE = "fs.s3a.metadatastore.authoritative"; + @Deprecated public static final boolean DEFAULT_METADATASTORE_AUTHORITATIVE = false; /** @@ -559,13 +570,16 @@ public final class Constants { /** * How long a directory listing in the MS is considered as authoritative. + * @deprecated no longer supported */ + @Deprecated public static final String METADATASTORE_METADATA_TTL = "fs.s3a.metadatastore.metadata.ttl"; /** * Default TTL in milliseconds: 15 minutes. */ + @Deprecated public static final long DEFAULT_METADATASTORE_METADATA_TTL = TimeUnit.MINUTES.toMillis(15); @@ -629,202 +643,117 @@ public final class Constants { @InterfaceAudience.Private public static final int MAX_MULTIPART_COUNT = 10000; - /* Constants. */ + /* + * Obsolete S3Guard-related options, retained purely because this file + * is @Public/@Evolving. + */ + @Deprecated public static final String S3_METADATA_STORE_IMPL = "fs.s3a.metadatastore.impl"; - - /** - * Whether to fail when there is an error writing to the metadata store. - */ + @Deprecated public static final String FAIL_ON_METADATA_WRITE_ERROR = "fs.s3a.metadatastore.fail.on.write.error"; - - /** - * Default value ({@value}) for FAIL_ON_METADATA_WRITE_ERROR. - */ + @Deprecated public static final boolean FAIL_ON_METADATA_WRITE_ERROR_DEFAULT = true; - - /** Minimum period of time (in milliseconds) to keep metadata (may only be - * applied when a prune command is manually run). - */ @InterfaceStability.Unstable + @Deprecated public static final String S3GUARD_CLI_PRUNE_AGE = "fs.s3a.s3guard.cli.prune.age"; - - /** - * The region of the DynamoDB service. - * - * This config has no default value. If the user does not set this, the - * S3Guard will operate table in the associated S3 bucket region. - */ + @Deprecated public static final String S3GUARD_DDB_REGION_KEY = "fs.s3a.s3guard.ddb.region"; - - /** - * The DynamoDB table name to use. - * - * This config has no default value. If the user does not set this, the - * S3Guard implementation will use the respective S3 bucket name. - */ + @Deprecated public static final String S3GUARD_DDB_TABLE_NAME_KEY = "fs.s3a.s3guard.ddb.table"; - - /** - * A prefix for adding tags to the DDB Table upon creation. - * - * For example: - * fs.s3a.s3guard.ddb.table.tag.mytag - */ + @Deprecated public static final String S3GUARD_DDB_TABLE_TAG = "fs.s3a.s3guard.ddb.table.tag."; - - /** - * Whether to create the DynamoDB table if the table does not exist. - * Value: {@value}. - */ + @Deprecated public static final String S3GUARD_DDB_TABLE_CREATE_KEY = "fs.s3a.s3guard.ddb.table.create"; - - /** - * Read capacity when creating a table. - * When it and the write capacity are both "0", a per-request table is - * created. - * Value: {@value}. - */ + @Deprecated public static final String S3GUARD_DDB_TABLE_CAPACITY_READ_KEY = "fs.s3a.s3guard.ddb.table.capacity.read"; - - /** - * Default read capacity when creating a table. - * Value: {@value}. - */ + @Deprecated public static final long S3GUARD_DDB_TABLE_CAPACITY_READ_DEFAULT = 0; - - /** - * Write capacity when creating a table. - * When it and the read capacity are both "0", a per-request table is - * created. - * Value: {@value}. - */ + @Deprecated public static final String S3GUARD_DDB_TABLE_CAPACITY_WRITE_KEY = "fs.s3a.s3guard.ddb.table.capacity.write"; - - /** - * Default write capacity when creating a table. - * Value: {@value}. - */ + @Deprecated public static final long S3GUARD_DDB_TABLE_CAPACITY_WRITE_DEFAULT = 0; - - /** - * Whether server-side encryption (SSE) is enabled or disabled on the table. - * By default it's disabled, meaning SSE is set to AWS owned CMK. - * @see com.amazonaws.services.dynamodbv2.model.SSESpecification#setEnabled - */ + @Deprecated public static final String S3GUARD_DDB_TABLE_SSE_ENABLED = "fs.s3a.s3guard.ddb.table.sse.enabled"; - - /** - * The KMS Master Key (CMK) used for the KMS encryption on the table. - * - * To specify a CMK, this config value can be its key ID, Amazon Resource - * Name (ARN), alias name, or alias ARN. Users only provide this config - * if the key is different from the default DynamoDB KMS Master Key, which is - * alias/aws/dynamodb. - */ + @Deprecated public static final String S3GUARD_DDB_TABLE_SSE_CMK = "fs.s3a.s3guard.ddb.table.sse.cmk"; - - /** - * The maximum put or delete requests per BatchWriteItem request. - * - * Refer to Amazon API reference for this limit. - */ + @Deprecated public static final int S3GUARD_DDB_BATCH_WRITE_REQUEST_LIMIT = 25; - + @Deprecated public static final String S3GUARD_DDB_MAX_RETRIES = "fs.s3a.s3guard.ddb.max.retries"; - - /** - * Max retries on batched/throttled DynamoDB operations before giving up and - * throwing an IOException. Default is {@value}. See core-default.xml for - * more detail. - */ + @Deprecated public static final int S3GUARD_DDB_MAX_RETRIES_DEFAULT = DEFAULT_MAX_ERROR_RETRIES; - + @Deprecated public static final String S3GUARD_DDB_THROTTLE_RETRY_INTERVAL = "fs.s3a.s3guard.ddb.throttle.retry.interval"; + @Deprecated public static final String S3GUARD_DDB_THROTTLE_RETRY_INTERVAL_DEFAULT = "100ms"; - - /** - * Period of time (in milliseconds) to sleep between batches of writes. - * Currently only applies to prune operations, as they are naturally a - * lower priority than other operations. - */ + @Deprecated @InterfaceStability.Unstable public static final String S3GUARD_DDB_BACKGROUND_SLEEP_MSEC_KEY = "fs.s3a.s3guard.ddb.background.sleep"; + @Deprecated public static final int S3GUARD_DDB_BACKGROUND_SLEEP_MSEC_DEFAULT = 25; /** * The default "Null" metadata store: {@value}. */ + @Deprecated public static final String S3GUARD_METASTORE_NULL = "org.apache.hadoop.fs.s3a.s3guard.NullMetadataStore"; - - /** - * Use Local memory for the metadata: {@value}. - * This is not coherent across processes and must be used for testing only. - */ + @Deprecated @InterfaceStability.Unstable public static final String S3GUARD_METASTORE_LOCAL = "org.apache.hadoop.fs.s3a.s3guard.LocalMetadataStore"; - - /** - * Maximum number of records in LocalMetadataStore. - */ @InterfaceStability.Unstable + @Deprecated public static final String S3GUARD_METASTORE_LOCAL_MAX_RECORDS = "fs.s3a.s3guard.local.max_records"; + @Deprecated public static final int DEFAULT_S3GUARD_METASTORE_LOCAL_MAX_RECORDS = 256; - - /** - * Time to live in milliseconds in LocalMetadataStore. - * If zero, time-based expiration is disabled. - */ @InterfaceStability.Unstable + @Deprecated public static final String S3GUARD_METASTORE_LOCAL_ENTRY_TTL = "fs.s3a.s3guard.local.ttl"; + @Deprecated public static final int DEFAULT_S3GUARD_METASTORE_LOCAL_ENTRY_TTL = 60 * 1000; - - /** - * Use DynamoDB for the metadata: {@value}. - */ + @Deprecated public static final String S3GUARD_METASTORE_DYNAMO = "org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore"; - - /** - * The warn level if S3Guard is disabled. - */ + @Deprecated public static final String S3GUARD_DISABLED_WARN_LEVEL = "fs.s3a.s3guard.disabled.warn.level"; + @Deprecated public static final String DEFAULT_S3GUARD_DISABLED_WARN_LEVEL = "SILENT"; /** * Inconsistency (visibility delay) injection settings. + * No longer used. */ - @InterfaceStability.Unstable + @Deprecated public static final String FAIL_INJECT_INCONSISTENCY_KEY = "fs.s3a.failinject.inconsistency.key.substring"; - @InterfaceStability.Unstable + @Deprecated public static final String FAIL_INJECT_INCONSISTENCY_MSEC = "fs.s3a.failinject.inconsistency.msec"; - @InterfaceStability.Unstable + @Deprecated public static final String FAIL_INJECT_INCONSISTENCY_PROBABILITY = "fs.s3a.failinject.inconsistency.probability"; @@ -984,17 +913,20 @@ public final class Constants { * Number of times to retry any repeatable S3 client request on failure, * excluding throttling requests: {@value}. */ + @Deprecated public static final String S3GUARD_CONSISTENCY_RETRY_LIMIT = "fs.s3a.s3guard.consistency.retry.limit"; /** * Default retry limit: {@value}. */ + @Deprecated public static final int S3GUARD_CONSISTENCY_RETRY_LIMIT_DEFAULT = 7; /** * Initial retry interval: {@value}. */ + @Deprecated public static final String S3GUARD_CONSISTENCY_RETRY_INTERVAL = "fs.s3a.s3guard.consistency.retry.interval"; @@ -1004,10 +936,12 @@ public final class Constants { * each probe can cause the S3 load balancers to retain any 404 in * its cache for longer. See HADOOP-16490. */ + @Deprecated public static final String S3GUARD_CONSISTENCY_RETRY_INTERVAL_DEFAULT = "2s"; public static final String AWS_SERVICE_IDENTIFIER_S3 = "S3"; + @Deprecated public static final String AWS_SERVICE_IDENTIFIER_DDB = "DDB"; public static final String AWS_SERVICE_IDENTIFIER_STS = "STS"; diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/FailureInjectionPolicy.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/FailureInjectionPolicy.java index f6bfc9aa928..f3e00cc61d7 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/FailureInjectionPolicy.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/FailureInjectionPolicy.java @@ -28,11 +28,6 @@ import static org.apache.hadoop.fs.s3a.Constants.*; /** * Simple object which stores current failure injection settings. - * "Delaying a key" can mean: - * - Removing it from the S3 client's listings while delay is in effect. - * - Causing input stream reads to fail. - * - Causing the S3 side of getFileStatus(), i.e. - * AmazonS3#getObjectMetadata(), to throw FileNotFound. */ public class FailureInjectionPolicy { /** @@ -40,29 +35,9 @@ public class FailureInjectionPolicy { */ public static final String DEFAULT_DELAY_KEY_SUBSTRING = "DELAY_LISTING_ME"; - /** - * How many seconds affected keys will have delayed visibility. - * This should probably be a config value. - */ - public static final long DEFAULT_DELAY_KEY_MSEC = 5 * 1000; - - public static final float DEFAULT_DELAY_KEY_PROBABILITY = 1.0f; - - /** Special config value since we can't store empty strings in XML. */ - public static final String MATCH_ALL_KEYS = "*"; - private static final Logger LOG = LoggerFactory.getLogger(InconsistentAmazonS3Client.class); - /** Empty string matches all keys. */ - private String delayKeySubstring; - - /** Probability to delay visibility of a matching key. */ - private float delayKeyProbability; - - /** Time in milliseconds to delay visibility of newly modified object. */ - private long delayKeyMsec; - /** * Probability of throttling a request. */ @@ -75,33 +50,10 @@ public class FailureInjectionPolicy { public FailureInjectionPolicy(Configuration conf) { - this.delayKeySubstring = conf.get(FAIL_INJECT_INCONSISTENCY_KEY, - DEFAULT_DELAY_KEY_SUBSTRING); - // "" is a substring of all strings, use it to match all keys. - if (this.delayKeySubstring.equals(MATCH_ALL_KEYS)) { - this.delayKeySubstring = ""; - } - this.delayKeyProbability = validProbability( - conf.getFloat(FAIL_INJECT_INCONSISTENCY_PROBABILITY, - DEFAULT_DELAY_KEY_PROBABILITY)); - this.delayKeyMsec = conf.getLong(FAIL_INJECT_INCONSISTENCY_MSEC, - DEFAULT_DELAY_KEY_MSEC); this.setThrottleProbability(conf.getFloat(FAIL_INJECT_THROTTLE_PROBABILITY, 0.0f)); } - public String getDelayKeySubstring() { - return delayKeySubstring; - } - - public float getDelayKeyProbability() { - return delayKeyProbability; - } - - public long getDelayKeyMsec() { - return delayKeyMsec; - } - public float getThrottleProbability() { return throttleProbability; } @@ -126,25 +78,10 @@ public class FailureInjectionPolicy { return Math.random() < p; } - /** - * Should we delay listing visibility for this key? - * @param key key which is being put - * @return true if we should delay - */ - public boolean shouldDelay(String key) { - float p = getDelayKeyProbability(); - boolean delay = key.contains(getDelayKeySubstring()); - delay = delay && trueWithProbability(p); - LOG.debug("{}, p={} -> {}", key, p, delay); - return delay; - } - @Override public String toString() { return String.format("FailureInjectionPolicy:" + - " %s msec delay, substring %s, delay probability %s;" + " throttle probability %s" + "; failure limit %d", - delayKeyMsec, delayKeySubstring, delayKeyProbability, throttleProbability, failureLimit); } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/InconsistentAmazonS3Client.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/InconsistentAmazonS3Client.java index e54c21d853e..317f2c78576 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/InconsistentAmazonS3Client.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/InconsistentAmazonS3Client.java @@ -18,13 +18,8 @@ package org.apache.hadoop.fs.s3a; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; @@ -60,12 +55,13 @@ import org.slf4j.LoggerFactory; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.Path; /** * A wrapper around {@link com.amazonaws.services.s3.AmazonS3} that injects - * inconsistency and/or errors. Used for testing S3Guard. - * Currently only delays listing visibility, not affecting GET. + * failures. + * It used to also inject inconsistency, but this was removed with S3Guard; + * what is retained is the ability to throttle AWS operations and for the + * input stream to be inconsistent. */ @InterfaceAudience.Private @InterfaceStability.Unstable @@ -81,38 +77,6 @@ public class InconsistentAmazonS3Client extends AmazonS3Client { */ private final AtomicLong failureCounter = new AtomicLong(0); - /** - * Composite of data we need to track about recently deleted objects: - * when it was deleted (same was with recently put objects) and the object - * summary (since we should keep returning it for sometime after its - * deletion). - */ - private static class Delete { - private Long time; - private S3ObjectSummary summary; - - Delete(Long time, S3ObjectSummary summary) { - this.time = time; - this.summary = summary; - } - - public Long time() { - return time; - } - - public S3ObjectSummary summary() { - return summary; - } - } - - /** - * Map of key to delay -> time it was deleted + object summary (object summary - * is null for prefixes. - */ - private Map delayedDeletes = new HashMap<>(); - - /** Map of key to delay -> time it was created. */ - private Map delayedPutKeys = new HashMap<>(); /** * Instantiate. @@ -130,19 +94,6 @@ public class InconsistentAmazonS3Client extends AmazonS3Client { policy = new FailureInjectionPolicy(conf); } - - /** - * Clear any accumulated inconsistency state. Used by tests to make paths - * visible again. - * @param fs S3AFileSystem under test - * @throws Exception on failure - */ - public static void clearInconsistency(S3AFileSystem fs) throws Exception { - AmazonS3 s3 = fs.getAmazonS3ClientForTesting("s3guard"); - InconsistentAmazonS3Client ic = InconsistentAmazonS3Client.castFrom(s3); - ic.clearInconsistency(); - } - /** * A way for tests to patch in a different fault injection policy at runtime. * @param fs filesystem under test @@ -166,17 +117,6 @@ public class InconsistentAmazonS3Client extends AmazonS3Client { policy, failureCounter.get()); } - /** - * Clear all oustanding inconsistent keys. After calling this function, - * listings should behave normally (no failure injection), until additional - * keys are matched for delay, e.g. via putObject(), deleteObject(). - */ - public void clearInconsistency() { - LOG.info("clearing all delayed puts / deletes"); - delayedDeletes.clear(); - delayedPutKeys.clear(); - } - /** * Convenience function for test code to cast from supertype. * @param c supertype to cast from @@ -199,12 +139,6 @@ public class InconsistentAmazonS3Client extends AmazonS3Client { deleteObjectsRequest) throws AmazonClientException, AmazonServiceException { maybeFail(); - LOG.info("registering bulk delete of objects"); - for (DeleteObjectsRequest.KeyVersion keyVersion : - deleteObjectsRequest.getKeys()) { - registerDeleteObject(keyVersion.getKey(), - deleteObjectsRequest.getBucketName()); - } return super.deleteObjects(deleteObjectsRequest); } @@ -214,7 +148,6 @@ public class InconsistentAmazonS3Client extends AmazonS3Client { String key = deleteObjectRequest.getKey(); LOG.debug("key {}", key); maybeFail(); - registerDeleteObject(key, deleteObjectRequest.getBucketName()); super.deleteObject(deleteObjectRequest); } @@ -224,7 +157,6 @@ public class InconsistentAmazonS3Client extends AmazonS3Client { throws AmazonClientException, AmazonServiceException { LOG.debug("key {}", putObjectRequest.getKey()); maybeFail(); - registerPutObject(putObjectRequest); return super.putObject(putObjectRequest); } @@ -233,283 +165,17 @@ public class InconsistentAmazonS3Client extends AmazonS3Client { public ObjectListing listObjects(ListObjectsRequest listObjectsRequest) throws AmazonClientException, AmazonServiceException { maybeFail(); - return innerlistObjects(listObjectsRequest); + return super.listObjects(listObjectsRequest); } - /** - * Run the list object call without any failure probability. - * This stops a very aggressive failure rate from completely overloading - * the retry logic. - * @param listObjectsRequest request - * @return listing - * @throws AmazonClientException failure - */ - private ObjectListing innerlistObjects(ListObjectsRequest listObjectsRequest) - throws AmazonClientException, AmazonServiceException { - LOG.debug("prefix {}", listObjectsRequest.getPrefix()); - ObjectListing listing = super.listObjects(listObjectsRequest); - listing = filterListObjects(listing); - listing = restoreListObjects(listObjectsRequest, listing); - return listing; - } - - /* We should only need to override these versions of listObjects() */ + /* consistent listing with possibility of failing. */ @Override public ListObjectsV2Result listObjectsV2(ListObjectsV2Request request) throws AmazonClientException, AmazonServiceException { maybeFail(); - return innerListObjectsV2(request); + return super.listObjectsV2(request); } - /** - * Non failing V2 list object request. - * @param request request - * @return result. - */ - private ListObjectsV2Result innerListObjectsV2(ListObjectsV2Request request) { - LOG.debug("prefix {}", request.getPrefix()); - ListObjectsV2Result listing = super.listObjectsV2(request); - listing = filterListObjectsV2(listing); - listing = restoreListObjectsV2(request, listing); - return listing; - } - - private void addSummaryIfNotPresent(List list, - S3ObjectSummary item) { - // Behavior of S3ObjectSummary - String key = item.getKey(); - if (list.stream().noneMatch((member) -> member.getKey().equals(key))) { - LOG.debug("Reinstate summary {}", key); - list.add(item); - } - } - - /** - * Add prefix of child to given list. The added prefix will be equal to - * ancestor plus one directory past ancestor. e.g.: - * if ancestor is "/a/b/c" and child is "/a/b/c/d/e/file" then "a/b/c/d" is - * added to list. - * @param prefixes list to add to - * @param ancestor path we are listing in - * @param child full path to get prefix from - */ - private void addPrefixIfNotPresent(List prefixes, String ancestor, - String child) { - Path prefixCandidate = new Path(child).getParent(); - Path ancestorPath = new Path(ancestor); - Preconditions.checkArgument(child.startsWith(ancestor), "%s does not " + - "start with %s", child, ancestor); - while (!prefixCandidate.isRoot()) { - Path nextParent = prefixCandidate.getParent(); - if (nextParent.equals(ancestorPath)) { - String prefix = prefixCandidate.toString(); - if (!prefixes.contains(prefix)) { - LOG.debug("Reinstate prefix {}", prefix); - prefixes.add(prefix); - } - return; - } - prefixCandidate = nextParent; - } - } - - /** - * Checks that the parent key is an ancestor of the child key. - * @param parent key that may be the parent. - * @param child key that may be the child. - * @param recursive if false, only return true for direct children. If - * true, any descendant will count. - * @return true if parent is an ancestor of child - */ - private boolean isDescendant(String parent, String child, boolean recursive) { - if (recursive) { - if (!parent.endsWith("/")) { - parent = parent + "/"; - } - return child.startsWith(parent); - } else { - Path actualParentPath = new Path(child).getParent(); - Path expectedParentPath = new Path(parent); - // children which are directory markers are excluded here - return actualParentPath.equals(expectedParentPath) - && !child.endsWith("/"); - } - } - - /** - * Simulate eventual consistency of delete for this list operation: Any - * recently-deleted keys will be added. - * @param request List request - * @param rawListing listing returned from underlying S3 - * @return listing with recently-deleted items restored - */ - private ObjectListing restoreListObjects(ListObjectsRequest request, - ObjectListing rawListing) { - List outputList = rawListing.getObjectSummaries(); - List outputPrefixes = rawListing.getCommonPrefixes(); - // recursive list has no delimiter, returns everything that matches a - // prefix. - boolean recursiveObjectList = !("/".equals(request.getDelimiter())); - String prefix = request.getPrefix(); - - restoreDeleted(outputList, outputPrefixes, recursiveObjectList, prefix); - return new CustomObjectListing(rawListing, outputList, outputPrefixes); - } - - /** - * V2 list API variant of - * {@link #restoreListObjects(ListObjectsRequest, ObjectListing)}. - * @param request original v2 list request - * @param result raw s3 result - */ - private ListObjectsV2Result restoreListObjectsV2(ListObjectsV2Request request, - ListObjectsV2Result result) { - List outputList = result.getObjectSummaries(); - List outputPrefixes = result.getCommonPrefixes(); - // recursive list has no delimiter, returns everything that matches a - // prefix. - boolean recursiveObjectList = !("/".equals(request.getDelimiter())); - String prefix = request.getPrefix(); - - restoreDeleted(outputList, outputPrefixes, recursiveObjectList, prefix); - return new CustomListObjectsV2Result(result, outputList, outputPrefixes); - } - - - /** - * Main logic for - * {@link #restoreListObjects(ListObjectsRequest, ObjectListing)} and - * the v2 variant above. - * @param summaries object summary list to modify. - * @param prefixes prefix list to modify - * @param recursive true if recursive list request - * @param prefix prefix for original list request - */ - private void restoreDeleted(List summaries, - List prefixes, boolean recursive, String prefix) { - - // Go through all deleted keys - for (String key : new HashSet<>(delayedDeletes.keySet())) { - Delete delete = delayedDeletes.get(key); - if (isKeyDelayed(delete.time(), key)) { - if (isDescendant(prefix, key, recursive)) { - if (delete.summary() != null) { - addSummaryIfNotPresent(summaries, delete.summary()); - } - } - // Non-recursive list has delimiter: will return rolled-up prefixes for - // all keys that are not direct children - if (!recursive) { - if (isDescendant(prefix, key, true)) { - addPrefixIfNotPresent(prefixes, prefix, key); - } - } - } else { - // Clean up any expired entries - LOG.debug("Remove expired key {}", key); - delayedDeletes.remove(key); - } - } - } - - private ObjectListing filterListObjects(ObjectListing rawListing) { - - // Filter object listing - List outputList = filterSummaries( - rawListing.getObjectSummaries()); - - // Filter prefixes (directories) - List outputPrefixes = filterPrefixes( - rawListing.getCommonPrefixes()); - - return new CustomObjectListing(rawListing, outputList, outputPrefixes); - } - - private ListObjectsV2Result filterListObjectsV2(ListObjectsV2Result raw) { - // Filter object listing - List outputList = filterSummaries( - raw.getObjectSummaries()); - - // Filter prefixes (directories) - List outputPrefixes = filterPrefixes(raw.getCommonPrefixes()); - - return new CustomListObjectsV2Result(raw, outputList, outputPrefixes); - } - - private List filterSummaries( - List summaries) { - List outputList = new ArrayList<>(); - for (S3ObjectSummary s : summaries) { - String key = s.getKey(); - if (!isKeyDelayed(delayedPutKeys.get(key), key)) { - outputList.add(s); - } - } - return outputList; - } - - private List filterPrefixes(List prefixes) { - return prefixes.stream() - .filter(key -> !isKeyDelayed(delayedPutKeys.get(key), key)) - .collect(Collectors.toList()); - } - - private boolean isKeyDelayed(Long enqueueTime, String key) { - if (enqueueTime == null) { - LOG.debug("no delay for key {}", key); - return false; - } - long currentTime = System.currentTimeMillis(); - long deadline = enqueueTime + policy.getDelayKeyMsec(); - if (currentTime >= deadline) { - delayedDeletes.remove(key); - LOG.debug("no longer delaying {}", key); - return false; - } else { - LOG.info("delaying {}", key); - return true; - } - } - - private void registerDeleteObject(String key, String bucket) { - if (policy.shouldDelay(key)) { - Delete delete = delayedDeletes.get(key); - if (delete != null && isKeyDelayed(delete.time(), key)) { - // there is already an entry in the delayed delete list, - // so ignore the operation - LOG.debug("Ignoring delete of already deleted object"); - } else { - // Record summary so we can add it back for some time post-deletion - ListObjectsRequest request = new ListObjectsRequest() - .withBucketName(bucket) - .withPrefix(key); - S3ObjectSummary summary = innerlistObjects(request).getObjectSummaries() - .stream() - .filter(result -> result.getKey().equals(key)) - .findFirst() - .orElse(null); - delayedDeletes.put(key, new Delete(System.currentTimeMillis(), - summary)); - } - } - } - - private void registerPutObject(PutObjectRequest req) { - String key = req.getKey(); - if (policy.shouldDelay(key)) { - enqueueDelayedPut(key); - } - } - - /** - * Record this key as something that should not become visible in - * listObject replies for a while, to simulate eventual list consistency. - * @param key key to delay visibility of - */ - private void enqueueDelayedPut(String key) { - LOG.debug("delaying put of {}", key); - delayedPutKeys.put(key, System.currentTimeMillis()); - } @Override public CompleteMultipartUploadResult completeMultipartUpload( @@ -542,10 +208,6 @@ public class InconsistentAmazonS3Client extends AmazonS3Client { return super.listMultipartUploads(listMultipartUploadsRequest); } - public long getDelayKeyMsec() { - return policy.getDelayKeyMsec(); - } - /** * Set the probability of throttling a request. * @param throttleProbability the probability of a request being throttled. @@ -565,7 +227,7 @@ public class InconsistentAmazonS3Client extends AmazonS3Client { throws AmazonClientException { // code structure here is to line up for more failures later AmazonServiceException ex = null; - if (policy.trueWithProbability(policy.getThrottleProbability())) { + if (FailureInjectionPolicy.trueWithProbability(policy.getThrottleProbability())) { // throttle the request ex = new AmazonServiceException(errorMsg + " count = " + (failureCounter.get() + 1), null); @@ -599,18 +261,16 @@ public class InconsistentAmazonS3Client extends AmazonS3Client { @Override public S3Object getObject(GetObjectRequest var1) throws SdkClientException, AmazonServiceException { - maybeFail("file not found", 404); - S3Object o = super.getObject(var1); - LOG.debug("Wrapping in InconsistentS3Object for key {}", var1.getKey()); - return new InconsistentS3Object(o, policy); + maybeFail(); + return super.getObject(var1); } @Override public S3Object getObject(String bucketName, String key) throws SdkClientException, AmazonServiceException { - S3Object o = super.getObject(bucketName, key); - LOG.debug("Wrapping in InconsistentS3Object for key {}", key); - return new InconsistentS3Object(o, policy); + maybeFail(); + return super.getObject(bucketName, key); + } /** Since ObjectListing is immutable, we just override it with wrapper. */ diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/InconsistentS3ClientFactory.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/InconsistentS3ClientFactory.java index c11581f1d5d..4bfcc8aba3a 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/InconsistentS3ClientFactory.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/InconsistentS3ClientFactory.java @@ -42,6 +42,7 @@ public class InconsistentS3ClientFactory extends DefaultS3ClientFactory { final ClientConfiguration awsConf, final S3ClientCreationParameters parameters) { LOG.warn("** FAILURE INJECTION ENABLED. Do not run in production! **"); + LOG.warn("List inconsistency is no longer emulated; only throttling and read errors"); InconsistentAmazonS3Client s3 = new InconsistentAmazonS3Client( parameters.getCredentialSet(), awsConf, getConf()); diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/InconsistentS3Object.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/InconsistentS3Object.java deleted file mode 100644 index 496ca1b9085..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/InconsistentS3Object.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * 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.hadoop.fs.s3a; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; - -import com.amazonaws.services.s3.internal.AmazonS3ExceptionBuilder; -import com.amazonaws.services.s3.model.AmazonS3Exception; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.S3Object; -import com.amazonaws.services.s3.model.S3ObjectInputStream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Wrapper around S3Object so we can do failure injection on - * getObjectContent() and S3ObjectInputStream. - * See also {@link InconsistentAmazonS3Client}. - */ -@SuppressWarnings({"NonSerializableFieldInSerializableClass", "serial"}) -public class InconsistentS3Object extends S3Object { - - // This should be configurable, probably. - public static final int MAX_READ_FAILURES = 100; - - private static int readFailureCounter = 0; - private transient S3Object wrapped; - private transient FailureInjectionPolicy policy; - private final static transient Logger LOG = LoggerFactory.getLogger( - InconsistentS3Object.class); - - public InconsistentS3Object(S3Object wrapped, FailureInjectionPolicy policy) { - this.wrapped = wrapped; - this.policy = policy; - } - - @Override - public S3ObjectInputStream getObjectContent() { - return new InconsistentS3InputStream(wrapped.getObjectContent()); - } - - @Override - public String toString() { - return "InconsistentS3Object wrapping: " + wrapped.toString(); - } - - @Override - public ObjectMetadata getObjectMetadata() { - return wrapped.getObjectMetadata(); - } - - @Override - public void setObjectMetadata(ObjectMetadata metadata) { - wrapped.setObjectMetadata(metadata); - } - - @Override - public void setObjectContent(S3ObjectInputStream objectContent) { - wrapped.setObjectContent(objectContent); - } - - @Override - public void setObjectContent(InputStream objectContent) { - wrapped.setObjectContent(objectContent); - } - - @Override - public String getBucketName() { - return wrapped.getBucketName(); - } - - @Override - public void setBucketName(String bucketName) { - wrapped.setBucketName(bucketName); - } - - @Override - public String getKey() { - return wrapped.getKey(); - } - - @Override - public void setKey(String key) { - wrapped.setKey(key); - } - - @Override - public String getRedirectLocation() { - return wrapped.getRedirectLocation(); - } - - @Override - public void setRedirectLocation(String redirectLocation) { - wrapped.setRedirectLocation(redirectLocation); - } - - @Override - public Integer getTaggingCount() { - return wrapped.getTaggingCount(); - } - - @Override - public void setTaggingCount(Integer taggingCount) { - wrapped.setTaggingCount(taggingCount); - } - - @Override - public void close() throws IOException { - wrapped.close(); - } - - @Override - public boolean isRequesterCharged() { - return wrapped.isRequesterCharged(); - } - - @Override - public void setRequesterCharged(boolean isRequesterCharged) { - wrapped.setRequesterCharged(isRequesterCharged); - } - - private AmazonS3Exception mockException(String msg, int httpResponse) { - AmazonS3ExceptionBuilder builder = new AmazonS3ExceptionBuilder(); - builder.setErrorMessage(msg); - builder.setStatusCode(httpResponse); // this is the important part - builder.setErrorCode(String.valueOf(httpResponse)); - return builder.build(); - } - - /** - * Insert a failiure injection point for a read call. - * @throw IOException, as codepath is on InputStream, not other SDK call. - */ - private void readFailpoint(int off, int len) throws IOException { - if (shouldInjectFailure(getKey())) { - String error = String.format( - "read(b, %d, %d) on key %s failed: injecting error %d/%d" + - " for test.", off, len, getKey(), readFailureCounter, - MAX_READ_FAILURES); - throw new FileNotFoundException(error); - } - } - - /** - * Insert a failiure injection point for an InputStream skip() call. - * @throw IOException, as codepath is on InputStream, not other SDK call. - */ - private void skipFailpoint(long len) throws IOException { - if (shouldInjectFailure(getKey())) { - String error = String.format( - "skip(%d) on key %s failed: injecting error %d/%d for test.", - len, getKey(), readFailureCounter, MAX_READ_FAILURES); - throw new FileNotFoundException(error); - } - } - - private boolean shouldInjectFailure(String key) { - if (policy.shouldDelay(key) && - readFailureCounter < MAX_READ_FAILURES) { - readFailureCounter++; - return true; - } - return false; - } - - /** - * Wraps S3ObjectInputStream and implements failure injection. - */ - protected class InconsistentS3InputStream extends S3ObjectInputStream { - private S3ObjectInputStream wrapped; - - public InconsistentS3InputStream(S3ObjectInputStream wrapped) { - // seems awkward to have the stream wrap itself. - super(wrapped, wrapped.getHttpRequest()); - this.wrapped = wrapped; - } - - @Override - public void abort() { - wrapped.abort(); - } - - @Override - public int available() throws IOException { - return wrapped.available(); - } - - @Override - public void close() throws IOException { - wrapped.close(); - } - - @Override - public long skip(long n) throws IOException { - skipFailpoint(n); - return wrapped.skip(n); - } - - @Override - public int read() throws IOException { - LOG.debug("read() for key {}", getKey()); - readFailpoint(0, 1); - return wrapped.read(); - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - LOG.debug("read(b, {}, {}) for key {}", off, len, getKey()); - readFailpoint(off, len); - return wrapped.read(b, off, len); - } - - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Invoker.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Invoker.java index a851f0fbac6..0663fe935db 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Invoker.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Invoker.java @@ -21,11 +21,11 @@ package org.apache.hadoop.fs.s3a; import java.io.IOException; import java.io.InterruptedIOException; import java.util.Optional; +import java.util.concurrent.Future; import javax.annotation.Nullable; import com.amazonaws.AmazonClientException; import com.amazonaws.SdkBaseException; -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,7 +34,9 @@ import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.io.retry.RetryPolicy; import org.apache.hadoop.util.DurationInfo; import org.apache.hadoop.util.functional.CallableRaisingIOE; +import org.apache.hadoop.util.functional.FutureIO; import org.apache.hadoop.util.functional.InvocationRaisingIOE; +import org.apache.hadoop.util.Preconditions; /** * Class to provide lambda expression invocation of AWS operations. @@ -137,6 +139,30 @@ public class Invoker { }); } + + /** + * + * Wait for a future, translating AmazonClientException into an IOException. + * @param action action to execute (used in error messages) + * @param path path of work (used in error messages) + * @param future future to await for + * @param type of return value + * @return the result of the function call + * @throws IOException any IOE raised, or translated exception + * @throws RuntimeException any other runtime exception + */ + @Retries.OnceTranslated + public static T onceInTheFuture(String action, + String path, + final Future future) + throws IOException { + try (DurationInfo ignored = new DurationInfo(LOG, false, "%s", action)) { + return FutureIO.awaitFuture(future); + } catch (AmazonClientException e) { + throw S3AUtils.translateException(action, path, e); + } + } + /** * Execute an operation and ignore all raised IOExceptions; log at INFO; * full stack only at DEBUG. diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Listing.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Listing.java index 5a4f5514d50..a1dd4d8df02 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Listing.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Listing.java @@ -18,15 +18,10 @@ package org.apache.hadoop.fs.s3a; -import javax.annotation.Nullable; - -import com.amazonaws.AmazonClientException; import com.amazonaws.services.s3.model.S3ObjectSummary; -import org.apache.hadoop.thirdparty.com.google.common.annotations.VisibleForTesting; - -import org.apache.commons.lang3.tuple.Triple; import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.VisibleForTesting; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.LocatedFileStatus; import org.apache.hadoop.fs.Path; @@ -35,10 +30,6 @@ import org.apache.hadoop.fs.RemoteIterator; import org.apache.hadoop.fs.s3a.impl.AbstractStoreOperation; import org.apache.hadoop.fs.s3a.impl.ListingOperationCallbacks; import org.apache.hadoop.fs.s3a.impl.StoreContext; -import org.apache.hadoop.fs.s3a.s3guard.DirListingMetadata; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStoreListFilesIterator; -import org.apache.hadoop.fs.s3a.s3guard.PathMetadata; -import org.apache.hadoop.fs.s3a.s3guard.S3Guard; import org.apache.hadoop.fs.statistics.IOStatistics; import org.apache.hadoop.fs.statistics.IOStatisticsSource; import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; @@ -48,30 +39,21 @@ import org.apache.hadoop.util.functional.RemoteIterators; import org.slf4j.Logger; import java.io.Closeable; -import java.io.FileNotFoundException; import java.io.IOException; -import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.ListIterator; -import java.util.Map; import java.util.NoSuchElementException; -import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.StringJoiner; -import static org.apache.hadoop.fs.impl.FutureIOSupport.awaitFuture; import static org.apache.hadoop.fs.s3a.Constants.S3N_FOLDER_SUFFIX; +import static org.apache.hadoop.fs.s3a.Invoker.onceInTheFuture; import static org.apache.hadoop.fs.s3a.S3AUtils.ACCEPT_ALL; import static org.apache.hadoop.fs.s3a.S3AUtils.createFileStatus; import static org.apache.hadoop.fs.s3a.S3AUtils.maybeAddTrailingSlash; import static org.apache.hadoop.fs.s3a.S3AUtils.objectRepresentsDirectory; import static org.apache.hadoop.fs.s3a.S3AUtils.stringify; -import static org.apache.hadoop.fs.s3a.S3AUtils.translateException; import static org.apache.hadoop.fs.s3a.auth.RoleModel.pathToKey; import static org.apache.hadoop.fs.statistics.StoreStatisticNames.OBJECT_CONTINUE_LIST_REQUEST; import static org.apache.hadoop.fs.statistics.StoreStatisticNames.OBJECT_LIST_REQUEST; @@ -136,30 +118,6 @@ public class Listing extends AbstractStoreOperation { Listing.ACCEPT_ALL_BUT_S3N::accept); } - /** - * Create a FileStatus iterator against a path, with a given list object - * request. - * - * @param listPath path of the listing - * @param request initial request to make - * @param filter the filter on which paths to accept - * @param acceptor the class/predicate to decide which entries to accept - * in the listing based on the full file status. - * @param span audit span for this iterator - * @return the iterator - * @throws IOException IO Problems - */ - @Retries.RetryRaw - public FileStatusListingIterator createFileStatusListingIterator( - Path listPath, - S3ListRequest request, - PathFilter filter, - Listing.FileStatusAcceptor acceptor, - AuditSpan span) throws IOException { - return createFileStatusListingIterator(listPath, request, filter, acceptor, - null, span); - } - /** * Create a FileStatus iterator against a path, with a given * list object request. @@ -168,8 +126,6 @@ public class Listing extends AbstractStoreOperation { * @param filter the filter on which paths to accept * @param acceptor the class/predicate to decide which entries to accept * in the listing based on the full file status. - * @param providedStatus the provided list of file status, which may contain - * items that are not listed from source. * @param span audit span for this iterator * @return the iterator * @throws IOException IO Problems @@ -179,14 +135,12 @@ public class Listing extends AbstractStoreOperation { Path listPath, S3ListRequest request, PathFilter filter, - Listing.FileStatusAcceptor acceptor, - RemoteIterator providedStatus, + FileStatusAcceptor acceptor, AuditSpan span) throws IOException { return new FileStatusListingIterator( createObjectListingIterator(listPath, request, span), filter, - acceptor, - providedStatus); + acceptor); } /** @@ -219,28 +173,6 @@ public class Listing extends AbstractStoreOperation { listingOperationCallbacks::toLocatedFileStatus); } - /** - * Create an located status iterator that wraps another to filter out a set - * of recently deleted items. - * @param iterator an iterator over the remote located status entries. - * @param tombstones set of paths that are recently deleted and should be - * filtered. - * @return a new remote iterator. - */ - @VisibleForTesting - RemoteIterator createTombstoneReconcilingIterator( - RemoteIterator iterator, - @Nullable Set tombstones) { - if (tombstones == null || tombstones.isEmpty()) { - // no need to filter. - return iterator; - } else { - return filteringRemoteIterator( - iterator, - candidate -> !tombstones.contains(candidate.getPath())); - } - } - /** * Create a remote iterator from a single status entry. * @param status status @@ -256,20 +188,14 @@ public class Listing extends AbstractStoreOperation { * @param path input path. * @param recursive recursive listing? * @param acceptor file status filter - * @param collectTombstones should tombstones be collected from S3Guard? - * @param forceNonAuthoritativeMS forces metadata store to act like non - * authoritative. This is useful when - * listFiles output is used by import tool. * @param span audit span for this iterator * @return an iterator over listing. * @throws IOException any exception. */ public RemoteIterator getListFilesAssumingDir( - Path path, - boolean recursive, Listing.FileStatusAcceptor acceptor, - boolean collectTombstones, - boolean forceNonAuthoritativeMS, - AuditSpan span) throws IOException { + Path path, + boolean recursive, FileStatusAcceptor acceptor, + AuditSpan span) throws IOException { String key = maybeAddTrailingSlash(pathToKey(path)); String delimiter = recursive ? null : "/"; @@ -279,82 +205,19 @@ public class Listing extends AbstractStoreOperation { LOG.debug("Requesting all entries under {} with delimiter '{}'", key, delimiter); } - final RemoteIterator cachedFilesIterator; - final Set tombstones; - boolean allowAuthoritative = listingOperationCallbacks - .allowAuthoritative(path); - if (recursive) { - final PathMetadata pm = getStoreContext() - .getMetadataStore() - .get(path, true); - if (pm != null) { - if (pm.isDeleted()) { - OffsetDateTime deletedAt = OffsetDateTime - .ofInstant(Instant.ofEpochMilli( - pm.getFileStatus().getModificationTime()), - ZoneOffset.UTC); - throw new FileNotFoundException("Path " + path + " is recorded as " + - "deleted by S3Guard at " + deletedAt); - } - } - MetadataStoreListFilesIterator metadataStoreListFilesIterator = - new MetadataStoreListFilesIterator( - getStoreContext().getMetadataStore(), - pm, - allowAuthoritative); - tombstones = metadataStoreListFilesIterator.listTombstones(); - // if all of the below is true - // - authoritative access is allowed for this metadatastore - // for this directory, - // - all the directory listings are authoritative on the client - // - the caller does not force non-authoritative access - // return the listing without any further s3 access - if (!forceNonAuthoritativeMS && - allowAuthoritative && - metadataStoreListFilesIterator.isRecursivelyAuthoritative()) { - S3AFileStatus[] statuses = S3AUtils.iteratorToStatuses( - metadataStoreListFilesIterator, tombstones); - cachedFilesIterator = createProvidedFileStatusIterator( - statuses, ACCEPT_ALL, acceptor); - return createLocatedFileStatusIterator(cachedFilesIterator); - } - cachedFilesIterator = metadataStoreListFilesIterator; - } else { - DirListingMetadata meta = - S3Guard.listChildrenWithTtl( - getStoreContext().getMetadataStore(), - path, - listingOperationCallbacks.getUpdatedTtlTimeProvider(), - allowAuthoritative); - if (meta != null) { - tombstones = meta.listTombstones(); - } else { - tombstones = null; - } - cachedFilesIterator = createProvidedFileStatusIterator( - S3Guard.dirMetaToStatuses(meta), ACCEPT_ALL, acceptor); - if (allowAuthoritative && meta != null && meta.isAuthoritative()) { - // metadata listing is authoritative, so return it directly - return createLocatedFileStatusIterator(cachedFilesIterator); - } - } - return createTombstoneReconcilingIterator( - createLocatedFileStatusIterator( - createFileStatusListingIterator(path, - listingOperationCallbacks - .createListObjectsRequest(key, - delimiter, - span), - ACCEPT_ALL, - acceptor, - cachedFilesIterator, - span)), - collectTombstones ? tombstones : null); + return createLocatedFileStatusIterator( + createFileStatusListingIterator(path, + listingOperationCallbacks + .createListObjectsRequest(key, + delimiter, + span), + ACCEPT_ALL, + acceptor, + span)); } /** * Generate list located status for a directory. - * Also performing tombstone reconciliation for guarded directories. * @param dir directory to check. * @param filter a path filter. * @param span audit span for this iterator @@ -365,51 +228,14 @@ public class Listing extends AbstractStoreOperation { Path dir, PathFilter filter, AuditSpan span) throws IOException { span.activate(); final String key = maybeAddTrailingSlash(pathToKey(dir)); - final Listing.FileStatusAcceptor acceptor = - new Listing.AcceptAllButSelfAndS3nDirs(dir); - boolean allowAuthoritative = listingOperationCallbacks - .allowAuthoritative(dir); - DirListingMetadata meta = - S3Guard.listChildrenWithTtl(getStoreContext().getMetadataStore(), - dir, - listingOperationCallbacks - .getUpdatedTtlTimeProvider(), - allowAuthoritative); - if (meta != null) { - // there's metadata - // convert to an iterator - final RemoteIterator cachedFileStatusIterator = - createProvidedFileStatusIterator( - S3Guard.dirMetaToStatuses(meta), filter, acceptor); - // if the dir is authoritative and the data considers itself - // to be authorititative. - if (allowAuthoritative && meta.isAuthoritative()) { - // return the list - return createLocatedFileStatusIterator(cachedFileStatusIterator); - } else { - // merge the datasets - return createTombstoneReconcilingIterator( - createLocatedFileStatusIterator( - createFileStatusListingIterator(dir, - listingOperationCallbacks - .createListObjectsRequest(key, "/", span), - filter, - acceptor, - cachedFileStatusIterator, - span)), - meta.listTombstones()); - } - } else { - // Unguarded - return createLocatedFileStatusIterator( - createFileStatusListingIterator(dir, - listingOperationCallbacks - .createListObjectsRequest(key, "/", span), - filter, - acceptor, - span)); - } + return createLocatedFileStatusIterator( + createFileStatusListingIterator(dir, + listingOperationCallbacks + .createListObjectsRequest(key, "/", span), + filter, + new AcceptAllButSelfAndS3nDirs(dir), + span)); } /** @@ -417,10 +243,11 @@ public class Listing extends AbstractStoreOperation { * to be a non-empty directory. * @param path input path. * @param span audit span for this iterator - * @return Triple of file statuses, metaData, auth flag. + * @return iterator of file statuses. * @throws IOException Any IO problems. */ - public Triple, DirListingMetadata, Boolean> + @Retries.RetryRaw + public RemoteIterator getFileStatusesAssumingNonEmptyDir(Path path, final AuditSpan span) throws IOException { String key = pathToKey(path); @@ -428,39 +255,16 @@ public class Listing extends AbstractStoreOperation { key = key + '/'; } - boolean allowAuthoritative = listingOperationCallbacks - .allowAuthoritative(path); - DirListingMetadata dirMeta = - S3Guard.listChildrenWithTtl( - getStoreContext().getMetadataStore(), - path, - listingOperationCallbacks.getUpdatedTtlTimeProvider(), - allowAuthoritative); - // In auth mode return directly with auth flag. - if (allowAuthoritative && dirMeta != null && dirMeta.isAuthoritative()) { - RemoteIterator mfsItr = createProvidedFileStatusIterator( - S3Guard.dirMetaToStatuses(dirMeta), - ACCEPT_ALL, - Listing.ACCEPT_ALL_BUT_S3N); - return Triple.of(mfsItr, - dirMeta, Boolean.TRUE); - } - S3ListRequest request = createListObjectsRequest(key, "/", span); LOG.debug("listStatus: doing listObjects for directory {}", key); - FileStatusListingIterator filesItr = createFileStatusListingIterator( - path, - request, - ACCEPT_ALL, - new Listing.AcceptAllButSelfAndS3nDirs(path), - span); - // return the results obtained from s3. - return Triple.of( - filesItr, - dirMeta, - Boolean.FALSE); + return createFileStatusListingIterator( + path, + request, + ACCEPT_ALL, + new AcceptAllButSelfAndS3nDirs(path), + span); } public S3ListRequest createListObjectsRequest(String key, @@ -542,8 +346,6 @@ public class Listing extends AbstractStoreOperation { /** Iterator over the current set of results. */ private ListIterator statusBatchIterator; - private final Map providedStatus; - private Iterator providedStatusIterator; /** * Create an iterator over file status entries. @@ -551,27 +353,17 @@ public class Listing extends AbstractStoreOperation { * @param filter the filter on which paths to accept * @param acceptor the class/predicate to decide which entries to accept * in the listing based on the full file status. - * @param providedStatus the provided list of file status, which may contain - * items that are not listed from source. * @throws IOException IO Problems */ @Retries.RetryTranslated FileStatusListingIterator(ObjectListingIterator source, PathFilter filter, - FileStatusAcceptor acceptor, - @Nullable RemoteIterator providedStatus) + FileStatusAcceptor acceptor) throws IOException { this.source = source; this.filter = filter; this.acceptor = acceptor; - this.providedStatus = new HashMap<>(); - for (; providedStatus != null && providedStatus.hasNext();) { - final S3AFileStatus status = providedStatus.next(); - Path path = status.getPath(); - if (filter.accept(path) && acceptor.accept(status)) { - this.providedStatus.put(path, status); - } - } + // build the first set of results. This will not trigger any // remote IO, assuming the source iterator is in its initial // iteration @@ -586,26 +378,17 @@ public class Listing extends AbstractStoreOperation { * Lastly, return true if the {@code providedStatusIterator} * has left items. * @return true if a call to {@link #next()} will succeed. - * @throws IOException + * @throws IOException IO Problems */ @Override @Retries.RetryTranslated public boolean hasNext() throws IOException { - return sourceHasNext() || providedStatusIterator.hasNext(); + return sourceHasNext(); } @Retries.RetryTranslated private boolean sourceHasNext() throws IOException { - if (statusBatchIterator.hasNext() || requestNextBatch()) { - return true; - } else { - // turn to file status that are only in provided list - if (providedStatusIterator == null) { - LOG.debug("Start iterating the provided status."); - providedStatusIterator = providedStatus.values().iterator(); - } - return false; - } + return statusBatchIterator.hasNext() || requestNextBatch(); } @Override @@ -614,25 +397,8 @@ public class Listing extends AbstractStoreOperation { final S3AFileStatus status; if (sourceHasNext()) { status = statusBatchIterator.next(); - // We remove from provided map the file status listed by S3 so that - // this does not return duplicate items. - - // The provided status is returned as it is assumed to have the better - // metadata (i.e. the eTag and versionId from S3Guard) - S3AFileStatus provided = providedStatus.remove(status.getPath()); - if (provided != null) { - LOG.debug( - "Removed and returned the status from provided file status {}", - status); - return provided; - } } else { - if (providedStatusIterator.hasNext()) { - status = providedStatusIterator.next(); - LOG.debug("Returning provided file status {}", status); - } else { - throw new NoSuchElementException(); - } + throw new NoSuchElementException(); } return status; } @@ -865,24 +631,20 @@ public class Listing extends AbstractStoreOperation { // clear the firstListing flag for future calls. firstListing = false; // Calculating the result of last async list call. - objects = awaitFuture(s3ListResultFuture); + objects = onceInTheFuture("listObjects()", listPath.toString(), s3ListResultFuture); fetchNextBatchAsyncIfPresent(); } else { - try { - if (objectsPrev!= null && !objectsPrev.isTruncated()) { - // nothing more to request: fail. - throw new NoSuchElementException("No more results in listing of " - + listPath); - } - // Calculating the result of last async list call. - objects = awaitFuture(s3ListResultFuture); - // Requesting next batch of results. - fetchNextBatchAsyncIfPresent(); - listingCount++; - LOG.debug("New listing status: {}", this); - } catch (AmazonClientException e) { - throw translateException("listObjects()", listPath, e); + if (objectsPrev!= null && !objectsPrev.isTruncated()) { + // nothing more to request: fail. + throw new NoSuchElementException("No more results in listing of " + + listPath); } + // Calculating the result of last async list call. + objects = onceInTheFuture("listObjects()", listPath.toString(), s3ListResultFuture); + // Requesting next batch of results. + fetchNextBatchAsyncIfPresent(); + listingCount++; + LOG.debug("New listing status: {}", this); } // Storing the current result to be used by hasNext() call. objectsPrev = objects; @@ -891,9 +653,8 @@ public class Listing extends AbstractStoreOperation { /** * If there are more listings present, call for next batch async. - * @throws IOException */ - private void fetchNextBatchAsyncIfPresent() throws IOException { + private void fetchNextBatchAsyncIfPresent() { if (objects.isTruncated()) { LOG.debug("[{}], Requesting next {} objects under {}", listingCount, maxKeys, listPath); diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/MetadataPersistenceException.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/MetadataPersistenceException.java deleted file mode 100644 index e55b7e8a5b1..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/MetadataPersistenceException.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.hadoop.fs.s3a; - -import org.apache.hadoop.fs.PathIOException; - -/** - * Indicates the metadata associated with the given Path could not be persisted - * to the metadata store (e.g. S3Guard / DynamoDB). When this occurs, the - * file itself has been successfully written to S3, but the metadata may be out - * of sync. The metadata can be corrected with the "s3guard import" command - * provided by {@link org.apache.hadoop.fs.s3a.s3guard.S3GuardTool}. - */ -public class MetadataPersistenceException extends PathIOException { - - /** - * Constructs a MetadataPersistenceException. - * @param path path of the affected file - * @param cause cause of the issue - */ - public MetadataPersistenceException(String path, Throwable cause) { - super(path, cause); - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/RemoteFileChangedException.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/RemoteFileChangedException.java index ce0b9a819b8..a3d07df8c4e 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/RemoteFileChangedException.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/RemoteFileChangedException.java @@ -25,7 +25,7 @@ import org.apache.hadoop.fs.PathIOException; /** * Indicates the S3 object is out of sync with the expected version. Thrown in * cases such as when the object is updated while an {@link S3AInputStream} is - * open, or when a file expected was never found. + * open, or when a file to be renamed disappeared during the operation. */ @SuppressWarnings("serial") @InterfaceAudience.Public @@ -36,18 +36,10 @@ public class RemoteFileChangedException extends PathIOException { "Constraints of request were unsatisfiable"; /** - * While trying to get information on a file known to S3Guard, the - * file never became visible in S3. - */ - public static final String FILE_NEVER_FOUND = - "File to rename not found on guarded S3 store after repeated attempts"; - - /** - * The file wasn't found in rename after a single attempt -the unguarded - * codepath. + * The file disappeaded during a rename between LIST and COPY. */ public static final String FILE_NOT_FOUND_SINGLE_ATTEMPT = - "File to rename not found on unguarded S3 store"; + "File to rename disappeared during the rename operation."; /** * Constructs a RemoteFileChangedException. diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3ABlockOutputStream.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3ABlockOutputStream.java index ddb933d6eb9..3a6f25a0b40 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3ABlockOutputStream.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3ABlockOutputStream.java @@ -674,7 +674,7 @@ class S3ABlockOutputStream extends OutputStream implements } // downgrading. WARN_ON_SYNCABLE.warn("Application invoked the Syncable API against" - + " stream writing to {}. This is unsupported", + + " stream writing to {}. This is Unsupported", key); // and log at debug LOG.debug("Downgrading Syncable call", ex); diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AFileSystem.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AFileSystem.java index 8ce11e23f03..496c4dc5a6d 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AFileSystem.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AFileSystem.java @@ -26,15 +26,11 @@ import java.net.URI; import java.nio.file.AccessDeniedException; import java.text.DateFormat; import java.text.SimpleDateFormat; -import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.EnumSet; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -80,19 +76,13 @@ import com.amazonaws.services.s3.transfer.model.CopyResult; import com.amazonaws.services.s3.transfer.model.UploadResult; import com.amazonaws.event.ProgressListener; -import org.apache.hadoop.fs.s3a.audit.AuditSpanS3A; -import org.apache.hadoop.fs.s3a.impl.CopyFromLocalOperation; -import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; -import org.apache.hadoop.fs.store.audit.ActiveThreadSpanSource; -import org.apache.hadoop.thirdparty.com.google.common.annotations.VisibleForTesting; -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.commons.lang3.tuple.Pair; -import org.apache.commons.lang3.tuple.Triple; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.classification.VisibleForTesting; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.CommonPathCapabilities; import org.apache.hadoop.fs.ContentSummary; @@ -102,12 +92,14 @@ import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.Globber; import org.apache.hadoop.fs.impl.OpenFileParameters; import org.apache.hadoop.fs.permission.FsAction; +import org.apache.hadoop.fs.s3a.audit.AuditSpanS3A; import org.apache.hadoop.fs.s3a.auth.SignerManager; import org.apache.hadoop.fs.s3a.auth.delegation.DelegationOperations; import org.apache.hadoop.fs.s3a.auth.delegation.DelegationTokenProvider; import org.apache.hadoop.fs.s3a.impl.BulkDeleteRetryHandler; import org.apache.hadoop.fs.s3a.impl.ChangeDetectionPolicy; import org.apache.hadoop.fs.s3a.impl.ContextAccessors; +import org.apache.hadoop.fs.s3a.impl.CopyFromLocalOperation; import org.apache.hadoop.fs.s3a.impl.CopyOutcome; import org.apache.hadoop.fs.s3a.impl.DeleteOperation; import org.apache.hadoop.fs.s3a.impl.DirectoryPolicy; @@ -117,7 +109,6 @@ import org.apache.hadoop.fs.s3a.impl.HeaderProcessing; import org.apache.hadoop.fs.s3a.impl.InternalConstants; import org.apache.hadoop.fs.s3a.impl.ListingOperationCallbacks; import org.apache.hadoop.fs.s3a.impl.MkdirOperation; -import org.apache.hadoop.fs.s3a.impl.MultiObjectDeleteSupport; import org.apache.hadoop.fs.s3a.impl.OperationCallbacks; import org.apache.hadoop.fs.s3a.impl.RenameOperation; import org.apache.hadoop.fs.s3a.impl.RequestFactoryImpl; @@ -125,7 +116,6 @@ import org.apache.hadoop.fs.s3a.impl.S3AMultipartUploaderBuilder; import org.apache.hadoop.fs.s3a.impl.StatusProbeEnum; import org.apache.hadoop.fs.s3a.impl.StoreContext; import org.apache.hadoop.fs.s3a.impl.StoreContextBuilder; -import org.apache.hadoop.fs.s3a.s3guard.BulkOperationState; import org.apache.hadoop.fs.s3a.select.InternalSelectConstants; import org.apache.hadoop.fs.s3a.tools.MarkerToolOperations; import org.apache.hadoop.fs.s3a.tools.MarkerToolOperationsImpl; @@ -134,16 +124,18 @@ import org.apache.hadoop.fs.statistics.DurationTrackerFactory; import org.apache.hadoop.fs.statistics.IOStatistics; import org.apache.hadoop.fs.statistics.IOStatisticsLogging; import org.apache.hadoop.fs.statistics.IOStatisticsSource; +import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; import org.apache.hadoop.fs.store.audit.AuditEntryPoint; +import org.apache.hadoop.fs.store.audit.ActiveThreadSpanSource; import org.apache.hadoop.fs.store.audit.AuditSpan; import org.apache.hadoop.fs.store.audit.AuditSpanSource; -import org.apache.hadoop.io.IOUtils; import org.apache.hadoop.io.Text; import org.apache.hadoop.security.AccessControlException; import org.apache.hadoop.security.token.DelegationTokenIssuer; import org.apache.hadoop.security.token.TokenIdentifier; import org.apache.hadoop.util.DurationInfo; import org.apache.hadoop.util.LambdaUtils; +import org.apache.hadoop.util.Preconditions; import org.apache.hadoop.fs.FileAlreadyExistsException; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; @@ -173,11 +165,7 @@ import org.apache.hadoop.fs.s3a.commit.MagicCommitIntegration; import org.apache.hadoop.fs.s3a.impl.ChangeTracker; import org.apache.hadoop.fs.s3a.select.SelectBinding; import org.apache.hadoop.fs.s3a.select.SelectConstants; -import org.apache.hadoop.fs.s3a.s3guard.DirListingMetadata; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.PathMetadata; import org.apache.hadoop.fs.s3a.s3guard.S3Guard; -import org.apache.hadoop.fs.s3a.s3guard.ITtlTimeProvider; import org.apache.hadoop.fs.s3a.statistics.BlockOutputStreamStatistics; import org.apache.hadoop.fs.s3a.statistics.CommitterStatistics; import org.apache.hadoop.fs.s3a.statistics.S3AStatisticsContext; @@ -217,14 +205,13 @@ import static org.apache.hadoop.fs.s3a.impl.CallableSupplier.waitForCompletionIg import static org.apache.hadoop.fs.s3a.impl.ErrorTranslation.isObjectNotFound; import static org.apache.hadoop.fs.s3a.impl.ErrorTranslation.isUnknownBucket; import static org.apache.hadoop.fs.s3a.impl.InternalConstants.CSE_PADDING_LENGTH; -import static org.apache.hadoop.fs.s3a.impl.InternalConstants.CSE_S3GUARD_INCOMPATIBLE; import static org.apache.hadoop.fs.s3a.impl.InternalConstants.DEFAULT_UPLOAD_PART_COUNT_LIMIT; import static org.apache.hadoop.fs.s3a.impl.InternalConstants.DELETE_CONSIDERED_IDEMPOTENT; import static org.apache.hadoop.fs.s3a.impl.InternalConstants.SC_404; import static org.apache.hadoop.fs.s3a.impl.InternalConstants.UPLOAD_PART_COUNT_LIMIT; import static org.apache.hadoop.fs.s3a.impl.NetworkBinding.fixBucketRegion; import static org.apache.hadoop.fs.s3a.impl.NetworkBinding.logDnsLookup; -import static org.apache.hadoop.fs.s3a.s3guard.S3Guard.dirMetaToStatuses; +import static org.apache.hadoop.fs.s3a.s3guard.S3Guard.checkNoS3Guard; import static org.apache.hadoop.fs.statistics.IOStatisticsLogging.logIOStatisticsAtLevel; import static org.apache.hadoop.fs.statistics.StoreStatisticNames.OBJECT_CONTINUE_LIST_REQUEST; import static org.apache.hadoop.fs.statistics.StoreStatisticNames.OBJECT_LIST_REQUEST; @@ -269,10 +256,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, // APIs on an uninitialized filesystem. private Invoker invoker = new Invoker(RetryPolicies.TRY_ONCE_THEN_FAIL, Invoker.LOG_EVENT); - // Only used for very specific code paths which behave differently for - // S3Guard. Retries FileNotFound, so be careful if you use this. - private Invoker s3guardInvoker = new Invoker(RetryPolicies.TRY_ONCE_THEN_FAIL, - Invoker.LOG_EVENT); + private final Retried onRetry = this::operationRetried; private String bucket; private int maxKeys; @@ -289,7 +273,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, LoggerFactory.getLogger("org.apache.hadoop.fs.s3a.S3AFileSystem.Progress"); private LocalDirAllocator directoryAllocator; private CannedAccessControlList cannedACL; - private boolean failOnMetadataWriteError; /** * This must never be null; until initialized it just declares that there @@ -308,8 +291,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, private ChangeDetectionPolicy changeDetectionPolicy; private final AtomicBoolean closed = new AtomicBoolean(false); private volatile boolean isClosed = false; - private MetadataStore metadataStore; - private boolean allowAuthoritativeMetadataStore; private Collection allowAuthoritativePaths; /** Delegation token integration; non-empty when DT support is enabled. */ @@ -321,15 +302,12 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, private String blockOutputBuffer; private S3ADataBlocks.BlockFactory blockFactory; private int blockOutputActiveBlocks; - private WriteOperationHelper writeHelper; private boolean useListV1; private MagicCommitIntegration committerIntegration; private AWSCredentialProviderList credentials; private SignerManager signerManager; - private ITtlTimeProvider ttlTimeProvider; - /** * Page size for deletions. */ @@ -450,12 +428,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, workingDir = new Path("/user", username) .makeQualified(this.uri, this.getWorkingDirectory()); - s3guardInvoker = new Invoker(new S3GuardExistsRetryPolicy(getConf()), - onRetry); - - failOnMetadataWriteError = conf.getBoolean(FAIL_ON_METADATA_WRITE_ERROR, - FAIL_ON_METADATA_WRITE_ERROR_DEFAULT); - maxKeys = intOption(conf, MAX_PAGING_KEYS, DEFAULT_MAX_PAGING_KEYS, 1); partSize = getMultipartSizeProperty(conf, MULTIPART_SIZE, DEFAULT_MULTIPART_SIZE); @@ -488,11 +460,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, // requires the audit manager to be initialized. requestFactory = createRequestFactory(); - // create the static write operation helper. - // this doesn't have a short-lived span; auditors which - // require one may reject usages. - writeHelper = createWriteOperationHelper(getActiveAuditSpan()); - // create an initial span for all other operations. span = createSpan(INITIALIZE_SPAN, bucket, null); @@ -539,29 +506,11 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, LOG.debug("Using S3ABlockOutputStream with buffer = {}; block={};" + " queue limit={}", blockOutputBuffer, partSize, blockOutputActiveBlocks); - long authDirTtl = conf.getTimeDuration(METADATASTORE_METADATA_TTL, - DEFAULT_METADATASTORE_METADATA_TTL, TimeUnit.MILLISECONDS); - ttlTimeProvider = new S3Guard.TtlTimeProvider(authDirTtl); + // verify there's no S3Guard in the store config. + checkNoS3Guard(this.getUri(), getConf()); - setMetadataStore(S3Guard.getMetadataStore(this, ttlTimeProvider)); - allowAuthoritativeMetadataStore = conf.getBoolean(METADATASTORE_AUTHORITATIVE, - DEFAULT_METADATASTORE_AUTHORITATIVE); allowAuthoritativePaths = S3Guard.getAuthoritativePaths(this); - if (hasMetadataStore()) { - LOG.debug("Using metadata store {}, authoritative store={}, authoritative path={}", - getMetadataStore(), allowAuthoritativeMetadataStore, allowAuthoritativePaths); - if (isCSEEnabled) { - throw new PathIOException(uri.toString(), CSE_S3GUARD_INCOMPATIBLE); - } - } - - // LOG if S3Guard is disabled on the warn level set in config - if (!hasMetadataStore()) { - String warnLevel = conf.getTrimmed(S3GUARD_DISABLED_WARN_LEVEL, - DEFAULT_S3GUARD_DISABLED_WARN_LEVEL); - S3Guard.logS3GuardDisabled(LOG, warnLevel, bucket); - } // directory policy, which may look at authoritative paths directoryPolicy = DirectoryPolicyImpl.getDirectoryPolicy(conf, this::allowAuthoritative); @@ -1514,9 +1463,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, final long readAheadRange, final AuditSpan auditSpan) { return new S3AReadOpContext(fileStatus.getPath(), - hasMetadataStore(), invoker, - s3guardInvoker, statistics, statisticsContext, fileStatus, @@ -1881,18 +1828,15 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, // threads. S3AFileStatus dstParentStatus = innerGetFileStatus(parent, false, StatusProbeEnum.FILE); - // if this doesn't raise an exception then it's one of - // raw S3: parent is a file: error - // guarded S3: parent is a file or a dir. + // if this doesn't raise an exception then + // the parent is a file or a dir. if (!dstParentStatus.isDirectory()) { throw new RenameFailedException(src, dst, "destination parent is not a directory"); } } catch (FileNotFoundException expected) { // nothing was found. Don't worry about it; - // expect rename to implicitly create the parent dir (raw S3) - // or the s3guard parents (guarded) - + // expect rename to implicitly create the parent dir } } } @@ -1934,7 +1878,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, // Initiate the rename. // this will call back into this class via the rename callbacks - // and interact directly with any metastore. RenameOperation renameOperation = new RenameOperation( createStoreContext(), src, srcKey, p.getLeft(), @@ -1992,13 +1935,11 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, @Retries.RetryTranslated public void deleteObjectAtPath(final Path path, final String key, - final boolean isFile, - final BulkOperationState operationState) + final boolean isFile) throws IOException { auditSpan.activate(); once("delete", path.toString(), () -> - S3AFileSystem.this.deleteObjectAtPath(path, key, isFile, - operationState)); + S3AFileSystem.this.deleteObjectAtPath(path, key, isFile)); } @Override @@ -2006,7 +1947,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, public RemoteIterator listFilesAndDirectoryMarkers( final Path path, final S3AFileStatus status, - final boolean collectTombstones, final boolean includeSelf) throws IOException { auditSpan.activate(); return innerListFiles( @@ -2015,9 +1955,8 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, includeSelf ? Listing.ACCEPT_ALL_BUT_S3N : new Listing.AcceptAllButSelfAndS3nDirs(path), - status, - collectTombstones, - true); + status + ); } @Override @@ -2034,13 +1973,11 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, public DeleteObjectsResult removeKeys( final List keysToDelete, final boolean deleteFakeDir, - final List undeletedObjectsOnFailure, - final BulkOperationState operationState, final boolean quiet) throws MultiObjectDeleteException, AmazonClientException, IOException { auditSpan.activate(); return S3AFileSystem.this.removeKeys(keysToDelete, deleteFakeDir, - undeletedObjectsOnFailure, operationState, quiet); + quiet); } @Override @@ -2051,17 +1988,12 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, if (!sourceRenamed.getParent().equals(destParent)) { LOG.debug("source & dest parents are different; fix up dir markers"); if (!keepDirectoryMarkers(destParent)) { - deleteUnnecessaryFakeDirectories(destParent, null); + deleteUnnecessaryFakeDirectories(destParent); } maybeCreateFakeParentDirectory(sourceRenamed); } } - @Override - public boolean allowAuthoritative(final Path p) { - return S3AFileSystem.this.allowAuthoritative(p); - } - @Override @Retries.RetryTranslated public RemoteIterator listObjects( @@ -2073,7 +2005,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, createListObjectsRequest(key, null), ACCEPT_ALL, Listing.ACCEPT_ALL_BUT_S3N, - null, auditSpan)); } } @@ -2088,12 +2019,10 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, ListingOperationCallbacks { @Override - @Retries.RetryRaw public CompletableFuture listObjectsAsync( S3ListRequest request, DurationTrackerFactory trackerFactory, - AuditSpan span) - throws IOException { + AuditSpan span) { return submit(unboundedThreadPool, span, () -> listObjects(request, pairedTrackerFactory(trackerFactory, @@ -2106,8 +2035,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, S3ListRequest request, S3ListResult prevResult, DurationTrackerFactory trackerFactory, - AuditSpan span) - throws IOException { + AuditSpan span) { return submit(unboundedThreadPool, span, () -> continueListObjects(request, prevResult, pairedTrackerFactory(trackerFactory, @@ -2140,15 +2068,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, return S3AFileSystem.this.getMaxKeys(); } - @Override - public ITtlTimeProvider getUpdatedTtlTimeProvider() { - return S3AFileSystem.this.ttlTimeProvider; - } - - @Override - public boolean allowAuthoritative(final Path p) { - return S3AFileSystem.this.allowAuthoritative(p); - } } /** @@ -2185,49 +2104,12 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, ChangeTracker changeTracker, Invoker changeInvoker, String operation) throws IOException { String key = pathToKey(path); - return once(operation, path.toString(), - () -> - // this always does a full HEAD to the object + return once(operation, path.toString(), () -> + // HEAD against the object getObjectMetadata( key, changeTracker, changeInvoker, operation)); } - /** - * Does this Filesystem have a metadata store? - * @return true iff the FS has been instantiated with a metadata store - */ - public boolean hasMetadataStore() { - return !S3Guard.isNullMetadataStore(metadataStore); - } - - /** - * Does the filesystem have an authoritative metadata store? - * @return true if there is a metadata store and the authoritative flag - * is set for this filesystem. - */ - @VisibleForTesting - public boolean hasAuthoritativeMetadataStore() { - return hasMetadataStore() && allowAuthoritativeMetadataStore; - } - - /** - * Get the metadata store. - * This will always be non-null, but may be bound to the - * {@code NullMetadataStore}. - * @return the metadata store of this FS instance - */ - @VisibleForTesting - public MetadataStore getMetadataStore() { - return metadataStore; - } - - /** For testing only. See ITestS3GuardEmptyDirs. */ - @VisibleForTesting - void setMetadataStore(MetadataStore ms) { - Preconditions.checkNotNull(ms); - metadataStore = ms; - } - /** * Entry point to an operation. * Increments the statistic; verifies the FS is active. @@ -2348,7 +2230,9 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, */ public void operationRetried(Exception ex) { if (isThrottleException(ex)) { - operationThrottled(false); + LOG.debug("Request throttled"); + incrementStatistic(STORE_IO_THROTTLED); + statisticsContext.addValueToQuantiles(STORE_IO_THROTTLE_RATE, 1); } else { incrementStatistic(STORE_IO_RETRY); incrementStatistic(IGNORED_ERRORS); @@ -2370,46 +2254,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, operationRetried(ex); } - /** - * Callback from {@link Invoker} when an operation against a metastore - * is retried. - * Always increments the {@link Statistic#S3GUARD_METADATASTORE_RETRY} - * statistic/counter; - * if it is a throttling exception will update the associated - * throttled metrics/statistics. - * - * @param ex exception - * @param retries number of retries - * @param idempotent is the method idempotent - */ - public void metastoreOperationRetried(Exception ex, - int retries, - boolean idempotent) { - incrementStatistic(S3GUARD_METADATASTORE_RETRY); - if (isThrottleException(ex)) { - operationThrottled(true); - } else { - incrementStatistic(IGNORED_ERRORS); - } - } - - /** - * Note that an operation was throttled -this will update - * specific counters/metrics. - * @param metastore was the throttling observed in the S3Guard metastore? - */ - private void operationThrottled(boolean metastore) { - LOG.debug("Request throttled on {}", metastore ? "S3": "DynamoDB"); - if (metastore) { - incrementStatistic(S3GUARD_METADATASTORE_THROTTLED); - statisticsContext.addValueToQuantiles(S3GUARD_METADATASTORE_THROTTLE_RATE, - 1); - } else { - incrementStatistic(STORE_IO_THROTTLED); - statisticsContext.addValueToQuantiles(STORE_IO_THROTTLE_RATE, 1); - } - } - /** * Get the storage statistics of this filesystem. * @return the storage statistics @@ -2611,8 +2455,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, } /** - * Delete an object. This is the low-level internal call which - * does not update the metastore. + * Delete an object. * Increments the {@code OBJECT_DELETE_REQUESTS} and write * operation statistics. * This call does not create any mock parent entries. @@ -2646,21 +2489,19 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, } /** - * Delete an object, also updating the metastore. + * Delete an object. * This call does not create any mock parent entries. * Retry policy: retry untranslated; delete considered idempotent. * @param f path path to delete * @param key key of entry * @param isFile is the path a file (used for instrumentation only) - * @param operationState (nullable) operational state for a bulk update * @throws AmazonClientException problems working with S3 - * @throws IOException IO failure in the metastore + * @throws IOException from invoker signature only -should not be raised. */ - @Retries.RetryMixed + @Retries.RetryRaw void deleteObjectAtPath(Path f, String key, - boolean isFile, - @Nullable final BulkOperationState operationState) + boolean isFile) throws AmazonClientException, IOException { if (isFile) { instrumentation.fileDeleted(1); @@ -2668,7 +2509,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, instrumentation.directoryDeleted(); } deleteObject(key); - metadataStore.delete(f, operationState); } /** @@ -2685,8 +2525,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, } /** - * Perform a bulk object delete operation against S3; leaves S3Guard - * alone. + * Perform a bulk object delete operation against S3. * Increments the {@code OBJECT_DELETE_REQUESTS} and write * operation statistics *

@@ -2804,14 +2643,11 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, * @param putObjectRequest the request * @return the upload initiated * @throws AmazonClientException on problems - * @throws MetadataPersistenceException if metadata about the write could - * not be saved to the metadata store and - * fs.s3a.metadatastore.fail.on.write.error=true */ @VisibleForTesting @Retries.OnceRaw("For PUT; post-PUT actions are RetryTranslated") PutObjectResult putObjectDirect(PutObjectRequest putObjectRequest) - throws AmazonClientException, MetadataPersistenceException { + throws AmazonClientException { long len = getPutRequestLength(putObjectRequest); LOG.debug("PUT {} bytes to {}", len, putObjectRequest.getKey()); incrementPutStartStatistics(len); @@ -2823,7 +2659,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, incrementPutCompletedStatistics(true, len); // update metadata finishedWrite(putObjectRequest.getKey(), len, - result.getETag(), result.getVersionId(), null); + result.getETag(), result.getVersionId()); return result; } catch (SdkBaseException e) { incrementPutCompletedStatistics(false, len); @@ -2922,7 +2758,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, /** * Delete a list of keys on a s3-backend. - * This does not update the metastore. * Retry policy: retry untranslated; delete considered idempotent. * @param keysToDelete collection of keys to delete on the s3-backend. * if empty, no request is made of the object store. @@ -2954,14 +2789,14 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, key.getVersion() != null ? key.getVersion() : ""); } } - DeleteObjectsResult result = null; if (keysToDelete.isEmpty()) { // exit fast if there are no keys to delete - return result; + return null; } for (DeleteObjectsRequest.KeyVersion keyVersion : keysToDelete) { blockRootDelete(keyVersion.getKey()); } + DeleteObjectsResult result = null; try { if (enableMultiObjectsDelete) { result = deleteObjects( @@ -2998,13 +2833,13 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, } /** - * Invoke {@link #removeKeysS3(List, boolean, boolean)} with handling of - * {@code MultiObjectDeleteException}. + * Invoke {@link #removeKeysS3(List, boolean, boolean)}. + * If a {@code MultiObjectDeleteException} is raised, the + * relevant statistics are updated. * * @param keysToDelete collection of keys to delete on the s3-backend. * if empty, no request is made of the object store. * @param deleteFakeDir indicates whether this is for deleting fake dirs - * @param operationState (nullable) operational state for a bulk update * @throws InvalidRequestException if the request was rejected due to * a mistaken attempt to delete the root directory. * @throws MultiObjectDeleteException one or more of the keys could not @@ -3013,34 +2848,21 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, * @throws IOException other IO Exception. */ @VisibleForTesting - @Retries.RetryMixed + @Retries.RetryRaw public void removeKeys( final List keysToDelete, - final boolean deleteFakeDir, - final BulkOperationState operationState) + final boolean deleteFakeDir) throws MultiObjectDeleteException, AmazonClientException, IOException { - removeKeys(keysToDelete, deleteFakeDir, new ArrayList<>(), operationState, + removeKeys(keysToDelete, deleteFakeDir, true); } /** - * Invoke {@link #removeKeysS3(List, boolean, boolean)} with handling of - * {@code MultiObjectDeleteException} before the exception is rethrown. - * Specifically: - *
    - *
  1. Failure and !deleteFakeDir: S3Guard is updated with all - * deleted entries
  2. - *
  3. Failure where deleteFakeDir == true: do nothing with S3Guard
  4. - *
  5. Success: do nothing with S3Guard
  6. - *
+ * Invoke {@link #removeKeysS3(List, boolean, boolean)}. * @param keysToDelete collection of keys to delete on the s3-backend. * if empty, no request is made of the object store. * @param deleteFakeDir indicates whether this is for deleting fake dirs. - * @param undeletedObjectsOnFailure List which will be built up of all - * files that were not deleted. This happens even as an exception - * is raised. - * @param operationState (nullable) operational state for a bulk update * @param quiet should a bulk query be quiet, or should its result list * all deleted keys * @return the deletion result if a multi object delete was invoked @@ -3052,39 +2874,15 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, * @throws AmazonClientException amazon-layer failure. * @throws IOException other IO Exception. */ - @Retries.RetryMixed + @Retries.RetryRaw private DeleteObjectsResult removeKeys( final List keysToDelete, final boolean deleteFakeDir, - final List undeletedObjectsOnFailure, - final BulkOperationState operationState, final boolean quiet) throws MultiObjectDeleteException, AmazonClientException, IOException { - undeletedObjectsOnFailure.clear(); try (DurationInfo ignored = new DurationInfo(LOG, false, "Deleting %d keys", keysToDelete.size())) { return removeKeysS3(keysToDelete, deleteFakeDir, quiet); - } catch (MultiObjectDeleteException ex) { - LOG.debug("Partial delete failure"); - // what to do if an IOE was raised? Given an exception was being - // raised anyway, and the failures are logged, do nothing. - if (!deleteFakeDir) { - // when deleting fake directories we don't want to delete metastore - // entries so we only process these failures on "real" deletes. - Triple, List, List>> results = - new MultiObjectDeleteSupport(createStoreContext(), operationState) - .processDeleteFailure(ex, keysToDelete, new ArrayList()); - undeletedObjectsOnFailure.addAll(results.getLeft()); - } - throw ex; - } catch (AmazonClientException | IOException ex) { - List paths = new MultiObjectDeleteSupport( - createStoreContext(), - operationState) - .processDeleteFailureGenericException(ex, keysToDelete); - // other failures. Assume nothing was deleted - undeletedObjectsOnFailure.addAll(paths); - throw ex; } } @@ -3206,8 +3004,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, Path path = qualify(f); return trackDurationAndSpan(INVOCATION_LIST_STATUS, path, () -> once("listStatus", path.toString(), - () -> iteratorToStatuses(innerListStatus(path), - new HashSet<>()))); + () -> iteratorToStatuses(innerListStatus(path)))); } /** @@ -3229,15 +3026,9 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, Path path = qualify(f); LOG.debug("List status for path: {}", path); - Triple, DirListingMetadata, Boolean> - statusesAssumingNonEmptyDir = listing - .getFileStatusesAssumingNonEmptyDir(path, getActiveAuditSpan()); - - if (!statusesAssumingNonEmptyDir.getLeft().hasNext() && - statusesAssumingNonEmptyDir.getRight()) { - // We are sure that this is an empty directory in auth mode. - return statusesAssumingNonEmptyDir.getLeft(); - } else if (!statusesAssumingNonEmptyDir.getLeft().hasNext()) { + final RemoteIterator statusIt = listing + .getFileStatusesAssumingNonEmptyDir(path, getActiveAuditSpan()); + if (!statusIt.hasNext()) { // We may have an empty dir, or may have file or may have nothing. // So we call innerGetFileStatus to get the status, this may throw // FileNotFoundException if we have nothing. @@ -3256,31 +3047,19 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, } } // Here we have a directory which may or may not be empty. - // So we update the metastore and return. - return S3Guard.dirListingUnion( - metadataStore, - path, - statusesAssumingNonEmptyDir.getLeft(), - statusesAssumingNonEmptyDir.getMiddle(), - allowAuthoritative(path), - ttlTimeProvider, p -> - listing.createProvidedFileStatusIterator( - dirMetaToStatuses(statusesAssumingNonEmptyDir.getMiddle()), - ACCEPT_ALL, - Listing.ACCEPT_ALL_BUT_S3N)); + return statusIt; } /** * Is a path to be considered as authoritative? - * True iff there is an authoritative metastore or if there - * is a non-auth store with the supplied path under + * is a store with the supplied path under * one of the paths declared as authoritative. * @param path path * @return true if the path is auth */ public boolean allowAuthoritative(final Path path) { return S3Guard.allowAuthoritative(path, this, - allowAuthoritativeMetadataStore, allowAuthoritativePaths); + allowAuthoritativePaths); } /** @@ -3502,13 +3281,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, } /** - * Get the status of a file or directory, first through S3Guard and then - * through S3. - * The S3 probes can leave 404 responses in the S3 load balancers; if - * a check is only needed for a directory, declaring this saves time and - * avoids creating one for the object. - * When only probing for directories, if an entry for a file is found in - * S3Guard it is returned, but checks for updated values are skipped. + * Get the status of a file or directory. * Internal version of {@link #getFileStatus(Path)}. * @param f The path we want information from * @param needEmptyDirectoryFlag if true, implementation will calculate @@ -3527,120 +3300,15 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, String key = pathToKey(path); LOG.debug("Getting path status for {} ({}); needEmptyDirectory={}", path, key, needEmptyDirectoryFlag); + return s3GetFileStatus(path, + key, + probes, + needEmptyDirectoryFlag); - boolean allowAuthoritative = allowAuthoritative(path); - // Check MetadataStore, if any. - PathMetadata pm = null; - if (hasMetadataStore()) { - pm = S3Guard.getWithTtl(metadataStore, path, ttlTimeProvider, - needEmptyDirectoryFlag, allowAuthoritative); - } - Set tombstones = Collections.emptySet(); - if (pm != null) { - S3AFileStatus msStatus = pm.getFileStatus(); - if (pm.isDeleted()) { - OffsetDateTime deletedAt = OffsetDateTime.ofInstant( - Instant.ofEpochMilli(msStatus.getModificationTime()), - ZoneOffset.UTC); - throw new FileNotFoundException("Path " + path + " is recorded as " + - "deleted by S3Guard at " + deletedAt); - } - - // if ms is not authoritative, check S3 if there's any recent - // modification - compare the modTime to check if metadata is up to date - // Skip going to s3 if the file checked is a directory. Because if the - // dest is also a directory, there's no difference. - - if (!msStatus.isDirectory() && - !allowAuthoritative && - probes.contains(StatusProbeEnum.Head)) { - // a file has been found in a non-auth path and the caller has not said - // they only care about directories - LOG.debug("Metadata for {} found in the non-auth metastore.", path); - final long msModTime = pm.getFileStatus().getModificationTime(); - - S3AFileStatus s3AFileStatus; - try { - s3AFileStatus = s3GetFileStatus(path, - key, - probes, - tombstones, - needEmptyDirectoryFlag); - } catch (FileNotFoundException fne) { - LOG.trace("File Not Found from probes for {}", key, fne); - s3AFileStatus = null; - } - if (s3AFileStatus == null) { - LOG.warn("Failed to find file {}. Either it is not yet visible, or " - + "it has been deleted.", path); - } else { - final long s3ModTime = s3AFileStatus.getModificationTime(); - - if(s3ModTime > msModTime) { - LOG.debug("S3Guard metadata for {} is outdated;" - + " s3modtime={}; msModTime={} updating metastore", - path, s3ModTime, msModTime); - return S3Guard.putAndReturn(metadataStore, s3AFileStatus, - ttlTimeProvider); - } - } - } - - if (needEmptyDirectoryFlag && msStatus.isDirectory()) { - // the caller needs to know if a directory is empty, - // and that this is a directory. - if (pm.isEmptyDirectory() != Tristate.UNKNOWN) { - // We have a definitive true / false from MetadataStore, we are done. - return msStatus; - } else { - // execute a S3Guard listChildren command to list tombstones under the - // path. - // This list will be used in the forthcoming s3GetFileStatus call. - DirListingMetadata children = - S3Guard.listChildrenWithTtl(metadataStore, path, ttlTimeProvider, - allowAuthoritative); - if (children != null) { - tombstones = children.listTombstones(); - } - LOG.debug("MetadataStore doesn't know if {} is empty, using S3.", - path); - } - } else { - // Either this is not a directory, or we don't care if it is empty - return msStatus; - } - - // now issue the S3 getFileStatus call. - try { - S3AFileStatus s3FileStatus = s3GetFileStatus(path, - key, - probes, - tombstones, - true); - // entry was found, so save in S3Guard and return the final value. - return S3Guard.putAndReturn(metadataStore, s3FileStatus, - ttlTimeProvider); - } catch (FileNotFoundException e) { - // If the metadata store has no children for it and it's not listed in - // S3 yet, we'll conclude that it is an empty directory - return S3AFileStatus.fromFileStatus(msStatus, Tristate.TRUE, - null, null); - } - } else { - // there was no entry in S3Guard - // retrieve the data and update the metadata store in the process. - return S3Guard.putAndReturn(metadataStore, - s3GetFileStatus(path, - key, - probes, - tombstones, - needEmptyDirectoryFlag), - ttlTimeProvider); - } } /** - * Raw {@code getFileStatus} that talks direct to S3. + * Probe store for file status with control of which probes are issued.. * Used to implement {@link #innerGetFileStatus(Path, boolean, Set)}, * and for direct management of empty directory blobs. * @@ -3651,13 +3319,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, * the key doesn't end in "/" * *
  • - * DirMarker: look for the directory marker -the key with a trailing / - * if not passed in. - * If an object was found with size 0 bytes, a directory status entry - * is returned which declares that the directory is empty. - *
  • - *
  • - * List: issue a LIST on the key (with / if needed), require one + * DirMarker/List: issue a LIST on the key (with / if needed), require one * entry to be found for the path to be considered a non-empty directory. *
  • * @@ -3665,12 +3327,8 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, * Notes: *
      *
    • - * Objects ending in / which are not 0-bytes long are not treated as - * directory markers, but instead as files. - *
    • - *
    • - * There's ongoing discussions about whether a dir marker - * should be interpreted as an empty dir. + * Objects ending in / are treated as directory markers, + * irrespective of length. *
    • *
    • * The HEAD requests require the permissions to read an object, @@ -3679,8 +3337,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, * the client for the HEAD to work. *
    • *
    • - * The List probe needs list permission; it is also more prone to - * inconsistency, even on newly created files. + * The List probe needs list permission. *
    • *
    * @@ -3688,7 +3345,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, * @param path Qualified path * @param key Key string for the path * @param probes probes to make - * @param tombstones tombstones to filter * @param needEmptyDirectoryFlag if true, implementation will calculate * a TRUE or FALSE value for {@link S3AFileStatus#isEmptyDirectory()} * @return Status @@ -3700,7 +3356,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, S3AFileStatus s3GetFileStatus(final Path path, final String key, final Set probes, - @Nullable final Set tombstones, final boolean needEmptyDirectoryFlag) throws IOException { LOG.debug("S3GetFileStatus {}", path); // either you aren't looking for the directory flag, or you are, @@ -3709,7 +3364,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, || probes.contains(StatusProbeEnum.List), "s3GetFileStatus(%s) wants to know if a directory is empty but" + " does not request a list probe", path); - if (key.isEmpty() && !needEmptyDirectoryFlag) { return new S3AFileStatus(Tristate.UNKNOWN, path, username); } @@ -3737,6 +3391,8 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, } catch (AmazonServiceException e) { // if the response is a 404 error, it just means that there is // no file at that path...the remaining checks will be needed. + // But: an empty bucket is also a 404, so check for that + // and fail. if (e.getStatusCode() != SC_404 || isUnknownBucket(e)) { throw translateException("getFileStatus", path, e); } @@ -3754,30 +3410,16 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, // children, so ask for two entries, so as to find // a child String dirKey = maybeAddTrailingSlash(key); - // list size is dir marker + at least one non-tombstone entry - // there's a corner case: more tombstones than you have in a - // single page list. We assume that if you have been deleting - // that many files, then the AWS listing will have purged some - // by the time of listing so that the response includes some - // which have not. + // list size is dir marker + at least one entry - int listSize; - if (tombstones == null) { - // no tombstones so look for a marker and at least one child. - listSize = 2; - } else { - // build a listing > tombstones. If the caller has many thousands - // of tombstones this won't work properly, which is why pruning - // of expired tombstones matters. - listSize = Math.min(2 + tombstones.size(), Math.max(2, maxKeys)); - } + final int listSize = 2; S3ListRequest request = createListObjectsRequest(dirKey, "/", listSize); // execute the request S3ListResult listResult = listObjects(request, getDurationTrackerFactory()); - if (listResult.hasPrefixesOrObjects(contextAccessors, tombstones)) { + if (listResult.hasPrefixesOrObjects()) { if (LOG.isDebugEnabled()) { LOG.debug("Found path as directory (with /)"); listResult.logAtDebug(LOG); @@ -3787,8 +3429,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, // children. // So the listing must contain the marker entry only. if (needEmptyDirectoryFlag - && listResult.representsEmptyDirectory( - contextAccessors, dirKey, tombstones)) { + && listResult.representsEmptyDirectory(dirKey)) { return new S3AFileStatus(Tristate.TRUE, path, username); } // either an empty directory is not needed, or the @@ -3812,8 +3453,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, } /** - * Raw version of {@link FileSystem#exists(Path)} which uses S3 only: - * S3Guard MetadataStore, if any, will be skipped. + * Probe S3 for a file or dir existing, with the given probe set. * Retry policy: retrying; translated. * @param path qualified path to look for * @param probes probes to make @@ -3825,7 +3465,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, throws IOException { String key = pathToKey(path); try { - s3GetFileStatus(path, key, probes, null, false); + s3GetFileStatus(path, key, probes, false); return true; } catch (FileNotFoundException e) { return false; @@ -3932,22 +3572,18 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, } /** - * Execute a PUT via the transfer manager, blocking for completion, - * updating the metastore afterwards. + * Execute a PUT via the transfer manager, blocking for completion. * If the waiting for completion is interrupted, the upload will be * aborted before an {@code InterruptedIOException} is thrown. * @param putObjectRequest request * @param progress optional progress callback * @return the upload result * @throws InterruptedIOException if the blocking was interrupted. - * @throws MetadataPersistenceException if metadata about the write could - * not be saved to the metadata store and - * fs.s3a.metadatastore.fail.on.write.error=true */ @Retries.OnceRaw("For PUT; post-PUT actions are RetryTranslated") UploadResult executePut(PutObjectRequest putObjectRequest, Progressable progress) - throws InterruptedIOException, MetadataPersistenceException { + throws InterruptedIOException { String key = putObjectRequest.getKey(); UploadInfo info = putObject(putObjectRequest); Upload upload = info.getUpload(); @@ -3958,7 +3594,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, listener.uploadCompleted(); // post-write actions finishedWrite(key, info.getLength(), - result.getETag(), result.getVersionId(), null); + result.getETag(), result.getVersionId()); return result; } @@ -4051,7 +3687,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, unboundedThreadPool = null; // other services are shutdown. cleanupWithLogger(LOG, - metadataStore, instrumentation, delegationTokens.orElse(null), signerManager, @@ -4154,7 +3789,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, /** * Build the AWS policy for restricted access to the resources needed * by this bucket. - * The policy generated includes S3 access, S3Guard access * if needed, and KMS operations. * @param access access level desired. * @return a policy for use in roles @@ -4176,11 +3810,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, // too much encryption access. statements.add(STATEMENT_ALLOW_SSE_KMS_RW); - // add any metastore policies - if (metadataStore instanceof AWSPolicyProvider) { - statements.addAll( - ((AWSPolicyProvider) metadataStore).listAWSPolicyRules(access)); - } return statements; } @@ -4232,21 +3861,14 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, } catch (FileNotFoundException e) { // if rename fails at this point it means that the expected file was not // found. - // The cause is believed to always be one of - // - File was deleted since LIST/S3Guard metastore.list.() knew of it. - // - S3Guard is asking for a specific version and it's been removed by - // lifecycle rules. - // - there's a 404 cached in the S3 load balancers. + // This means the File was deleted since LIST enumerated it. LOG.debug("getObjectMetadata({}) failed to find an expected file", srcKey, e); // We create an exception, but the text depends on the S3Guard state - String message = hasMetadataStore() - ? RemoteFileChangedException.FILE_NEVER_FOUND - : RemoteFileChangedException.FILE_NOT_FOUND_SINGLE_ATTEMPT; throw new RemoteFileChangedException( keyToQualifiedPath(srcKey).toString(), action, - message, + RemoteFileChangedException.FILE_NOT_FOUND_SINGLE_ATTEMPT, e); } @@ -4311,7 +3933,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, *
      *
    1. * Calling - * {@link #deleteUnnecessaryFakeDirectories(Path, BulkOperationState)} + * {@link #deleteUnnecessaryFakeDirectories(Path)} * if directory markers are not being retained. *
    2. *
    3. @@ -4323,22 +3945,15 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, * @param length total length of file written * @param eTag eTag of the written object * @param versionId S3 object versionId of the written object - * @param operationState state of any ongoing bulk operation. - * @throws MetadataPersistenceException if metadata about the write could - * not be saved to the metadata store and - * fs.s3a.metadatastore.fail.on.write.error=true */ @InterfaceAudience.Private @Retries.RetryTranslated("Except if failOnMetadataWriteError=false, in which" + " case RetryExceptionsSwallowed") - void finishedWrite(String key, long length, String eTag, String versionId, - @Nullable final BulkOperationState operationState) - throws MetadataPersistenceException { + void finishedWrite(String key, long length, String eTag, String versionId) { LOG.debug("Finished write to {}, len {}. etag {}, version {}", key, length, eTag, versionId); Path p = keyToQualifiedPath(key); Preconditions.checkArgument(length >= 0, "content length is negative"); - final boolean isDir = objectRepresentsDirectory(key); // kick off an async delete CompletableFuture deletion; if (!keepDirectoryMarkers(p)) { @@ -4346,69 +3961,16 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, unboundedThreadPool, getActiveAuditSpan(), () -> { deleteUnnecessaryFakeDirectories( - p.getParent(), - operationState); + p.getParent() + ); return null; }); } else { deletion = null; } - // this is only set if there is a metastore to update and the - // operationState parameter passed in was null. - BulkOperationState stateToClose = null; - // See note about failure semantics in S3Guard documentation - try { - if (hasMetadataStore()) { - BulkOperationState activeState = operationState; - if (activeState == null) { - // create an operation state if there was none, so that the - // information gleaned from addAncestors is preserved into the - // subsequent put. - stateToClose = S3Guard.initiateBulkWrite(metadataStore, - isDir - ? BulkOperationState.OperationType.Mkdir - : BulkOperationState.OperationType.Put, - keyToPath(key)); - activeState = stateToClose; - } - S3Guard.addAncestors(metadataStore, p, ttlTimeProvider, activeState); - S3AFileStatus status = createUploadFileStatus(p, - isDir, length, - getDefaultBlockSize(p), username, eTag, versionId); - boolean authoritative = false; - if (isDir) { - // this is a directory marker so put it as such. - status.setIsEmptyDirectory(Tristate.TRUE); - // and maybe mark as auth - authoritative = allowAuthoritative(p); - } - if (!authoritative) { - // for files and non-auth directories - S3Guard.putAndReturn(metadataStore, status, - ttlTimeProvider, - activeState); - } else { - // authoritative directory - S3Guard.putAuthDirectoryMarker(metadataStore, status, - ttlTimeProvider, - activeState); - } - } - // and catch up with any delete operation. - waitForCompletionIgnoringExceptions(deletion); - } catch (IOException e) { - if (failOnMetadataWriteError) { - throw new MetadataPersistenceException(p.toString(), e); - } else { - LOG.error("S3Guard: Error updating MetadataStore for write to {}", - p, e); - } - instrumentation.errorIgnored(); - } finally { - // if a new operation state was created, close it. - IOUtils.cleanupWithLogger(LOG, stateToClose); - } + // catch up with any delete operation. + waitForCompletionIgnoringExceptions(deletion); } /** @@ -4426,11 +3988,10 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, * Delete mock parent directories which are no longer needed. * Retry policy: retrying; exceptions swallowed. * @param path path - * @param operationState (nullable) operational state for a bulk update + * */ @Retries.RetryExceptionsSwallowed - private void deleteUnnecessaryFakeDirectories(Path path, - final BulkOperationState operationState) { + private void deleteUnnecessaryFakeDirectories(Path path) { List keysToRemove = new ArrayList<>(); while (!path.isRoot()) { String key = pathToKey(path); @@ -4440,7 +4001,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, path = path.getParent(); } try { - removeKeys(keysToRemove, true, operationState); + removeKeys(keysToRemove, true); } catch(AmazonClientException | IOException e) { instrumentation.errorIgnored(); if (LOG.isDebugEnabled()) { @@ -4527,8 +4088,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, sb.append(", blockFactory=").append(blockFactory); } sb.append(", auditManager=").append(auditManager); - sb.append(", metastore=").append(metadataStore); - sb.append(", authoritativeStore=").append(allowAuthoritativeMetadataStore); sb.append(", authoritativePath=").append(allowAuthoritativePaths); sb.append(", useListV1=").append(useListV1); if (committerIntegration != null) { @@ -4846,7 +4405,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, return toLocatedFileStatusIterator( trackDurationAndSpan(INVOCATION_LIST_FILES, path, () -> innerListFiles(path, recursive, - new Listing.AcceptFilesOnly(path), null, true, false))); + new Listing.AcceptFilesOnly(path), null))); } /** @@ -4865,64 +4424,22 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, return trackDurationAndSpan(INVOCATION_LIST_FILES, path, () -> innerListFiles(path, recursive, Listing.ACCEPT_ALL_BUT_S3N, - null, true, false)); - } - - /** - * Recursive List of files and empty directories, force metadatastore - * to act like it is non-authoritative. - * @param f path to list from - * @param recursive recursive listing? - * @return an iterator. - * @throws IOException failure - */ - @InterfaceAudience.Private - @Retries.RetryTranslated - @AuditEntryPoint - public RemoteIterator listFilesAndEmptyDirectoriesForceNonAuth( - Path f, boolean recursive) throws IOException { - final Path path = qualify(f); - return trackDurationAndSpan(INVOCATION_LIST_FILES, path, () -> - innerListFiles(path, recursive, - Listing.ACCEPT_ALL_BUT_S3N, - null, true, true)); + null)); } /** * List files under the path. *
        *
      1. - * If the path is authoritative on the client, - * only S3Guard will be queried. - *
      2. - *
      3. - * Otherwise, the S3Guard values are returned first, then the S3 - * entries will be retrieved and returned if not already listed.
      4. - *
      5. - * when collectTombstones} is true, S3Guard tombstones will - * be used to filter out deleted files. - * They MUST be used for normal listings; it is only for - * deletion and low-level operations that they MAY be bypassed. - *
      6. - *
      7. * The optional {@code status} parameter will be used to skip the * initial getFileStatus call. *
      8. *
      * - * In case of recursive listing, if any of the directories reachable from - * the path are not authoritative on the client, this method will query S3 - * for all the directories in the listing in addition to returning S3Guard - * entries. - * * @param f path * @param recursive recursive listing? * @param acceptor file status filter * @param status optional status of path to list. - * @param collectTombstones should tombstones be collected from S3Guard? - * @param forceNonAuthoritativeMS forces metadata store to act like non - * authoritative. This is useful when - * listFiles output is used by import tool. * @return an iterator over the listing. * @throws IOException failure */ @@ -4931,9 +4448,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, final Path f, final boolean recursive, final Listing.FileStatusAcceptor acceptor, - final S3AFileStatus status, - final boolean collectTombstones, - final boolean forceNonAuthoritativeMS) throws IOException { + final S3AFileStatus status) throws IOException { Path path = qualify(f); LOG.debug("listFiles({}, {})", path, recursive); try { @@ -4950,9 +4465,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, listing.getListFilesAssumingDir(path, recursive, acceptor, - collectTombstones, - forceNonAuthoritativeMS, - getActiveAuditSpan()); + getActiveAuditSpan()); // If there are no list entries present, we // fallback to file existence check as the path // can be a file or empty directory. @@ -5231,17 +4744,6 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, return credentials.share(); } - @VisibleForTesting - public ITtlTimeProvider getTtlTimeProvider() { - return ttlTimeProvider; - } - - @VisibleForTesting - protected void setTtlTimeProvider(ITtlTimeProvider ttlTimeProvider) { - this.ttlTimeProvider = ttlTimeProvider; - metadataStore.setTtlTimeProvider(ttlTimeProvider); - } - /** * This is a proof of concept of a select API. * @param source path to source data @@ -5321,7 +4823,7 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, /** * Extract the status from the optional parameter, querying - * S3Guard/s3 if it is absent. + * S3 if it is absent. * @param path path of the status * @param optStatus optional status * @return a file status @@ -5334,19 +4836,19 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, S3AFileStatus fileStatus; if (optStatus.isPresent()) { fileStatus = optStatus.get(); + // we check here for the passed in status + // being a directory + if (fileStatus.isDirectory()) { + throw new FileNotFoundException(path.toString() + " is a directory"); + } } else { - // this looks at S3guard and gets any type of status back, - // if it falls back to S3 it does a HEAD only. - // therefore: if there is no S3Guard and there is a dir, this + // Executes a HEAD only. + // therefore: if there is is a dir marker, this // will raise a FileNotFoundException fileStatus = innerGetFileStatus(path, false, StatusProbeEnum.HEAD_ONLY); } - // we check here for the passed in status or the S3Guard value - // for being a directory - if (fileStatus.isDirectory()) { - throw new FileNotFoundException(path.toString() + " is a directory"); - } + return fileStatus; } @@ -5472,10 +4974,8 @@ public class S3AFileSystem extends FileSystem implements StreamCapabilities, .setInputPolicy(getInputPolicy()) .setChangeDetectionPolicy(changeDetectionPolicy) .setMultiObjectDeleteEnabled(enableMultiObjectsDelete) - .setMetadataStore(metadataStore) .setUseListV1(useListV1) .setContextAccessors(new ContextAccessorsImpl()) - .setTimeProvider(getTtlTimeProvider()) .setAuditor(getAuditor()) .setEnableCSE(isCSEEnabled) .build(); diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AInputStream.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AInputStream.java index 86317ab21c0..054f40bdf4b 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AInputStream.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AInputStream.java @@ -366,10 +366,6 @@ public class S3AInputStream extends FSInputStream implements CanSetReadahead, @Retries.RetryTranslated private void lazySeek(long targetPos, long len) throws IOException { - // With S3Guard, the metadatastore gave us metadata for the file in - // open(), so we use a slightly different retry policy, but only on initial - // open. After that, an exception generally means the file has changed - // and there is no point retrying anymore. Invoker invoker = context.getReadInvoker(); invoker.maybeRetry(streamStatistics.getOpenOperations() == 0, "lazySeek", pathStr, true, @@ -397,7 +393,7 @@ public class S3AInputStream extends FSInputStream implements CanSetReadahead, } @Override - @Retries.RetryTranslated // Some retries only happen w/ S3Guard, as intended. + @Retries.RetryTranslated public synchronized int read() throws IOException { checkNotClosed(); if (this.contentLength == 0 || (nextReadPos >= contentLength)) { @@ -410,10 +406,6 @@ public class S3AInputStream extends FSInputStream implements CanSetReadahead, return -1; } - // With S3Guard, the metadatastore gave us metadata for the file in - // open(), so we use a slightly different retry policy. - // read() may not be likely to fail, but reopen() does a GET which - // certainly could. Invoker invoker = context.getReadInvoker(); int byteRead = invoker.retry("read", pathStr, true, () -> { @@ -478,7 +470,7 @@ public class S3AInputStream extends FSInputStream implements CanSetReadahead, * @throws IOException if there are other problems */ @Override - @Retries.RetryTranslated // Some retries only happen w/ S3Guard, as intended. + @Retries.RetryTranslated public synchronized int read(byte[] buf, int off, int len) throws IOException { checkNotClosed(); @@ -499,10 +491,6 @@ public class S3AInputStream extends FSInputStream implements CanSetReadahead, return -1; } - // With S3Guard, the metadatastore gave us metadata for the file in - // open(), so we use a slightly different retry policy. - // read() may not be likely to fail, but reopen() does a GET which - // certainly could. Invoker invoker = context.getReadInvoker(); streamStatistics.readOperationStarted(nextReadPos, len); @@ -766,7 +754,7 @@ public class S3AInputStream extends FSInputStream implements CanSetReadahead, * */ @Override - @Retries.RetryTranslated // Some retries only happen w/ S3Guard, as intended. + @Retries.RetryTranslated public void readFully(long position, byte[] buffer, int offset, int length) throws IOException { checkNotClosed(); diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AInstrumentation.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AInstrumentation.java index cb4e6ac9173..a1b8bab3abf 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AInstrumentation.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AInstrumentation.java @@ -27,7 +27,6 @@ import org.slf4j.LoggerFactory; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.fs.s3a.s3guard.MetastoreInstrumentation; import org.apache.hadoop.fs.s3a.statistics.BlockOutputStreamStatistics; import org.apache.hadoop.fs.s3a.statistics.ChangeTrackerStatistics; import org.apache.hadoop.fs.s3a.statistics.CommitterStatistics; @@ -164,13 +163,7 @@ public class S3AInstrumentation implements Closeable, MetricsSource, private final MetricsRegistry registry = new MetricsRegistry("s3aFileSystem").setContext(CONTEXT); - private final MutableQuantiles putLatencyQuantile; private final MutableQuantiles throttleRateQuantile; - private final MutableQuantiles s3GuardThrottleRateQuantile; - - /** Instantiate this without caring whether or not S3Guard is enabled. */ - private final S3GuardInstrumentation s3GuardInstrumentation - = new S3GuardInstrumentation(); /** * This is the IOStatistics store for the S3AFileSystem @@ -224,10 +217,6 @@ public class S3AInstrumentation implements Closeable, MetricsSource, //todo need a config for the quantiles interval? int interval = 1; - putLatencyQuantile = quantiles(S3GUARD_METADATASTORE_PUT_PATH_LATENCY, - "ops", "latency", interval); - s3GuardThrottleRateQuantile = quantiles(S3GUARD_METADATASTORE_THROTTLE_RATE, - "events", "frequency (Hz)", interval); throttleRateQuantile = quantiles(STORE_IO_THROTTLE_RATE, "events", "frequency (Hz)", interval); @@ -676,15 +665,6 @@ public class S3AInstrumentation implements Closeable, MetricsSource, return new InputStreamStatistics(filesystemStatistics); } - /** - * Create a MetastoreInstrumentation instrumentation instance. - * There's likely to be at most one instance of this per FS instance. - * @return the S3Guard instrumentation point. - */ - public MetastoreInstrumentation getS3GuardInstrumentation() { - return s3GuardInstrumentation; - } - /** * Create a new instance of the committer statistics. * @return a new committer statistics instance @@ -702,9 +682,7 @@ public class S3AInstrumentation implements Closeable, MetricsSource, synchronized (METRICS_SYSTEM_LOCK) { // it is critical to close each quantile, as they start a scheduled // task in a shared thread pool. - putLatencyQuantile.stop(); throttleRateQuantile.stop(); - s3GuardThrottleRateQuantile.stop(); metricsSystem.unregisterSource(metricsSourceName); metricsSourceActiveCounter--; int activeSources = metricsSourceActiveCounter; @@ -1616,64 +1594,6 @@ public class S3AInstrumentation implements Closeable, MetricsSource, } } - /** - * Instrumentation exported to S3Guard. - */ - private final class S3GuardInstrumentation - implements MetastoreInstrumentation { - - @Override - public void initialized() { - incrementCounter(S3GUARD_METADATASTORE_INITIALIZATION, 1); - } - - @Override - public void storeClosed() { - - } - - @Override - public void throttled() { - // counters are incremented by owner. - } - - @Override - public void retrying() { - // counters are incremented by owner. - } - - @Override - public void recordsDeleted(int count) { - incrementCounter(S3GUARD_METADATASTORE_RECORD_DELETES, count); - } - - @Override - public void recordsRead(int count) { - incrementCounter(S3GUARD_METADATASTORE_RECORD_READS, count); - } - - @Override - public void recordsWritten(int count) { - incrementCounter(S3GUARD_METADATASTORE_RECORD_WRITES, count); - } - - @Override - public void directoryMarkedAuthoritative() { - incrementCounter( - S3GUARD_METADATASTORE_AUTHORITATIVE_DIRECTORIES_UPDATED, - 1); - } - - @Override - public void entryAdded(final long durationNanos) { - addValueToQuantiles( - S3GUARD_METADATASTORE_PUT_PATH_LATENCY, - durationNanos); - incrementCounter(S3GUARD_METADATASTORE_PUT_PATH_REQUEST, 1); - } - - } - /** * Instrumentation exported to S3A Committers. * The S3AInstrumentation metrics and diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AOpContext.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AOpContext.java index aca1fa5e140..27920b09845 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AOpContext.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AOpContext.java @@ -38,66 +38,36 @@ import org.apache.hadoop.fs.s3a.statistics.S3AStatisticsContext; @SuppressWarnings("visibilitymodifier") public class S3AOpContext extends ActiveOperationContext { - final boolean isS3GuardEnabled; final Invoker invoker; @Nullable final FileSystem.Statistics stats; - @Nullable final Invoker s3guardInvoker; /** FileStatus for "destination" path being operated on. */ protected final FileStatus dstFileStatus; /** - * Alternate constructor that allows passing in two invokers, the common - * one, and another with the S3Guard Retry Policy. - * @param isS3GuardEnabled true if s3Guard is active + * Constructor. * @param invoker invoker, which contains retry policy - * @param s3guardInvoker s3guard-specific retry policy invoker * @param stats optional stats object * @param instrumentation instrumentation to use * @param dstFileStatus file status from existence check */ - public S3AOpContext(boolean isS3GuardEnabled, Invoker invoker, - @Nullable Invoker s3guardInvoker, + public S3AOpContext(Invoker invoker, @Nullable FileSystem.Statistics stats, S3AStatisticsContext instrumentation, FileStatus dstFileStatus) { super(newOperationId(), - instrumentation, - null); + instrumentation + ); Preconditions.checkNotNull(invoker, "Null invoker arg"); Preconditions.checkNotNull(instrumentation, "Null instrumentation arg"); Preconditions.checkNotNull(dstFileStatus, "Null dstFileStatus arg"); - this.isS3GuardEnabled = isS3GuardEnabled; - Preconditions.checkArgument(!isS3GuardEnabled || s3guardInvoker != null, - "S3Guard invoker required: S3Guard is enabled."); + this.invoker = invoker; - this.s3guardInvoker = s3guardInvoker; this.stats = stats; this.dstFileStatus = dstFileStatus; } - /** - * Constructor using common invoker and retry policy. - * @param isS3GuardEnabled true if s3Guard is active - * @param invoker invoker, which contains retry policy - * @param stats optional stats object - * @param instrumentation instrumentation to use - * @param dstFileStatus file status from existence check - */ - public S3AOpContext(boolean isS3GuardEnabled, - Invoker invoker, - @Nullable FileSystem.Statistics stats, - S3AStatisticsContext instrumentation, - FileStatus dstFileStatus) { - this(isS3GuardEnabled, invoker, null, stats, instrumentation, - dstFileStatus); - } - - public boolean isS3GuardEnabled() { - return isS3GuardEnabled; - } - public Invoker getInvoker() { return invoker; } @@ -107,11 +77,6 @@ public class S3AOpContext extends ActiveOperationContext { return stats; } - @Nullable - public Invoker getS3guardInvoker() { - return s3guardInvoker; - } - public FileStatus getDstFileStatus() { return dstFileStatus; } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AReadOpContext.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AReadOpContext.java index 43398fc4a16..b001d8e2587 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AReadOpContext.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AReadOpContext.java @@ -61,9 +61,7 @@ public class S3AReadOpContext extends S3AOpContext { /** * Instantiate. * @param path path of read - * @param isS3GuardEnabled true iff S3Guard is enabled. * @param invoker invoker for normal retries. - * @param s3guardInvoker S3Guard-specific retry invoker. * @param stats Fileystem statistics (may be null) * @param instrumentation statistics context * @param dstFileStatus target file status @@ -74,9 +72,7 @@ public class S3AReadOpContext extends S3AOpContext { */ public S3AReadOpContext( final Path path, - boolean isS3GuardEnabled, Invoker invoker, - @Nullable Invoker s3guardInvoker, @Nullable FileSystem.Statistics stats, S3AStatisticsContext instrumentation, FileStatus dstFileStatus, @@ -85,7 +81,7 @@ public class S3AReadOpContext extends S3AOpContext { final long readahead, final AuditSpan auditSpan) { - super(isS3GuardEnabled, invoker, s3guardInvoker, stats, instrumentation, + super(invoker, stats, instrumentation, dstFileStatus); this.path = checkNotNull(path); this.auditSpan = auditSpan; @@ -98,17 +94,10 @@ public class S3AReadOpContext extends S3AOpContext { /** * Get invoker to use for read operations. - * When S3Guard is enabled we use the S3Guard invoker, - * which deals with things like FileNotFoundException - * differently. * @return invoker to use for read codepaths */ public Invoker getReadInvoker() { - if (isS3GuardEnabled) { - return s3guardInvoker; - } else { - return invoker; - } + return invoker; } /** diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3ARetryPolicy.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3ARetryPolicy.java index 286dc213b21..30427f7672a 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3ARetryPolicy.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3ARetryPolicy.java @@ -31,8 +31,7 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import com.amazonaws.AmazonClientException; -import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughputExceededException; -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; +import org.apache.hadoop.util.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -77,7 +76,6 @@ import static org.apache.hadoop.fs.s3a.Constants.*; * untranslated exceptions, as well as the translated ones. * @see S3 Error responses * @see Amazon S3 Error Best Practices - * @see Dynamo DB Commmon errors */ @SuppressWarnings("visibilitymodifier") // I want a struct of finals, for real. public class S3ARetryPolicy implements RetryPolicy { @@ -191,10 +189,6 @@ public class S3ARetryPolicy implements RetryPolicy { policyMap.put(UnknownStoreException.class, fail); policyMap.put(InvalidRequestException.class, fail); - // metadata stores should do retries internally when it makes sense - // so there is no point doing another layer of retries after that - policyMap.put(MetadataPersistenceException.class, fail); - // once the file has changed, trying again is not going to help policyMap.put(RemoteFileChangedException.class, fail); @@ -234,11 +228,6 @@ public class S3ARetryPolicy implements RetryPolicy { policyMap.put(AWSS3IOException.class, retryIdempotentCalls); policyMap.put(SocketTimeoutException.class, retryIdempotentCalls); - // Dynamo DB exceptions - // asking for more than you should get. It's a retry but should be logged - // trigger sleep - policyMap.put(ProvisionedThroughputExceededException.class, throttlePolicy); - return policyMap; } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AUtils.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AUtils.java index 8a824173fa5..a7f48c9043e 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AUtils.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AUtils.java @@ -27,10 +27,6 @@ import com.amazonaws.SdkBaseException; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.EnvironmentVariableCredentialsProvider; import com.amazonaws.retry.RetryUtils; -import com.amazonaws.services.dynamodbv2.model.AmazonDynamoDBException; -import com.amazonaws.services.dynamodbv2.model.LimitExceededException; -import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughputExceededException; -import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException; import com.amazonaws.services.s3.model.AmazonS3Exception; import com.amazonaws.services.s3.model.MultiObjectDeleteException; import com.amazonaws.services.s3.model.S3ObjectSummary; @@ -92,7 +88,6 @@ import static org.apache.hadoop.fs.s3a.impl.ErrorTranslation.isUnknownBucket; import static org.apache.hadoop.fs.s3a.impl.InternalConstants.CSE_PADDING_LENGTH; import static org.apache.hadoop.fs.s3a.impl.MultiObjectDeleteSupport.translateDeleteException; import static org.apache.hadoop.io.IOUtils.cleanupWithLogger; -import static org.apache.hadoop.util.functional.RemoteIterators.filteringRemoteIterator; /** * Utility methods for S3A code. @@ -163,7 +158,6 @@ public final class S3AUtils { * * @see S3 Error responses * @see Amazon S3 Error Best Practices - * @see Dynamo DB Commmon errors * @param operation operation * @param path path operated on (must not be null) * @param exception amazon exception raised @@ -213,11 +207,6 @@ public final class S3AUtils { } return new AWSClientIOException(message, exception); } else { - if (exception instanceof AmazonDynamoDBException) { - // special handling for dynamo DB exceptions - return translateDynamoDBException(path, message, - (AmazonDynamoDBException)exception); - } IOException ioe; AmazonServiceException ase = (AmazonServiceException) exception; // this exception is non-null if the service exception is an s3 one @@ -401,8 +390,7 @@ public final class S3AUtils { /** * Is the exception an instance of a throttling exception. That - * is an AmazonServiceException with a 503 response, any - * exception from DynamoDB for limits exceeded, an + * is an AmazonServiceException with a 503 response, an * {@link AWSServiceThrottledException}, * or anything which the AWS SDK's RetryUtils considers to be * a throttling exception. @@ -411,8 +399,6 @@ public final class S3AUtils { */ public static boolean isThrottleException(Exception ex) { return ex instanceof AWSServiceThrottledException - || ex instanceof ProvisionedThroughputExceededException - || ex instanceof LimitExceededException || (ex instanceof AmazonServiceException && 503 == ((AmazonServiceException)ex).getStatusCode()) || (ex instanceof SdkBaseException @@ -431,49 +417,6 @@ public final class S3AUtils { ex.toString().contains(EOF_READ_DIFFERENT_LENGTH); } - /** - * Translate a DynamoDB exception into an IOException. - * - * @param path path in the DDB - * @param message preformatted message for the exception - * @param ddbException exception - * @return an exception to throw. - */ - public static IOException translateDynamoDBException(final String path, - final String message, - final AmazonDynamoDBException ddbException) { - if (isThrottleException(ddbException)) { - return new AWSServiceThrottledException(message, ddbException); - } - if (ddbException instanceof ResourceNotFoundException) { - return (FileNotFoundException) new FileNotFoundException(message) - .initCause(ddbException); - } - final int statusCode = ddbException.getStatusCode(); - final String errorCode = ddbException.getErrorCode(); - IOException result = null; - // 400 gets used a lot by DDB - if (statusCode == 400) { - switch (errorCode) { - case "AccessDeniedException": - result = (IOException) new AccessDeniedException( - path, - null, - ddbException.toString()) - .initCause(ddbException); - break; - - default: - result = new AWSBadRequestException(message, ddbException); - } - - } - if (result == null) { - result = new AWSServiceIOException(message, ddbException); - } - return result; - } - /** * Get low level details of an amazon exception for logging; multi-line. * @param e exception @@ -1248,7 +1191,7 @@ public final class S3AUtils { * @param conf The Hadoop configuration * @param bucket Optional bucket to use to look up per-bucket proxy secrets * @param awsServiceIdentifier a string representing the AWS service (S3, - * DDB, etc) for which the ClientConfiguration is being created. + * etc) for which the ClientConfiguration is being created. * @return new AWS client configuration * @throws IOException problem creating AWS client configuration */ @@ -1265,9 +1208,6 @@ public final class S3AUtils { case AWS_SERVICE_IDENTIFIER_S3: configKey = SIGNING_ALGORITHM_S3; break; - case AWS_SERVICE_IDENTIFIER_DDB: - configKey = SIGNING_ALGORITHM_DDB; - break; case AWS_SERVICE_IDENTIFIER_STS: configKey = SIGNING_ALGORITHM_STS; break; @@ -1430,21 +1370,16 @@ public final class S3AUtils { /** * Convert the data of an iterator of {@link S3AFileStatus} to - * an array. Given tombstones are filtered out. If the iterator - * does return any item, an empty array is returned. + * an array. * @param iterator a non-null iterator - * @param tombstones possibly empty set of tombstones * @return a possibly-empty array of file status entries * @throws IOException failure */ public static S3AFileStatus[] iteratorToStatuses( - RemoteIterator iterator, Set tombstones) + RemoteIterator iterator) throws IOException { - // this will close the span afterwards - RemoteIterator source = filteringRemoteIterator(iterator, - st -> !tombstones.contains(st.getPath())); S3AFileStatus[] statuses = RemoteIterators - .toArray(source, new S3AFileStatus[0]); + .toArray(iterator, new S3AFileStatus[0]); return statuses; } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3GuardExistsRetryPolicy.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3GuardExistsRetryPolicy.java deleted file mode 100644 index 079c94e445a..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3GuardExistsRetryPolicy.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.hadoop.fs.s3a; - -import java.io.FileNotFoundException; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.io.retry.RetryPolicy; - -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_CONSISTENCY_RETRY_INTERVAL; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_CONSISTENCY_RETRY_INTERVAL_DEFAULT; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_CONSISTENCY_RETRY_LIMIT; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_CONSISTENCY_RETRY_LIMIT_DEFAULT; -import static org.apache.hadoop.io.retry.RetryPolicies.retryUpToMaximumCountWithProportionalSleep; - -/** - * Slightly-modified retry policy for cases when the file is present in the - * MetadataStore, but may be still throwing FileNotFoundException from S3. - */ -public class S3GuardExistsRetryPolicy extends S3ARetryPolicy { - - private static final Logger LOG = LoggerFactory.getLogger( - S3GuardExistsRetryPolicy.class); - - /** - * Instantiate. - * @param conf configuration to read. - */ - public S3GuardExistsRetryPolicy(Configuration conf) { - super(conf); - } - - @Override - protected Map, RetryPolicy> createExceptionMap() { - Map, RetryPolicy> b = super.createExceptionMap(); - Configuration conf = getConfiguration(); - - // base policy from configuration - int limit = conf.getInt(S3GUARD_CONSISTENCY_RETRY_LIMIT, - S3GUARD_CONSISTENCY_RETRY_LIMIT_DEFAULT); - long interval = conf.getTimeDuration(S3GUARD_CONSISTENCY_RETRY_INTERVAL, - S3GUARD_CONSISTENCY_RETRY_INTERVAL_DEFAULT, - TimeUnit.MILLISECONDS); - RetryPolicy retryPolicy = retryUpToMaximumCountWithProportionalSleep( - limit, - interval, - TimeUnit.MILLISECONDS); - LOG.debug("Retrying on recoverable S3Guard table/S3 inconsistencies {}" - + " times with an initial interval of {}ms", limit, interval); - - b.put(FileNotFoundException.class, retryPolicy); - b.put(RemoteFileChangedException.class, retryPolicy); - return b; - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3ListResult.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3ListResult.java index 69794c04db5..69c42bfe147 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3ListResult.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3ListResult.java @@ -20,7 +20,6 @@ package org.apache.hadoop.fs.s3a; import java.util.Collection; import java.util.List; -import java.util.Set; import java.util.stream.Collectors; import com.amazonaws.services.s3.model.ListObjectsV2Result; @@ -28,9 +27,6 @@ import com.amazonaws.services.s3.model.ObjectListing; import com.amazonaws.services.s3.model.S3ObjectSummary; import org.slf4j.Logger; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.impl.ContextAccessors; - /** * API version-independent container for S3 List responses. */ @@ -101,25 +97,6 @@ public class S3ListResult { } } - /** - * Is the list of object summaries empty - * after accounting for tombstone markers (if provided)? - * @param accessors callback for key to path mapping. - * @param tombstones Set of tombstone markers, or null if not applicable. - * @return false if summaries contains objects not accounted for by - * tombstones. - */ - public boolean isEmptyOfObjects( - final ContextAccessors accessors, - final Set tombstones) { - if (tombstones == null) { - return getObjectSummaries().isEmpty(); - } - return isEmptyOfKeys(accessors, - objectSummaryKeys(), - tombstones); - } - /** * Get the list of keys in the object summary. * @return a possibly empty list @@ -131,55 +108,22 @@ public class S3ListResult { } /** - * Does this listing have prefixes or objects after entries with - * tombstones have been stripped? - * @param accessors callback for key to path mapping. - * @param tombstones Set of tombstone markers, or null if not applicable. - * @return true if the reconciled list is non-empty + * Does this listing have prefixes or objects? + * @return true if the result is non-empty */ - public boolean hasPrefixesOrObjects( - final ContextAccessors accessors, - final Set tombstones) { + public boolean hasPrefixesOrObjects() { - return !isEmptyOfKeys(accessors, getCommonPrefixes(), tombstones) - || !isEmptyOfObjects(accessors, tombstones); - } - - /** - * Helper function to determine if a collection of keys is empty - * after accounting for tombstone markers (if provided). - * @param accessors callback for key to path mapping. - * @param keys Collection of path (prefixes / directories or keys). - * @param tombstones Set of tombstone markers, or null if not applicable. - * @return true if the list is considered empty. - */ - public boolean isEmptyOfKeys( - final ContextAccessors accessors, - final Collection keys, - final Set tombstones) { - if (tombstones == null) { - return keys.isEmpty(); - } - for (String key : keys) { - Path qualified = accessors.keyToPath(key); - if (!tombstones.contains(qualified)) { - return false; - } - } - return true; + return !(getCommonPrefixes()).isEmpty() + || !getObjectSummaries().isEmpty(); } /** * Does this listing represent an empty directory? - * @param contextAccessors callback for key to path mapping. * @param dirKey directory key - * @param tombstones Set of tombstone markers, or null if not applicable. * @return true if the list is considered empty. */ public boolean representsEmptyDirectory( - final ContextAccessors contextAccessors, - final String dirKey, - final Set tombstones) { + final String dirKey) { // If looking for an empty directory, the marker must exist but // no children. // So the listing must contain the marker entry only as an object, @@ -190,7 +134,7 @@ public class S3ListResult { } /** - * Dmp the result at debug level. + * Dump the result at debug level. * @param log log to use */ public void logAtDebug(Logger log) { diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Statistic.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Statistic.java index fb28a40c3b8..ed16a7c4fd8 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Statistic.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Statistic.java @@ -455,47 +455,6 @@ public enum Statistic { "Duration Tracking of files uploaded from a local staging path", TYPE_DURATION), - /* S3guard stats */ - S3GUARD_METADATASTORE_PUT_PATH_REQUEST( - "s3guard_metadatastore_put_path_request", - "S3Guard metadata store put one metadata path request", - TYPE_COUNTER), - S3GUARD_METADATASTORE_PUT_PATH_LATENCY( - "s3guard_metadatastore_put_path_latency", - "S3Guard metadata store put one metadata path latency", - TYPE_QUANTILE), - S3GUARD_METADATASTORE_INITIALIZATION( - "s3guard_metadatastore_initialization", - "S3Guard metadata store initialization times", - TYPE_COUNTER), - S3GUARD_METADATASTORE_RECORD_DELETES( - "s3guard_metadatastore_record_deletes", - "S3Guard metadata store records deleted", - TYPE_COUNTER), - S3GUARD_METADATASTORE_RECORD_READS( - "s3guard_metadatastore_record_reads", - "S3Guard metadata store records read", - TYPE_COUNTER), - S3GUARD_METADATASTORE_RECORD_WRITES( - "s3guard_metadatastore_record_writes", - "S3Guard metadata store records written", - TYPE_COUNTER), - S3GUARD_METADATASTORE_RETRY("s3guard_metadatastore_retry", - "S3Guard metadata store retry events", - TYPE_COUNTER), - S3GUARD_METADATASTORE_THROTTLED("s3guard_metadatastore_throttled", - "S3Guard metadata store throttled events", - TYPE_COUNTER), - S3GUARD_METADATASTORE_THROTTLE_RATE( - "s3guard_metadatastore_throttle_rate", - "S3Guard metadata store throttle rate", - TYPE_QUANTILE), - S3GUARD_METADATASTORE_AUTHORITATIVE_DIRECTORIES_UPDATED( - "s3guard_metadatastore_authoritative_directories_updated", - "S3Guard metadata store authoritative directories updated from S3", - TYPE_COUNTER), - - /* General Store operations */ STORE_EXISTS_PROBE(StoreStatisticNames.STORE_EXISTS_PROBE, "Store Existence Probe", diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/WriteOperationHelper.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/WriteOperationHelper.java index 2eb4841d9bd..03bdbe3a3bf 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/WriteOperationHelper.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/WriteOperationHelper.java @@ -53,8 +53,6 @@ import org.apache.hadoop.fs.PathIOException; import org.apache.hadoop.fs.s3a.api.RequestFactory; import org.apache.hadoop.fs.s3a.impl.StoreContext; import org.apache.hadoop.fs.s3a.statistics.S3AStatisticsContext; -import org.apache.hadoop.fs.s3a.s3guard.BulkOperationState; -import org.apache.hadoop.fs.s3a.s3guard.S3Guard; import org.apache.hadoop.fs.s3a.select.SelectBinding; import org.apache.hadoop.fs.store.audit.AuditSpan; import org.apache.hadoop.fs.store.audit.AuditSpanSource; @@ -82,7 +80,7 @@ import static org.apache.hadoop.fs.store.audit.AuditingFunctions.withinAuditSpan * upload process.
    4. *
    5. Other low-level access to S3 functions, for private use.
    6. *
    7. Failure handling, including converting exceptions to IOEs.
    8. - *
    9. Integration with instrumentation and S3Guard.
    10. + *
    11. Integration with instrumentation.
    12. *
    13. Evolution to add more low-level operations, such as S3 select.
    14. * * @@ -324,7 +322,7 @@ public class WriteOperationHelper implements WriteOperations { /** * Finalize a multipart PUT operation. * This completes the upload, and, if that works, calls - * {@link S3AFileSystem#finishedWrite(String, long, String, String, BulkOperationState)} + * {@link S3AFileSystem#finishedWrite(String, long, String, String)} * to update the filesystem. * Retry policy: retrying, translated. * @param destKey destination of the commit @@ -332,7 +330,6 @@ public class WriteOperationHelper implements WriteOperations { * @param partETags list of partial uploads * @param length length of the upload * @param retrying retrying callback - * @param operationState (nullable) operational state for a bulk update * @return the result of the operation. * @throws IOException on problems. */ @@ -342,8 +339,7 @@ public class WriteOperationHelper implements WriteOperations { String uploadId, List partETags, long length, - Retried retrying, - @Nullable BulkOperationState operationState) throws IOException { + Retried retrying) throws IOException { if (partETags.isEmpty()) { throw new PathIOException(destKey, "No upload parts in multipart upload"); @@ -361,7 +357,7 @@ public class WriteOperationHelper implements WriteOperations { request); }); owner.finishedWrite(destKey, length, uploadResult.getETag(), - uploadResult.getVersionId(), operationState); + uploadResult.getVersionId()); return uploadResult; } } @@ -397,8 +393,8 @@ public class WriteOperationHelper implements WriteOperations { uploadId, partETags, length, - (text, e, r, i) -> errorCount.incrementAndGet(), - null); + (text, e, r, i) -> errorCount.incrementAndGet() + ); } /** @@ -587,16 +583,14 @@ public class WriteOperationHelper implements WriteOperations { * Relies on retry code in filesystem * @throws IOException on problems * @param destKey destination key - * @param operationState operational state for a bulk update */ @Retries.OnceTranslated - public void revertCommit(String destKey, - @Nullable BulkOperationState operationState) throws IOException { + public void revertCommit(String destKey) throws IOException { once("revert commit", destKey, withinAuditSpan(getAuditSpan(), () -> { Path destPath = owner.keyToQualifiedPath(destKey); owner.deleteObjectAtPath(destPath, - destKey, true, operationState); + destKey, true); owner.maybeCreateFakeParentDirectory(destPath); })); } @@ -610,7 +604,6 @@ public class WriteOperationHelper implements WriteOperations { * @param uploadId multipart operation Id * @param partETags list of partial uploads * @param length length of the upload - * @param operationState operational state for a bulk update * @return the result of the operation. * @throws IOException if problems arose which could not be retried, or * the retry count was exceeded @@ -620,8 +613,7 @@ public class WriteOperationHelper implements WriteOperations { String destKey, String uploadId, List partETags, - long length, - @Nullable BulkOperationState operationState) + long length) throws IOException { checkNotNull(uploadId); checkNotNull(partETags); @@ -631,32 +623,8 @@ public class WriteOperationHelper implements WriteOperations { uploadId, partETags, length, - Invoker.NO_OP, - operationState); - } - - /** - * Initiate a commit operation through any metastore. - * @param path path under which the writes will all take place. - * @return an possibly null operation state from the metastore. - * @throws IOException failure to instantiate. - */ - public BulkOperationState initiateCommitOperation( - Path path) throws IOException { - return initiateOperation(path, BulkOperationState.OperationType.Commit); - } - - /** - * Initiate a commit operation through any metastore. - * @param path path under which the writes will all take place. - * @param operationType operation to initiate - * @return an possibly null operation state from the metastore. - * @throws IOException failure to instantiate. - */ - public BulkOperationState initiateOperation(final Path path, - final BulkOperationState.OperationType operationType) throws IOException { - return S3Guard.initiateBulkWrite(owner.getMetadataStore(), - operationType, path); + Invoker.NO_OP + ); } /** diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/WriteOperations.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/WriteOperations.java index 9400ef2c490..7604bbe46b5 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/WriteOperations.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/WriteOperations.java @@ -43,7 +43,6 @@ import com.amazonaws.services.s3.transfer.model.UploadResult; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.PathIOException; -import org.apache.hadoop.fs.s3a.s3guard.BulkOperationState; import org.apache.hadoop.fs.store.audit.AuditSpanSource; import org.apache.hadoop.util.functional.CallableRaisingIOE; @@ -261,11 +260,9 @@ public interface WriteOperations extends AuditSpanSource, Closeable { * Relies on retry code in filesystem * @throws IOException on problems * @param destKey destination key - * @param operationState operational state for a bulk update */ @Retries.OnceTranslated - void revertCommit(String destKey, - @Nullable BulkOperationState operationState) throws IOException; + void revertCommit(String destKey) throws IOException; /** * This completes a multipart upload to the destination key via @@ -276,7 +273,6 @@ public interface WriteOperations extends AuditSpanSource, Closeable { * @param uploadId multipart operation Id * @param partETags list of partial uploads * @param length length of the upload - * @param operationState operational state for a bulk update * @return the result of the operation. * @throws IOException if problems arose which could not be retried, or * the retry count was exceeded @@ -286,29 +282,9 @@ public interface WriteOperations extends AuditSpanSource, Closeable { String destKey, String uploadId, List partETags, - long length, - @Nullable BulkOperationState operationState) + long length) throws IOException; - /** - * Initiate a commit operation through any metastore. - * @param path path under which the writes will all take place. - * @return an possibly null operation state from the metastore. - * @throws IOException failure to instantiate. - */ - BulkOperationState initiateCommitOperation( - Path path) throws IOException; - - /** - * Initiate a commit operation through any metastore. - * @param path path under which the writes will all take place. - * @param operationType operation to initiate - * @return an possibly null operation state from the metastore. - * @throws IOException failure to instantiate. - */ - BulkOperationState initiateOperation(Path path, - BulkOperationState.OperationType operationType) throws IOException; - /** * Upload part of a multi-partition file. * @param request request diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/RoleModel.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/RoleModel.java index 6676b49b6a4..972bf64421d 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/RoleModel.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/RoleModel.java @@ -45,7 +45,6 @@ import static java.util.Objects.requireNonNull; * don't expect to be able to parse everything. * It can generate simple models. * @see Example S3 Policies - * @see Dynamno DB Permissions */ @InterfaceAudience.LimitedPrivate("Tests") @InterfaceStability.Unstable diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/RolePolicies.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/RolePolicies.java index 3d8d7ca2574..aa88a320ff2 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/RolePolicies.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/RolePolicies.java @@ -32,7 +32,7 @@ import static org.apache.hadoop.fs.s3a.auth.RoleModel.*; /** * Operations, statements and policies covering the operations - * needed to work with S3 and S3Guard. + * needed to work with S3. */ @InterfaceAudience.LimitedPrivate("Tests") @InterfaceStability.Unstable @@ -200,7 +200,7 @@ public final class RolePolicies { /** * Actions needed to read a file in S3 through S3A, excluding - * S3Guard and SSE-KMS. + * SSE-KMS. */ private static final String[] S3_PATH_READ_OPERATIONS = new String[]{ @@ -213,7 +213,6 @@ public final class RolePolicies { *
        *
      1. bucket-level operations
      2. *
      3. SSE-KMS key operations
      4. - *
      5. DynamoDB operations for S3Guard.
      6. *
      * As this excludes the bucket list operations, it is not sufficient * to read from a bucket on its own. @@ -230,7 +229,6 @@ public final class RolePolicies { * Policies which can be applied to bucket resources for read operations. *
        *
      1. SSE-KMS key operations
      2. - *
      3. DynamoDB operations for S3Guard.
      4. *
      */ public static final String[] S3_BUCKET_READ_OPERATIONS = @@ -242,7 +240,7 @@ public final class RolePolicies { /** * Actions needed to write data to an S3A Path. * This includes the appropriate read operations, but - * not SSE-KMS or S3Guard support. + * not SSE-KMS support. */ public static final List S3_PATH_RW_OPERATIONS = Collections.unmodifiableList(Arrays.asList(new String[]{ @@ -258,7 +256,7 @@ public final class RolePolicies { * This is purely the extra operations needed for writing atop * of the read operation set. * Deny these and a path is still readable, but not writeable. - * Excludes: bucket-ARN, SSE-KMS and S3Guard permissions. + * Excludes: bucket-ARN and SSE-KMS permissions. */ public static final List S3_PATH_WRITE_OPERATIONS = Collections.unmodifiableList(Arrays.asList(new String[]{ @@ -270,7 +268,7 @@ public final class RolePolicies { /** * Actions needed for R/W IO from the root of a bucket. - * Excludes: bucket-ARN, SSE-KMS and S3Guard permissions. + * Excludes: bucket-ARN and SSE-KMS permissions. */ public static final List S3_ROOT_RW_OPERATIONS = Collections.unmodifiableList(Arrays.asList(new String[]{ @@ -281,79 +279,9 @@ public final class RolePolicies { S3_ABORT_MULTIPART_UPLOAD, })); - /** - * All DynamoDB operations: {@value}. - */ - public static final String DDB_ALL_OPERATIONS = "dynamodb:*"; - - /** - * Operations needed for DDB/S3Guard Admin. - * For now: make this {@link #DDB_ALL_OPERATIONS}. - */ - public static final String DDB_ADMIN = DDB_ALL_OPERATIONS; - - /** - * Permission for DDB describeTable() operation: {@value}. - * This is used during initialization. - */ - public static final String DDB_DESCRIBE_TABLE = "dynamodb:DescribeTable"; - - /** - * Permission to query the DDB table: {@value}. - */ - public static final String DDB_QUERY = "dynamodb:Query"; - - /** - * Permission for DDB operation to get a record: {@value}. - */ - public static final String DDB_GET_ITEM = "dynamodb:GetItem"; - - /** - * Permission for DDB write record operation: {@value}. - */ - public static final String DDB_PUT_ITEM = "dynamodb:PutItem"; - - /** - * Permission for DDB update single item operation: {@value}. - */ - public static final String DDB_UPDATE_ITEM = "dynamodb:UpdateItem"; - - /** - * Permission for DDB delete operation: {@value}. - */ - public static final String DDB_DELETE_ITEM = "dynamodb:DeleteItem"; - - /** - * Permission for DDB operation: {@value}. - */ - public static final String DDB_BATCH_GET_ITEM = "dynamodb:BatchGetItem"; - - /** - * Batch write permission for DDB: {@value}. - */ - public static final String DDB_BATCH_WRITE_ITEM = "dynamodb:BatchWriteItem"; - - /** - * All DynamoDB tables: {@value}. - */ - public static final String ALL_DDB_TABLES = "arn:aws:dynamodb:*"; - - /** - * Statement to allow all DDB access. - */ - public static final Statement STATEMENT_ALL_DDB = - allowAllDynamoDBOperations(ALL_DDB_TABLES); - - /** - * Statement to allow all client operations needed for S3Guard, - * but none of the admin operations. - */ - public static final Statement STATEMENT_S3GUARD_CLIENT = - allowS3GuardClientOperations(ALL_DDB_TABLES); - /** * Allow all S3 Operations. - * This does not cover DDB or S3-KMS + * This does not cover S3-KMS */ public static final Statement STATEMENT_ALL_S3 = statement(true, S3_ALL_BUCKETS, @@ -368,36 +296,6 @@ public final class RolePolicies { S3_ALL_BUCKETS, S3_GET_BUCKET_LOCATION); - /** - * Policy for all S3 and S3Guard operations, and SSE-KMS. - */ - public static final Policy ALLOW_S3_AND_SGUARD = policy( - STATEMENT_ALL_S3, - STATEMENT_ALL_DDB, - STATEMENT_ALLOW_SSE_KMS_RW, - STATEMENT_ALL_S3_GET_BUCKET_LOCATION - ); - - public static Statement allowS3GuardClientOperations(String tableArn) { - return statement(true, - tableArn, - DDB_BATCH_GET_ITEM, - DDB_BATCH_WRITE_ITEM, - DDB_DELETE_ITEM, - DDB_DESCRIBE_TABLE, - DDB_GET_ITEM, - DDB_PUT_ITEM, - DDB_QUERY, - DDB_UPDATE_ITEM - ); - } - - public static Statement allowAllDynamoDBOperations(String tableArn) { - return statement(true, - tableArn, - DDB_ALL_OPERATIONS); - } - /** * From an S3 bucket name, build an ARN to refer to it. * @param bucket bucket name. diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/delegation/AWSPolicyProvider.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/delegation/AWSPolicyProvider.java index aaca10f1aea..371a0dceaf7 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/delegation/AWSPolicyProvider.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/delegation/AWSPolicyProvider.java @@ -31,8 +31,8 @@ import org.apache.hadoop.fs.s3a.auth.RoleModel; * The permissions requested are from the perspective of * S3A filesystem operations on the data, not the simpler * model of "permissions on the the remote service". - * As an example, to use S3Guard effectively, the client needs full CRUD - * access to the table, even for {@link AccessLevel#READ}. + * As an example, AWS-KMS encryption permissions must + * also be requested. */ public interface AWSPolicyProvider { diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/delegation/AbstractDelegationTokenBinding.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/delegation/AbstractDelegationTokenBinding.java index fc7b095f0dd..6e31c006e26 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/delegation/AbstractDelegationTokenBinding.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/delegation/AbstractDelegationTokenBinding.java @@ -210,7 +210,7 @@ public abstract class AbstractDelegationTokenBinding extends AbstractDTService { /** * Bind to the token identifier, returning the credential providers to use - * for the owner to talk to S3, DDB and related AWS Services. + * for the owner to talk to S3 and related AWS Services. * @param retrievedIdentifier the unmarshalled data * @return non-empty list of AWS credential providers to use for * authenticating this client with AWS services. diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/delegation/S3ADelegationTokens.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/delegation/S3ADelegationTokens.java index 05917fe9c63..41140073931 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/delegation/S3ADelegationTokens.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/auth/delegation/S3ADelegationTokens.java @@ -128,8 +128,7 @@ public class S3ADelegationTokens extends AbstractDTService { /** * The access policies we want for operations. - * There's no attempt to ask for "admin" permissions here, e.g. - * those to manipulate S3Guard tables. + * There's no attempt to ask for "admin" permissions here. */ protected static final EnumSet ACCESS_POLICY = EnumSet.of( @@ -420,8 +419,6 @@ public class S3ADelegationTokens extends AbstractDTService { requireServiceStarted(); checkArgument(encryptionSecrets != null, "Null encryption secrets"); - // this isn't done in in advance as it needs S3Guard initialized in the - // filesystem before it can generate complete policies. List statements = getPolicyProvider() .listAWSPolicyRules(ACCESS_POLICY); Optional rolePolicy = diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/AbstractS3ACommitter.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/AbstractS3ACommitter.java index 2005bc8b7f0..665092d1930 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/AbstractS3ACommitter.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/AbstractS3ACommitter.java @@ -441,7 +441,7 @@ public abstract class AbstractS3ACommitter extends PathOutputCommitter return false; } /** - * Task recovery considered unsupported: Warn and fail. + * Task recovery considered Unsupported: Warn and fail. * @param taskContext Context of the task whose output is being recovered * @throws IOException always. */ @@ -457,7 +457,7 @@ public abstract class AbstractS3ACommitter extends PathOutputCommitter * if the job requires a success marker on a successful job, * create the file {@link CommitConstants#_SUCCESS}. * - * While the classic committers create a 0-byte file, the S3Guard committers + * While the classic committers create a 0-byte file, the S3A committers * PUT up a the contents of a {@link SuccessData} file. * @param context job context * @param pending the pending commits @@ -481,7 +481,7 @@ public abstract class AbstractS3ACommitter extends PathOutputCommitter * if the job requires a success marker on a successful job, * create the file {@link CommitConstants#_SUCCESS}. * - * While the classic committers create a 0-byte file, the S3Guard committers + * While the classic committers create a 0-byte file, the S3A committers * PUT up a the contents of a {@link SuccessData} file. * @param context job context * @param filenames list of filenames. diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/CommitOperations.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/CommitOperations.java index a4f13b78a3f..d287cca4b45 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/CommitOperations.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/CommitOperations.java @@ -18,7 +18,6 @@ package org.apache.hadoop.fs.s3a.commit; -import javax.annotation.Nullable; import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; @@ -39,7 +38,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.commons.lang3.tuple.Pair; -import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.LocatedFileStatus; import org.apache.hadoop.fs.Path; @@ -55,12 +53,10 @@ import org.apache.hadoop.fs.s3a.commit.files.SuccessData; import org.apache.hadoop.fs.s3a.impl.AbstractStoreOperation; import org.apache.hadoop.fs.s3a.impl.HeaderProcessing; import org.apache.hadoop.fs.s3a.impl.InternalConstants; -import org.apache.hadoop.fs.s3a.s3guard.BulkOperationState; import org.apache.hadoop.fs.s3a.statistics.CommitterStatistics; import org.apache.hadoop.fs.statistics.DurationTracker; import org.apache.hadoop.fs.statistics.IOStatistics; import org.apache.hadoop.fs.statistics.IOStatisticsSource; -import org.apache.hadoop.io.IOUtils; import org.apache.hadoop.util.DurationInfo; import org.apache.hadoop.util.Progressable; @@ -70,7 +66,6 @@ import static org.apache.hadoop.fs.s3a.Statistic.COMMITTER_COMMIT_JOB; import static org.apache.hadoop.fs.s3a.Statistic.COMMITTER_MATERIALIZE_FILE; import static org.apache.hadoop.fs.s3a.Statistic.COMMITTER_STAGE_FILE_UPLOAD; import static org.apache.hadoop.fs.s3a.commit.CommitConstants.*; -import static org.apache.hadoop.fs.s3a.Constants.*; import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.trackDuration; import static org.apache.hadoop.util.functional.RemoteIterators.cleanupRemoteIterator; @@ -171,13 +166,11 @@ public class CommitOperations extends AbstractStoreOperation /** * Commit the operation, throwing an exception on any failure. * @param commit commit to execute - * @param operationState S3Guard state of ongoing operation. * @throws IOException on a failure */ private void commitOrFail( - final SinglePendingCommit commit, - final BulkOperationState operationState) throws IOException { - commit(commit, commit.getFilename(), operationState).maybeRethrow(); + final SinglePendingCommit commit) throws IOException { + commit(commit, commit.getFilename()).maybeRethrow(); } /** @@ -185,13 +178,11 @@ public class CommitOperations extends AbstractStoreOperation * and converted to an outcome. * @param commit entry to commit * @param origin origin path/string for outcome text - * @param operationState S3Guard state of ongoing operation. * @return the outcome */ private MaybeIOE commit( final SinglePendingCommit commit, - final String origin, - final BulkOperationState operationState) { + final String origin) { LOG.debug("Committing single commit {}", commit); MaybeIOE outcome; String destKey = "unknown destination"; @@ -203,7 +194,7 @@ public class CommitOperations extends AbstractStoreOperation commit.validate(); destKey = commit.getDestinationKey(); long l = trackDuration(statistics, COMMITTER_MATERIALIZE_FILE.getSymbol(), - () -> innerCommit(commit, operationState)); + () -> innerCommit(commit)); LOG.debug("Successful commit of file length {}", l); outcome = MaybeIOE.NONE; statistics.commitCompleted(commit.getLength()); @@ -226,20 +217,18 @@ public class CommitOperations extends AbstractStoreOperation /** * Inner commit operation. * @param commit entry to commit - * @param operationState S3Guard state of ongoing operation. * @return bytes committed. * @throws IOException failure */ private long innerCommit( - final SinglePendingCommit commit, - final BulkOperationState operationState) throws IOException { + final SinglePendingCommit commit) throws IOException { // finalize the commit writeOperations.commitUpload( commit.getDestinationKey(), commit.getUploadId(), toPartEtags(commit.getEtags()), - commit.getLength(), - operationState); + commit.getLength() + ); return commit.getLength(); } @@ -439,14 +428,6 @@ public class CommitOperations extends AbstractStoreOperation if (addMetrics) { addFileSystemStatistics(successData.getMetrics()); } - // add any diagnostics - Configuration conf = fs.getConf(); - successData.addDiagnostic(S3_METADATA_STORE_IMPL, - conf.getTrimmed(S3_METADATA_STORE_IMPL, "")); - successData.addDiagnostic(METADATASTORE_AUTHORITATIVE, - conf.getTrimmed(METADATASTORE_AUTHORITATIVE, "false")); - successData.addDiagnostic(AUTHORITATIVE_PATH, - conf.getTrimmed(AUTHORITATIVE_PATH, "")); // now write Path markerPath = new Path(outputPath, _SUCCESS); @@ -461,14 +442,12 @@ public class CommitOperations extends AbstractStoreOperation /** * Revert a pending commit by deleting the destination. * @param commit pending commit - * @param operationState nullable operational state for a bulk update * @throws IOException failure */ - public void revertCommit(SinglePendingCommit commit, - BulkOperationState operationState) throws IOException { + public void revertCommit(SinglePendingCommit commit) throws IOException { LOG.info("Revert {}", commit); try { - writeOperations.revertCommit(commit.getDestinationKey(), operationState); + writeOperations.revertCommit(commit.getDestinationKey()); } finally { statistics.commitReverted(); } @@ -617,7 +596,7 @@ public class CommitOperations extends AbstractStoreOperation * @throws IOException failure. */ public CommitContext initiateCommitOperation(Path path) throws IOException { - return new CommitContext(writeOperations.initiateCommitOperation(path)); + return new CommitContext(); } /** @@ -647,11 +626,7 @@ public class CommitOperations extends AbstractStoreOperation * Commit context. * * It is used to manage the final commit sequence where files become - * visible. It contains a {@link BulkOperationState} field, which, if - * there is a metastore, will be requested from the store so that it - * can track multiple creation operations within the same overall operation. - * This will be null if there is no metastore, or the store chooses not - * to provide one. + * visible. * * This can only be created through {@link #initiateCommitOperation(Path)}. * @@ -660,40 +635,34 @@ public class CommitOperations extends AbstractStoreOperation */ public final class CommitContext implements Closeable { - /** - * State of any metastore. - */ - private final BulkOperationState operationState; /** * Create. - * @param operationState any S3Guard bulk state. */ - private CommitContext(@Nullable final BulkOperationState operationState) { - this.operationState = operationState; + private CommitContext() { } /** * Commit the operation, throwing an exception on any failure. - * See {@link CommitOperations#commitOrFail(SinglePendingCommit, BulkOperationState)}. + * See {@link CommitOperations#commitOrFail(SinglePendingCommit)}. * @param commit commit to execute * @throws IOException on a failure */ public void commitOrFail(SinglePendingCommit commit) throws IOException { - CommitOperations.this.commitOrFail(commit, operationState); + CommitOperations.this.commitOrFail(commit); } /** * Commit a single pending commit; exceptions are caught * and converted to an outcome. - * See {@link CommitOperations#commit(SinglePendingCommit, String, BulkOperationState)}. + * See {@link CommitOperations#commit(SinglePendingCommit, String)}. * @param commit entry to commit * @param origin origin path/string for outcome text * @return the outcome */ public MaybeIOE commit(SinglePendingCommit commit, String origin) { - return CommitOperations.this.commit(commit, origin, operationState); + return CommitOperations.this.commit(commit, origin); } /** @@ -708,13 +677,13 @@ public class CommitOperations extends AbstractStoreOperation } /** - * See {@link CommitOperations#revertCommit(SinglePendingCommit, BulkOperationState)}. + * See {@link CommitOperations#revertCommit(SinglePendingCommit)}. * @param commit pending commit * @throws IOException failure */ public void revertCommit(final SinglePendingCommit commit) throws IOException { - CommitOperations.this.revertCommit(commit, operationState); + CommitOperations.this.revertCommit(commit); } /** @@ -733,14 +702,12 @@ public class CommitOperations extends AbstractStoreOperation @Override public void close() throws IOException { - IOUtils.cleanupWithLogger(LOG, operationState); } @Override public String toString() { final StringBuilder sb = new StringBuilder( "CommitContext{"); - sb.append("operationState=").append(operationState); sb.append('}'); return sb.toString(); } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/SuccessData.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/SuccessData.java index 4500e1429e4..00f196a0b40 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/SuccessData.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/commit/files/SuccessData.java @@ -47,7 +47,7 @@ import org.apache.hadoop.util.JsonSerialization; *
        *
      1. File length == 0: classic {@code FileOutputCommitter}.
      2. *
      3. Loadable as {@link SuccessData}: - * A s3guard committer with name in in {@link #committer} field.
      4. + * An S3A committer with name in in {@link #committer} field. *
      5. Not loadable? Something else.
      6. *
      * diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/ActiveOperationContext.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/ActiveOperationContext.java index 3ad2bbff3ba..9ad3c0a5410 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/ActiveOperationContext.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/ActiveOperationContext.java @@ -18,12 +18,9 @@ package org.apache.hadoop.fs.s3a.impl; -import javax.annotation.Nullable; - import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; -import org.apache.hadoop.fs.s3a.s3guard.BulkOperationState; import org.apache.hadoop.fs.s3a.statistics.S3AStatisticsContext; /** @@ -41,19 +38,12 @@ public class ActiveOperationContext { */ private final S3AStatisticsContext statisticsContext; - /** - * S3Guard bulk operation state, if (currently) set. - */ - @Nullable private BulkOperationState bulkOperationState; - public ActiveOperationContext( final long operationId, - final S3AStatisticsContext statisticsContext, - @Nullable final BulkOperationState bulkOperationState) { + final S3AStatisticsContext statisticsContext) { this.operationId = operationId; this.statisticsContext = Objects.requireNonNull(statisticsContext, "null statistics context"); - this.bulkOperationState = bulkOperationState; } @Override @@ -61,16 +51,10 @@ public class ActiveOperationContext { final StringBuilder sb = new StringBuilder( "ActiveOperation{"); sb.append("operationId=").append(operationId); - sb.append(", bulkOperationState=").append(bulkOperationState); sb.append('}'); return sb.toString(); } - @Nullable - public BulkOperationState getBulkOperationState() { - return bulkOperationState; - } - public long getOperationId() { return operationId; } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/ChangeTracker.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/ChangeTracker.java index 48050563cde..2e144bbfcd0 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/ChangeTracker.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/ChangeTracker.java @@ -44,7 +44,7 @@ import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions. /** * Change tracking for input streams: the version ID or etag of the object is * tracked and compared on open/re-open. An initial version ID or etag may or - * may not be available, depending on usage (e.g. if S3Guard is utilized). + * may not be available. * * Self-contained for testing and use in different streams. */ diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/DeleteOperation.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/DeleteOperation.java index 41415c94bc5..877616c0990 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/DeleteOperation.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/DeleteOperation.java @@ -18,7 +18,6 @@ package org.apache.hadoop.fs.s3a.impl; -import javax.annotation.Nullable; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -40,10 +39,6 @@ import org.apache.hadoop.fs.s3a.Retries; import org.apache.hadoop.fs.s3a.S3AFileStatus; import org.apache.hadoop.fs.s3a.S3ALocatedFileStatus; import org.apache.hadoop.fs.s3a.Tristate; -import org.apache.hadoop.fs.s3a.s3guard.BulkOperationState; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.S3Guard; -import org.apache.hadoop.io.IOUtils; import org.apache.hadoop.util.DurationInfo; import static org.apache.hadoop.fs.store.audit.AuditingFunctions.callableWithinAuditSpan; @@ -53,69 +48,22 @@ import static org.apache.hadoop.fs.s3a.impl.CallableSupplier.submit; /** * Implementation of the delete() operation. - *

      - * How S3Guard/Store inconsistency is handled: - *

        - *
      1. - * The list operation does not ask for tombstone markers; objects - * under tombstones will be found and deleted. - * The {@code extraFilesDeleted} counter will be incremented here. - *
      2. - *
      3. - * That may result in recently deleted files being found and - * duplicate delete requests issued. This is mostly harmless. - *
      4. - *
      5. - * If a path is considered authoritative on the client, so only S3Guard - * is used for listings, we wrap up the delete with a scan of raw S3. - * This will find and eliminate OOB additions. - *
      6. - *
      7. - * Exception 1: simple directory markers of the form PATH + "/". - * These are treated as a signal that there are no children; no - * listing is made. - *
      8. - *
      9. - * Exception 2: delete(path, true) where path has a tombstone in S3Guard. - * Here the delete is downgraded to a no-op even before this operation - * is created. Thus: no listings of S3. - *
      10. - *
      - * If this class is logged at debug, requests will be audited: - * the response to a bulk delete call will be reviewed to see if there - * were fewer files deleted than requested; that will be printed - * at WARN level. This is independent of handling rejected delete - * requests which raise exceptions -those are processed lower down. - *

      - * Performance tuning: - *

      - * The operation to POST a delete request (or issue many individual - * DELETE calls) then update the S3Guard table is done in an async - * operation so that it can overlap with the LIST calls for data. - * However, only one single operation is queued at a time. - *

      - * Executing more than one batch delete is possible, it just - * adds complexity in terms of error handling as well as in - * the datastructures used to track outstanding operations. + * This issues only one bulk delete at a time, + * intending to update S3Guard after every request succeeded. + * Now that S3Guard has been removed, it + * would be possible to issue multiple delete calls + * in parallel. * If this is done, then it may be good to experiment with different * page sizes. The default value is * {@link InternalConstants#MAX_ENTRIES_TO_DELETE}, the maximum a single * POST permits. *

      - * 1. Smaller pages executed in parallel may have different - * performance characteristics when deleting very large directories, - * because it will be the DynamoDB calls which will come to dominate. + * Smaller pages executed in parallel may have different + * performance characteristics when deleting very large directories. * Any exploration of options here MUST be done with performance - * measurements taken from test runs in EC2 against local DDB and S3 stores, + * measurements taken from test runs in EC2 against local S3 stores, * so as to ensure network latencies do not skew the results. - *

      - * 2. Note that as the DDB thread/connection pools will be shared across - * all active delete operations, speedups will be minimal unless - * those pools are large enough to cope the extra load. - *

      - * There are also some opportunities to explore in - * {@code DynamoDBMetadataStore} with batching delete requests - * in the DDB APIs. + */ public class DeleteOperation extends ExecutingStoreOperation { @@ -142,11 +90,6 @@ public class DeleteOperation extends ExecutingStoreOperation { */ private final int pageSize; - /** - * Metastore -never null but may be the NullMetadataStore. - */ - private final MetadataStore metadataStore; - /** * Executor for async operations. */ @@ -157,35 +100,16 @@ public class DeleteOperation extends ExecutingStoreOperation { */ private List keys; - /** - * List of paths built up for incremental deletion on tree delete. - * At the end of the entire delete the full tree is scanned in S3Guard - * and tombstones added. For this reason this list of paths must not - * include directory markers, as that will break the scan. - */ - private List paths; - /** * The single async delete operation, or null. */ private CompletableFuture deleteFuture; - /** - * Bulk Operation state if this is a bulk operation. - */ - private BulkOperationState operationState; - /** * Counter of deleted files. */ private long filesDeleted; - /** - * Counter of files found in the S3 Store during a raw scan of the store - * after the previous listing was in auth-mode. - */ - private long extraFilesDeleted; - /** * Constructor. * @param context store context @@ -208,7 +132,6 @@ public class DeleteOperation extends ExecutingStoreOperation { && pageSize <= InternalConstants.MAX_ENTRIES_TO_DELETE, "page size out of range: %s", pageSize); this.pageSize = pageSize; - metadataStore = context.getMetadataStore(); executor = MoreExecutors.listeningDecorator( context.createThrottledExecutor(1)); } @@ -217,10 +140,6 @@ public class DeleteOperation extends ExecutingStoreOperation { return filesDeleted; } - public long getExtraFilesDeleted() { - return extraFilesDeleted; - } - /** * Delete a file or directory tree. *

      @@ -230,10 +149,14 @@ public class DeleteOperation extends ExecutingStoreOperation { * Only one delete at a time is submitted, however, to reduce the * complexity of recovering from failures. *

      - * The DynamoDB store deletes paths in parallel itself, so that - * potentially slow part of the process is somewhat speeded up. - * The extra parallelization here is to list files from the store/DDB while - * that delete operation is in progress. + * With S3Guard removed, the problem of updating any + * DynamoDB store has gone away -delete calls could now be issued + * in parallel. + * However, rate limiting may be required to keep write load + * below the throttling point. Every entry in a single + * bulk delete call counts as a single write request -overloading + * an S3 partition with delete calls has been a problem in + * the past. * * @return true, except in the corner cases of root directory deletion * @throws PathIsNotEmptyDirectoryException if the path is a dir and this @@ -294,9 +217,7 @@ public class DeleteOperation extends ExecutingStoreOperation { * Delete a directory tree. *

      * This is done by asking the filesystem for a list of all objects under - * the directory path, without using any S3Guard tombstone markers to hide - * objects which may be returned in S3 listings but which are considered - * deleted. + * the directory path. *

      * Once the first {@link #pageSize} worth of objects has been listed, a batch * delete is queued for execution in a separate thread; subsequent batches @@ -304,9 +225,6 @@ public class DeleteOperation extends ExecutingStoreOperation { * being deleted in the separate thread. *

      * After all listed objects are queued for deletion, - * if the path is considered authoritative in the client, a final scan - * of S3 without S3Guard is executed, so as to find and delete - * any out-of-band objects in the tree. * @param path directory path * @param dirKey directory key * @throws IOException failure @@ -314,12 +232,7 @@ public class DeleteOperation extends ExecutingStoreOperation { @Retries.RetryTranslated protected void deleteDirectoryTree(final Path path, final String dirKey) throws IOException { - // create an operation state so that the store can manage the bulk - // operation if it needs to - operationState = S3Guard.initiateBulkWrite( - metadataStore, - BulkOperationState.OperationType.Delete, - path); + try (DurationInfo ignored = new DurationInfo(LOG, false, "deleting %s", dirKey)) { @@ -327,11 +240,11 @@ public class DeleteOperation extends ExecutingStoreOperation { resetDeleteList(); deleteFuture = null; - // list files including any under tombstones through S3Guard + // list files LOG.debug("Getting objects for directory prefix {} to delete", dirKey); final RemoteIterator locatedFiles = callbacks.listFilesAndDirectoryMarkers(path, status, - false, true); + true); // iterate through and delete. The next() call will block when a new S3 // page is required; this any active delete submitted to the executor @@ -345,50 +258,6 @@ public class DeleteOperation extends ExecutingStoreOperation { submitNextBatch(); maybeAwaitCompletion(deleteFuture); - // if s3guard is authoritative we follow up with a bulk list and - // delete process on S3 this helps recover from any situation where S3 - // and S3Guard have become inconsistent. - // This is only needed for auth paths; by performing the previous listing - // without tombstone filtering, any files returned by the non-auth - // S3 list which were hidden under tombstones will have been found - // and deleted. - - if (callbacks.allowAuthoritative(path)) { - LOG.debug("Path is authoritatively guarded;" - + " listing files on S3 for completeness"); - // let the ongoing delete finish to avoid duplicates - final RemoteIterator objects = - callbacks.listObjects(path, dirKey); - - // iterate through and delete. The next() call will block when a new S3 - // page is required; this any active delete submitted to the executor - // will run in parallel with this. - while (objects.hasNext()) { - // get the next entry in the listing. - extraFilesDeleted++; - S3AFileStatus next = objects.next(); - LOG.debug("Found Unlisted entry {}", next); - queueForDeletion(deletionKey(next), null, - next.isDirectory()); - } - if (extraFilesDeleted > 0) { - LOG.debug("Raw S3 Scan found {} extra file(s) to delete", - extraFilesDeleted); - // there is no more data: - // await any ongoing operation - submitNextBatch(); - maybeAwaitCompletion(deleteFuture); - } - } - - // final cleanup of the directory tree in the metastore, including the - // directory entry itself. - try (DurationInfo ignored2 = - new DurationInfo(LOG, false, "Delete metastore")) { - metadataStore.deleteSubtree(path, operationState); - } - } finally { - IOUtils.cleanupWithLogger(LOG, operationState); } LOG.debug("Delete \"{}\" completed; deleted {} objects", path, filesDeleted); @@ -412,7 +281,7 @@ public class DeleteOperation extends ExecutingStoreOperation { */ private void queueForDeletion( final S3AFileStatus stat) throws IOException { - queueForDeletion(deletionKey(stat), stat.getPath(), stat.isDirectory()); + queueForDeletion(deletionKey(stat), stat.isDirectory()); } /** @@ -422,21 +291,13 @@ public class DeleteOperation extends ExecutingStoreOperation { * complete. * * @param key key to delete - * @param deletePath nullable path of the key * @param isDirMarker is the entry a directory? * @throws IOException failure of the previous batch of deletions. */ private void queueForDeletion(final String key, - @Nullable final Path deletePath, boolean isDirMarker) throws IOException { LOG.debug("Adding object to delete: \"{}\"", key); keys.add(new DeleteEntry(key, isDirMarker)); - if (deletePath != null) { - if (!isDirMarker) { - paths.add(deletePath); - } - } - if (keys.size() == pageSize) { submitNextBatch(); } @@ -455,7 +316,7 @@ public class DeleteOperation extends ExecutingStoreOperation { maybeAwaitCompletion(deleteFuture); // delete the current page of keys and paths - deleteFuture = submitDelete(keys, paths); + deleteFuture = submitDelete(keys); // reset the references so a new list can be built up. resetDeleteList(); } @@ -466,7 +327,6 @@ public class DeleteOperation extends ExecutingStoreOperation { */ private void resetDeleteList() { keys = new ArrayList<>(pageSize); - paths = new ArrayList<>(pageSize); } /** @@ -484,33 +344,28 @@ public class DeleteOperation extends ExecutingStoreOperation { throws IOException { LOG.debug("delete: {} {}", (isFile ? "file" : "dir marker"), key); filesDeleted++; - callbacks.deleteObjectAtPath(path, key, isFile, operationState); + callbacks.deleteObjectAtPath(path, key, isFile); } /** - * Delete a single page of keys and optionally the metadata. - * For a large page, it is the metadata size which dominates. - * Its possible to invoke this with empty lists of keys or paths. - * If both lists are empty no work is submitted and null is returned. + * Delete a single page of keys. + * If the list is empty no work is submitted and null is returned. * * @param keyList keys to delete. - * @param pathList paths to update the metastore with. * @return the submitted future or null */ private CompletableFuture submitDelete( - final List keyList, - final List pathList) { + final List keyList) { - if (keyList.isEmpty() && pathList.isEmpty()) { + if (keyList.isEmpty()) { return null; } filesDeleted += keyList.size(); return submit(executor, callableWithinAuditSpan( getAuditSpan(), () -> { - asyncDeleteAction(operationState, + asyncDeleteAction( keyList, - pathList, LOG.isDebugEnabled()); return null; })); @@ -520,26 +375,21 @@ public class DeleteOperation extends ExecutingStoreOperation { * The action called in the asynchronous thread to delete * the keys from S3 and paths from S3Guard. * - * @param state ongoing operation state * @param keyList keys to delete. - * @param pathList paths to update the metastore with. * @param auditDeletedKeys should the results be audited and undeleted * entries logged? * @throws IOException failure */ @Retries.RetryTranslated private void asyncDeleteAction( - final BulkOperationState state, final List keyList, - final List pathList, final boolean auditDeletedKeys) throws IOException { List deletedObjects = new ArrayList<>(); try (DurationInfo ignored = new DurationInfo(LOG, false, "Delete page of %d keys", keyList.size())) { - DeleteObjectsResult result = null; - List undeletedObjects = new ArrayList<>(); + DeleteObjectsResult result; if (!keyList.isEmpty()) { // first delete the files. List files = keyList.stream() @@ -552,8 +402,6 @@ public class DeleteOperation extends ExecutingStoreOperation { () -> callbacks.removeKeys( files, false, - undeletedObjects, - state, !auditDeletedKeys)); if (result != null) { deletedObjects.addAll(result.getDeletedObjects()); @@ -564,26 +412,17 @@ public class DeleteOperation extends ExecutingStoreOperation { .map(e -> e.keyVersion) .collect(Collectors.toList()); LOG.debug("Deleting of {} directory markers", dirs.size()); - // This is invoked with deleteFakeDir = true, so - // S3Guard is not updated. + // This is invoked with deleteFakeDir. result = Invoker.once("Remove S3 Dir Markers", status.getPath().toString(), () -> callbacks.removeKeys( dirs, true, - undeletedObjects, - state, !auditDeletedKeys)); if (result != null) { deletedObjects.addAll(result.getDeletedObjects()); } } - if (!pathList.isEmpty()) { - // delete file paths only. This stops tombstones - // being added until the final directory cleanup - // (HADOOP-17244) - metadataStore.deletePaths(pathList, state); - } if (auditDeletedKeys) { // audit the deleted keys if (deletedObjects.size() != keyList.size()) { @@ -605,8 +444,11 @@ public class DeleteOperation extends ExecutingStoreOperation { } /** - * Deletion entry; dir marker state is tracked to control S3Guard - * update policy. + * Deletion entry; dir marker state is tracked to allow + * delete requests to be split into file + * and marker delete phases. + * Without S3Guard, the split is only used + * to choose which statistics to update. */ private static final class DeleteEntry { private final DeleteObjectsRequest.KeyVersion keyVersion; diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/InternalConstants.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/InternalConstants.java index 4dd52d9b88d..51b1bf60a2f 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/InternalConstants.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/InternalConstants.java @@ -134,9 +134,4 @@ public final class InternalConstants { */ public static final int CSE_PADDING_LENGTH = 16; - /** - * Error message to indicate S3-CSE is incompatible with S3Guard. - */ - public static final String CSE_S3GUARD_INCOMPATIBLE = "S3-CSE cannot be " - + "used with S3Guard"; } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/ListingOperationCallbacks.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/ListingOperationCallbacks.java index 99fb6809d93..f4d7a4349e4 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/ListingOperationCallbacks.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/ListingOperationCallbacks.java @@ -28,7 +28,6 @@ import org.apache.hadoop.fs.s3a.S3AFileStatus; import org.apache.hadoop.fs.s3a.S3ALocatedFileStatus; import org.apache.hadoop.fs.s3a.S3ListRequest; import org.apache.hadoop.fs.s3a.S3ListResult; -import org.apache.hadoop.fs.s3a.s3guard.ITtlTimeProvider; import org.apache.hadoop.fs.statistics.DurationTrackerFactory; import org.apache.hadoop.fs.store.audit.AuditSpan; @@ -44,37 +43,33 @@ public interface ListingOperationCallbacks { * Initiate a {@code listObjectsAsync} operation, incrementing metrics * in the process. * - * Retry policy: retry untranslated. + * Retry policy: failures will come from the future. * @param request request to initiate * @param trackerFactory tracker with statistics to update * @param span audit span for this operation * @return the results - * @throws IOException if the retry invocation raises one (it shouldn't). */ @Retries.RetryRaw CompletableFuture listObjectsAsync( S3ListRequest request, DurationTrackerFactory trackerFactory, - AuditSpan span) - throws IOException; + AuditSpan span); /** * List the next set of objects. - * Retry policy: retry untranslated. + * Retry policy: failures will come from the future. * @param request last list objects request to continue * @param prevResult last paged result to continue from * @param trackerFactory tracker with statistics to update * @param span audit span for the IO * @return the next result object - * @throws IOException none, just there for retryUntranslated. */ @Retries.RetryRaw CompletableFuture continueListObjectsAsync( S3ListRequest request, S3ListResult prevResult, DurationTrackerFactory trackerFactory, - AuditSpan span) - throws IOException; + AuditSpan span); /** * Build a {@link S3ALocatedFileStatus} from a {@link FileStatus} instance. @@ -116,19 +111,4 @@ public interface ListingOperationCallbacks { */ int getMaxKeys(); - /** - * Get the updated time provider for the current fs instance. - * @return implementation of {@link ITtlTimeProvider} - */ - ITtlTimeProvider getUpdatedTtlTimeProvider(); - - /** - * Is the path for this instance considered authoritative on the client, - * that is: will listing/status operations only be handled by the metastore, - * with no fallback to S3. - * @param p path - * @return true iff the path is authoritative on the client. - */ - boolean allowAuthoritative(Path p); - } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/MkdirOperation.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/MkdirOperation.java index 1d824201abe..2b9a0e89b1d 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/MkdirOperation.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/MkdirOperation.java @@ -116,8 +116,7 @@ public class MkdirOperation extends ExecutingStoreOperation { // if we get here there is no directory at the destination. // so create one. String key = getStoreContext().pathToKey(dir); - // this will create the marker file, delete the parent entries - // and update S3Guard + // Create the marker file, maybe delete the parent entries callbacks.createFakeDirectory(key); return true; } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/MultiObjectDeleteSupport.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/MultiObjectDeleteSupport.java index d2b3899a691..96e32f362df 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/MultiObjectDeleteSupport.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/MultiObjectDeleteSupport.java @@ -20,50 +20,27 @@ package org.apache.hadoop.fs.s3a.impl; import java.io.IOException; import java.nio.file.AccessDeniedException; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; -import java.util.Objects; -import java.util.function.Function; -import java.util.stream.Collectors; -import com.amazonaws.services.s3.model.DeleteObjectsRequest; import com.amazonaws.services.s3.model.MultiObjectDeleteException; -import org.apache.hadoop.thirdparty.com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.commons.lang3.tuple.Triple; -import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.s3a.AWSS3IOException; -import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.apache.hadoop.fs.s3a.Tristate; -import org.apache.hadoop.fs.s3a.s3guard.BulkOperationState; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.PathMetadata; - -import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkNotNull; /** * Support for Multi Object Deletion. + * This is used to be a complex piece of code as it was required to + * update s3guard. + * Now all that is left is the exception extraction for better + * reporting, */ -public final class MultiObjectDeleteSupport extends AbstractStoreOperation { +public final class MultiObjectDeleteSupport { private static final Logger LOG = LoggerFactory.getLogger( MultiObjectDeleteSupport.class); - private final BulkOperationState operationState; - - /** - * Initiate with a store context. - * @param context store context. - * @param operationState any ongoing bulk operation. - */ - public MultiObjectDeleteSupport(final StoreContext context, - final BulkOperationState operationState) { - super(context); - this.operationState = operationState; + private MultiObjectDeleteSupport() { } /** @@ -89,7 +66,7 @@ public final class MultiObjectDeleteSupport extends AbstractStoreOperation { final MultiObjectDeleteException deleteException) { List errors = deleteException.getErrors(); - LOG.warn("Bulk delete operation failed to delete all objects;" + LOG.info("Bulk delete operation failed to delete all objects;" + " failure count = {}", errors.size()); final StringBuilder result = new StringBuilder( @@ -104,7 +81,7 @@ public final class MultiObjectDeleteSupport extends AbstractStoreOperation { ? (" (" + error.getVersionId() + ")") : ""), error.getMessage()); - LOG.warn(item); + LOG.info(item); result.append(item); if (exitCode == null || exitCode.isEmpty() || ACCESS_DENIED.equals(code)) { exitCode = code; @@ -117,301 +94,4 @@ public final class MultiObjectDeleteSupport extends AbstractStoreOperation { return new AWSS3IOException(result.toString(), deleteException); } } - - /** - * Process a multi object delete exception by building two paths from - * the delete request: one of all deleted files, one of all undeleted values. - * The latter are those rejected in the delete call. - * @param deleteException the delete exception. - * @param keysToDelete the keys in the delete request - * @return tuple of (undeleted, deleted) paths. - */ - public Pair, List> splitUndeletedKeys( - final MultiObjectDeleteException deleteException, - final Collection keysToDelete) { - LOG.debug("Processing delete failure; keys to delete count = {};" - + " errors in exception {}; successful deletions = {}", - keysToDelete.size(), - deleteException.getErrors().size(), - deleteException.getDeletedObjects().size()); - // convert the collection of keys being deleted into paths - final List pathsBeingDeleted = keysToKeyPaths(keysToDelete); - // Take this ist of paths - // extract all undeleted entries contained in the exception and - // then remove them from the original list. - List undeleted = removeUndeletedPaths(deleteException, - pathsBeingDeleted, - getStoreContext()::keyToPath); - return Pair.of(undeleted, pathsBeingDeleted); - } - - /** - * Given a list of delete requests, convert them all to paths. - * @param keysToDelete list of keys for the delete operation. - * @return the paths. - */ - public List keysToPaths( - final Collection keysToDelete) { - return toPathList(keysToKeyPaths(keysToDelete)); - } - - /** - * Given a list of delete requests, convert them all to keypaths. - * @param keysToDelete list of keys for the delete operation. - * @return list of keypath entries - */ - public List keysToKeyPaths( - final Collection keysToDelete) { - return convertToKeyPaths(keysToDelete, - getStoreContext()::keyToPath); - } - - /** - * Given a list of delete requests, convert them all to paths. - * @param keysToDelete list of keys for the delete operation. - * @param qualifier path qualifier - * @return the paths. - */ - public static List convertToKeyPaths( - final Collection keysToDelete, - final Function qualifier) { - List l = new ArrayList<>(keysToDelete.size()); - for (DeleteObjectsRequest.KeyVersion kv : keysToDelete) { - String key = kv.getKey(); - Path p = qualifier.apply(key); - boolean isDir = key.endsWith("/"); - l.add(new KeyPath(key, p, isDir)); - } - return l; - } - - /** - * Process a delete failure by removing from the metastore all entries - * which where deleted, as inferred from the delete failures exception - * and the original list of files to delete declares to have been deleted. - * @param deleteException the delete exception. - * @param keysToDelete collection of keys which had been requested. - * @param retainedMarkers list built up of retained markers. - * @return a tuple of (undeleted, deleted, failures) - */ - public Triple, List, List>> - processDeleteFailure( - final MultiObjectDeleteException deleteException, - final List keysToDelete, - final List retainedMarkers) { - final MetadataStore metadataStore = - checkNotNull(getStoreContext().getMetadataStore(), - "context metadatastore"); - final List> failures = new ArrayList<>(); - final Pair, List> outcome = - splitUndeletedKeys(deleteException, keysToDelete); - List deleted = outcome.getRight(); - List deletedPaths = new ArrayList<>(); - List undeleted = outcome.getLeft(); - retainedMarkers.clear(); - List undeletedPaths = toPathList((List) undeleted); - // sort shorter keys first, - // so that if the left key is longer than the first it is considered - // smaller, so appears in the list first. - // thus when we look for a dir being empty, we know it holds - deleted.sort((l, r) -> r.getKey().length() - l.getKey().length()); - - // now go through and delete from S3Guard all paths listed in - // the result which are either files or directories with - // no children. - deleted.forEach(kp -> { - Path path = kp.getPath(); - try{ - boolean toDelete = true; - if (kp.isDirectoryMarker()) { - // its a dir marker, which could be an empty dir - // (which is then tombstoned), or a non-empty dir, which - // is not tombstoned. - // for this to be handled, we have to have removed children - // from the store first, which relies on the sort - PathMetadata pmentry = metadataStore.get(path, true); - if (pmentry != null && !pmentry.isDeleted()) { - toDelete = pmentry.getFileStatus().isEmptyDirectory() - == Tristate.TRUE; - } else { - toDelete = false; - } - } - if (toDelete) { - LOG.debug("Removing deleted object from S3Guard Store {}", path); - metadataStore.delete(path, operationState); - } else { - LOG.debug("Retaining S3Guard directory entry {}", path); - retainedMarkers.add(path); - } - } catch (IOException e) { - // trouble: we failed to delete the far end entry - // try with the next one. - // if this is a big network failure, this is going to be noisy. - LOG.warn("Failed to update S3Guard store with deletion of {}", path); - failures.add(Pair.of(path, e)); - } - // irrespective of the S3Guard outcome, it is declared as deleted, as - // it is no longer in the S3 store. - deletedPaths.add(path); - }); - if (LOG.isDebugEnabled()) { - undeleted.forEach(p -> LOG.debug("Deleted {}", p)); - } - return Triple.of(undeletedPaths, deletedPaths, failures); - } - - /** - * Given a list of keypaths, convert to a list of paths. - * @param keyPaths source list - * @return a listg of paths - */ - public static List toPathList(final List keyPaths) { - return keyPaths.stream() - .map(KeyPath::getPath) - .collect(Collectors.toList()); - } - - /** - * Build a list of undeleted paths from a {@code MultiObjectDeleteException}. - * Outside of unit tests, the qualifier function should be - * {@link S3AFileSystem#keyToQualifiedPath(String)}. - * @param deleteException the delete exception. - * @param qualifierFn function to qualify paths - * @return the possibly empty list of paths. - */ - @VisibleForTesting - public static List extractUndeletedPaths( - final MultiObjectDeleteException deleteException, - final Function qualifierFn) { - return toPathList(extractUndeletedKeyPaths(deleteException, qualifierFn)); - } - - /** - * Build a list of undeleted paths from a {@code MultiObjectDeleteException}. - * Outside of unit tests, the qualifier function should be - * {@link S3AFileSystem#keyToQualifiedPath(String)}. - * @param deleteException the delete exception. - * @param qualifierFn function to qualify paths - * @return the possibly empty list of paths. - */ - @VisibleForTesting - public static List extractUndeletedKeyPaths( - final MultiObjectDeleteException deleteException, - final Function qualifierFn) { - - List errors - = deleteException.getErrors(); - return errors.stream() - .map((error) -> { - String key = error.getKey(); - Path path = qualifierFn.apply(key); - boolean isDir = key.endsWith("/"); - return new KeyPath(key, path, isDir); - }) - .collect(Collectors.toList()); - } - - /** - * Process a {@code MultiObjectDeleteException} by - * removing all undeleted paths from the list of paths being deleted. - * The original list is updated, and so becomes the list of successfully - * deleted paths. - * @param deleteException the delete exception. - * @param pathsBeingDeleted list of paths which were being deleted. - * This has all undeleted paths removed, leaving only those deleted. - * @return the list of undeleted entries - */ - @VisibleForTesting - static List removeUndeletedPaths( - final MultiObjectDeleteException deleteException, - final Collection pathsBeingDeleted, - final Function qualifier) { - // get the undeleted values - List undeleted = extractUndeletedKeyPaths(deleteException, - qualifier); - // and remove them from the undeleted list, matching on key - for (KeyPath undel : undeleted) { - pathsBeingDeleted.removeIf(kp -> kp.getPath().equals(undel.getPath())); - } - return undeleted; - } - - /** - * A delete operation failed. - * Currently just returns the list of all paths. - * @param ex exception. - * @param keysToDelete the keys which were being deleted. - * @return all paths which were not deleted. - */ - public List processDeleteFailureGenericException(Exception ex, - final List keysToDelete) { - return keysToPaths(keysToDelete); - } - - /** - * Representation of a (key, path) which couldn't be deleted; - * the dir marker flag is inferred from the key suffix. - *

      - * Added because Pairs of Lists of Triples was just too complex - * for Java code. - *

      - */ - public static final class KeyPath { - /** Key in bucket. */ - private final String key; - /** Full path. */ - private final Path path; - /** Is this a directory marker? */ - private final boolean directoryMarker; - - public KeyPath(final String key, - final Path path, - final boolean directoryMarker) { - this.key = key; - this.path = path; - this.directoryMarker = directoryMarker; - } - - public String getKey() { - return key; - } - - public Path getPath() { - return path; - } - - public boolean isDirectoryMarker() { - return directoryMarker; - } - - @Override - public String toString() { - return "KeyPath{" + - "key='" + key + '\'' + - ", path=" + path + - ", directoryMarker=" + directoryMarker + - '}'; - } - - /** - * Equals test is on key alone. - */ - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - KeyPath keyPath = (KeyPath) o; - return key.equals(keyPath.key); - } - - @Override - public int hashCode() { - return Objects.hash(key); - } - } } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/NetworkBinding.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/NetworkBinding.java index 409ac7bebc4..575a3d1b2de 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/NetworkBinding.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/NetworkBinding.java @@ -118,7 +118,7 @@ public final class NetworkBinding { * See also {@code com.amazonaws.services.s3.model.Region.fromValue()} * for its conversion logic. * @param region region from S3 call. - * @return the region to use in DDB etc. + * @return the region to use in AWS services. */ public static String fixBucketRegion(final String region) { return region == null || region.equals("US") diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/OperationCallbacks.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/OperationCallbacks.java index 3391097fc57..a72dc7e10b3 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/OperationCallbacks.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/OperationCallbacks.java @@ -37,7 +37,6 @@ import org.apache.hadoop.fs.s3a.S3AFileStatus; import org.apache.hadoop.fs.s3a.S3ALocatedFileStatus; import org.apache.hadoop.fs.s3a.S3AReadOpContext; import org.apache.hadoop.fs.s3a.S3ObjectAttributes; -import org.apache.hadoop.fs.s3a.s3guard.BulkOperationState; /** * These are all the callbacks which the {@link RenameOperation} @@ -87,21 +86,18 @@ public interface OperationCallbacks { void finishRename(Path sourceRenamed, Path destCreated) throws IOException; /** - * Delete an object, also updating the metastore. + * Delete an object. * This call does not create any mock parent entries. * Retry policy: retry untranslated; delete considered idempotent. * @param path path to delete * @param key key of entry * @param isFile is the path a file (used for instrumentation only) - * @param operationState (nullable) operational state for a bulk update - * @throws AmazonClientException problems working with S3 - * @throws IOException IO failure in the metastore + * @throws IOException from invoker signature only -should not be raised. */ @Retries.RetryTranslated void deleteObjectAtPath(Path path, String key, - boolean isFile, - BulkOperationState operationState) + boolean isFile) throws IOException; /** @@ -109,7 +105,6 @@ public interface OperationCallbacks { * * @param path path to list from * @param status optional status of path to list. - * @param collectTombstones should tombstones be collected from S3Guard? * @param includeSelf should the listing include this path if present? * @return an iterator. * @throws IOException failure @@ -118,7 +113,6 @@ public interface OperationCallbacks { RemoteIterator listFilesAndDirectoryMarkers( Path path, S3AFileStatus status, - boolean collectTombstones, boolean includeSelf) throws IOException; /** @@ -140,17 +134,10 @@ public interface OperationCallbacks { throws IOException; /** - * Remove keys from the store, updating the metastore on a - * partial delete represented as a MultiObjectDeleteException failure by - * deleting all those entries successfully deleted and then rethrowing - * the MultiObjectDeleteException. + * Remove keys from the store. * @param keysToDelete collection of keys to delete on the s3-backend. * if empty, no request is made of the object store. * @param deleteFakeDir indicates whether this is for deleting fake dirs. - * @param undeletedObjectsOnFailure List which will be built up of all - * files that were not deleted. This happens even as an exception - * is raised. - * @param operationState bulk operation state * @param quiet should a bulk query be quiet, or should its result list * all deleted keys * @return the deletion result if a multi object delete was invoked @@ -162,28 +149,16 @@ public interface OperationCallbacks { * @throws AmazonClientException amazon-layer failure. * @throws IOException other IO Exception. */ - @Retries.RetryMixed + @Retries.RetryRaw DeleteObjectsResult removeKeys( List keysToDelete, boolean deleteFakeDir, - List undeletedObjectsOnFailure, - BulkOperationState operationState, boolean quiet) throws MultiObjectDeleteException, AmazonClientException, IOException; /** - * Is the path for this instance considered authoritative on the client, - * that is: will listing/status operations only be handled by the metastore, - * with no fallback to S3. - * @param p path - * @return true iff the path is authoritative on the client. - */ - boolean allowAuthoritative(Path p); - - /** - * Create an iterator over objects in S3 only; S3Guard - * is not involved. + * Create an iterator over objects in S3. * The listing includes the key itself, if found. * @param path path of the listing. * @param key object key diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/RenameOperation.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/RenameOperation.java index d3c6e930e67..c1700ef389c 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/RenameOperation.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/RenameOperation.java @@ -26,14 +26,14 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicLong; import com.amazonaws.AmazonClientException; +import com.amazonaws.SdkBaseException; import com.amazonaws.services.s3.model.DeleteObjectsRequest; -import com.amazonaws.services.s3.transfer.model.CopyResult; -import org.apache.hadoop.thirdparty.com.google.common.collect.Lists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.RemoteIterator; +import org.apache.hadoop.fs.s3a.Invoker; import org.apache.hadoop.fs.s3a.RenameFailedException; import org.apache.hadoop.fs.s3a.Retries; import org.apache.hadoop.fs.s3a.S3AFileStatus; @@ -41,23 +41,17 @@ import org.apache.hadoop.fs.s3a.S3ALocatedFileStatus; import org.apache.hadoop.fs.s3a.S3AReadOpContext; import org.apache.hadoop.fs.s3a.S3ObjectAttributes; import org.apache.hadoop.fs.s3a.Tristate; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.RenameTracker; import org.apache.hadoop.util.DurationInfo; import org.apache.hadoop.util.OperationDuration; +import static org.apache.hadoop.fs.s3a.S3AUtils.translateException; import static org.apache.hadoop.fs.store.audit.AuditingFunctions.callableWithinAuditSpan; -import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkNotNull; -import static org.apache.hadoop.fs.s3a.Constants.FS_S3A_BLOCK_SIZE; -import static org.apache.hadoop.fs.s3a.S3AUtils.objectRepresentsDirectory; import static org.apache.hadoop.fs.s3a.impl.CallableSupplier.submit; import static org.apache.hadoop.fs.s3a.impl.CallableSupplier.waitForCompletion; -import static org.apache.hadoop.fs.s3a.impl.InternalConstants.DEFAULT_BLOCKSIZE; import static org.apache.hadoop.fs.s3a.impl.InternalConstants.RENAME_PARALLEL_LIMIT; /** - * A parallelized rename operation which updates the metastore in the - * process, through whichever {@link RenameTracker} the store provides. + * A parallelized rename operation. *

      * The parallel execution is in groups of size * {@link InternalConstants#RENAME_PARALLEL_LIMIT}; it is only @@ -68,9 +62,6 @@ import static org.apache.hadoop.fs.s3a.impl.InternalConstants.RENAME_PARALLEL_LI * is initiated. * If it succeeds, the rename continues with the next group of files. *

      - * The RenameTracker has the task of keeping the metastore up to date - * as the rename proceeds. - *

      * Directory Markers which have child entries are never copied; only those * which represent empty directories are copied in the rename. * The {@link DirMarkerTracker} tracks which markers must be copied, and @@ -121,11 +112,6 @@ public class RenameOperation extends ExecutingStoreOperation { */ private final int pageSize; - /** - * Rename tracker. - */ - private RenameTracker renameTracker; - /** * List of active copies. */ @@ -138,14 +124,6 @@ public class RenameOperation extends ExecutingStoreOperation { private final List keysToDelete = new ArrayList<>(); - /** - * List of paths to delete, which will be passed to the rename - * tracker after the deletion succeeds. - */ - private final List pathsToDelete = new ArrayList<>(); - - private final long blocksize; - /** * Initiate the rename. * @@ -177,8 +155,6 @@ public class RenameOperation extends ExecutingStoreOperation { this.destKey = destKey; this.destStatus = destStatus; this.callbacks = callbacks; - blocksize = storeContext.getConfiguration() - .getLongBytes(FS_S3A_BLOCK_SIZE, DEFAULT_BLOCKSIZE); this.pageSize = pageSize; } @@ -212,13 +188,6 @@ public class RenameOperation extends ExecutingStoreOperation { * Only queuing objects here whose copy operation has * been submitted and so is in that thread pool. * - *
    15. - * If a path is supplied, then after the delete is executed - * (and completes) the rename tracker from S3Guard will be - * told of its deletion. Do not set this for directory - * markers with children, as it may mistakenly add - * tombstones into the table. - *
    16. *
    * This method must only be called from the primary thread. * @param path path to the object. @@ -226,9 +195,6 @@ public class RenameOperation extends ExecutingStoreOperation { */ private void queueToDelete(Path path, String key) { LOG.debug("Queueing to delete {}", path); - if (path != null) { - pathsToDelete.add(path); - } keysToDelete.add(new DeleteObjectsRequest.KeyVersion(key)); } @@ -272,28 +238,15 @@ public class RenameOperation extends ExecutingStoreOperation { throws IOException { completeActiveCopies(reason); removeSourceObjects( - keysToDelete, - pathsToDelete); + keysToDelete + ); // now reset the lists. keysToDelete.clear(); - pathsToDelete.clear(); } @Retries.RetryMixed public Long execute() throws IOException { executeOnlyOnce(); - final StoreContext storeContext = getStoreContext(); - final MetadataStore metadataStore = checkNotNull( - storeContext.getMetadataStore(), - "No metadata store in context"); - - // Validation completed: time to begin the operation. - // The store-specific rename tracker is used to keep the store - // to date with the in-progress operation. - // for the null store, these are all no-ops. - renameTracker = metadataStore.initiateRenameOperation( - storeContext, - sourcePath, sourceStatus, destPath); // The path to whichever file or directory is created by the // rename. When deleting markers all parents of @@ -317,21 +270,12 @@ public class RenameOperation extends ExecutingStoreOperation { try { completeActiveCopies("failure handling"); } catch (IOException e) { - // a failure to update the metastore after a rename failure is what - // we'd see on a network problem, expired credentials and other - // unrecoverable errors. // Downgrading to warn because an exception is already // about to be thrown. LOG.warn("While completing all active copies", e); } - // notify the rename tracker of the failure - throw renameTracker.renameFailed(ex); + throw convertToIOException(ex); } - - // At this point the rename has completed successfully in the S3 store. - // Tell the metastore this fact and let it complete its changes - renameTracker.completeRename(); - callbacks.finishRename(sourcePath, destCreated); return bytesCopied.get(); } @@ -362,19 +306,15 @@ public class RenameOperation extends ExecutingStoreOperation { // destination either does not exist or is a file to overwrite. LOG.debug("rename: renaming file {} to {}", sourcePath, copyDestinationPath); - copySourceAndUpdateTracker( - sourcePath, + copySource( sourceKey, sourceAttributes, readContext, copyDestinationPath, - copyDestinationKey, - false); + copyDestinationKey); bytesCopied.addAndGet(sourceStatus.getLen()); // delete the source - callbacks.deleteObjectAtPath(sourcePath, sourceKey, true, null); - // and update the tracker - renameTracker.sourceObjectsDeleted(Lists.newArrayList(sourcePath)); + callbacks.deleteObjectAtPath(sourcePath, sourceKey, true); return copyDestinationPath; } @@ -402,15 +342,12 @@ public class RenameOperation extends ExecutingStoreOperation { if (destStatus != null && destStatus.isEmptyDirectory() == Tristate.TRUE) { // delete unnecessary fake directory at the destination. - // this MUST be done before anything else so that - // rollback code doesn't get confused and insert a tombstone - // marker. + LOG.debug("Deleting fake directory marker at destination {}", destStatus.getPath()); // Although the dir marker policy doesn't always need to do this, // it's simplest just to be consistent here. - // note: updates the metastore as well a S3. - callbacks.deleteObjectAtPath(destStatus.getPath(), dstKey, false, null); + callbacks.deleteObjectAtPath(destStatus.getPath(), dstKey, false); } Path parentPath = storeContext.keyToPath(srcKey); @@ -423,7 +360,6 @@ public class RenameOperation extends ExecutingStoreOperation { final RemoteIterator iterator = callbacks.listFilesAndDirectoryMarkers(parentPath, sourceStatus, - true, true); while (iterator.hasNext()) { // get the next entry in the listing. @@ -464,7 +400,7 @@ public class RenameOperation extends ExecutingStoreOperation { queueToDelete(childSourcePath, key); // now begin the single copy CompletableFuture copy = initiateCopy(child, key, - childSourcePath, newDestKey, childDestPath); + newDestKey, childDestPath); activeCopies.add(copy); bytesCopied.addAndGet(sourceStatus.getLen()); } @@ -483,9 +419,6 @@ public class RenameOperation extends ExecutingStoreOperation { // have been deleted. completeActiveCopiesAndDeleteSources("final copy and delete"); - // We moved all the children, now move the top-level dir - // Empty directory should have been added as the object summary - renameTracker.moveSourceDirectory(); } /** @@ -511,7 +444,6 @@ public class RenameOperation extends ExecutingStoreOperation { /** * Process all directory markers at the end of the rename. * All leaf markers are queued to be copied in the store; - * this updates the metastore tracker as it does so. *

    * Why not simply create new markers? All the metadata * gets copied too, so if there was anything relevant then @@ -553,7 +485,6 @@ public class RenameOperation extends ExecutingStoreOperation { "copying %d leaf markers with %d surplus not copied", leafMarkers.size(), surplus.size()); for (DirMarkerTracker.Marker entry: leafMarkers.values()) { - Path source = entry.getPath(); String key = entry.getKey(); String newDestKey = dstKey + key.substring(srcKey.length()); @@ -564,7 +495,6 @@ public class RenameOperation extends ExecutingStoreOperation { initiateCopy( entry.getStatus(), key, - source, newDestKey, childDestPath)); queueToDelete(entry); @@ -579,7 +509,6 @@ public class RenameOperation extends ExecutingStoreOperation { * Initiate a copy operation in the executor. * @param source status of the source object. * @param key source key - * @param childSourcePath source as a path. * @param newDestKey destination key * @param childDestPath destination path. * @return the future. @@ -587,7 +516,6 @@ public class RenameOperation extends ExecutingStoreOperation { protected CompletableFuture initiateCopy( final S3ALocatedFileStatus source, final String key, - final Path childSourcePath, final String newDestKey, final Path childDestPath) { S3ObjectAttributes sourceAttributes = @@ -599,113 +527,67 @@ public class RenameOperation extends ExecutingStoreOperation { // queue the copy operation for execution in the thread pool return submit(getStoreContext().getExecutor(), callableWithinAuditSpan(getAuditSpan(), () -> - copySourceAndUpdateTracker( - childSourcePath, + copySource( key, sourceAttributes, callbacks.createReadContext(source), childDestPath, - newDestKey, - true))); + newDestKey))); } /** - * This invoked to copy a file or directory marker then update the - * rename operation on success. + * This is invoked to copy a file or directory marker. * It may be called in its own thread. - * @param sourceFile source path of the copy; may have a trailing / on it. * @param srcKey source key * @param srcAttributes status of the source object * @param destination destination as a qualified path. * @param destinationKey destination key - * @param addAncestors should ancestors be added to the metastore? * @return the destination path. * @throws IOException failure */ @Retries.RetryTranslated - private Path copySourceAndUpdateTracker( - final Path sourceFile, + private Path copySource( final String srcKey, final S3ObjectAttributes srcAttributes, final S3AReadOpContext readContext, final Path destination, - final String destinationKey, - final boolean addAncestors) throws IOException { + final String destinationKey) throws IOException { long len = srcAttributes.getLen(); - CopyResult copyResult; try (DurationInfo ignored = new DurationInfo(LOG, false, "Copy file from %s to %s (length=%d)", srcKey, destinationKey, len)) { - copyResult = callbacks.copyFile(srcKey, destinationKey, + callbacks.copyFile(srcKey, destinationKey, srcAttributes, readContext); } - if (objectRepresentsDirectory(srcKey)) { - renameTracker.directoryMarkerCopied( - sourceFile, - destination, - addAncestors); - } else { - S3ObjectAttributes destAttributes = new S3ObjectAttributes( - destination, - copyResult, - srcAttributes.getServerSideEncryptionAlgorithm(), - srcAttributes.getServerSideEncryptionKey(), - len); - renameTracker.fileCopied( - sourceFile, - srcAttributes, - destAttributes, - destination, - blocksize, - addAncestors); - } return destination; } /** - * Remove source objects and update the metastore by way of - * the rename tracker. + * Remove source objects. * @param keys list of keys to delete - * @param paths list of paths matching the keys to delete 1:1. * @throws IOException failure */ @Retries.RetryTranslated private void removeSourceObjects( - final List keys, - final List paths) + final List keys) throws IOException { - List undeletedObjects = new ArrayList<>(); - try { - // remove the keys + // remove the keys - // list what is being deleted for the interest of anyone - // who is trying to debug why objects are no longer there. - if (LOG.isDebugEnabled()) { - LOG.debug("Initiating delete operation for {} objects", keys.size()); - for (DeleteObjectsRequest.KeyVersion key : keys) { - LOG.debug(" {} {}", key.getKey(), - key.getVersion() != null ? key.getVersion() : ""); - } + // list what is being deleted for the interest of anyone + // who is trying to debug why objects are no longer there. + if (LOG.isDebugEnabled()) { + LOG.debug("Initiating delete operation for {} objects", keys.size()); + for (DeleteObjectsRequest.KeyVersion key : keys) { + LOG.debug(" {} {}", key.getKey(), + key.getVersion() != null ? key.getVersion() : ""); } - // this will update the metastore on a failure, but on - // a successful operation leaves the store as is. - callbacks.removeKeys( - keys, - false, - undeletedObjects, - renameTracker.getOperationState(), - true); - // and clear the list. - } catch (AmazonClientException | IOException e) { - // Failed. - // Notify the rename tracker. - // removeKeys will have already purged the metastore of - // all keys it has known to delete; this is just a final - // bit of housekeeping and a chance to tune exception - // reporting. - // The returned IOE is rethrown. - throw renameTracker.deleteFailed(e, paths, undeletedObjects); } - renameTracker.sourceObjectsDeleted(paths); + + Invoker.once("rename " + sourcePath + " to " + destPath, + sourcePath.toString(), () -> + callbacks.removeKeys( + keys, + false, + true)); } /** @@ -724,4 +606,22 @@ public class RenameOperation extends ExecutingStoreOperation { } } + /** + * Convert a passed in exception (expected to be an IOE or AWS exception) + * into an IOException. + * @param ex exception caught + * @return the exception to throw in the failure handler. + */ + protected IOException convertToIOException(final Exception ex) { + if (ex instanceof IOException) { + return (IOException) ex; + } else if (ex instanceof SdkBaseException) { + return translateException("rename " + sourcePath + " to " + destPath, + sourcePath.toString(), + (SdkBaseException) ex); + } else { + // should never happen, but for completeness + return new IOException(ex); + } + } } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/S3AMultipartUploader.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/S3AMultipartUploader.java index 5ff116fe38e..db6beaff5b2 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/S3AMultipartUploader.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/S3AMultipartUploader.java @@ -38,13 +38,12 @@ import com.amazonaws.services.s3.model.CompleteMultipartUploadResult; import com.amazonaws.services.s3.model.PartETag; import com.amazonaws.services.s3.model.UploadPartRequest; import com.amazonaws.services.s3.model.UploadPartResult; -import org.apache.hadoop.thirdparty.com.google.common.annotations.VisibleForTesting; import org.apache.hadoop.thirdparty.com.google.common.base.Charsets; -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.classification.VisibleForTesting; import org.apache.hadoop.fs.BBPartHandle; import org.apache.hadoop.fs.BBUploadHandle; import org.apache.hadoop.fs.PartHandle; @@ -55,8 +54,8 @@ import org.apache.hadoop.fs.UploadHandle; import org.apache.hadoop.fs.impl.AbstractMultipartUploader; import org.apache.hadoop.fs.s3a.WriteOperations; import org.apache.hadoop.fs.s3a.statistics.S3AMultipartUploaderStatistics; -import org.apache.hadoop.fs.s3a.s3guard.BulkOperationState; import org.apache.hadoop.fs.statistics.IOStatistics; +import org.apache.hadoop.util.Preconditions; import static org.apache.hadoop.fs.statistics.IOStatisticsLogging.ioStatisticsToString; @@ -80,16 +79,6 @@ class S3AMultipartUploader extends AbstractMultipartUploader { private final S3AMultipartUploaderStatistics statistics; - /** - * Bulk state; demand created and then retained. - */ - private BulkOperationState operationState; - - /** - * Was an operation state requested but not returned? - */ - private boolean noOperationState; - /** * Instatiate; this is called by the builder. * @param builder builder @@ -109,14 +98,6 @@ class S3AMultipartUploader extends AbstractMultipartUploader { this.statistics = Objects.requireNonNull(statistics); } - @Override - public void close() throws IOException { - if (operationState != null) { - operationState.close(); - } - super.close(); - } - @Override public IOStatistics getIOStatistics() { return statistics.getIOStatistics(); @@ -133,22 +114,6 @@ class S3AMultipartUploader extends AbstractMultipartUploader { return sb.toString(); } - /** - * Retrieve the operation state; create one on demand if needed - * and there has been no unsuccessful attempt to create one. - * @return an active operation state. - * @throws IOException failure - */ - private synchronized BulkOperationState retrieveOperationState() - throws IOException { - if (operationState == null && !noOperationState) { - operationState = writeOperations.initiateOperation(getBasePath(), - BulkOperationState.OperationType.Upload); - noOperationState = operationState != null; - } - return operationState; - } - @Override public CompletableFuture startUpload( final Path filePath) @@ -238,7 +203,6 @@ class S3AMultipartUploader extends AbstractMultipartUploader { "Duplicate PartHandles"); // retrieve/create operation state for scalability of completion. - final BulkOperationState state = retrieveOperationState(); long finalLen = totalLength; return context.submit(new CompletableFuture<>(), () -> { @@ -247,8 +211,8 @@ class S3AMultipartUploader extends AbstractMultipartUploader { key, uploadIdStr, eTags, - finalLen, - state); + finalLen + ); byte[] eTag = result.getETag().getBytes(Charsets.UTF_8); statistics.uploadCompleted(); diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/StoreContext.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/StoreContext.java index d60e1a2cd28..4b8a28f3e7b 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/StoreContext.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/StoreContext.java @@ -40,8 +40,6 @@ import org.apache.hadoop.fs.s3a.S3AInputPolicy; import org.apache.hadoop.fs.s3a.S3AStorageStatistics; import org.apache.hadoop.fs.s3a.Statistic; import org.apache.hadoop.fs.s3a.statistics.S3AStatisticsContext; -import org.apache.hadoop.fs.s3a.s3guard.ITtlTimeProvider; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; import org.apache.hadoop.fs.store.audit.ActiveThreadSpanSource; import org.apache.hadoop.fs.store.audit.AuditSpan; import org.apache.hadoop.fs.store.audit.AuditSpanSource; @@ -111,21 +109,8 @@ public class StoreContext implements ActiveThreadSpanSource { /** List algorithm. */ private final boolean useListV1; - /** - * To allow this context to be passed down to the metastore, this field - * wll be null until initialized. - */ - private final MetadataStore metadataStore; - private final ContextAccessors contextAccessors; - /** - * Source of time. - */ - - /** Time source for S3Guard TTLs. */ - private final ITtlTimeProvider timeProvider; - /** Operation Auditor. */ private final AuditSpanSource auditor; @@ -149,10 +134,8 @@ public class StoreContext implements ActiveThreadSpanSource { final S3AInputPolicy inputPolicy, final ChangeDetectionPolicy changeDetectionPolicy, final boolean multiObjectDeleteEnabled, - final MetadataStore metadataStore, final boolean useListV1, final ContextAccessors contextAccessors, - final ITtlTimeProvider timeProvider, final AuditSpanSource auditor, final boolean isCSEEnabled) { this.fsURI = fsURI; @@ -171,10 +154,8 @@ public class StoreContext implements ActiveThreadSpanSource { this.inputPolicy = inputPolicy; this.changeDetectionPolicy = changeDetectionPolicy; this.multiObjectDeleteEnabled = multiObjectDeleteEnabled; - this.metadataStore = metadataStore; this.useListV1 = useListV1; this.contextAccessors = contextAccessors; - this.timeProvider = timeProvider; this.auditor = auditor; this.isCSEEnabled = isCSEEnabled; } @@ -224,10 +205,6 @@ public class StoreContext implements ActiveThreadSpanSource { return multiObjectDeleteEnabled; } - public MetadataStore getMetadataStore() { - return metadataStore; - } - public boolean isUseListV1() { return useListV1; } @@ -368,14 +345,6 @@ public class StoreContext implements ActiveThreadSpanSource { return contextAccessors.getBucketLocation(); } - /** - * Get the time provider. - * @return the time source. - */ - public ITtlTimeProvider getTimeProvider() { - return timeProvider; - } - /** * Build the full S3 key for a request from the status entry, * possibly adding a "/" if it represents directory and it does diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/StoreContextBuilder.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/StoreContextBuilder.java index d4021a731cc..cff38b9fc4b 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/StoreContextBuilder.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/StoreContextBuilder.java @@ -27,8 +27,6 @@ import org.apache.hadoop.fs.s3a.S3AInputPolicy; import org.apache.hadoop.fs.s3a.S3AStorageStatistics; import org.apache.hadoop.fs.s3a.audit.AuditSpanS3A; import org.apache.hadoop.fs.s3a.statistics.S3AStatisticsContext; -import org.apache.hadoop.fs.s3a.s3guard.ITtlTimeProvider; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; import org.apache.hadoop.fs.store.audit.AuditSpanSource; import org.apache.hadoop.security.UserGroupInformation; @@ -63,14 +61,10 @@ public class StoreContextBuilder { private boolean multiObjectDeleteEnabled = true; - private MetadataStore metadataStore; - private boolean useListV1 = false; private ContextAccessors contextAccessors; - private ITtlTimeProvider timeProvider; - private AuditSpanSource auditor; private boolean isCSEEnabled; @@ -147,12 +141,6 @@ public class StoreContextBuilder { return this; } - public StoreContextBuilder setMetadataStore( - final MetadataStore store) { - this.metadataStore = store; - return this; - } - public StoreContextBuilder setUseListV1( final boolean useV1) { this.useListV1 = useV1; @@ -165,12 +153,6 @@ public class StoreContextBuilder { return this; } - public StoreContextBuilder setTimeProvider( - final ITtlTimeProvider provider) { - this.timeProvider = provider; - return this; - } - /** * Set builder value. * @param value new value @@ -193,7 +175,6 @@ public class StoreContextBuilder { return this; } - @SuppressWarnings("deprecation") public StoreContext build() { return new StoreContext(fsURI, bucket, @@ -208,10 +189,8 @@ public class StoreContextBuilder { inputPolicy, changeDetectionPolicy, multiObjectDeleteEnabled, - metadataStore, useListV1, contextAccessors, - timeProvider, auditor, isCSEEnabled); } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/AbstractS3GuardDynamoDBDiagnostic.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/AbstractS3GuardDynamoDBDiagnostic.java deleted file mode 100644 index 83495ca3106..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/AbstractS3GuardDynamoDBDiagnostic.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import javax.annotation.Nullable; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.List; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.apache.hadoop.service.launcher.AbstractLaunchableService; -import org.apache.hadoop.service.launcher.ServiceLaunchException; - -import static org.apache.hadoop.service.launcher.LauncherExitCodes.EXIT_FAIL; -import static org.apache.hadoop.service.launcher.LauncherExitCodes.EXIT_USAGE; - -/** - * Entry point for S3Guard diagnostics operations against DynamoDB tables. - */ -public class AbstractS3GuardDynamoDBDiagnostic - extends AbstractLaunchableService { - - private S3AFileSystem filesystem; - - private DynamoDBMetadataStore store; - - private URI uri; - - private List arguments; - - /** - * Constructor. - * @param name entry point name. - */ - public AbstractS3GuardDynamoDBDiagnostic(final String name) { - super(name); - } - - /** - * Constructor. If the store is set then that is the store for the operation, - * otherwise the filesystem's binding is used instead. - * @param name entry point name. - * @param filesystem filesystem - * @param store optional metastore. - * @param uri URI. Must be set if filesystem == null. - */ - public AbstractS3GuardDynamoDBDiagnostic( - final String name, - @Nullable final S3AFileSystem filesystem, - @Nullable final DynamoDBMetadataStore store, - @Nullable final URI uri) { - super(name); - this.store = store; - this.filesystem = filesystem; - if (store == null) { - require(filesystem != null, "No filesystem or URI"); - bindStore(filesystem); - } - if (uri == null) { - require(filesystem != null, "No filesystem or URI"); - setUri(filesystem.getUri()); - } else { - setUri(uri); - } - } - - /** - * Require a condition to hold, otherwise an exception is thrown. - * @param condition condition to be true - * @param error text on failure. - * @throws ServiceLaunchException if the condition is not met - */ - protected static void require(boolean condition, String error) { - if (!condition) { - throw failure(error); - } - } - - /** - * Generate a failure exception for throwing. - * @param message message - * @param ex optional nested exception. - * @return an exception to throw - */ - protected static ServiceLaunchException failure(String message, - Throwable ex) { - return new ServiceLaunchException(EXIT_FAIL, message, ex); - } - - /** - * Generate a failure exception for throwing. - * @param message message - * @return an exception to throw - */ - protected static ServiceLaunchException failure(String message) { - return new ServiceLaunchException(EXIT_FAIL, message); - } - - @Override - public Configuration bindArgs(final Configuration config, - final List args) - throws Exception { - this.arguments = args; - return super.bindArgs(config, args); - } - - /** - * Get the argument list. - * @return the argument list. - */ - protected List getArguments() { - return arguments; - } - - /** - * Bind to the store from a CLI argument. - * @param fsURI filesystem URI - * @throws IOException failure - */ - protected void bindFromCLI(String fsURI) - throws IOException { - Configuration conf = getConfig(); - setUri(fsURI); - FileSystem fs = FileSystem.get(getUri(), conf); - require(fs instanceof S3AFileSystem, - "Not an S3A Filesystem: " + fsURI); - filesystem = (S3AFileSystem) fs; - bindStore(filesystem); - setUri(fs.getUri()); - } - - /** - * Binds the {@link #store} field to the metastore of - * the filesystem -which must have a DDB metastore. - * @param fs filesystem to bind the store to. - */ - private void bindStore(final S3AFileSystem fs) { - require(fs.hasMetadataStore(), - "Filesystem has no metadata store: " + fs.getUri()); - MetadataStore ms = fs.getMetadataStore(); - require(ms instanceof DynamoDBMetadataStore, - "Filesystem " + fs.getUri() - + " does not have a DynamoDB metadata store: " + ms); - store = (DynamoDBMetadataStore) ms; - } - - protected DynamoDBMetadataStore getStore() { - return store; - } - - public S3AFileSystem getFilesystem() { - return filesystem; - } - - public URI getUri() { - return uri; - } - - public void setUri(final URI uri) { - String fsURI = uri.toString(); - if (!fsURI.endsWith("/")) { - setUri(fsURI); - } else { - this.uri = uri; - } - } - - /** - * Set the URI from a string; will add a "/" if needed. - * @param fsURI filesystem URI. - * @throws RuntimeException if the fsURI parameter is not a valid URI. - */ - public void setUri(String fsURI) { - if (fsURI != null) { - if (!fsURI.endsWith("/")) { - fsURI += "/"; - } - try { - setUri(new URI(fsURI)); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - } - } - - /** - * Get the list of arguments, after validating the list size. - * @param argMin minimum number of entries. - * @param argMax maximum number of entries. - * @param usage Usage message. - * @return the argument list, which will be in the range. - * @throws ServiceLaunchException if the argument list is not valid. - */ - protected List getArgumentList(final int argMin, - final int argMax, - final String usage) { - List arg = getArguments(); - if (arg == null || arg.size() < argMin || arg.size() > argMax) { - // no arguments: usage message - throw new ServiceLaunchException(EXIT_USAGE, usage); - } - return arg; - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/AuthoritativeAuditOperation.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/AuthoritativeAuditOperation.java deleted file mode 100644 index 111dd46fedd..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/AuthoritativeAuditOperation.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.IOException; -import java.util.ArrayDeque; -import java.util.Collection; -import java.util.Queue; - -import org.apache.hadoop.thirdparty.com.google.common.annotations.VisibleForTesting; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.commons.lang3.tuple.Pair; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.PathIOException; -import org.apache.hadoop.fs.s3a.impl.AbstractStoreOperation; -import org.apache.hadoop.fs.s3a.impl.StoreContext; -import org.apache.hadoop.service.launcher.LauncherExitCodes; -import org.apache.hadoop.util.DurationInfo; -import org.apache.hadoop.util.ExitCodeProvider; -import org.apache.hadoop.util.ExitUtil; - -import static org.apache.hadoop.service.launcher.LauncherExitCodes.EXIT_NOT_ACCEPTABLE; - -/** - * Audit a directory tree for being authoritative. - * One aspect of the audit to be aware of: the root directory is - * always considered authoritative, even though, because there is no - * matching entry in any of the stores, it is not strictly true. - */ -public class AuthoritativeAuditOperation extends AbstractStoreOperation { - - private static final Logger LOG = LoggerFactory.getLogger( - AuthoritativeAuditOperation.class); - - /** - * Exception error code when a path is non-auth in the DB}. - */ - public static final int ERROR_ENTRY_NOT_AUTH_IN_DDB = EXIT_NOT_ACCEPTABLE; - - /** - * Exception error code when a path is not configured to be - * auth in the S3A FS Config: {@value}. - */ - public static final int ERROR_PATH_NOT_AUTH_IN_FS = 5; - - /** - * Exception error string: {@value}. - */ - public static final String E_NONAUTH - = "Directory is not marked as authoritative in the S3Guard store"; - - /** The metastore to audit. */ - private final DynamoDBMetadataStore metastore; - - /** require all directories to be authoritative. */ - private final boolean requireAuthoritative; - - /** - * Verbose switch. - */ - private final boolean verbose; - - /** - * Constructor. - * @param storeContext store context. - * @param metastore metastore - * @param requireAuthoritative require all directories to be authoritative - * @param verbose verbose output - */ - public AuthoritativeAuditOperation( - final StoreContext storeContext, - final DynamoDBMetadataStore metastore, - final boolean requireAuthoritative, - final boolean verbose) { - super(storeContext); - this.metastore = metastore; - this.requireAuthoritative = requireAuthoritative; - this.verbose = verbose; - } - - /** - * Examine the path metadata and verify that the dir is authoritative. - * @param md metadata. - * @param requireAuth require all directories to be authoritative - * @throws NonAuthoritativeDirException if it is !auth and requireAuth=true. - */ - private void verifyAuthDir(final DDBPathMetadata md, - final boolean requireAuth) - throws PathIOException { - final Path path = md.getFileStatus().getPath(); - boolean isAuth = path.isRoot() || md.isAuthoritativeDir(); - if (!isAuth && requireAuth) { - throw new NonAuthoritativeDirException(path); - } - } - - /** - * Examine the path metadata, declare whether it should be queued for - * recursive scanning. - * @param md metadata. - * @return true if it is a dir to scan. - */ - private boolean isDirectory(PathMetadata md) { - return !md.getFileStatus().isFile(); - } - - /** - * Audit the tree. - * @param path qualified path to scan - * @return tuple(dirs scanned, nonauth dirs found) - * @throws IOException IO failure - * @throws ExitUtil.ExitException if a non-auth dir was found. - */ - public Pair audit(Path path) throws IOException { - try (DurationInfo ignored = - new DurationInfo(LOG, "Audit %s", path)) { - return executeAudit(path, requireAuthoritative, true); - } - } - - /** - * Audit the tree. - * This is the internal code which throws a NonAuthoritativePathException - * on failures; tests may use it. - * @param path path to scan - * @param requireAuth require all directories to be authoritative - * @param recursive recurse? - * @return tuple(dirs scanned, nonauth dirs found) - * @throws IOException IO failure - * @throws NonAuthoritativeDirException if a non-auth dir was found. - */ - @VisibleForTesting - Pair executeAudit( - final Path path, - final boolean requireAuth, - final boolean recursive) throws IOException { - int dirs = 0; - int nonauth = 0; - final Queue queue = new ArrayDeque<>(); - final boolean isRoot = path.isRoot(); - final DDBPathMetadata baseData = metastore.get(path); - if (baseData == null) { - throw new ExitUtil.ExitException(LauncherExitCodes.EXIT_NOT_FOUND, - "No S3Guard entry for path " + path); - } - - if (isRoot || isDirectory(baseData)) { - // we have the root entry or an authoritative a directory - queue.add(baseData); - } else { - LOG.info("Path represents file"); - return Pair.of(0, 0); - } - - while (!queue.isEmpty()) { - dirs++; - final DDBPathMetadata dir = queue.poll(); - final Path p = dir.getFileStatus().getPath(); - LOG.debug("Directory {}", dir.prettyPrint()); - // log a message about the dir state, with root treated specially - if (!p.isRoot()) { - if (!dir.isAuthoritativeDir()) { - LOG.warn("Directory {} is not authoritative", p); - nonauth++; - verifyAuthDir(dir, requireAuth); - } else { - LOG.info("Directory {}", p); - } - } else { - // this is done to avoid the confusing message about root not being - // authoritative - LOG.info("Root directory {}", p); - } - - // list its children - if (recursive) { - final DirListingMetadata entry = metastore.listChildren(p); - - if (entry != null) { - final Collection listing = entry.getListing(); - int files = 0, subdirs = 0; - for (PathMetadata e : listing) { - if (isDirectory(e)) { - // queue for auditing - queue.add((DDBPathMetadata) e); - subdirs++; - } else { - files++; - } - } - if (verbose && files > 0 || subdirs > 0) { - LOG.info(" files {}; directories {}", files, subdirs); - } - } else { - LOG.info("Directory {} has been deleted", dir); - } - } - } - // end of scan - if (dirs == 1 && isRoot) { - LOG.info("The store has no directories to scan"); - } else { - LOG.info("Scanned {} directories - {} were not marked as authoritative", - dirs, nonauth); - } - return Pair.of(dirs, nonauth); - } - - /** - * A directory was found which was non-authoritative. - * The exit code for this operation is - * {@link LauncherExitCodes#EXIT_NOT_ACCEPTABLE} -This is what the S3Guard - * will return. - */ - public static final class NonAuthoritativeDirException - extends PathIOException implements ExitCodeProvider { - - /** - * Instantiate. - * @param path the path which is non-authoritative. - */ - private NonAuthoritativeDirException(final Path path) { - super(path.toString(), E_NONAUTH); - } - - @Override - public int getExitCode() { - return ERROR_ENTRY_NOT_AUTH_IN_DDB; - } - - @Override - public String toString() { - return getMessage(); - } - } - -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/BulkOperationState.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/BulkOperationState.java deleted file mode 100644 index b4974b73565..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/BulkOperationState.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.Closeable; -import java.io.IOException; - -/** - * This represents state which may be passed to bulk IO operations - * to enable them to store information about the state of the ongoing - * operation across invocations. - *

    - * A bulk operation state MUST only be be used for the single store - * from which it was created, and MUSTonly for the duration of a single - * bulk update operation. - *

    - * Passing in the state is to allow the stores to maintain state about - * updates they have already made to their store during this single operation: - * a cache of what has happened. It is not a list of operations to be applied. - * If a list of operations to perform is built up (e.g. during rename) - * that is the duty of the caller, not this state. - *

    - * After the operation has completed, it MUST be closed so - * as to guarantee that all state is released. - */ -public class BulkOperationState implements Closeable { - - private final OperationType operation; - - /** - * Constructor. - * @param operation the type of the operation. - */ - public BulkOperationState(final OperationType operation) { - this.operation = operation; - } - - /** - * Get the operation type. - * @return the operation type. - */ - public OperationType getOperation() { - return operation; - } - - @Override - public void close() throws IOException { - - } - - /** - * Enumeration of operations which can be performed in bulk. - * This can be used by the stores however they want. - * One special aspect: renames are to be done through a {@link RenameTracker}. - * Callers will be blocked from initiating a rename through - * {@code S3Guard#initiateBulkWrite()} - */ - public enum OperationType { - /** Writing data. */ - Put, - /** - * Rename: add and delete. - * After the rename, the tree under the destination path - * can be tagged as authoritative. - */ - Rename, - /** Pruning: deleting entries and updating parents. */ - Prune, - /** Commit operation. */ - Commit, - /** Deletion operation. */ - Delete, - /** FSCK operation. */ - Fsck, - /** - * Bulk directory tree import. - * After an import, the entire tree under the path has been - * enumerated and should be tagged as authoritative. - */ - Import, - /** - * Listing update. - */ - Listing, - /** - * Mkdir operation. - */ - Mkdir, - /** - * Multipart upload operation. - */ - Upload - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DDBPathMetadata.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DDBPathMetadata.java deleted file mode 100644 index 292c0161348..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DDBPathMetadata.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.Tristate; - -/** - * {@code DDBPathMetadata} wraps {@link PathMetadata} and adds the - * isAuthoritativeDir flag to provide support for authoritative directory - * listings in {@link DynamoDBMetadataStore}. - */ -public class DDBPathMetadata extends PathMetadata { - - private boolean isAuthoritativeDir; - - public DDBPathMetadata(PathMetadata pmd) { - super(pmd.getFileStatus(), pmd.isEmptyDirectory(), pmd.isDeleted(), - pmd.getLastUpdated()); - this.isAuthoritativeDir = false; - this.setLastUpdated(pmd.getLastUpdated()); - } - - public DDBPathMetadata(S3AFileStatus fileStatus) { - super(fileStatus); - this.isAuthoritativeDir = false; - } - - public DDBPathMetadata(S3AFileStatus fileStatus, Tristate isEmptyDir, - boolean isDeleted, long lastUpdated) { - super(fileStatus, isEmptyDir, isDeleted, lastUpdated); - this.isAuthoritativeDir = false; - } - - public DDBPathMetadata(S3AFileStatus fileStatus, Tristate isEmptyDir, - boolean isDeleted, boolean isAuthoritativeDir, long lastUpdated) { - super(fileStatus, isEmptyDir, isDeleted, lastUpdated); - this.isAuthoritativeDir = isAuthoritativeDir; - } - - public boolean isAuthoritativeDir() { - return isAuthoritativeDir; - } - - public void setAuthoritativeDir(boolean authoritativeDir) { - isAuthoritativeDir = authoritativeDir; - } - - @Override - public boolean equals(Object o) { - return super.equals(o); - } - - @Override public int hashCode() { - return super.hashCode(); - } - - @Override public String toString() { - return "DDBPathMetadata{" + - "isAuthoritativeDir=" + isAuthoritativeDir + - ", PathMetadata=" + super.toString() + - '}'; - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DelayedUpdateRenameTracker.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DelayedUpdateRenameTracker.java deleted file mode 100644 index 51a9821ace1..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DelayedUpdateRenameTracker.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import com.amazonaws.SdkBaseException; - -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3ObjectAttributes; -import org.apache.hadoop.fs.s3a.Tristate; -import org.apache.hadoop.fs.s3a.impl.StoreContext; -import org.apache.hadoop.util.DurationInfo; - -import static org.apache.hadoop.fs.s3a.s3guard.S3Guard.addMoveAncestors; -import static org.apache.hadoop.fs.s3a.s3guard.S3Guard.addMoveDir; -import static org.apache.hadoop.fs.s3a.s3guard.S3Guard.addMoveFile; - -/** - * This is the rename updating strategy originally used: - * a collection of source paths and a list of destinations are created, - * then updated at the end (possibly slow). - *

    - * It is not currently instantiated by any of the active trackers, - * but is preserved to show that the original rename strategy - * can be implemented via the tracker model. - */ -public class DelayedUpdateRenameTracker extends RenameTracker { - - private final MetadataStore metadataStore; - - private final Collection sourcePaths = new HashSet<>(); - - private final List destMetas = new ArrayList<>(); - - private final List deletedPaths = new ArrayList<>(); - - public DelayedUpdateRenameTracker( - final StoreContext storeContext, - final MetadataStore metadataStore, - final Path sourceRoot, - final Path dest, - final BulkOperationState operationState) { - super("DelayedUpdateRenameTracker", storeContext, metadataStore, - sourceRoot, dest, operationState); - this.metadataStore = storeContext.getMetadataStore(); - } - - @Override - public synchronized void fileCopied( - final Path sourcePath, - final S3ObjectAttributes sourceAttributes, - final S3ObjectAttributes destAttributes, - final Path destPath, - final long blockSize, - final boolean addAncestors) throws IOException { - - addMoveFile(metadataStore, - sourcePaths, - destMetas, - sourcePath, - destPath, - sourceAttributes.getLen(), - blockSize, - getOwner(), - destAttributes.getETag(), - destAttributes.getVersionId()); - // Ancestor directories may not be listed, so we explicitly add them - if (addAncestors) { - addMoveAncestors(metadataStore, - sourcePaths, - destMetas, - getSourceRoot(), - sourcePath, - destPath, - getOwner()); - } - } - - @Override - public synchronized void directoryMarkerCopied(final Path sourcePath, - final Path destPath, - final boolean addAncestors) throws IOException { - addMoveDir(metadataStore, sourcePaths, destMetas, - sourcePath, - destPath, getOwner()); - // Ancestor directories may not be listed, so we explicitly add them - if (addAncestors) { - addMoveAncestors(metadataStore, - sourcePaths, - destMetas, - getSourceRoot(), - sourcePath, - destPath, - getOwner()); - } - } - - @Override - public synchronized void moveSourceDirectory() throws IOException { - if (!sourcePaths.contains(getSourceRoot())) { - addMoveDir(metadataStore, sourcePaths, destMetas, - getSourceRoot(), - getDest(), getOwner()); - } - } - - @Override - public synchronized void sourceObjectsDeleted( - final Collection paths) throws IOException { - // add to the list of deleted paths. - deletedPaths.addAll(paths); - } - - @Override - public void completeRename() throws IOException { - metadataStore.move(sourcePaths, destMetas, getOperationState()); - super.completeRename(); - } - - @Override - public IOException renameFailed(final Exception ex) { - LOG.warn("Rename has failed; updating s3guard with destination state"); - try (DurationInfo ignored = new DurationInfo(LOG, - "Cleaning up deleted paths")) { - // the destination paths are updated; the source is left alone. - metadataStore.move(new ArrayList<>(0), destMetas, getOperationState()); - for (Path deletedPath : deletedPaths) { - // this is not ideal in that it may leave parent stuff around. - metadataStore.delete(deletedPath, getOperationState()); - } - deleteParentPaths(); - } catch (IOException | SdkBaseException e) { - LOG.warn("Ignoring error raised in AWS SDK ", e); - } - - return super.renameFailed(ex); - } - - /** - * Delete all the parent paths we know to be empty (by walking up the tree - * deleting as appropriate). - * @throws IOException failure - */ - private void deleteParentPaths() throws IOException { - Set parentPaths = new HashSet<>(); - for (Path deletedPath : deletedPaths) { - Path parent = deletedPath.getParent(); - if (!parent.equals(getSourceRoot())) { - parentPaths.add(parent); - } - } - // now there's a set of parent paths. We now want to - // get them ordered by depth, so that deeper entries come first - // that way: when we check for a parent path existing we can - // see if it really is empty. - List parents = new ArrayList<>(parentPaths); - parents.sort(PathOrderComparators.TOPMOST_PATH_LAST); - for (Path parent : parents) { - PathMetadata md = metadataStore.get(parent, true); - if (md != null && md.isEmptyDirectory() == Tristate.TRUE) { - // if were confident that this is empty: delete it. - metadataStore.delete(parent, getOperationState()); - } - } - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DescendantsIterator.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DescendantsIterator.java deleted file mode 100644 index a69109c9c88..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DescendantsIterator.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.IOException; -import java.util.Collection; -import java.util.LinkedList; -import java.util.NoSuchElementException; -import java.util.Queue; - -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; - -import org.apache.hadoop.classification.InterfaceAudience; -import org.apache.hadoop.classification.InterfaceStability; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.RemoteIterator; -import org.apache.hadoop.fs.s3a.S3AFileStatus; - -/** - * {@code DescendantsIterator} is a {@link RemoteIterator} that implements - * pre-ordering breadth-first traversal (BFS) of a path and all of its - * descendants recursively. After visiting each path, that path's direct - * children are discovered by calling {@link MetadataStore#listChildren(Path)}. - * Each iteration returns the next direct child, and if that child is a - * directory, also pushes it onto a queue to discover its children later. - * - * For example, assume the consistent store contains metadata representing this - * file system structure: - * - *

    - * /dir1
    - * |-- dir2
    - * |   |-- file1
    - * |   `-- file2
    - * `-- dir3
    - *     |-- dir4
    - *     |   `-- file3
    - *     |-- dir5
    - *     |   `-- file4
    - *     `-- dir6
    - * 
    - * - * Consider this code sample: - *
    - * final PathMetadata dir1 = get(new Path("/dir1"));
    - * for (DescendantsIterator descendants = new DescendantsIterator(dir1);
    - *     descendants.hasNext(); ) {
    - *   final FileStatus status = descendants.next().getFileStatus();
    - *   System.out.printf("%s %s%n", status.isDirectory() ? 'D' : 'F',
    - *       status.getPath());
    - * }
    - * 
    - * - * The output is: - *
    - * D /dir1
    - * D /dir1/dir2
    - * D /dir1/dir3
    - * F /dir1/dir2/file1
    - * F /dir1/dir2/file2
    - * D /dir1/dir3/dir4
    - * D /dir1/dir3/dir5
    - * F /dir1/dir3/dir4/file3
    - * F /dir1/dir3/dir5/file4
    - * D /dir1/dir3/dir6
    - * 
    - */ -@InterfaceAudience.Private -@InterfaceStability.Evolving -public class DescendantsIterator implements RemoteIterator { - - private final MetadataStore metadataStore; - private final Queue queue = new LinkedList<>(); - - /** - * Creates a new {@code DescendantsIterator}. - * - * @param ms the associated {@link MetadataStore} - * @param meta base path for descendants iteration, which will be the first - * returned during iteration (except root). Null makes empty iterator. - * @throws IOException if errors happen during metadata store listing - */ - public DescendantsIterator(MetadataStore ms, PathMetadata meta) - throws IOException { - Preconditions.checkNotNull(ms); - this.metadataStore = ms; - - if (meta != null) { - final Path path = meta.getFileStatus().getPath(); - if (path.isRoot()) { - DirListingMetadata rootListing = ms.listChildren(path); - if (rootListing != null) { - rootListing = rootListing.withoutTombstones(); - queue.addAll(rootListing.getListing()); - } - } else { - queue.add(meta); - } - } - } - - @Override - public boolean hasNext() throws IOException { - return !queue.isEmpty(); - } - - @Override - public S3AFileStatus next() throws IOException { - if (!hasNext()) { - throw new NoSuchElementException("No more descendants."); - } - PathMetadata next; - next = queue.poll(); - if (next.getFileStatus().isDirectory()) { - final Path path = next.getFileStatus().getPath(); - DirListingMetadata meta = metadataStore.listChildren(path); - if (meta != null) { - Collection more = meta.withoutTombstones().getListing(); - if (!more.isEmpty()) { - queue.addAll(more); - } - } - } - return next.getFileStatus(); - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DirListingMetadata.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DirListingMetadata.java deleted file mode 100644 index 63600b3a942..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DirListingMetadata.java +++ /dev/null @@ -1,372 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; - -import org.apache.hadoop.classification.InterfaceAudience; -import org.apache.hadoop.classification.InterfaceStability; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.Tristate; - -/** - * {@code DirListingMetadata} models a directory listing stored in a - * {@link MetadataStore}. Instances of this class are mutable and thread-safe. - */ -@InterfaceAudience.Private -@InterfaceStability.Evolving -public class DirListingMetadata extends ExpirableMetadata { - - /** - * Convenience parameter for passing into constructor. - */ - public static final Collection EMPTY_DIR = - Collections.emptyList(); - - private final Path path; - - /** Using a map for fast find / remove with large directories. */ - private Map listMap = new ConcurrentHashMap<>(); - - private boolean isAuthoritative; - - /** - * Create a directory listing metadata container. - * - * @param path Path of the directory. If this path has a host component, then - * all paths added later via {@link #put(PathMetadata)} must also have - * the same host. - * @param listing Entries in the directory. - * @param isAuthoritative true iff listing is the full contents of the - * directory, and the calling client reports that this may be cached as - * the full and authoritative listing of all files in the directory. - * @param lastUpdated last updated time on which expiration is based. - */ - public DirListingMetadata(Path path, Collection listing, - boolean isAuthoritative, long lastUpdated) { - - checkPathAbsolute(path); - this.path = path; - - if (listing != null) { - for (PathMetadata entry : listing) { - Path childPath = entry.getFileStatus().getPath(); - checkChildPath(childPath); - listMap.put(childPath, entry); - } - } - this.isAuthoritative = isAuthoritative; - this.setLastUpdated(lastUpdated); - } - - public DirListingMetadata(Path path, Collection listing, - boolean isAuthoritative) { - this(path, listing, isAuthoritative, 0); - } - - /** - * Copy constructor. - * @param d the existing {@link DirListingMetadata} object. - */ - public DirListingMetadata(DirListingMetadata d) { - path = d.path; - isAuthoritative = d.isAuthoritative; - this.setLastUpdated(d.getLastUpdated()); - listMap = new ConcurrentHashMap<>(d.listMap); - } - - /** - * @return {@code Path} of the directory that contains this listing. - */ - public Path getPath() { - return path; - } - - /** - * @return entries in the directory - */ - public Collection getListing() { - return Collections.unmodifiableCollection(listMap.values()); - } - - /** - * List all tombstones. - * @return all tombstones in the listing. - */ - public Set listTombstones() { - Set tombstones = new HashSet<>(); - for (PathMetadata meta : listMap.values()) { - if (meta.isDeleted()) { - tombstones.add(meta.getFileStatus().getPath()); - } - } - return tombstones; - } - - /** - * Get the directory listing excluding tombstones. - * Returns a new DirListingMetadata instances, without the tombstones -the - * lastUpdated field is copied from this instance. - * @return a new DirListingMetadata without the tombstones. - */ - public DirListingMetadata withoutTombstones() { - Collection filteredList = new ArrayList<>(); - for (PathMetadata meta : listMap.values()) { - if (!meta.isDeleted()) { - filteredList.add(meta); - } - } - return new DirListingMetadata(path, filteredList, isAuthoritative, - this.getLastUpdated()); - } - - /** - * @return number of entries tracked. This is not the same as the number - * of entries in the actual directory unless {@link #isAuthoritative()} is - * true. - * It will also include any tombstones. - */ - public int numEntries() { - return listMap.size(); - } - - /** - * @return true iff this directory listing is full and authoritative within - * the scope of the {@code MetadataStore} that returned it. - */ - public boolean isAuthoritative() { - return isAuthoritative; - } - - - /** - * Is the underlying directory known to be empty? - * @return FALSE if directory is known to have a child entry, TRUE if - * directory is known to be empty, UNKNOWN otherwise. - */ - public Tristate isEmpty() { - if (getListing().isEmpty()) { - if (isAuthoritative()) { - return Tristate.TRUE; - } else { - // This listing is empty, but may not be full list of underlying dir. - return Tristate.UNKNOWN; - } - } else { // not empty listing - // There exists at least one child, dir not empty. - return Tristate.FALSE; - } - } - - /** - * Marks this directory listing as full and authoritative. - * @param authoritative see {@link #isAuthoritative()}. - */ - public void setAuthoritative(boolean authoritative) { - this.isAuthoritative = authoritative; - } - - /** - * Lookup entry within this directory listing. This may return null if the - * {@code MetadataStore} only tracks a partial set of the directory entries. - * In the case where {@link #isAuthoritative()} is true, however, this - * function returns null iff the directory is known not to contain the listing - * at given path (within the scope of the {@code MetadataStore} that returned - * it). - * - * @param childPath path of entry to look for. - * @return entry, or null if it is not present or not being tracked. - */ - public PathMetadata get(Path childPath) { - checkChildPath(childPath); - return listMap.get(childPath); - } - - /** - * Replace an entry with a tombstone. - * @param childPath path of entry to replace. - */ - public void markDeleted(Path childPath, long lastUpdated) { - checkChildPath(childPath); - listMap.put(childPath, PathMetadata.tombstone(childPath, lastUpdated)); - } - - /** - * Remove entry from this directory. - * - * @param childPath path of entry to remove. - */ - public void remove(Path childPath) { - checkChildPath(childPath); - listMap.remove(childPath); - } - - /** - * Add an entry to the directory listing. If this listing already contains a - * {@code FileStatus} with the same path, it will be replaced. - * - * @param childPathMetadata entry to add to this directory listing. - * @return true if the status was added or replaced with a new value. False - * if the same FileStatus value was already present. - */ - public boolean put(PathMetadata childPathMetadata) { - Preconditions.checkNotNull(childPathMetadata, - "childPathMetadata must be non-null"); - final S3AFileStatus fileStatus = childPathMetadata.getFileStatus(); - Path childPath = childStatusToPathKey(fileStatus); - PathMetadata newValue = childPathMetadata; - PathMetadata oldValue = listMap.put(childPath, childPathMetadata); - return oldValue == null || !oldValue.equals(newValue); - } - - @Override - public String toString() { - return "DirListingMetadata{" + - "path=" + path + - ", listMap=" + listMap + - ", isAuthoritative=" + isAuthoritative + - ", lastUpdated=" + this.getLastUpdated() + - '}'; - } - - /** - * Remove expired entries from the listing based on TTL. - * @param ttl the ttl time - * @param now the current time - * @return the expired values. - */ - public synchronized List removeExpiredEntriesFromListing( - long ttl, long now) { - List expired = new ArrayList<>(); - final Iterator> iterator = - listMap.entrySet().iterator(); - while (iterator.hasNext()) { - final Map.Entry entry = iterator.next(); - // we filter iff the lastupdated is not 0 and the entry is expired - PathMetadata metadata = entry.getValue(); - if (metadata.getLastUpdated() != 0 - && (metadata.getLastUpdated() + ttl) <= now) { - expired.add(metadata); - iterator.remove(); - } - } - return expired; - } - - /** - * Log contents to supplied StringBuilder in a pretty fashion. - * @param sb target StringBuilder - */ - public void prettyPrint(StringBuilder sb) { - sb.append(String.format("DirMeta %-20s %-18s", - path.toString(), - isAuthoritative ? "Authoritative" : "Not Authoritative")); - for (Map.Entry entry : listMap.entrySet()) { - sb.append("\n key: ").append(entry.getKey()).append(": "); - entry.getValue().prettyPrint(sb); - } - sb.append("\n"); - } - - public String prettyPrint() { - StringBuilder sb = new StringBuilder(); - prettyPrint(sb); - return sb.toString(); - } - - /** - * Checks that child path is valid. - * @param childPath path to check. - */ - private void checkChildPath(Path childPath) { - checkPathAbsolute(childPath); - - // If this dir's path has host (and thus scheme), so must its children - URI parentUri = path.toUri(); - URI childUri = childPath.toUri(); - if (parentUri.getHost() != null) { - Preconditions.checkNotNull(childUri.getHost(), "Expected non-null URI " + - "host: %s", childUri); - Preconditions.checkArgument( - childUri.getHost().equals(parentUri.getHost()), - "childUri %s and parentUri %s must have the same host", - childUri, parentUri); - Preconditions.checkNotNull(childUri.getScheme(), "No scheme in path %s", - childUri); - } - Preconditions.checkArgument(!childPath.isRoot(), - "childPath cannot be the root path: %s", childPath); - Preconditions.checkArgument(parentUri.getPath().equals( - childPath.getParent().toUri().getPath()), - "childPath %s must be a child of %s", childPath, path); - } - - /** - * For Paths that are handed in directly, we assert they are in consistent - * format with checkPath(). For paths that are supplied embedded in - * FileStatus, we attempt to fill in missing scheme and host, when this - * DirListingMetadata is associated with one. - * - * @return Path suitable for consistent hashtable lookups - * @throws NullPointerException null status argument - * @throws IllegalArgumentException bad status values or failure to - * create a URI. - */ - private Path childStatusToPathKey(FileStatus status) { - Path p = status.getPath(); - Preconditions.checkNotNull(p, "Child status' path cannot be null"); - Preconditions.checkArgument(!p.isRoot(), - "childPath cannot be the root path: %s", p); - Preconditions.checkArgument(p.getParent().equals(path), - "childPath %s must be a child of %s", p, path); - URI uri = p.toUri(); - URI parentUri = path.toUri(); - // If FileStatus' path is missing host, but should have one, add it. - if (uri.getHost() == null && parentUri.getHost() != null) { - try { - return new Path(new URI(parentUri.getScheme(), parentUri.getHost(), - uri.getPath(), uri.getFragment())); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("FileStatus path invalid with" + - " added " + parentUri.getScheme() + "://" + parentUri.getHost() + - " added", e); - } - } - return p; - } - - private void checkPathAbsolute(Path p) { - Preconditions.checkNotNull(p, "path must be non-null"); - Preconditions.checkArgument(p.isAbsolute(), "path must be absolute: %s", p); - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DumpS3GuardDynamoTable.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DumpS3GuardDynamoTable.java deleted file mode 100644 index 2a7cb4c1b72..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DumpS3GuardDynamoTable.java +++ /dev/null @@ -1,793 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import javax.annotation.Nullable; -import java.io.Closeable; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.PrintWriter; -import java.net.URI; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.Deque; -import java.util.List; - -import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; -import org.apache.hadoop.thirdparty.com.google.common.collect.Lists; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.commons.lang3.tuple.Pair; -import org.apache.hadoop.classification.InterfaceAudience; -import org.apache.hadoop.classification.InterfaceStability; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.PathIOException; -import org.apache.hadoop.fs.RemoteIterator; -import org.apache.hadoop.fs.s3a.Listing; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.apache.hadoop.fs.s3a.S3ALocatedFileStatus; -import org.apache.hadoop.fs.s3a.S3ListRequest; -import org.apache.hadoop.fs.store.audit.AuditSpan; -import org.apache.hadoop.service.Service; -import org.apache.hadoop.service.launcher.LauncherExitCodes; -import org.apache.hadoop.service.launcher.ServiceLaunchException; -import org.apache.hadoop.service.launcher.ServiceLauncher; -import org.apache.hadoop.util.DurationInfo; -import org.apache.hadoop.util.ExitUtil; - -import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkNotNull; -import static org.apache.hadoop.fs.s3a.S3AUtils.ACCEPT_ALL; - -/** - * This is a low-level diagnostics entry point which does a CVE/TSV dump of - * the DDB state. - * As it also lists the filesystem, it actually changes the state of the store - * during the operation. - */ -@InterfaceAudience.Private -@InterfaceStability.Unstable -public class DumpS3GuardDynamoTable extends AbstractS3GuardDynamoDBDiagnostic { - - private static final Logger LOG = - LoggerFactory.getLogger(DumpS3GuardDynamoTable.class); - - /** - * Application name. - */ - public static final String NAME = "DumpS3GuardDynamoTable"; - - /** - * Usage. - */ - private static final String USAGE_MESSAGE = NAME - + " "; - - /** - * Suffix for the flat list: {@value}. - */ - public static final String FLAT_CSV = "-flat.csv"; - - /** - * Suffix for the raw S3 dump: {@value}. - */ - public static final String RAW_CSV = "-s3.csv"; - - /** - * Suffix for the DDB scan: {@value}. - */ - public static final String SCAN_CSV = "-scan.csv"; - - /** - * Suffix for the second DDB scan: : {@value}. - */ - public static final String SCAN2_CSV = "-scan-2.csv"; - - /** - * Suffix for the treewalk scan of the S3A Filesystem: {@value}. - */ - public static final String TREE_CSV = "-tree.csv"; - - /** - * Suffix for a recursive treewalk through the metastore: {@value}. - */ - public static final String STORE_CSV = "-store.csv"; - - /** - * Path in the local filesystem to save the data. - */ - private String destPath; - - private Pair scanEntryResult; - - private Pair secondScanResult; - - private long rawObjectStoreCount; - - private long listStatusCount; - - private long treewalkCount; - - /** - * Instantiate. - * @param name application name. - */ - public DumpS3GuardDynamoTable(final String name) { - super(name); - } - - /** - * Instantiate with default name. - */ - public DumpS3GuardDynamoTable() { - this(NAME); - } - - /** - * Bind to a specific FS + store. - * @param fs filesystem - * @param store metastore to use - * @param destFile the base filename for output - * @param uri URI of store -only needed if FS is null. - */ - public DumpS3GuardDynamoTable( - final S3AFileSystem fs, - final DynamoDBMetadataStore store, - final File destFile, - final URI uri) { - super(NAME, fs, store, uri); - this.destPath = destFile.getAbsolutePath(); - } - - /** - * Bind to the argument list, including validating the CLI. - * @throws Exception failure. - */ - @Override - protected void serviceStart() throws Exception { - if (getStore() == null) { - List arg = getArgumentList(2, 2, USAGE_MESSAGE); - bindFromCLI(arg.get(0)); - destPath = arg.get(1); - } - } - - /** - * Dump the filesystem and the metastore. - * @return the exit code. - * @throws ServiceLaunchException on failure. - * @throws IOException IO failure. - */ - @Override - public int execute() throws ServiceLaunchException, IOException { - - try { - final File scanFile = new File( - destPath + SCAN_CSV).getCanonicalFile(); - File parentDir = scanFile.getParentFile(); - if (!parentDir.mkdirs() && !parentDir.isDirectory()) { - throw new PathIOException(parentDir.toString(), - "Could not create destination directory"); - } - - try (CsvFile csv = new CsvFile(scanFile); - DurationInfo ignored = new DurationInfo(LOG, - "scanFile dump to %s", scanFile)) { - scanEntryResult = scanMetastore(csv); - } - - if (getFilesystem() != null) { - - Path basePath = getFilesystem().qualify(new Path(getUri())); - - final File destFile = new File(destPath + STORE_CSV) - .getCanonicalFile(); - LOG.info("Writing Store details to {}", destFile); - try (CsvFile csv = new CsvFile(destFile); - DurationInfo ignored = new DurationInfo(LOG, "List metastore")) { - - LOG.info("Base path: {}", basePath); - dumpMetastore(csv, basePath); - } - - // these operations all update the metastore as they list, - // that is: they are side-effecting. - final File treewalkFile = new File(destPath + TREE_CSV) - .getCanonicalFile(); - - try (CsvFile csv = new CsvFile(treewalkFile); - DurationInfo ignored = new DurationInfo(LOG, - "Treewalk to %s", treewalkFile)) { - treewalkCount = treewalkFilesystem(csv, basePath); - } - final File flatlistFile = new File( - destPath + FLAT_CSV).getCanonicalFile(); - - try (CsvFile csv = new CsvFile(flatlistFile); - DurationInfo ignored = new DurationInfo(LOG, - "Flat list to %s", flatlistFile)) { - listStatusCount = listStatusFilesystem(csv, basePath); - } - final File rawFile = new File( - destPath + RAW_CSV).getCanonicalFile(); - - try (CsvFile csv = new CsvFile(rawFile); - DurationInfo ignored = new DurationInfo(LOG, - "Raw dump to %s", rawFile)) { - rawObjectStoreCount = dumpRawS3ObjectStore(csv); - } - final File scanFile2 = new File( - destPath + SCAN2_CSV).getCanonicalFile(); - - try (CsvFile csv = new CsvFile(scanFile); - DurationInfo ignored = new DurationInfo(LOG, - "scanFile dump to %s", scanFile2)) { - secondScanResult = scanMetastore(csv); - } - } - - return LauncherExitCodes.EXIT_SUCCESS; - } catch (IOException | RuntimeException e) { - LOG.error("failure", e); - throw e; - } - } - - /** - * Push all elements of a list to a queue, such that the first entry - * on the list becomes the head of the queue. - * @param queue queue to update - * @param entries list of entries to add. - * @param type of queue - */ - private void pushAll(Deque queue, List entries) { - List reversed = Lists.reverse(entries); - for (T t : reversed) { - queue.push(t); - } - } - - /** - * Dump the filesystem via a treewalk. - * If metastore entries mark directories as deleted, this - * walk will not explore them. - * @param csv destination. - * @param base base path. - * @return number of entries found. - * @throws IOException IO failure. - */ - protected long treewalkFilesystem( - final CsvFile csv, - final Path base) throws IOException { - ArrayDeque queue = new ArrayDeque<>(); - queue.add(base); - long count = 0; - while (!queue.isEmpty()) { - Path path = queue.pop(); - count++; - FileStatus[] fileStatuses; - try { - fileStatuses = getFilesystem().listStatus(path); - } catch (FileNotFoundException e) { - LOG.warn("File {} was not found", path); - continue; - } - // entries - for (FileStatus fileStatus : fileStatuses) { - csv.entry((S3AFileStatus) fileStatus); - } - // scan through the list, building up a reverse list of all directories - // found. - List dirs = new ArrayList<>(fileStatuses.length); - for (FileStatus fileStatus : fileStatuses) { - if (fileStatus.isDirectory() - && !(fileStatus.getPath().equals(path))) { - // directory: add to the end of the queue. - dirs.add(fileStatus.getPath()); - } else { - // file: just increment the count - count++; - } - // now push the dirs list in reverse - // so that they have been added in the sort order as returned. - pushAll(queue, dirs); - } - } - return count; - } - - /** - * Dump the filesystem via a recursive listStatus call. - * @param csv destination. - * @return number of entries found. - * @throws IOException IO failure. - */ - protected long listStatusFilesystem( - final CsvFile csv, - final Path path) throws IOException { - long count = 0; - RemoteIterator iterator = getFilesystem() - .listFilesAndEmptyDirectories(path, true); - while (iterator.hasNext()) { - S3ALocatedFileStatus status = iterator.next(); - csv.entry(status.toS3AFileStatus()); - } - return count; - } - - /** - * Dump the raw S3 Object Store. - * @param csv destination. - * @return number of entries found. - * @throws IOException IO failure. - */ - protected long dumpRawS3ObjectStore( - final CsvFile csv) throws IOException { - S3AFileSystem fs = getFilesystem(); - long count = 0; - Path rootPath = fs.qualify(new Path("/")); - try (AuditSpan span = fs.createSpan("DumpS3GuardDynamoTable", - rootPath.toString(), null)) { - Listing listing = fs.getListing(); - S3ListRequest request = listing.createListObjectsRequest("", null, span); - count = 0; - RemoteIterator st = - listing.createFileStatusListingIterator(rootPath, request, - ACCEPT_ALL, - new Listing.AcceptAllButSelfAndS3nDirs(rootPath), - span); - while (st.hasNext()) { - count++; - S3AFileStatus next = st.next(); - LOG.debug("[{}] {}", count, next); - csv.entry(next); - } - LOG.info("entry count: {}", count); - } - return count; - } - - /** - * list children under the metastore from a base path, through - * a recursive query + walk strategy. - * @param csv dest - * @param basePath base path - * @throws IOException failure. - */ - protected void dumpMetastore(final CsvFile csv, - final Path basePath) throws IOException { - dumpStoreEntries(csv, getStore().listChildren(basePath)); - } - - /** - * Recursive Store Dump. - * @param csv open CSV file. - * @param dir directory listing - * @return (directories, files) - * @throws IOException failure - */ - private Pair dumpStoreEntries( - CsvFile csv, - DirListingMetadata dir) throws IOException { - ArrayDeque queue = new ArrayDeque<>(); - queue.add(dir); - long files = 0, dirs = 1; - while (!queue.isEmpty()) { - DirListingMetadata next = queue.pop(); - List childDirs = new ArrayList<>(); - Collection listing = next.getListing(); - // sort by name - List sorted = new ArrayList<>(listing); - sorted.sort(new PathOrderComparators.PathMetadataComparator( - (l, r) -> l.compareTo(r))); - - for (PathMetadata pmd : sorted) { - DDBPathMetadata ddbMd = (DDBPathMetadata) pmd; - dumpEntry(csv, ddbMd); - if (ddbMd.getFileStatus().isDirectory()) { - childDirs.add(ddbMd); - } else { - files++; - } - } - List childMD = new ArrayList<>(childDirs.size()); - for (DDBPathMetadata childDir : childDirs) { - childMD.add(getStore().listChildren( - childDir.getFileStatus().getPath())); - } - pushAll(queue, childMD); - } - - return Pair.of(dirs, files); - } - - - /** - * Dump a single entry, and log it. - * @param csv CSV output file. - * @param md metadata to log. - */ - private void dumpEntry(CsvFile csv, DDBPathMetadata md) { - LOG.debug("{}", md.prettyPrint()); - csv.entry(md); - } - - /** - * Scan the metastore for all entries and dump them. - * There's no attempt to sort the output. - * @param csv file - * @return tuple of (live entries, tombstones). - */ - private Pair scanMetastore(CsvFile csv) { - S3GuardTableAccess tableAccess = new S3GuardTableAccess(getStore()); - ExpressionSpecBuilder builder = new ExpressionSpecBuilder(); - Iterable results = - getStore().wrapWithRetries(tableAccess.scanMetadata(builder)); - long live = 0; - long tombstone = 0; - for (DDBPathMetadata md : results) { - if (!(md instanceof S3GuardTableAccess.VersionMarker)) { - // print it - csv.entry(md); - if (md.isDeleted()) { - tombstone++; - } else { - live++; - } - - } - } - return Pair.of(live, tombstone); - } - - public Pair getScanEntryResult() { - return scanEntryResult; - } - - public Pair getSecondScanResult() { - return secondScanResult; - } - - public long getRawObjectStoreCount() { - return rawObjectStoreCount; - } - - public long getListStatusCount() { - return listStatusCount; - } - - public long getTreewalkCount() { - return treewalkCount; - } - - /** - * Convert a timestamp in milliseconds to a human string. - * @param millis epoch time in millis - * @return a string for the CSV file. - */ - private static String stringify(long millis) { - return new Date(millis).toString(); - } - - /** - * This is the JVM entry point for the service launcher. - * - * Converts the arguments to a list, then invokes - * {@link #serviceMain(List, AbstractS3GuardDynamoDBDiagnostic)}. - * @param args command line arguments. - */ - public static void main(String[] args) { - try { - serviceMain(Arrays.asList(args), new DumpS3GuardDynamoTable()); - } catch (ExitUtil.ExitException e) { - ExitUtil.terminate(e); - } - } - - /** - * The real main function, which takes the arguments as a list. - * Argument 0 MUST be the service classname - * @param argsList the list of arguments - * @param service service to launch. - */ - static void serviceMain( - final List argsList, - final AbstractS3GuardDynamoDBDiagnostic service) { - ServiceLauncher serviceLauncher = - new ServiceLauncher<>(service.getName()); - - ExitUtil.ExitException ex = serviceLauncher.launchService( - new Configuration(), - service, - argsList, - false, - true); - if (ex != null) { - throw ex; - } - } - - /** - * Entry point to dump the metastore and s3 store world views - *

    - * Both the FS and the store will be dumped: the store is scanned - * before and after the sequence to show what changes were made to - * the store during the list operation. - * @param fs fs to dump. If null a store must be provided. - * @param store store to dump (fallback to FS) - * @param conf configuration to use (fallback to fs) - * @param destFile base name of the output files. - * @param uri URI of store -only needed if FS is null. - * @throws ExitUtil.ExitException failure. - * @return the store - */ - public static DumpS3GuardDynamoTable dumpStore( - @Nullable final S3AFileSystem fs, - @Nullable DynamoDBMetadataStore store, - @Nullable Configuration conf, - final File destFile, - @Nullable URI uri) throws ExitUtil.ExitException { - ServiceLauncher serviceLauncher = - new ServiceLauncher<>(NAME); - - if (conf == null) { - conf = checkNotNull(fs, "No filesystem").getConf(); - } - if (store == null) { - store = (DynamoDBMetadataStore) checkNotNull(fs, "No filesystem") - .getMetadataStore(); - } - DumpS3GuardDynamoTable dump = new DumpS3GuardDynamoTable(fs, - store, - destFile, - uri); - ExitUtil.ExitException ex = serviceLauncher.launchService( - conf, - dump, - Collections.emptyList(), - false, - true); - if (ex != null && ex.getExitCode() != 0) { - throw ex; - } - LOG.info("Results:"); - Pair r = dump.getScanEntryResult(); - LOG.info("Metastore entries: {}", r); - LOG.info("Metastore scan total {}, entries {}, tombstones {}", - r.getLeft() + r.getRight(), - r.getLeft(), - r.getRight()); - LOG.info("S3 count {}", dump.getRawObjectStoreCount()); - LOG.info("Treewalk Count {}", dump.getTreewalkCount()); - LOG.info("List Status Count {}", dump.getListStatusCount()); - r = dump.getSecondScanResult(); - if (r != null) { - LOG.info("Second metastore scan total {}, entries {}, tombstones {}", - r.getLeft() + r.getRight(), - r.getLeft(), - r.getRight()); - } - return dump; - } - - /** - * Writer for generating test CSV files. - * - * Quotes are manged by passing in a long whose specific bits control - * whether or not a row is quoted, bit 0 for column 0, etc. - * - * There is no escaping of values here. - */ - private static final class CsvFile implements Closeable { - - - /** constant to quote all columns. */ - public static final long ALL_QUOTES = 0x7fffffff; - - /** least significant bit is used for first column; 1 mean 'quote'. */ - public static final int ROW_QUOTE_MAP = 0b1110_1001_1111; - - /** quote nothing: {@value}. */ - public static final long NO_QUOTES = 0; - - private final Path path; - - private final PrintWriter out; - - private final String separator; - - private final String eol; - - private final String quote; - - /** - * Create. - * @param path filesystem path. - * @param out output write. - * @param separator separator of entries. - * @param eol EOL marker. - * @param quote quote marker. - * @throws IOException failure. - */ - private CsvFile( - final Path path, - final PrintWriter out, - final String separator, - final String eol, - final String quote) throws IOException { - this.separator = checkNotNull(separator); - this.eol = checkNotNull(eol); - this.quote = checkNotNull(quote); - this.path = path; - this.out = checkNotNull(out); - header(); - } - - /** - * Create to a file, with UTF-8 output and the standard - * options of the TSV file. - * @param file destination file. - * @throws IOException failure. - */ - private CsvFile(File file) throws IOException { - this(null, - new PrintWriter(file, "UTF-8"), "\t", "\n", "\""); - } - - /** - * Close the file, if not already done. - * @throws IOException on a failure. - */ - @Override - public synchronized void close() throws IOException { - if (out != null) { - out.close(); - } - } - - public Path getPath() { - return path; - } - - public String getSeparator() { - return separator; - } - - public String getEol() { - return eol; - } - - /** - * Write a row. - * Entries are quoted if the bit for that column is true. - * @param quotes quote policy: every bit defines the rule for that element - * @param columns columns to write - * @return self for ease of chaining. - */ - public CsvFile row(long quotes, Object... columns) { - checkNotNull(out); - for (int i = 0; i < columns.length; i++) { - if (i != 0) { - out.write(separator); - } - boolean toQuote = (quotes & 1) == 1; - // unsigned right shift to make next column flag @ position 0 - quotes = quotes >>> 1; - if (toQuote) { - out.write(quote); - } - Object column = columns[i]; - out.write(column != null ? column.toString() : ""); - if (toQuote) { - out.write(quote); - } - } - out.write(eol); - return this; - } - - /** - * Write a line. - * @param line line to print - * @return self for ease of chaining. - */ - public CsvFile line(String line) { - out.write(line); - out.write(eol); - return this; - } - - /** - * Get the output stream. - * @return the stream. - */ - public PrintWriter getOut() { - return out; - } - - /** - * Print the header. - */ - void header() { - row(CsvFile.ALL_QUOTES, - "type", - "deleted", - "path", - "is_auth_dir", - "is_empty_dir", - "len", - "updated", - "updated_s", - "last_modified", - "last_modified_s", - "etag", - "version"); - } - - /** - * Add a metadata entry. - * @param md metadata. - */ - void entry(DDBPathMetadata md) { - S3AFileStatus fileStatus = md.getFileStatus(); - row(ROW_QUOTE_MAP, - fileStatus.isDirectory() ? "dir" : "file", - md.isDeleted(), - fileStatus.getPath().toString(), - md.isAuthoritativeDir(), - md.isEmptyDirectory().name(), - fileStatus.getLen(), - md.getLastUpdated(), - stringify(md.getLastUpdated()), - fileStatus.getModificationTime(), - stringify(fileStatus.getModificationTime()), - fileStatus.getETag(), - fileStatus.getVersionId()); - } - - /** - * filesystem entry: no metadata. - * @param fileStatus file status - */ - void entry(S3AFileStatus fileStatus) { - row(ROW_QUOTE_MAP, - fileStatus.isDirectory() ? "dir" : "file", - "false", - fileStatus.getPath().toString(), - "", - fileStatus.isEmptyDirectory().name(), - fileStatus.getLen(), - "", - "", - fileStatus.getModificationTime(), - stringify(fileStatus.getModificationTime()), - fileStatus.getETag(), - fileStatus.getVersionId()); - } - } - -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DynamoDBClientFactory.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DynamoDBClientFactory.java deleted file mode 100644 index e4275ccb51c..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DynamoDBClientFactory.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.IOException; - -import com.amazonaws.ClientConfiguration; -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.regions.Regions; -import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; -import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.hadoop.classification.InterfaceAudience; -import org.apache.hadoop.conf.Configurable; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.conf.Configured; -import org.apache.hadoop.fs.s3a.Constants; -import org.apache.hadoop.fs.s3a.S3AUtils; - -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_REGION_KEY; - -/** - * Interface to create a DynamoDB client. - * - * Implementation should be configured for setting and getting configuration. - */ -@InterfaceAudience.Private -public interface DynamoDBClientFactory extends Configurable { - Logger LOG = LoggerFactory.getLogger(DynamoDBClientFactory.class); - - /** - * Create a DynamoDB client object from configuration. - * - * The DynamoDB client to create does not have to relate to any S3 buckets. - * All information needed to create a DynamoDB client is from the hadoop - * configuration. Specially, if the region is not configured, it will use the - * provided region parameter. If region is neither configured nor provided, - * it will indicate an error. - * - * @param defaultRegion the default region of the AmazonDynamoDB client - * @param bucket Optional bucket to use to look up per-bucket proxy secrets - * @param credentials credentials to use for authentication. - * @return a new DynamoDB client - * @throws IOException if any IO error happens - */ - AmazonDynamoDB createDynamoDBClient(final String defaultRegion, - final String bucket, - final AWSCredentialsProvider credentials) throws IOException; - - /** - * The default implementation for creating an AmazonDynamoDB. - */ - class DefaultDynamoDBClientFactory extends Configured - implements DynamoDBClientFactory { - @Override - public AmazonDynamoDB createDynamoDBClient(String defaultRegion, - final String bucket, - final AWSCredentialsProvider credentials) - throws IOException { - Preconditions.checkNotNull(getConf(), - "Should have been configured before usage"); - - final Configuration conf = getConf(); - final ClientConfiguration awsConf = S3AUtils - .createAwsConf(conf, bucket, Constants.AWS_SERVICE_IDENTIFIER_DDB); - - final String region = getRegion(conf, defaultRegion); - LOG.debug("Creating DynamoDB client in region {}", region); - - return AmazonDynamoDBClientBuilder.standard() - .withCredentials(credentials) - .withClientConfiguration(awsConf) - .withRegion(region) - .build(); - } - - /** - * Helper method to get and validate the AWS region for DynamoDBClient. - * - * @param conf configuration - * @param defaultRegion the default region - * @return configured region or else the provided default region - * @throws IOException if the region is not valid - */ - static String getRegion(Configuration conf, String defaultRegion) - throws IOException { - String region = conf.getTrimmed(S3GUARD_DDB_REGION_KEY); - if (StringUtils.isEmpty(region)) { - region = defaultRegion; - } - try { - Regions.fromName(region); - } catch (IllegalArgumentException | NullPointerException e) { - throw new IOException("Invalid region specified: " + region + "; " + - "Region can be configured with " + S3GUARD_DDB_REGION_KEY + ": " + - validRegionsString()); - } - return region; - } - - private static String validRegionsString() { - final String delimiter = ", "; - Regions[] regions = Regions.values(); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < regions.length; i++) { - if (i > 0) { - sb.append(delimiter); - } - sb.append(regions[i].getName()); - } - return sb.toString(); - - } - } - -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DynamoDBMetadataStore.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DynamoDBMetadataStore.java deleted file mode 100644 index 3c991b35453..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DynamoDBMetadataStore.java +++ /dev/null @@ -1,2534 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import javax.annotation.Nullable; -import java.io.IOException; -import java.io.InterruptedIOException; -import java.io.UncheckedIOException; -import java.net.URI; -import java.nio.file.AccessDeniedException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.TreeMap; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; - -import com.amazonaws.AmazonServiceException; -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; -import com.amazonaws.services.dynamodbv2.document.BatchWriteItemOutcome; -import com.amazonaws.services.dynamodbv2.document.DynamoDB; -import com.amazonaws.services.dynamodbv2.document.Item; -import com.amazonaws.services.dynamodbv2.document.ItemCollection; -import com.amazonaws.services.dynamodbv2.document.PrimaryKey; -import com.amazonaws.services.dynamodbv2.document.PutItemOutcome; -import com.amazonaws.services.dynamodbv2.document.QueryOutcome; -import com.amazonaws.services.dynamodbv2.document.ScanOutcome; -import com.amazonaws.services.dynamodbv2.document.Table; -import com.amazonaws.services.dynamodbv2.document.TableWriteItems; -import com.amazonaws.services.dynamodbv2.document.internal.IteratorSupport; -import com.amazonaws.services.dynamodbv2.document.spec.GetItemSpec; -import com.amazonaws.services.dynamodbv2.document.spec.QuerySpec; -import com.amazonaws.services.dynamodbv2.document.utils.ValueMap; -import com.amazonaws.services.dynamodbv2.model.AmazonDynamoDBException; -import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughputDescription; -import com.amazonaws.services.dynamodbv2.model.TableDescription; -import com.amazonaws.services.dynamodbv2.model.WriteRequest; - -import org.apache.hadoop.fs.s3a.impl.InternalConstants; -import org.apache.hadoop.thirdparty.com.google.common.annotations.VisibleForTesting; -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; -import org.apache.hadoop.thirdparty.com.google.common.collect.Lists; -import org.apache.hadoop.thirdparty.com.google.common.util.concurrent.ListeningExecutorService; -import org.apache.hadoop.thirdparty.com.google.common.util.concurrent.MoreExecutors; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.hadoop.classification.InterfaceAudience; -import org.apache.hadoop.classification.InterfaceStability; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.PathIOException; -import org.apache.hadoop.fs.RemoteIterator; -import org.apache.hadoop.util.functional.CallableRaisingIOE; -import org.apache.hadoop.util.functional.RemoteIterators; -import org.apache.hadoop.fs.s3a.AWSCredentialProviderList; -import org.apache.hadoop.fs.s3a.AWSServiceThrottledException; -import org.apache.hadoop.fs.s3a.Constants; -import org.apache.hadoop.fs.s3a.Invoker; -import org.apache.hadoop.fs.s3a.Retries; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.apache.hadoop.fs.s3a.S3AUtils; -import org.apache.hadoop.fs.s3a.Tristate; -import org.apache.hadoop.fs.s3a.auth.RoleModel; -import org.apache.hadoop.fs.s3a.auth.RolePolicies; -import org.apache.hadoop.fs.s3a.auth.delegation.AWSPolicyProvider; -import org.apache.hadoop.fs.s3a.impl.StoreContext; -import org.apache.hadoop.io.retry.RetryPolicies; -import org.apache.hadoop.io.retry.RetryPolicy; -import org.apache.hadoop.security.UserGroupInformation; -import org.apache.hadoop.util.BlockingThreadPoolExecutorService; -import org.apache.hadoop.util.DurationInfo; -import org.apache.hadoop.util.ReflectionUtils; - -import static org.apache.hadoop.fs.s3a.Constants.*; -import static org.apache.hadoop.fs.s3a.S3AUtils.*; -import static org.apache.hadoop.fs.s3a.auth.RolePolicies.allowAllDynamoDBOperations; -import static org.apache.hadoop.fs.s3a.auth.RolePolicies.allowS3GuardClientOperations; -import static org.apache.hadoop.fs.s3a.impl.CallableSupplier.submit; -import static org.apache.hadoop.fs.s3a.impl.CallableSupplier.waitForCompletion; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.*; -import static org.apache.hadoop.fs.s3a.s3guard.PathOrderComparators.TOPMOST_PM_LAST; -import static org.apache.hadoop.fs.s3a.s3guard.S3Guard.*; - -/** - * DynamoDBMetadataStore is a {@link MetadataStore} that persists - * file system metadata to DynamoDB. - * - * The current implementation uses a schema consisting of a single table. The - * name of the table can be configured by config key - * {@link org.apache.hadoop.fs.s3a.Constants#S3GUARD_DDB_TABLE_NAME_KEY}. - * By default, it matches the name of the S3 bucket. Each item in the table - * represents a single directory or file. Its path is split into separate table - * attributes: - *

      - *
    • parent (absolute path of the parent, with bucket name inserted as - * first path component).
    • - *
    • child (path of that specific child, relative to parent).
    • - *
    • optional boolean attribute tracking whether the path is a directory. - * Absence or a false value indicates the path is a file.
    • - *
    • optional long attribute revealing modification time of file. - * This attribute is meaningful only to file items.
    • - *
    • optional long attribute revealing file length. - * This attribute is meaningful only to file items.
    • - *
    • optional long attribute revealing block size of the file. - * This attribute is meaningful only to file items.
    • - *
    • optional string attribute tracking the s3 eTag of the file. - * May be absent if the metadata was entered with a version of S3Guard - * before this was tracked. - * This attribute is meaningful only to file items.
    • - *
    • optional string attribute tracking the s3 versionId of the file. - * May be absent if the metadata was entered with a version of S3Guard - * before this was tracked. - * This attribute is meaningful only to file items.
    • - *
    - * - * The DynamoDB partition key is the parent, and the range key is the child. - * - * To allow multiple buckets to share the same DynamoDB table, the bucket - * name is treated as the root directory. - * - * For example, assume the consistent store contains metadata representing this - * file system structure: - * - *
    - * s3a://bucket/dir1
    - * |-- dir2
    - * |   |-- file1
    - * |   `-- file2
    - * `-- dir3
    - *     |-- dir4
    - *     |   `-- file3
    - *     |-- dir5
    - *     |   `-- file4
    - *     `-- dir6
    - * 
    - * - * This is persisted to a single DynamoDB table as: - * - *
    - * ====================================================================================
    - * | parent                 | child | is_dir | mod_time | len | etag | ver_id |  ...  |
    - * ====================================================================================
    - * | /bucket                | dir1  | true   |          |     |      |        |       |
    - * | /bucket/dir1           | dir2  | true   |          |     |      |        |       |
    - * | /bucket/dir1           | dir3  | true   |          |     |      |        |       |
    - * | /bucket/dir1/dir2      | file1 |        |   100    | 111 | abc  |  mno   |       |
    - * | /bucket/dir1/dir2      | file2 |        |   200    | 222 | def  |  pqr   |       |
    - * | /bucket/dir1/dir3      | dir4  | true   |          |     |      |        |       |
    - * | /bucket/dir1/dir3      | dir5  | true   |          |     |      |        |       |
    - * | /bucket/dir1/dir3/dir4 | file3 |        |   300    | 333 | ghi  |  stu   |       |
    - * | /bucket/dir1/dir3/dir5 | file4 |        |   400    | 444 | jkl  |  vwx   |       |
    - * | /bucket/dir1/dir3      | dir6  | true   |          |     |      |        |       |
    - * ====================================================================================
    - * 
    - * - * This choice of schema is efficient for read access patterns. - * {@link #get(Path)} can be served from a single item lookup. - * {@link #listChildren(Path)} can be served from a query against all rows - * matching the parent (the partition key) and the returned list is guaranteed - * to be sorted by child (the range key). Tracking whether or not a path is a - * directory helps prevent unnecessary queries during traversal of an entire - * sub-tree. - * - * Some mutating operations, notably - * {@link MetadataStore#deleteSubtree(Path, BulkOperationState)} and - * {@link MetadataStore#move(Collection, Collection, BulkOperationState)} - * are less efficient with this schema. - * They require mutating multiple items in the DynamoDB table. - * - * By default, DynamoDB access is performed within the same AWS region as - * the S3 bucket that hosts the S3A instance. During initialization, it checks - * the location of the S3 bucket and creates a DynamoDB client connected to the - * same region. The region may also be set explicitly by setting the config - * parameter {@code fs.s3a.s3guard.ddb.region} to the corresponding region. - */ -@SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") -@InterfaceAudience.Private -@InterfaceStability.Evolving -public class DynamoDBMetadataStore implements MetadataStore, - AWSPolicyProvider { - public static final Logger LOG = LoggerFactory.getLogger( - DynamoDBMetadataStore.class); - - /** - * Name of the operations log. - */ - public static final String OPERATIONS_LOG_NAME = - "org.apache.hadoop.fs.s3a.s3guard.Operations"; - - /** - * A log of all state changing operations to the store; - * only updated at debug level. - */ - public static final Logger OPERATIONS_LOG = LoggerFactory.getLogger( - OPERATIONS_LOG_NAME); - - /** parent/child name to use in the version marker. */ - public static final String VERSION_MARKER_ITEM_NAME = "../VERSION"; - - /** parent/child name to use in the version marker. */ - public static final String VERSION_MARKER_TAG_NAME = "s3guard_version"; - - /** Current version number. */ - public static final int VERSION = 100; - - @VisibleForTesting - static final String BILLING_MODE - = "billing-mode"; - - @VisibleForTesting - static final String BILLING_MODE_PER_REQUEST - = "per-request"; - - @VisibleForTesting - static final String BILLING_MODE_PROVISIONED - = "provisioned"; - - @VisibleForTesting - static final String DESCRIPTION - = "S3Guard metadata store in DynamoDB"; - @VisibleForTesting - static final String READ_CAPACITY = "read-capacity"; - @VisibleForTesting - static final String WRITE_CAPACITY = "write-capacity"; - @VisibleForTesting - static final String STATUS = "status"; - @VisibleForTesting - static final String TABLE = "table"; - - @VisibleForTesting - static final String HINT_DDB_IOPS_TOO_LOW - = " This may be because the write threshold of DynamoDB is set too low."; - - @VisibleForTesting - static final String THROTTLING = "Throttling"; - - public static final String E_ON_DEMAND_NO_SET_CAPACITY - = "Neither ReadCapacityUnits nor WriteCapacityUnits can be specified when BillingMode is PAY_PER_REQUEST"; - - @VisibleForTesting - static final String E_INCONSISTENT_UPDATE - = "Duplicate and inconsistent entry in update operation"; - - private static final ValueMap DELETE_TRACKING_VALUE_MAP = - new ValueMap().withBoolean(":false", false); - - /** - * The maximum number of outstanding operations to submit - * before blocking to await completion of all the executors. - * Paging work like this is less efficient, but it ensures that - * failure (auth, network, etc) are picked up before many more - * operations are submitted. - * - * Arbitrary Choice. - * Value: {@value}. - */ - private static final int S3GUARD_DDB_SUBMITTED_TASK_LIMIT = 50; - - private AmazonDynamoDB amazonDynamoDB; - private DynamoDB dynamoDB; - private AWSCredentialProviderList credentials; - private String region; - private Table table; - private String tableName; - private Configuration conf; - private String username; - - /** - * This policy is mostly for batched writes, not for processing - * exceptions in invoke() calls. - * It also has a role purpose in - * {@link DynamoDBMetadataStoreTableManager#getVersionMarkerItem()}; - * look at that method for the details. - */ - private RetryPolicy batchWriteRetryPolicy; - - /** - * The instrumentation is never null -if/when bound to an owner file system - * That filesystem statistics will be updated as appropriate. - */ - private MetastoreInstrumentation instrumentation - = new MetastoreInstrumentationImpl(); - - /** Owner FS: only valid if configured with an owner FS. */ - private S3AFileSystem owner; - - /** Invoker for IO. Until configured properly, use try-once. */ - private Invoker invoker = new Invoker(RetryPolicies.TRY_ONCE_THEN_FAIL, - Invoker.NO_OP - ); - - /** Invoker for read operations. */ - private Invoker readOp; - - /** Invoker for write operations. */ - private Invoker writeOp; - - /** Invoker for scan operations. */ - private Invoker scanOp; - - private final AtomicLong readThrottleEvents = new AtomicLong(0); - private final AtomicLong writeThrottleEvents = new AtomicLong(0); - private final AtomicLong scanThrottleEvents = new AtomicLong(0); - private final AtomicLong batchWriteCapacityExceededEvents = new AtomicLong(0); - - /** - * Total limit on the number of throttle events after which - * we stop warning in the log. Keeps the noise down. - */ - private static final int THROTTLE_EVENT_LOG_LIMIT = 100; - - /** - * Count of the total number of throttle events; used to crank back logging. - */ - private AtomicInteger throttleEventCount = new AtomicInteger(0); - - /** - * Executor for submitting operations. - */ - private ListeningExecutorService executor; - - /** - * Time source. This is used during writes when parent - * entries need to be created. - */ - private ITtlTimeProvider ttlTimeProvider; - - private DynamoDBMetadataStoreTableManager tableHandler; - - /** - * A utility function to create DynamoDB instance. - * @param conf the file system configuration - * @param s3Region region of the associated S3 bucket (if any). - * @param bucket Optional bucket to use to look up per-bucket proxy secrets - * @param credentials credentials. - * @return DynamoDB instance. - * @throws IOException I/O error. - */ - private DynamoDB createDynamoDB( - final Configuration conf, - final String s3Region, - final String bucket, - final AWSCredentialsProvider credentials) - throws IOException { - if (amazonDynamoDB == null) { - Preconditions.checkNotNull(conf); - final Class cls = - conf.getClass(S3GUARD_DDB_CLIENT_FACTORY_IMPL, - S3GUARD_DDB_CLIENT_FACTORY_IMPL_DEFAULT, DynamoDBClientFactory.class); - LOG.debug("Creating DynamoDB client {} with S3 region {}", cls, s3Region); - amazonDynamoDB = ReflectionUtils.newInstance(cls, conf) - .createDynamoDBClient(s3Region, bucket, credentials); - } - return new DynamoDB(amazonDynamoDB); - } - - /** - * {@inheritDoc}. - * The credentials for authenticating with S3 are requested from the - * FS via {@link S3AFileSystem#shareCredentials(String)}; this will - * increment the reference counter of these credentials. - * @param fs {@code S3AFileSystem} associated with the MetadataStore - * @param ttlTp the time provider to use for metadata expiry - * @throws IOException on a failure - */ - @Override - @Retries.OnceRaw - public void initialize(FileSystem fs, ITtlTimeProvider ttlTp) - throws IOException { - Preconditions.checkNotNull(fs, "Null filesystem"); - Preconditions.checkArgument(fs instanceof S3AFileSystem, - "DynamoDBMetadataStore only supports S3A filesystem - not %s", - fs); - bindToOwnerFilesystem((S3AFileSystem) fs); - final String bucket = owner.getBucket(); - String confRegion = conf.getTrimmed(S3GUARD_DDB_REGION_KEY); - if (!StringUtils.isEmpty(confRegion)) { - region = confRegion; - LOG.debug("Overriding S3 region with configured DynamoDB region: {}", - region); - } else { - try { - region = owner.getBucketLocation(); - } catch (AccessDeniedException e) { - // access denied here == can't call getBucket. Report meaningfully - URI uri = owner.getUri(); - String message = - "Failed to get bucket location as client lacks permission " - + RolePolicies.S3_GET_BUCKET_LOCATION + " for " + uri; - LOG.error(message); - throw (IOException)new AccessDeniedException(message).initCause(e); - } - LOG.debug("Inferring DynamoDB region from S3 bucket: {}", region); - } - credentials = owner.shareCredentials("s3guard"); - dynamoDB = createDynamoDB(conf, region, bucket, credentials); - - // use the bucket as the DynamoDB table name if not specified in config - tableName = conf.getTrimmed(S3GUARD_DDB_TABLE_NAME_KEY, bucket); - initDataAccessRetries(conf); - - this.ttlTimeProvider = ttlTp; - - tableHandler = new DynamoDBMetadataStoreTableManager( - dynamoDB, tableName, region, amazonDynamoDB, conf, readOp, - batchWriteRetryPolicy); - this.table = tableHandler.initTable(); - - instrumentation.initialized(); - } - - /** - * Declare that this table is owned by the specific S3A FS instance. - * This will bind some fields to the values provided by the owner, - * including wiring up the instrumentation. - * @param fs owner filesystem - */ - @VisibleForTesting - void bindToOwnerFilesystem(final S3AFileSystem fs) { - owner = fs; - conf = owner.getConf(); - StoreContext context = owner.createStoreContext(); - instrumentation = context.getInstrumentation() - .getS3GuardInstrumentation(); - username = context.getUsername(); - executor = MoreExecutors.listeningDecorator( - context.createThrottledExecutor()); - ttlTimeProvider = Preconditions.checkNotNull( - context.getTimeProvider(), - "ttlTimeProvider must not be null"); - } - - /** - * Performs one-time initialization of the metadata store via configuration. - * - * This initialization depends on the configuration object to get AWS - * credentials, DynamoDBFactory implementation class, DynamoDB endpoints, - * DynamoDB table names etc. After initialization, this metadata store does - * not explicitly relate to any S3 bucket, which be nonexistent. - * - * This is used to operate the metadata store directly beyond the scope of the - * S3AFileSystem integration, e.g. command line tools. - * Generally, callers should use - * {@link MetadataStore#initialize(FileSystem, ITtlTimeProvider)} - * with an initialized {@code S3AFileSystem} instance. - * - * Without a filesystem to act as a reference point, the configuration itself - * must declare the table name and region in the - * {@link Constants#S3GUARD_DDB_TABLE_NAME_KEY} and - * {@link Constants#S3GUARD_DDB_REGION_KEY} respectively. - * It also creates a new credential provider list from the configuration, - * using the base fs.s3a.* options, as there is no bucket to infer per-bucket - * settings from. - * - * @see MetadataStore#initialize(FileSystem, ITtlTimeProvider) - * @throws IOException if there is an error - * @throws IllegalArgumentException if the configuration is incomplete - */ - @Override - @Retries.OnceRaw - public void initialize(Configuration config, - ITtlTimeProvider ttlTp) throws IOException { - conf = config; - // use the bucket as the DynamoDB table name if not specified in config - tableName = conf.getTrimmed(S3GUARD_DDB_TABLE_NAME_KEY); - - Preconditions.checkArgument(!StringUtils.isEmpty(tableName), - "No DynamoDB table name configured"); - region = conf.getTrimmed(S3GUARD_DDB_REGION_KEY); - Preconditions.checkArgument(!StringUtils.isEmpty(region), - "No DynamoDB region configured"); - // there's no URI here, which complicates life: you cannot - // create AWS providers here which require one. - credentials = createAWSCredentialProviderSet(null, conf); - dynamoDB = createDynamoDB(conf, region, null, credentials); - - username = UserGroupInformation.getCurrentUser().getShortUserName(); - // without an executor from the owner FS, create one using - // the executor capacity for work. - int executorCapacity = intOption(conf, - EXECUTOR_CAPACITY, DEFAULT_EXECUTOR_CAPACITY, 1); - executor = MoreExecutors.listeningDecorator( - BlockingThreadPoolExecutorService.newInstance( - executorCapacity, - executorCapacity * 2, - longOption(conf, KEEPALIVE_TIME, - DEFAULT_KEEPALIVE_TIME, 0), - TimeUnit.SECONDS, - "s3a-ddb-" + tableName)); - initDataAccessRetries(conf); - this.ttlTimeProvider = ttlTp; - - tableHandler = new DynamoDBMetadataStoreTableManager( - dynamoDB, tableName, region, amazonDynamoDB, conf, readOp, - batchWriteRetryPolicy); - this.table = tableHandler.initTable(); - } - - /** - * Set retry policy. This is driven by the value of - * {@link Constants#S3GUARD_DDB_MAX_RETRIES} with an exponential backoff - * between each attempt of {@link Constants#S3GUARD_DDB_THROTTLE_RETRY_INTERVAL} - * milliseconds. - * @param config configuration for data access - */ - private void initDataAccessRetries(Configuration config) { - batchWriteRetryPolicy = RetryPolicies - .exponentialBackoffRetry( - config.getInt(S3GUARD_DDB_MAX_RETRIES, - S3GUARD_DDB_MAX_RETRIES_DEFAULT), - conf.getTimeDuration(S3GUARD_DDB_THROTTLE_RETRY_INTERVAL, - S3GUARD_DDB_THROTTLE_RETRY_INTERVAL_DEFAULT, - TimeUnit.MILLISECONDS), - TimeUnit.MILLISECONDS); - final RetryPolicy throttledRetryRetryPolicy - = new S3GuardDataAccessRetryPolicy(config); - readOp = new Invoker(throttledRetryRetryPolicy, this::readRetryEvent); - writeOp = new Invoker(throttledRetryRetryPolicy, this::writeRetryEvent); - scanOp = new Invoker(throttledRetryRetryPolicy, this::scanRetryEvent); - } - - @Override - @Retries.RetryTranslated - public void delete(Path path, - final BulkOperationState operationState) - throws IOException { - innerDelete(path, true, - extractOrCreate(operationState, - BulkOperationState.OperationType.Delete)); - } - - @Override - @Retries.RetryTranslated - public void forgetMetadata(Path path) throws IOException { - LOG.debug("Forget metadata for {}", path); - innerDelete(path, false, null); - } - - /** - * Inner delete option, action based on the {@code tombstone} flag. - * No tombstone: delete the entry. Tombstone: create a tombstone entry. - * There is no check as to whether the entry exists in the table first. - * @param path path to delete - * @param tombstone flag to create a tombstone marker - * @param ancestorState ancestor state for context. - * @throws IOException I/O error. - */ - @Retries.RetryTranslated - private void innerDelete(final Path path, - final boolean tombstone, - final AncestorState ancestorState) - throws IOException { - checkPath(path); - LOG.debug("Deleting from table {} in region {}: {}", - tableName, region, path); - - // deleting nonexistent item consumes 1 write capacity; skip it - if (path.isRoot()) { - LOG.debug("Skip deleting root directory as it does not exist in table"); - return; - } - // the policy on whether repeating delete operations is based - // on that of S3A itself - boolean idempotent = InternalConstants.DELETE_CONSIDERED_IDEMPOTENT; - if (tombstone) { - Preconditions.checkArgument(ttlTimeProvider != null, "ttlTimeProvider " - + "must not be null"); - final PathMetadata pmTombstone = PathMetadata.tombstone(path, - ttlTimeProvider.getNow()); - Item item = PathMetadataDynamoDBTranslation.pathMetadataToItem( - new DDBPathMetadata(pmTombstone)); - writeOp.retry( - "Put tombstone", - path.toString(), - idempotent, - () -> { - logPut(ancestorState, item); - recordsWritten(1); - table.putItem(item); - }); - } else { - PrimaryKey key = pathToKey(path); - writeOp.retry( - "Delete key", - path.toString(), - idempotent, - () -> { - // record the attempt so even on retry the counter goes up. - logDelete(ancestorState, key); - recordsDeleted(1); - table.deleteItem(key); - }); - } - } - - @Override - @Retries.RetryTranslated - public void deleteSubtree(Path path, - final BulkOperationState operationState) - throws IOException { - checkPath(path); - LOG.debug("Deleting subtree from table {} in region {}: {}", - tableName, region, path); - - final PathMetadata meta = get(path); - if (meta == null) { - LOG.debug("Subtree path {} does not exist; this will be a no-op", path); - return; - } - if (meta.isDeleted()) { - LOG.debug("Subtree path {} is deleted; this will be a no-op", path); - return; - } - deleteEntries(RemoteIterators.mappingRemoteIterator( - new DescendantsIterator(this, meta), - FileStatus::getPath), - operationState); - } - - @Override - @Retries.RetryTranslated - public void deletePaths(Collection paths, - final BulkOperationState operationState) - throws IOException { - deleteEntries(RemoteIterators.remoteIteratorFromIterable(paths), - operationState); - } - - /** - * Delete the entries under an iterator. - * There's no attempt to order the paths: they are - * deleted in the order passed in. - * @param entries entries to delete. - * @param operationState Nullable operation state - * @throws IOException failure - */ - @Retries.RetryTranslated - private void deleteEntries(RemoteIterator entries, - final BulkOperationState operationState) - throws IOException { - final List> futures = new ArrayList<>(); - AncestorState state = extractOrCreate(operationState, - BulkOperationState.OperationType.Delete); - - while (entries.hasNext()) { - final Path pathToDelete = entries.next(); - futures.add(submit(executor, () -> { - innerDelete(pathToDelete, true, state); - return null; - })); - if (futures.size() > S3GUARD_DDB_SUBMITTED_TASK_LIMIT) { - // first batch done; block for completion. - waitForCompletion(futures); - futures.clear(); - } - } - // now wait for the final set. - waitForCompletion(futures); - } - - /** - * Get a consistent view of an item. - * @param path path to look up in the database - * @return the result - * @throws IOException failure - */ - @Retries.RetryTranslated - private Item getConsistentItem(final Path path) throws IOException { - PrimaryKey key = pathToKey(path); - final GetItemSpec spec = new GetItemSpec() - .withPrimaryKey(key) - .withConsistentRead(true); // strictly consistent read - return readOp.retry("get", - path.toString(), - true, - () -> { - recordsRead(1); - return table.getItem(spec); - }); - } - - @Override - @Retries.RetryTranslated - public DDBPathMetadata get(Path path) throws IOException { - return get(path, false); - } - - @Override - @Retries.RetryTranslated - public DDBPathMetadata get(Path path, boolean wantEmptyDirectoryFlag) - throws IOException { - checkPath(path); - LOG.debug("Get from table {} in region {}: {} ; wantEmptyDirectory={}", - tableName, region, path, wantEmptyDirectoryFlag); - DDBPathMetadata result = innerGet(path, wantEmptyDirectoryFlag); - LOG.debug("result of get {} is: {}", path, result); - return result; - } - - /** - * Inner get operation, as invoked in the retry logic. - * @param path the path to get - * @param wantEmptyDirectoryFlag Set to true to give a hint to the - * MetadataStore that it should try to compute the empty directory flag. - * @return metadata for {@code path}, {@code null} if not found - * @throws IOException IO problem - */ - @Retries.RetryTranslated - private DDBPathMetadata innerGet(Path path, boolean wantEmptyDirectoryFlag) - throws IOException { - final DDBPathMetadata meta; - if (path.isRoot()) { - // Root does not persist in the table - meta = - new DDBPathMetadata(makeDirStatus(username, path)); - } else { - final Item item = getConsistentItem(path); - meta = itemToPathMetadata(item, username); - LOG.debug("Get from table {} in region {} returning for {}: {}", - tableName, region, path, meta); - } - - if (wantEmptyDirectoryFlag && meta != null && !meta.isDeleted()) { - final FileStatus status = meta.getFileStatus(); - // for a non-deleted directory, we query its direct undeleted children - // to determine the isEmpty bit. There's no TTL checking going on here. - if (status.isDirectory()) { - final QuerySpec spec = new QuerySpec() - .withHashKey(pathToParentKeyAttribute(path)) - .withConsistentRead(true) - .withFilterExpression(IS_DELETED + " = :false") - .withValueMap(DELETE_TRACKING_VALUE_MAP); - boolean hasChildren = readOp.retry("get/hasChildren", - path.toString(), - true, - () -> { - // issue the query - final IteratorSupport it = table.query( - spec).iterator(); - // if non empty, log the result to aid with some debugging - if (it.hasNext()) { - if (LOG.isDebugEnabled()) { - LOG.debug("Dir {} is non-empty", status.getPath()); - while(it.hasNext()) { - LOG.debug("{}", itemToPathMetadata(it.next(), username)); - } - } - return true; - } else { - return false; - } - }); - - // If directory is authoritative, we can set the empty directory flag - // to TRUE or FALSE. Otherwise FALSE, or UNKNOWN. - if (meta.isAuthoritativeDir()) { - meta.setIsEmptyDirectory( - hasChildren ? Tristate.FALSE : Tristate.TRUE); - } else { - meta.setIsEmptyDirectory( - hasChildren ? Tristate.FALSE : Tristate.UNKNOWN); - } - } - } - - return meta; - } - - /** - * Make a S3AFileStatus object for a directory at given path. - * The FileStatus only contains what S3A needs, and omits mod time - * since S3A uses its own implementation which returns current system time. - * @param dirOwner username of owner - * @param path path to dir - * @return new S3AFileStatus - */ - private S3AFileStatus makeDirStatus(String dirOwner, Path path) { - return new S3AFileStatus(Tristate.UNKNOWN, path, dirOwner); - } - - @Override - @Retries.RetryTranslated - public DirListingMetadata listChildren(final Path path) throws IOException { - checkPath(path); - LOG.debug("Listing table {} in region {}: {}", tableName, region, path); - - final QuerySpec spec = new QuerySpec() - .withHashKey(pathToParentKeyAttribute(path)) - .withConsistentRead(true); // strictly consistent read - final List metas = new ArrayList<>(); - // find the children in the table - final ItemCollection items = scanOp.retry( - "listChildren", - path.toString(), - true, - () -> table.query(spec)); - // now wrap the result with retry logic - try { - for (Item item : wrapWithRetries(items)) { - metas.add(itemToPathMetadata(item, username)); - } - } catch (UncheckedIOException e) { - // failure in the iterators; unwrap. - throw e.getCause(); - } - - // Minor race condition here - if the path is deleted between - // getting the list of items and the directory metadata we might - // get a null in DDBPathMetadata. - return getDirListingMetadataFromDirMetaAndList(path, metas, - get(path)); - } - - DirListingMetadata getDirListingMetadataFromDirMetaAndList(Path path, - List metas, DDBPathMetadata dirPathMeta) { - boolean isAuthoritative = false; - if (dirPathMeta != null) { - isAuthoritative = dirPathMeta.isAuthoritativeDir(); - } - - LOG.trace("Listing table {} in region {} for {} returning {}", - tableName, region, path, metas); - - if (!metas.isEmpty() && dirPathMeta == null) { - // We handle this case as the directory is deleted. - LOG.warn("Directory marker is deleted, but the list of the directory " - + "elements is not empty: {}. This case is handled as if the " - + "directory was deleted.", metas); - return null; - } - - if(metas.isEmpty() && dirPathMeta == null) { - return null; - } - - return new DirListingMetadata(path, metas, isAuthoritative, - dirPathMeta.getLastUpdated()); - } - - /** - * Origin of entries in the ancestor map built up in - * {@link #completeAncestry(Collection, AncestorState)}. - * This is done to stop generated ancestor entries to overwriting those - * in the store, while allowing those requested in the API call to do this. - */ - private enum EntryOrigin { - Requested, // requested in method call - Retrieved, // retrieved from DDB: do not resubmit - Generated // generated ancestor. - } - - /** - * Build the list of all parent entries. - *

    - * Thread safety: none. Callers must synchronize access. - *

    - * Callers are required to synchronize on ancestorState. - * @param pathsToCreate paths to create - * @param ancestorState ongoing ancestor state. - * @return the full ancestry paths - */ - private Collection completeAncestry( - final Collection pathsToCreate, - final AncestorState ancestorState) throws IOException { - // Key on path to allow fast lookup - Map> ancestry = new HashMap<>(); - LOG.debug("Completing ancestry for {} paths", pathsToCreate.size()); - // we sort the inputs to guarantee that the topmost entries come first. - // that way if the put request contains both parents and children - // then the existing parents will not be re-created -they will just - // be added to the ancestor list first. - List sortedPaths = new ArrayList<>(pathsToCreate); - sortedPaths.sort(PathOrderComparators.TOPMOST_PM_FIRST); - // iterate through the paths. - for (DDBPathMetadata entry : sortedPaths) { - Preconditions.checkArgument(entry != null); - Path path = entry.getFileStatus().getPath(); - LOG.debug("Adding entry {}", path); - if (path.isRoot()) { - // this is a root entry: do not add it. - break; - } - // add it to the ancestor state, failing if it is already there and - // of a different type. - DDBPathMetadata oldEntry = ancestorState.put(path, entry); - boolean addAncestors = true; - if (oldEntry != null) { - // check for and warn if the existing bulk operation has an inconsistent - // entry. - // two directories or two files are both allowed. - // file-over-file can happen in multipart uploaders when the same - // uploader is overwriting file entries to the same destination as - // part of its bulk operation. - boolean oldWasDir = oldEntry.getFileStatus().isDirectory(); - boolean newIsDir = entry.getFileStatus().isDirectory(); - if ((oldWasDir && !newIsDir) - || (!oldWasDir && newIsDir)) { - LOG.warn("Overwriting a S3Guard file created in the operation: {}", - oldEntry); - LOG.warn("With new entry: {}", entry); - // restore the old state - ancestorState.put(path, oldEntry); - // then raise an exception - throw new PathIOException(path.toString(), - String.format("%s old %s new %s", - E_INCONSISTENT_UPDATE, - oldEntry, - entry)); - } else { - // a directory is already present. Log and continue. - LOG.debug("Directory at {} being updated with value {}", - path, entry); - // and we skip the the subsequent parent scan as we've already been - // here - addAncestors = false; - } - } - // add the entry to the ancestry map as an explicitly requested entry. - ancestry.put(path, Pair.of(EntryOrigin.Requested, entry)); - // now scan up the ancestor tree to see if there are any - // immediately missing entries. - Path parent = path.getParent(); - while (addAncestors - && !parent.isRoot() && !ancestry.containsKey(parent)) { - if (!ancestorState.findEntry(parent, true)) { - // there is no entry in the ancestor state. - // look in the store - DDBPathMetadata md; - Pair newEntry; - final Item item = getConsistentItem(parent); - if (item != null && !itemToPathMetadata(item, username).isDeleted()) { - // This is an undeleted entry found in the database. - // register it in ancestor state and in the map of entries to create - // as a retrieved entry - md = itemToPathMetadata(item, username); - LOG.debug("Found existing entry for parent: {}", md); - newEntry = Pair.of(EntryOrigin.Retrieved, md); - // and we break, assuming that if there is an entry, its parents - // are valid too. - addAncestors = false; - } else { - // A directory entry was not found in the DB. Create one. - LOG.debug("auto-create ancestor path {} for child path {}", - parent, path); - final S3AFileStatus status = makeDirStatus(parent, username); - md = new DDBPathMetadata(status, Tristate.FALSE, - false, false, ttlTimeProvider.getNow()); - // declare to be a generated entry - newEntry = Pair.of(EntryOrigin.Generated, md); - } - // insert into the ancestor state to avoid further checks - ancestorState.put(parent, md); - ancestry.put(parent, newEntry); - } - parent = parent.getParent(); - } - } - // we now have a list of entries which were not in the operation state. - // Filter out those which were retrieved, to produce a list of those - // which must be written to the database. - // TODO sort in reverse order of existence - return ancestry.values().stream() - .filter(p -> p.getLeft() != EntryOrigin.Retrieved) - .map(Pair::getRight) - .collect(Collectors.toList()); - } - - /** - * {@inheritDoc} - *

    - * The implementation scans all up the directory tree and does a get() - * for each entry; at each level one is found it is added to the ancestor - * state. - *

    - * The original implementation would stop on finding the first non-empty - * parent. This (re) implementation issues a GET for every parent entry - * and so detects and recovers from a tombstone marker further up the tree - * (i.e. an inconsistent store is corrected for). - *

    - * if {@code operationState} is not null, when this method returns the - * operation state will be updated with all new entries created. - * This ensures that subsequent operations with the same store will not - * trigger new updates. - * @param qualifiedPath path to update - * @param operationState (nullable) operational state for a bulk update - * @throws IOException on failure. - */ - @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") - @Override - @Retries.RetryTranslated - public void addAncestors(final Path qualifiedPath, - @Nullable final BulkOperationState operationState) throws IOException { - - Collection newDirs = new ArrayList<>(); - final AncestorState ancestorState = extractOrCreate(operationState, - BulkOperationState.OperationType.Put); - Path parent = qualifiedPath.getParent(); - boolean entryFound = false; - - // Iterate up the parents. - // note that only ancestorState get/set operations are synchronized; - // the DDB read between them is not. As a result, more than one - // thread may probe the state, find the entry missing, do the database - // query and add the entry. - // This is done to avoid making the remote dynamo query part of the - // synchronized block. - // If a race does occur, the cost is simply one extra GET and potentially - // one extra PUT. - while (!parent.isRoot()) { - synchronized (ancestorState) { - if (ancestorState.contains(parent)) { - // the ancestry map contains the key, so no need to even look for it. - break; - } - } - // we don't worry about tombstone expiry here as expired or not, - // a directory entry will go in. - PathMetadata directory = get(parent); - if (directory == null || directory.isDeleted()) { - if (entryFound) { - LOG.warn("Inconsistent S3Guard table: adding directory {}", parent); - } - S3AFileStatus status = makeDirStatus(username, parent); - LOG.debug("Adding new ancestor entry {}", status); - DDBPathMetadata meta = new DDBPathMetadata(status, Tristate.FALSE, - false, ttlTimeProvider.getNow()); - newDirs.add(meta); - // Do not update ancestor state here, as it - // will happen in the innerPut() call. Were we to add it - // here that put operation would actually (mistakenly) skip - // creating the entry. - } else { - // an entry was found. Check its type - entryFound = true; - if (directory.getFileStatus().isFile()) { - throw new PathIOException(parent.toString(), - "Cannot overwrite parent file: metastore is" - + " in an inconsistent state"); - } - // the directory exists. Add it to the ancestor state for next time. - synchronized (ancestorState) { - ancestorState.put(parent, new DDBPathMetadata(directory)); - } - } - parent = parent.getParent(); - } - // the listing of directories to put is all those parents which we know - // are not in the store or BulkOperationState. - if (!newDirs.isEmpty()) { - // patch up the time. - patchLastUpdated(newDirs, ttlTimeProvider); - innerPut(newDirs, operationState); - } - } - - /** - * {@inheritDoc}. - * - * The DDB implementation sorts all the paths such that new items - * are ordered highest level entry first; deleted items are ordered - * lowest entry first. - * - * This is to ensure that if a client failed partway through the update, - * there will no entries in the table which lack parent entries. - * @param pathsToDelete Collection of all paths that were removed from the - * source directory tree of the move. - * @param pathsToCreate Collection of all PathMetadata for the new paths - * that were created at the destination of the rename - * (). - * @param operationState Any ongoing state supplied to the rename tracker - * which is to be passed in with each move operation. - * @throws IOException if there is an error - */ - @Override - @Retries.RetryTranslated - public void move(@Nullable Collection pathsToDelete, - @Nullable Collection pathsToCreate, - @Nullable final BulkOperationState operationState) throws IOException { - if (pathsToDelete == null && pathsToCreate == null) { - return; - } - - LOG.debug("Moving paths of table {} in region {}: {} paths to delete and {}" - + " paths to create", tableName, region, - pathsToDelete == null ? 0 : pathsToDelete.size(), - pathsToCreate == null ? 0 : pathsToCreate.size()); - LOG.trace("move: pathsToDelete = {}, pathsToCreate = {}", pathsToDelete, - pathsToCreate); - - // In DynamoDBMetadataStore implementation, we assume that if a path - // exists, all its ancestors will also exist in the table. - // Following code is to maintain this invariant by putting all ancestor - // directories of the paths to create. - // ancestor paths that are not explicitly added to paths to create - AncestorState ancestorState = extractOrCreate(operationState, - BulkOperationState.OperationType.Rename); - List newItems = new ArrayList<>(); - if (pathsToCreate != null) { - // create all parent entries. - // this is synchronized on the move state so that across both serialized - // and parallelized renames, duplicate ancestor entries are not created. - synchronized (ancestorState) { - newItems.addAll( - completeAncestry( - pathMetaToDDBPathMeta(pathsToCreate), - ancestorState)); - } - } - // sort all the new items topmost first. - newItems.sort(PathOrderComparators.TOPMOST_PM_FIRST); - - // now process the deletions. - if (pathsToDelete != null) { - List tombstones = new ArrayList<>(pathsToDelete.size()); - for (Path meta : pathsToDelete) { - Preconditions.checkArgument(ttlTimeProvider != null, "ttlTimeProvider" - + " must not be null"); - final PathMetadata pmTombstone = PathMetadata.tombstone(meta, - ttlTimeProvider.getNow()); - tombstones.add(new DDBPathMetadata(pmTombstone)); - } - // sort all the tombstones lowest first. - tombstones.sort(TOPMOST_PM_LAST); - newItems.addAll(tombstones); - } - - processBatchWriteRequest(ancestorState, - null, pathMetadataToItem(newItems)); - } - - /** - * Helper method to issue a batch write request to DynamoDB. - *

      - *
    1. Keys to delete are processed ahead of writing new items.
    2. - *
    3. No attempt is made to sort the input: the caller must do that
    4. - *
    - * As well as retrying on the operation invocation, incomplete - * batches are retried until all have been processed. - * - * @param ancestorState ancestor state for logging - * @param keysToDelete primary keys to be deleted; can be null - * @param itemsToPut new items to be put; can be null - * @return the number of iterations needed to complete the call. - */ - @Retries.RetryTranslated("Outstanding batch items are updated with backoff") - private int processBatchWriteRequest( - @Nullable AncestorState ancestorState, - PrimaryKey[] keysToDelete, - Item[] itemsToPut) throws IOException { - final int totalToDelete = (keysToDelete == null ? 0 : keysToDelete.length); - final int totalToPut = (itemsToPut == null ? 0 : itemsToPut.length); - if (totalToPut == 0 && totalToDelete == 0) { - LOG.debug("Ignoring empty batch write request"); - return 0; - } - int count = 0; - int batches = 0; - while (count < totalToDelete + totalToPut) { - final TableWriteItems writeItems = new TableWriteItems(tableName); - int numToDelete = 0; - if (keysToDelete != null - && count < totalToDelete) { - numToDelete = Math.min(S3GUARD_DDB_BATCH_WRITE_REQUEST_LIMIT, - totalToDelete - count); - PrimaryKey[] toDelete = Arrays.copyOfRange(keysToDelete, - count, count + numToDelete); - LOG.debug("Deleting {} entries: {}", toDelete.length, toDelete); - writeItems.withPrimaryKeysToDelete(toDelete); - count += numToDelete; - } - - if (numToDelete < S3GUARD_DDB_BATCH_WRITE_REQUEST_LIMIT - && itemsToPut != null - && count < totalToDelete + totalToPut) { - final int numToPut = Math.min( - S3GUARD_DDB_BATCH_WRITE_REQUEST_LIMIT - numToDelete, - totalToDelete + totalToPut - count); - final int index = count - totalToDelete; - writeItems.withItemsToPut( - Arrays.copyOfRange(itemsToPut, index, index + numToPut)); - count += numToPut; - } - - // if there's a retry and another process updates things then it's not - // quite idempotent, but this was the case anyway - batches++; - BatchWriteItemOutcome res = writeOp.retry( - "batch write", - "", - true, - () -> dynamoDB.batchWriteItem(writeItems)); - // Check for unprocessed keys in case of exceeding provisioned throughput - Map> unprocessed = res.getUnprocessedItems(); - int retryCount = 0; - while (!unprocessed.isEmpty()) { - batchWriteCapacityExceededEvents.incrementAndGet(); - batches++; - retryBackoffOnBatchWrite(retryCount++); - // use a different reference to keep the compiler quiet - final Map> upx = unprocessed; - res = writeOp.retry( - "batch write", - "", - true, - () -> dynamoDB.batchWriteItemUnprocessed(upx)); - unprocessed = res.getUnprocessedItems(); - } - } - if (itemsToPut != null) { - recordsWritten(itemsToPut.length); - logPut(ancestorState, itemsToPut); - } - if (keysToDelete != null) { - recordsDeleted(keysToDelete.length); - logDelete(ancestorState, keysToDelete); - - } - return batches; - } - - /** - * Put the current thread to sleep to implement exponential backoff - * depending on retryCount. If max retries are exceeded, throws an - * exception instead. - * - * @param retryCount number of retries so far - * @throws IOException when max retryCount is exceeded. - */ - private void retryBackoffOnBatchWrite(int retryCount) throws IOException { - try { - // Our RetryPolicy ignores everything but retryCount here. - RetryPolicy.RetryAction action = batchWriteRetryPolicy.shouldRetry( - null, - retryCount, 0, true); - if (action.action == RetryPolicy.RetryAction.RetryDecision.FAIL) { - // Create an AWSServiceThrottledException, with a fake inner cause - // which we fill in to look like a real exception so - // error messages look sensible - AmazonServiceException cause = new AmazonServiceException( - "Throttling"); - cause.setServiceName("S3Guard"); - cause.setStatusCode(AWSServiceThrottledException.STATUS_CODE); - cause.setErrorCode(THROTTLING); // used in real AWS errors - cause.setErrorType(AmazonServiceException.ErrorType.Service); - cause.setErrorMessage(THROTTLING); - cause.setRequestId("n/a"); - throw new AWSServiceThrottledException( - String.format("Max retries during batch write exceeded" - + " (%d) for DynamoDB." - + HINT_DDB_IOPS_TOO_LOW, - retryCount), - cause); - } else { - LOG.debug("Sleeping {} msec before next retry", action.delayMillis); - Thread.sleep(action.delayMillis); - } - } catch (InterruptedException e) { - throw (IOException)new InterruptedIOException(e.toString()).initCause(e); - } catch (IOException e) { - throw e; - } catch (Exception e) { - throw new IOException("Unexpected exception " + e, e); - } - } - - @Override - @Retries.RetryTranslated - public void put(final PathMetadata meta) throws IOException { - put(meta, null); - } - - @Override - @Retries.RetryTranslated - public void put( - final PathMetadata meta, - @Nullable final BulkOperationState operationState) throws IOException { - // For a deeply nested path, this method will automatically create the full - // ancestry and save respective item in DynamoDB table. - // So after put operation, we maintain the invariant that if a path exists, - // all its ancestors will also exist in the table. - // For performance purpose, we generate the full paths to put and use batch - // write item request to save the items. - LOG.debug("Saving to table {} in region {}: {}", tableName, region, meta); - - Collection wrapper = new ArrayList<>(1); - wrapper.add(meta); - put(wrapper, operationState); - } - - @Override - @Retries.RetryTranslated - public void put( - final Collection metas, - @Nullable final BulkOperationState operationState) throws IOException { - innerPut(pathMetaToDDBPathMeta(metas), operationState); - } - - /** - * Internal put operation. - *

    - * The ancestors to all entries are added to the set of entries to write, - * provided they are not already stored in any supplied operation state. - * Both the supplied metadata entries and ancestor entries are sorted - * so that the topmost entries are written first. - * This is to ensure that a failure partway through the operation will not - * create entries in the table without parents. - * @param metas metadata entries to write. - * @param operationState (nullable) operational state for a bulk update - * @throws IOException failure. - */ - @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") - @Retries.RetryTranslated - private void innerPut( - final Collection metas, - @Nullable final BulkOperationState operationState) throws IOException { - if (metas.isEmpty()) { - // Happens when someone calls put() with an empty list. - LOG.debug("Ignoring empty list of entries to put"); - return; - } - // always create or retrieve an ancestor state instance, so it can - // always be used for synchronization. - final AncestorState ancestorState = extractOrCreate(operationState, - BulkOperationState.OperationType.Put); - - Item[] items; - synchronized (ancestorState) { - items = pathMetadataToItem( - completeAncestry(metas, ancestorState)); - } - LOG.debug("Saving batch of {} items to table {}, region {}", items.length, - tableName, region); - processBatchWriteRequest(ancestorState, null, items); - } - - /** - * Get full path of ancestors that are nonexistent in table. - * - * This queries DDB when looking for parents which are not in - * any supplied ongoing operation state. - * Updates the operation state with found entries to reduce further checks. - * - * @param meta metadata to put - * @param operationState ongoing bulk state - * @return a possibly empty list of entries to put. - * @throws IOException failure - */ - @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") - @VisibleForTesting - @Retries.RetryTranslated - List fullPathsToPut(DDBPathMetadata meta, - @Nullable BulkOperationState operationState) - throws IOException { - checkPathMetadata(meta); - final List metasToPut = new ArrayList<>(); - // root path is not persisted - if (!meta.getFileStatus().getPath().isRoot()) { - metasToPut.add(meta); - } - - // put all its ancestors if not present; as an optimization we return at its - // first existent ancestor - final AncestorState ancestorState = extractOrCreate(operationState, - BulkOperationState.OperationType.Put); - Path path = meta.getFileStatus().getPath().getParent(); - while (path != null && !path.isRoot()) { - synchronized (ancestorState) { - if (ancestorState.findEntry(path, true)) { - break; - } - } - final Item item = getConsistentItem(path); - if (!itemExists(item)) { - final S3AFileStatus status = makeDirStatus(path, username); - metasToPut.add(new DDBPathMetadata(status, Tristate.FALSE, false, - meta.isAuthoritativeDir(), meta.getLastUpdated())); - path = path.getParent(); - } else { - // found the entry in the table, so add it to the ancestor state - synchronized (ancestorState) { - ancestorState.put(path, itemToPathMetadata(item, username)); - } - // then break out of the loop. - break; - } - } - return metasToPut; - } - - /** - * Does an item represent an object which exists? - * @param item item retrieved in a query. - * @return true iff the item isn't null and, if there is an is_deleted - * column, that its value is false. - */ - private static boolean itemExists(Item item) { - if (item == null) { - return false; - } - if (item.hasAttribute(IS_DELETED) && - item.getBoolean(IS_DELETED)) { - return false; - } - return true; - } - - /** - * Get the value of an optional boolean attribute, falling back to the - * default value if the attribute is absent. - * @param item Item - * @param attrName Attribute name - * @param defVal Default value - * @return The value or the default - */ - private static boolean getBoolAttribute(Item item, - String attrName, - boolean defVal) { - return item.hasAttribute(attrName) ? item.getBoolean(attrName) : defVal; - } - - /** Create a directory FileStatus using 0 for the lastUpdated time. */ - static S3AFileStatus makeDirStatus(Path f, String owner) { - return new S3AFileStatus(Tristate.UNKNOWN, f, owner); - } - - /** - * {@inheritDoc}. - * There is retry around building the list of paths to update, but - * the call to - * {@link #processBatchWriteRequest(DynamoDBMetadataStore.AncestorState, PrimaryKey[], Item[])} - * is only tried once. - * @param meta Directory listing metadata. - * @param unchangedEntries unchanged child entry paths - * @param operationState operational state for a bulk update - * @throws IOException IO problem - */ - @Override - @Retries.RetryTranslated - public void put( - final DirListingMetadata meta, - final List unchangedEntries, - @Nullable final BulkOperationState operationState) throws IOException { - LOG.debug("Saving {} dir meta for {} to table {} in region {}: {}", - meta.isAuthoritative() ? "auth" : "nonauth", - meta.getPath(), - tableName, region, meta); - // directory path - Path path = meta.getPath(); - DDBPathMetadata ddbPathMeta = - new DDBPathMetadata(makeDirStatus(path, username), meta.isEmpty(), - false, meta.isAuthoritative(), meta.getLastUpdated()); - // put all its ancestors if not present - final AncestorState ancestorState = extractOrCreate(operationState, - BulkOperationState.OperationType.Put); - // First add any missing ancestors... - final List metasToPut = fullPathsToPut(ddbPathMeta, - ancestorState); - - // next add all changed children of the directory - // ones that came from the previous listing are left as-is - final Collection children = meta.getListing() - .stream() - .filter(e -> !unchangedEntries.contains(e.getFileStatus().getPath())) - .collect(Collectors.toList()); - - metasToPut.addAll(pathMetaToDDBPathMeta(children)); - - // sort so highest-level entries are written to the store first. - // if a sequence fails, no orphan entries will have been written. - metasToPut.sort(PathOrderComparators.TOPMOST_PM_FIRST); - processBatchWriteRequest(ancestorState, - null, - pathMetadataToItem(metasToPut)); - // and add the ancestors - synchronized (ancestorState) { - metasToPut.forEach(ancestorState::put); - } - } - - @Override - public synchronized void close() { - instrumentation.storeClosed(); - try { - if (dynamoDB != null) { - LOG.debug("Shutting down {}", this); - dynamoDB.shutdown(); - dynamoDB = null; - } - } finally { - closeAutocloseables(LOG, credentials); - credentials = null; - } - } - - @Override - @Retries.RetryTranslated - public void destroy() throws IOException { - tableHandler.destroy(); - } - - @Retries.RetryTranslated - private ItemCollection expiredFiles(PruneMode pruneMode, - long cutoff, String keyPrefix) throws IOException { - - String filterExpression; - String projectionExpression; - ValueMap map; - - switch (pruneMode) { - case ALL_BY_MODTIME: - // filter all files under the given parent older than the modtime. - // this implicitly skips directories, because they lack a modtime field. - // however we explicitly exclude directories to make clear that - // directories are to be excluded and avoid any confusion - // see: HADOOP-16725. - // note: files lack the is_dir field entirely, so we use a `not` to - // filter out the directories. - filterExpression = - "mod_time < :mod_time and begins_with(parent, :parent)" - + " and not is_dir = :is_dir"; - projectionExpression = "parent,child"; - map = new ValueMap() - .withLong(":mod_time", cutoff) - .withString(":parent", keyPrefix) - .withBoolean(":is_dir", true); - break; - case TOMBSTONES_BY_LASTUPDATED: - filterExpression = - "last_updated < :last_updated and begins_with(parent, :parent) " - + "and is_deleted = :is_deleted"; - projectionExpression = "parent,child,is_deleted"; - map = new ValueMap() - .withLong(":last_updated", cutoff) - .withString(":parent", keyPrefix) - .withBoolean(":is_deleted", true); - break; - default: - throw new UnsupportedOperationException("Unsupported prune mode: " - + pruneMode); - } - - return readOp.retry( - "scan", - keyPrefix, - true, - () -> table.scan(filterExpression, projectionExpression, null, map)); - } - - @Override - @Retries.RetryTranslated - public void prune(PruneMode pruneMode, long cutoff) throws IOException { - prune(pruneMode, cutoff, "/"); - } - - /** - * Prune files, in batches. There's optionally a sleep between each batch. - * - * @param pruneMode The mode of operation for the prune For details see - * {@link MetadataStore#prune(PruneMode, long)} - * @param cutoff Oldest modification time to allow - * @param keyPrefix The prefix for the keys that should be removed - * @throws IOException Any IO/DDB failure. - * @throws InterruptedIOException if the prune was interrupted - * @return count of pruned items. - */ - @Override - @Retries.RetryTranslated - public long prune(PruneMode pruneMode, long cutoff, String keyPrefix) - throws IOException { - LOG.debug("Prune {} under {} with age {}", - pruneMode == PruneMode.ALL_BY_MODTIME - ? "files and tombstones" : "tombstones", - keyPrefix, cutoff); - final ItemCollection items = - expiredFiles(pruneMode, cutoff, keyPrefix); - return innerPrune(pruneMode, cutoff, keyPrefix, items); - } - - /** - * Prune files, in batches. There's optionally a sleep between each batch. - * - * @param pruneMode The mode of operation for the prune For details see - * {@link MetadataStore#prune(PruneMode, long)} - * @param cutoff Oldest modification time to allow - * @param keyPrefix The prefix for the keys that should be removed - * @param items expired items - * @return count of pruned items. - * @throws IOException Any IO/DDB failure. - * @throws InterruptedIOException if the prune was interrupted - */ - private int innerPrune( - final PruneMode pruneMode, final long cutoff, final String keyPrefix, - final ItemCollection items) - throws IOException { - int itemCount = 0; - try (AncestorState state = initiateBulkWrite( - BulkOperationState.OperationType.Prune, null); - DurationInfo ignored = - new DurationInfo(LOG, "Pruning DynamoDB Store")) { - ArrayList deletionBatch = - new ArrayList<>(S3GUARD_DDB_BATCH_WRITE_REQUEST_LIMIT); - long delay = conf.getTimeDuration( - S3GUARD_DDB_BACKGROUND_SLEEP_MSEC_KEY, - S3GUARD_DDB_BACKGROUND_SLEEP_MSEC_DEFAULT, - TimeUnit.MILLISECONDS); - Set parentPathSet = new HashSet<>(); - Set clearedParentPathSet = new HashSet<>(); - // declare the operation to delete a batch as a function so - // as to keep the code consistent across multiple uses. - CallableRaisingIOE deleteBatchOperation = - () -> { - // lowest path entries get deleted first. - deletionBatch.sort(PathOrderComparators.TOPMOST_PATH_LAST); - processBatchWriteRequest(state, pathToKey(deletionBatch), null); - - // set authoritative false for each pruned dir listing - // if at least one entry was not a tombstone - removeAuthoritativeDirFlag(parentPathSet, state); - // already cleared parent paths. - clearedParentPathSet.addAll(parentPathSet); - parentPathSet.clear(); - return null; - }; - for (Item item : items) { - DDBPathMetadata md = PathMetadataDynamoDBTranslation - .itemToPathMetadata(item, username); - Path path = md.getFileStatus().getPath(); - boolean tombstone = md.isDeleted(); - LOG.debug("Prune entry {}", path); - deletionBatch.add(path); - - // add parent path of item so it can be marked as non-auth. - // this is only done if - // * it has not already been processed - // * the entry pruned is not a tombstone (no need to update) - // * the file is not in the root dir - Path parentPath = path.getParent(); - if (!tombstone - && parentPath != null - && !parentPath.isRoot() - && !clearedParentPathSet.contains(parentPath)) { - parentPathSet.add(parentPath); - } - - itemCount++; - if (deletionBatch.size() == S3GUARD_DDB_BATCH_WRITE_REQUEST_LIMIT) { - deleteBatchOperation.apply(); - deletionBatch.clear(); - if (delay > 0) { - Thread.sleep(delay); - } - } - } - // final batch of deletes - if (!deletionBatch.isEmpty()) { - deleteBatchOperation.apply(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new InterruptedIOException("Pruning was interrupted"); - } catch (AmazonDynamoDBException e) { - throw translateDynamoDBException(keyPrefix, - "Prune of " + keyPrefix + " failed", e); - } - LOG.info("Finished pruning {} items in batches of {}", itemCount, - S3GUARD_DDB_BATCH_WRITE_REQUEST_LIMIT); - return itemCount; - } - - /** - * Remove the Authoritative Directory Marker from a set of paths, if - * those paths are in the store. - *

    - * This operation is onlyfor pruning; it does not raise an error - * if, during the prune phase, the table appears inconsistent. - * This is not unusual as it can happen in a number of ways - *

      - *
    1. The state of the table changes during a slow prune operation which - * deliberately inserts pauses to avoid overloading prepaid IO capacity. - *
    2. - *
    3. Tombstone markers have been left in the table after many other - * operations have taken place, including deleting/replacing - * parents.
    4. - *
    - *

    - * - * If an exception is raised in the get/update process, then the exception - * is caught and only rethrown after all the other paths are processed. - * This is to ensure a best-effort attempt to update the store. - * @param pathSet set of paths. - * @param state ongoing operation state. - * @throws IOException only after a best effort is made to update the store. - */ - private void removeAuthoritativeDirFlag( - final Set pathSet, - final AncestorState state) throws IOException { - - AtomicReference rIOException = new AtomicReference<>(); - - Set metas = pathSet.stream().map(path -> { - try { - if (path.isRoot()) { - LOG.debug("ignoring root path"); - return null; - } - if (state != null && state.get(path) != null) { - // there's already an entry for this path - LOG.debug("Ignoring update of entry already in the state map"); - return null; - } - DDBPathMetadata ddbPathMetadata = get(path); - if (ddbPathMetadata == null) { - // there is no entry. - LOG.debug("No parent {}; skipping", path); - return null; - } - if (ddbPathMetadata.isDeleted()) { - // the parent itself is deleted - LOG.debug("Parent has been deleted {}; skipping", path); - return null; - } - if (!ddbPathMetadata.getFileStatus().isDirectory()) { - // the parent itself is deleted - LOG.debug("Parent is not a directory {}; skipping", path); - return null; - } - LOG.debug("Setting isAuthoritativeDir==false on {}", ddbPathMetadata); - ddbPathMetadata.setAuthoritativeDir(false); - ddbPathMetadata.setLastUpdated(ttlTimeProvider.getNow()); - return ddbPathMetadata; - } catch (IOException e) { - String msg = String.format("IOException while getting PathMetadata " - + "on path: %s.", path); - LOG.error(msg, e); - rIOException.set(e); - return null; - } - }).filter(Objects::nonNull).collect(Collectors.toSet()); - - try { - LOG.debug("innerPut on metas: {}", metas); - if (!metas.isEmpty()) { - innerPut(metas, state); - } - } catch (IOException e) { - String msg = String.format("IOException while setting false " - + "authoritative directory flag on: %s.", metas); - LOG.error(msg, e); - rIOException.set(e); - } - - if (rIOException.get() != null) { - throw rIOException.get(); - } - } - - @VisibleForTesting - public AmazonDynamoDB getAmazonDynamoDB() { - return amazonDynamoDB; - } - - @Override - public String toString() { - return getClass().getSimpleName() + '{' - + "region=" + region - + ", tableName=" + tableName - + ", tableArn=" + tableHandler.getTableArn() - + '}'; - } - - /** - * The administrative policy includes all DDB table operations; - * application access is restricted to those operations S3Guard operations - * require when working with data in a guarded bucket. - * @param access access level desired. - * @return a possibly empty list of statements. - */ - @Override - public List listAWSPolicyRules( - final Set access) { - Preconditions.checkState(tableHandler.getTableArn() != null, - "TableARN not known"); - if (access.isEmpty()) { - return Collections.emptyList(); - } - RoleModel.Statement stat; - if (access.contains(AccessLevel.ADMIN)) { - stat = allowAllDynamoDBOperations(tableHandler.getTableArn()); - } else { - stat = allowS3GuardClientOperations(tableHandler.getTableArn()); - } - return Lists.newArrayList(stat); - } - - /** - * PUT a single item to the table. - * @param item item to put - * @return the outcome. - */ - @Retries.OnceRaw - private PutItemOutcome putItem(Item item) { - LOG.debug("Putting item {}", item); - return table.putItem(item); - } - - @VisibleForTesting - Table getTable() { - return table; - } - - String getRegion() { - return region; - } - - @VisibleForTesting - public String getTableName() { - return tableName; - } - - @VisibleForTesting - DynamoDB getDynamoDB() { - return dynamoDB; - } - - /** - * Validates a path object; it must be absolute, have an s3a:/// scheme - * and contain a host (bucket) component. - * @param path path to check - * @return the path passed in - */ - private Path checkPath(Path path) { - Preconditions.checkNotNull(path); - Preconditions.checkArgument(path.isAbsolute(), "Path %s is not absolute", - path); - URI uri = path.toUri(); - Preconditions.checkNotNull(uri.getScheme(), "Path %s missing scheme", path); - Preconditions.checkArgument(uri.getScheme().equals(Constants.FS_S3A), - "Path %s scheme must be %s", path, Constants.FS_S3A); - Preconditions.checkArgument(!StringUtils.isEmpty(uri.getHost()), "Path %s" + - " is missing bucket.", path); - return path; - } - - /** - * Validates a path meta-data object. - */ - private static void checkPathMetadata(PathMetadata meta) { - Preconditions.checkNotNull(meta); - Preconditions.checkNotNull(meta.getFileStatus()); - Preconditions.checkNotNull(meta.getFileStatus().getPath()); - } - - @Override - @Retries.OnceRaw - public Map getDiagnostics() throws IOException { - Map map = new TreeMap<>(); - if (table != null) { - TableDescription desc = getTableDescription(true); - map.put("name", desc.getTableName()); - map.put(STATUS, desc.getTableStatus()); - map.put("ARN", desc.getTableArn()); - map.put("size", desc.getTableSizeBytes().toString()); - map.put(TABLE, desc.toString()); - ProvisionedThroughputDescription throughput - = desc.getProvisionedThroughput(); - map.put(READ_CAPACITY, throughput.getReadCapacityUnits().toString()); - map.put(WRITE_CAPACITY, throughput.getWriteCapacityUnits().toString()); - map.put(BILLING_MODE, - throughput.getWriteCapacityUnits() == 0 - ? BILLING_MODE_PER_REQUEST - : BILLING_MODE_PROVISIONED); - map.put("sse", desc.getSSEDescription() == null - ? "DISABLED" - : desc.getSSEDescription().toString()); - map.put(MetadataStoreCapabilities.PERSISTS_AUTHORITATIVE_BIT, - Boolean.toString(true)); - } else { - map.put("name", "DynamoDB Metadata Store"); - map.put(TABLE, "none"); - map.put(STATUS, "undefined"); - } - map.put("description", DESCRIPTION); - map.put("region", region); - if (batchWriteRetryPolicy != null) { - map.put("retryPolicy", batchWriteRetryPolicy.toString()); - } - return map; - } - - @Retries.OnceRaw - private TableDescription getTableDescription(boolean forceUpdate) { - TableDescription desc = table.getDescription(); - if (desc == null || forceUpdate) { - desc = table.describe(); - } - return desc; - } - - @Override - @Retries.OnceRaw - public void updateParameters(Map parameters) - throws IOException { - Preconditions.checkNotNull(table, "Not initialized"); - TableDescription desc = getTableDescription(true); - ProvisionedThroughputDescription current - = desc.getProvisionedThroughput(); - - long currentRead = current.getReadCapacityUnits(); - long newRead = getLongParam(parameters, - S3GUARD_DDB_TABLE_CAPACITY_READ_KEY, - currentRead); - long currentWrite = current.getWriteCapacityUnits(); - long newWrite = getLongParam(parameters, - S3GUARD_DDB_TABLE_CAPACITY_WRITE_KEY, - currentWrite); - - if (currentRead == 0 || currentWrite == 0) { - // table is pay on demand - throw new IOException(E_ON_DEMAND_NO_SET_CAPACITY); - } - - if (newRead != currentRead || newWrite != currentWrite) { - LOG.info("Current table capacity is read: {}, write: {}", - currentRead, currentWrite); - LOG.info("Changing capacity of table to read: {}, write: {}", - newRead, newWrite); - tableHandler.provisionTableBlocking(newRead, newWrite); - } else { - LOG.info("Table capacity unchanged at read: {}, write: {}", - newRead, newWrite); - } - } - - private long getLongParam(Map parameters, - String key, - long defVal) { - String k = parameters.get(key); - if (k != null) { - return Long.parseLong(k); - } else { - return defVal; - } - } - - /** - * Callback on a read operation retried. - * @param text text of the operation - * @param ex exception - * @param attempts number of attempts - * @param idempotent is the method idempotent (this is assumed to be true) - */ - void readRetryEvent( - String text, - IOException ex, - int attempts, - boolean idempotent) { - readThrottleEvents.incrementAndGet(); - retryEvent(text, ex, attempts, true); - } - - /** - * Callback on a write operation retried. - * @param text text of the operation - * @param ex exception - * @param attempts number of attempts - * @param idempotent is the method idempotent (this is assumed to be true) - */ - void writeRetryEvent( - String text, - IOException ex, - int attempts, - boolean idempotent) { - writeThrottleEvents.incrementAndGet(); - retryEvent(text, ex, attempts, idempotent); - } - - /** - * Callback on a scan operation retried. - * @param text text of the operation - * @param ex exception - * @param attempts number of attempts - * @param idempotent is the method idempotent (this is assumed to be true) - */ - void scanRetryEvent( - String text, - IOException ex, - int attempts, - boolean idempotent) { - scanThrottleEvents.incrementAndGet(); - retryEvent(text, ex, attempts, idempotent); - } - - /** - * Callback from {@link Invoker} when an operation is retried. - * @param text text of the operation - * @param ex exception - * @param attempts number of attempts - * @param idempotent is the method idempotent - */ - void retryEvent( - String text, - IOException ex, - int attempts, - boolean idempotent) { - if (S3AUtils.isThrottleException(ex)) { - // throttled - instrumentation.throttled(); - int eventCount = throttleEventCount.addAndGet(1); - if (attempts == 1 && eventCount < THROTTLE_EVENT_LOG_LIMIT) { - LOG.warn("DynamoDB IO limits reached in {};" - + " consider increasing capacity: {}", text, ex.toString()); - LOG.debug("Throttled", ex); - } else { - // user has been warned already, log at debug only. - LOG.debug("DynamoDB IO limits reached in {};" - + " consider increasing capacity: {}", text, ex.toString()); - } - } else if (attempts == 1) { - // not throttled. Log on the first attempt only - LOG.info("Retrying {}: {}", text, ex.toString()); - LOG.debug("Retrying {}", text, ex); - } - - // note a retry - instrumentation.retrying(); - if (owner != null) { - owner.metastoreOperationRetried(ex, attempts, idempotent); - } - } - - /** - * Get the count of read throttle events. - * @return the current count of read throttle events. - */ - @VisibleForTesting - public long getReadThrottleEventCount() { - return readThrottleEvents.get(); - } - - /** - * Get the count of write throttle events. - * @return the current count of write throttle events. - */ - @VisibleForTesting - public long getWriteThrottleEventCount() { - return writeThrottleEvents.get(); - } - - /** - * Get the count of scan throttle events. - * @return the current count of scan throttle events. - */ - @VisibleForTesting - public long getScanThrottleEventCount() { - return scanThrottleEvents.get(); - } - - @VisibleForTesting - public long getBatchWriteCapacityExceededCount() { - return batchWriteCapacityExceededEvents.get(); - } - - /** - * Get the operation invoker for write operations. - * @return an invoker for retrying mutating operations on a store. - */ - public Invoker getInvoker() { - return writeOp; - } - - /** - * Wrap an iterator returned from any scan with a retrying one. - * This includes throttle handling. - * Retries will update the relevant counters/metrics for scan operations. - * @param source source iterator - * @return a retrying iterator. - */ - public Iterable wrapWithRetries( - final Iterable source) { - return new RetryingCollection<>("scan dynamoDB table", scanOp, source); - } - - /** - * Record the number of records written. - * @param count count of records. - */ - private void recordsWritten(final int count) { - instrumentation.recordsWritten(count); - } - - /** - * Record the number of records read. - * @param count count of records. - */ - private void recordsRead(final int count) { - instrumentation.recordsRead(count); - } - /** - * Record the number of records deleted. - * @param count count of records. - */ - private void recordsDeleted(final int count) { - instrumentation.recordsDeleted(count); - } - - /** - * Initiate the rename operation by creating the tracker for the filesystem - * to keep up to date with state changes in the S3A bucket. - * @param storeContext store context. - * @param source source path - * @param sourceStatus status of the source file/dir - * @param dest destination path. - * @return the rename tracker - */ - @Override - public RenameTracker initiateRenameOperation( - final StoreContext storeContext, - final Path source, - final S3AFileStatus sourceStatus, - final Path dest) { - return new ProgressiveRenameTracker(storeContext, this, source, dest, - new AncestorState(this, BulkOperationState.OperationType.Rename, dest)); - } - - /** - * Mark the directories instantiated under the destination path - * as authoritative. That is: all entries in the - * operationState (which must be an AncestorState instance), - * that are under the destination path. - * - * The database update synchronized on the operationState, so all other - * threads trying to update that state will be blocked until completion. - * - * This operation is only used in import and at the end of a rename, - * so this is not considered an issue. - * @param dest destination path. - * @param operationState active state. - * @throws IOException failure. - * @return the number of directories marked. - */ - @Override - public int markAsAuthoritative( - final Path dest, - final BulkOperationState operationState) throws IOException { - if (operationState == null) { - return 0; - } - Preconditions.checkArgument(operationState instanceof AncestorState, - "Not an AncestorState %s", operationState); - final AncestorState state = (AncestorState)operationState; - // only mark paths under the dest as auth - final String simpleDestKey = pathToParentKey(dest); - final String destPathKey = simpleDestKey + "/"; - final String opId = AncestorState.stateAsString(state); - LOG.debug("{}: marking directories under {} as authoritative", - opId, destPathKey); - - // the list of dirs to build up. - final List dirsToUpdate = new ArrayList<>(); - synchronized (state) { - for (Map.Entry entry : - state.getAncestry().entrySet()) { - final Path path = entry.getKey(); - final DDBPathMetadata md = entry.getValue(); - final String key = pathToParentKey(path); - if (md.getFileStatus().isDirectory() - && (key.equals(simpleDestKey) || key.startsWith(destPathKey))) { - // the updated entry is under the destination. - md.setAuthoritativeDir(true); - md.setLastUpdated(ttlTimeProvider.getNow()); - LOG.debug("{}: added {}", opId, key); - dirsToUpdate.add(md); - } - } - processBatchWriteRequest(state, - null, pathMetadataToItem(dirsToUpdate)); - } - return dirsToUpdate.size(); - } - - @Override - public AncestorState initiateBulkWrite( - final BulkOperationState.OperationType operation, - final Path dest) { - return new AncestorState(this, operation, dest); - } - - @Override - public void setTtlTimeProvider(ITtlTimeProvider ttlTimeProvider) { - this.ttlTimeProvider = ttlTimeProvider; - } - - /** - * Username. - * @return the current username - */ - String getUsername() { - return username; - } - - /** - * Log a PUT into the operations log at debug level. - * @param state optional ancestor state. - * @param items items which have been PUT - */ - private static void logPut( - @Nullable AncestorState state, - Item[] items) { - if (OPERATIONS_LOG.isDebugEnabled()) { - // log the operations - String stateStr = AncestorState.stateAsString(state); - for (Item item : items) { - boolean tombstone = !itemExists(item); - boolean isDir = getBoolAttribute(item, IS_DIR, false); - boolean auth = getBoolAttribute(item, IS_AUTHORITATIVE, false); - OPERATIONS_LOG.debug("{} {} {}{}{}", - stateStr, - tombstone ? "TOMBSTONE" : "PUT", - itemPrimaryKeyToString(item), - auth ? " [auth]" : "", - isDir ? " directory" : ""); - } - } - } - - /** - * Log a PUT into the operations log at debug level. - * @param state optional ancestor state. - * @param item item PUT. - */ - private static void logPut( - @Nullable AncestorState state, - Item item) { - if (OPERATIONS_LOG.isDebugEnabled()) { - // log the operations - logPut(state, new Item[]{item}); - } - } - - /** - * Log a DELETE into the operations log at debug level. - * @param state optional ancestor state. - * @param keysDeleted keys which were deleted. - */ - private static void logDelete( - @Nullable AncestorState state, - PrimaryKey[] keysDeleted) { - if (OPERATIONS_LOG.isDebugEnabled()) { - // log the operations - String stateStr = AncestorState.stateAsString(state); - for (PrimaryKey key : keysDeleted) { - OPERATIONS_LOG.debug("{} DELETE {}", - stateStr, primaryKeyToString(key)); - } - } - } - - /** - * Log a DELETE into the operations log at debug level. - * @param state optional ancestor state. - * @param key Deleted key - */ - private static void logDelete( - @Nullable AncestorState state, - PrimaryKey key) { - if (OPERATIONS_LOG.isDebugEnabled()) { - logDelete(state, new PrimaryKey[]{key}); - } - } - - /** - * Get the move state passed in; create a new one if needed. - * @param state state. - * @param operation the type of the operation to use if the state is created. - * @return the cast or created state. - */ - private AncestorState extractOrCreate(@Nullable BulkOperationState state, - BulkOperationState.OperationType operation) { - if (state != null) { - return (AncestorState) state; - } else { - return new AncestorState(this, operation, null); - } - } - - @Override - public MetastoreInstrumentation getInstrumentation() { - return instrumentation; - } - - /** - * This tracks all the ancestors created, - * across multiple move/write operations. - * This is to avoid duplicate creation of ancestors during bulk commits - * and rename operations managed by a rename tracker. - * - * There is no thread safety: callers must synchronize as appropriate. - */ - @VisibleForTesting - static final class AncestorState extends BulkOperationState { - - /** - * Counter of IDs issued. - */ - private static final AtomicLong ID_COUNTER = new AtomicLong(0); - - /** Owning store. */ - private final DynamoDBMetadataStore store; - - /** The ID of the state; for logging. */ - private final long id; - - /** - * Map of ancestors. - */ - private final Map ancestry = new HashMap<>(); - - /** - * Destination path. - */ - private final Path dest; - - /** - * Create the state. - * @param store the store, for use in validation. - * If null: no validation (test only operation) - * @param operation the type of the operation. - * @param dest destination path. - */ - AncestorState( - @Nullable final DynamoDBMetadataStore store, - final OperationType operation, - @Nullable final Path dest) { - super(operation); - this.store = store; - this.dest = dest; - this.id = ID_COUNTER.addAndGet(1); - } - - int size() { - return ancestry.size(); - } - - /** - * Get the ancestry. Not thread safe. - * @return the map of ancestors. - */ - Map getAncestry() { - return ancestry; - } - - public Path getDest() { - return dest; - } - - long getId() { - return id; - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder( - "AncestorState{"); - sb.append("operation=").append(getOperation()); - sb.append("id=").append(id); - sb.append("; dest=").append(dest); - sb.append("; size=").append(size()); - sb.append("; paths={") - .append(StringUtils.join(ancestry.keySet(), " ")) - .append('}'); - sb.append('}'); - return sb.toString(); - } - - /** - * Does the ancestor state contain a path? - * @param p path to check - * @return true if the state has an entry - */ - boolean contains(Path p) { - return get(p) != null; - } - - DDBPathMetadata put(Path p, DDBPathMetadata md) { - return ancestry.put(p, md); - } - - DDBPathMetadata put(DDBPathMetadata md) { - return ancestry.put(md.getFileStatus().getPath(), md); - } - - DDBPathMetadata get(Path p) { - return ancestry.get(p); - } - - /** - * Find an entry in the ancestor state, warning and optionally - * raising an exception if there is a file at the path. - * @param path path to look up - * @param failOnFile fail if a file was found. - * @return true iff a directory was found in the ancestor state. - * @throws PathIOException if there was a file at the path. - */ - boolean findEntry( - final Path path, - final boolean failOnFile) throws PathIOException { - final DDBPathMetadata ancestor = get(path); - if (ancestor != null) { - // there's an entry in the ancestor state - if (!ancestor.getFileStatus().isDirectory()) { - // but: its a file, which means this update is now inconsistent. - final String message = E_INCONSISTENT_UPDATE + " entry is " + ancestor - .getFileStatus(); - LOG.error(message); - if (failOnFile) { - // errors trigger failure - throw new PathIOException(path.toString(), message); - } - } - return true; - } else { - return false; - } - } - - /** - * If debug logging is enabled, this does an audit of the store state. - * it only logs this; the error messages are created so as they could - * be turned into exception messages. - * Audit failures aren't being turned into IOEs is that - * rename operations delete the source entry and that ends up in the - * ancestor state as present - * @throws IOException failure - */ - @Override - public void close() throws IOException { - if (LOG.isDebugEnabled() && store != null) { - LOG.debug("Auditing {}", stateAsString(this)); - for (Map.Entry entry : ancestry - .entrySet()) { - Path path = entry.getKey(); - DDBPathMetadata expected = entry.getValue(); - if (expected.isDeleted()) { - // file was deleted in bulk op; we don't care about it - // any more - continue; - } - DDBPathMetadata actual; - try { - actual = store.get(path); - } catch (IOException e) { - LOG.debug("Retrieving {}", path, e); - // this is for debug; don't be ambitious - return; - } - if (actual == null || actual.isDeleted()) { - String message = "Metastore entry for path " - + path + " deleted during bulk " - + getOperation() + " operation"; - LOG.debug(message); - } else { - if (actual.getFileStatus().isDirectory() != - expected.getFileStatus().isDirectory()) { - // the type of the entry has changed - String message = "Metastore entry for path " - + path + " changed during bulk " - + getOperation() + " operation" - + " from " + expected - + " to " + actual; - LOG.debug(message); - } - } - - } - } - } - - /** - * Create a string from the state including operation and ID. - * @param state state to use -may be null - * @return a string for logging. - */ - private static String stateAsString(@Nullable AncestorState state) { - String stateStr; - if (state != null) { - stateStr = String.format("#(%s-%04d)", - state.getOperation(), - state.getId()); - } else { - stateStr = "#()"; - } - return stateStr; - } - } - - protected DynamoDBMetadataStoreTableManager getTableHandler() { - Preconditions.checkNotNull(tableHandler, "Not initialized"); - return tableHandler; - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DynamoDBMetadataStoreTableManager.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DynamoDBMetadataStoreTableManager.java deleted file mode 100644 index d04ea3ebdc7..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/DynamoDBMetadataStoreTableManager.java +++ /dev/null @@ -1,756 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InterruptedIOException; -import java.nio.file.AccessDeniedException; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.SdkBaseException; -import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; -import com.amazonaws.services.dynamodbv2.document.DynamoDB; -import com.amazonaws.services.dynamodbv2.document.Item; -import com.amazonaws.services.dynamodbv2.document.PrimaryKey; -import com.amazonaws.services.dynamodbv2.document.PutItemOutcome; -import com.amazonaws.services.dynamodbv2.document.Table; -import com.amazonaws.services.dynamodbv2.model.AmazonDynamoDBException; -import com.amazonaws.services.dynamodbv2.model.BillingMode; -import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; -import com.amazonaws.services.dynamodbv2.model.ListTagsOfResourceRequest; -import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; -import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughputDescription; -import com.amazonaws.services.dynamodbv2.model.ResourceInUseException; -import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException; -import com.amazonaws.services.dynamodbv2.model.SSESpecification; -import com.amazonaws.services.dynamodbv2.model.ScanRequest; -import com.amazonaws.services.dynamodbv2.model.ScanResult; -import com.amazonaws.services.dynamodbv2.model.TableDescription; -import com.amazonaws.services.dynamodbv2.model.Tag; -import com.amazonaws.services.dynamodbv2.model.TagResourceRequest; -import com.amazonaws.waiters.WaiterTimedOutException; -import org.apache.hadoop.thirdparty.com.google.common.annotations.VisibleForTesting; -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.s3a.AWSClientIOException; -import org.apache.hadoop.fs.s3a.Invoker; -import org.apache.hadoop.fs.s3a.Retries; -import org.apache.hadoop.io.retry.RetryPolicies; -import org.apache.hadoop.io.retry.RetryPolicy; - -import static java.lang.String.valueOf; - -import static org.apache.commons.lang3.StringUtils.isEmpty; - -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_TABLE_CAPACITY_READ_DEFAULT; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_TABLE_CAPACITY_READ_KEY; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_TABLE_CAPACITY_WRITE_DEFAULT; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_TABLE_CAPACITY_WRITE_KEY; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_TABLE_CREATE_KEY; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_TABLE_SSE_CMK; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_TABLE_SSE_ENABLED; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_TABLE_TAG; -import static org.apache.hadoop.fs.s3a.S3AUtils.lookupPassword; -import static org.apache.hadoop.fs.s3a.S3AUtils.translateDynamoDBException; -import static org.apache.hadoop.fs.s3a.S3AUtils.translateException; -import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore.E_ON_DEMAND_NO_SET_CAPACITY; -import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore.VERSION; -import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore.VERSION_MARKER_ITEM_NAME; -import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore.VERSION_MARKER_TAG_NAME; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.attributeDefinitions; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.createVersionMarker; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.createVersionMarkerPrimaryKey; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.extractCreationTimeFromMarker; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.extractVersionFromMarker; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.keySchema; - -/** - * Managing dynamo tables for S3Guard dynamodb based metadatastore. - * Factored out from DynamoDBMetadataStore. - */ -public class DynamoDBMetadataStoreTableManager { - public static final Logger LOG = LoggerFactory.getLogger( - DynamoDBMetadataStoreTableManager.class); - - /** Error: version marker not found in table but the table is not empty. */ - public static final String E_NO_VERSION_MARKER_AND_NOT_EMPTY - = "S3Guard table lacks version marker, and it is not empty."; - - /** Error: version mismatch. */ - public static final String E_INCOMPATIBLE_TAG_VERSION - = "Database table is from an incompatible S3Guard version based on table TAG."; - - /** Error: version mismatch. */ - public static final String E_INCOMPATIBLE_ITEM_VERSION - = "Database table is from an incompatible S3Guard version based on table ITEM."; - - /** The AWS managed CMK for DynamoDB server side encryption. */ - public static final String SSE_DEFAULT_MASTER_KEY = "alias/aws/dynamodb"; - - /** Invoker for IO. Until configured properly, use try-once. */ - private Invoker invoker = new Invoker(RetryPolicies.TRY_ONCE_THEN_FAIL, - Invoker.NO_OP - ); - - final private AmazonDynamoDB amazonDynamoDB; - final private DynamoDB dynamoDB; - final private String tableName; - final private String region; - final private Configuration conf; - final private Invoker readOp; - final private RetryPolicy batchWriteRetryPolicy; - - private Table table; - private String tableArn; - - public DynamoDBMetadataStoreTableManager(DynamoDB dynamoDB, - String tableName, - String region, - AmazonDynamoDB amazonDynamoDB, - Configuration conf, - Invoker readOp, - RetryPolicy batchWriteCapacityExceededEvents) { - this.dynamoDB = dynamoDB; - this.amazonDynamoDB = amazonDynamoDB; - this.tableName = tableName; - this.region = region; - this.conf = conf; - this.readOp = readOp; - this.batchWriteRetryPolicy = batchWriteCapacityExceededEvents; - } - - /** - * Create a table if it does not exist and wait for it to become active. - * - * If a table with the intended name already exists, then it uses that table. - * Otherwise, it will automatically create the table if the config - * {@link org.apache.hadoop.fs.s3a.Constants#S3GUARD_DDB_TABLE_CREATE_KEY} is - * enabled. The DynamoDB table creation API is asynchronous. This method wait - * for the table to become active after sending the creation request, so - * overall, this method is synchronous, and the table is guaranteed to exist - * after this method returns successfully. - * - * The wait for a table becoming active is Retry+Translated; it can fail - * while a table is not yet ready. - * - * @throws IOException if table does not exist and auto-creation is disabled; - * or table is being deleted, or any other I/O exception occurred. - */ - @VisibleForTesting - @Retries.RetryTranslated - Table initTable() throws IOException { - table = dynamoDB.getTable(tableName); - try { - try { - LOG.debug("Binding to table {}", tableName); - TableDescription description = table.describe(); - LOG.debug("Table state: {}", description); - tableArn = description.getTableArn(); - final String status = description.getTableStatus(); - switch (status) { - case "CREATING": - LOG.debug("Table {} in region {} is being created/updated. This may" - + " indicate that the table is being operated by another " - + "concurrent thread or process. Waiting for active...", - tableName, region); - waitForTableActive(table); - break; - case "DELETING": - throw new FileNotFoundException("DynamoDB table " - + "'" + tableName + "' is being " - + "deleted in region " + region); - case "UPDATING": - // table being updated; it can still be used. - LOG.debug("Table is being updated."); - break; - case "ACTIVE": - break; - default: - throw new IOException("Unknown DynamoDB table status " + status - + ": tableName='" + tableName + "', region=" + region); - } - - verifyVersionCompatibility(); - final Item versionMarker = getVersionMarkerItem(); - Long created = extractCreationTimeFromMarker(versionMarker); - LOG.debug("Using existing DynamoDB table {} in region {} created {}", - tableName, region, (created != null) ? new Date(created) : null); - } catch (ResourceNotFoundException rnfe) { - if (conf.getBoolean(S3GUARD_DDB_TABLE_CREATE_KEY, false)) { - long readCapacity = conf.getLong(S3GUARD_DDB_TABLE_CAPACITY_READ_KEY, - S3GUARD_DDB_TABLE_CAPACITY_READ_DEFAULT); - long writeCapacity = conf.getLong( - S3GUARD_DDB_TABLE_CAPACITY_WRITE_KEY, - S3GUARD_DDB_TABLE_CAPACITY_WRITE_DEFAULT); - ProvisionedThroughput capacity; - if (readCapacity > 0 && writeCapacity > 0) { - capacity = new ProvisionedThroughput( - readCapacity, - writeCapacity); - } else { - // at least one capacity value is <= 0 - // verify they are both exactly zero - Preconditions.checkArgument( - readCapacity == 0 && writeCapacity == 0, - "S3Guard table read capacity %d and and write capacity %d" - + " are inconsistent", readCapacity, writeCapacity); - // and set the capacity to null for per-request billing. - capacity = null; - } - - createTable(capacity); - } else { - throw (FileNotFoundException) new FileNotFoundException( - "DynamoDB table '" + tableName + "' does not " - + "exist in region " + region + - "; auto-creation is turned off") - .initCause(rnfe); - } - } - - } catch (AmazonClientException e) { - throw translateException("initTable", tableName, e); - } - - return table; - } - - protected void tagTableWithVersionMarker() throws AmazonDynamoDBException { - try { - TagResourceRequest tagResourceRequest = new TagResourceRequest() - .withResourceArn(table.getDescription().getTableArn()) - .withTags(newVersionMarkerTag()); - amazonDynamoDB.tagResource(tagResourceRequest); - } catch (AmazonDynamoDBException e) { - LOG.debug("Exception during tagging table: {}", e.getMessage(), e); - } - } - - protected static Item getVersionMarkerFromTags(Table table, - AmazonDynamoDB addb) throws IOException { - List tags = null; - try { - final TableDescription description = table.describe(); - ListTagsOfResourceRequest listTagsOfResourceRequest = - new ListTagsOfResourceRequest() - .withResourceArn(description.getTableArn()); - tags = addb.listTagsOfResource(listTagsOfResourceRequest).getTags(); - } catch (ResourceNotFoundException e) { - LOG.error("Table: {} not found.", table.getTableName()); - throw e; - } catch (AmazonDynamoDBException e) { - LOG.debug("Exception while getting tags from the dynamo table: {}", - e.getMessage(), e); - throw translateDynamoDBException(table.getTableName(), - "Retrieving tags.", e); - } - - if (tags == null) { - return null; - } - - final Optional first = tags.stream() - .filter(tag -> tag.getKey().equals(VERSION_MARKER_TAG_NAME)).findFirst(); - if (first.isPresent()) { - final Tag vmTag = first.get(); - return createVersionMarker( - vmTag.getKey(), Integer.parseInt(vmTag.getValue()), 0 - ); - } else { - return null; - } - } - - /** - * Create a table, wait for it to become active, then add the version - * marker. - * Creating an setting up the table isn't wrapped by any retry operations; - * the wait for a table to become available is RetryTranslated. - * The tags are added to the table during creation, not after creation. - * We can assume that tagging and creating the table is a single atomic - * operation. - * - * @param capacity capacity to provision. If null: create a per-request - * table. - * @throws IOException on any failure. - * @throws InterruptedIOException if the wait was interrupted - */ - @Retries.OnceMixed - private void createTable(ProvisionedThroughput capacity) throws IOException { - try { - String mode; - CreateTableRequest request = new CreateTableRequest() - .withTableName(tableName) - .withKeySchema(keySchema()) - .withAttributeDefinitions(attributeDefinitions()) - .withSSESpecification(getSseSpecFromConfig()) - .withTags(getTableTagsFromConfig()); - if (capacity != null) { - mode = String.format("with provisioned read capacity %d and" - + " write capacity %s", - capacity.getReadCapacityUnits(), capacity.getWriteCapacityUnits()); - request.withProvisionedThroughput(capacity); - } else { - mode = "with pay-per-request billing"; - request.withBillingMode(BillingMode.PAY_PER_REQUEST); - } - LOG.info("Creating non-existent DynamoDB table {} in region {} {}", - tableName, region, mode); - table = dynamoDB.createTable(request); - LOG.debug("Awaiting table becoming active"); - } catch (ResourceInUseException e) { - LOG.warn("ResourceInUseException while creating DynamoDB table {} " - + "in region {}. This may indicate that the table was " - + "created by another concurrent thread or process.", - tableName, region); - } - waitForTableActive(table); - putVersionMarkerItemToTable(); - } - - /** - * Get DynamoDB table server side encryption (SSE) settings from configuration. - */ - private SSESpecification getSseSpecFromConfig() { - final SSESpecification sseSpecification = new SSESpecification(); - boolean enabled = conf.getBoolean(S3GUARD_DDB_TABLE_SSE_ENABLED, false); - if (!enabled) { - // Do not set other options if SSE is disabled. Otherwise it will throw - // ValidationException. - return sseSpecification; - } - sseSpecification.setEnabled(Boolean.TRUE); - String cmk = null; - try { - // Get DynamoDB table SSE CMK from a configuration/credential provider. - cmk = lookupPassword("", conf, S3GUARD_DDB_TABLE_SSE_CMK); - } catch (IOException e) { - LOG.error("Cannot retrieve " + S3GUARD_DDB_TABLE_SSE_CMK, e); - } - if (isEmpty(cmk)) { - // Using Amazon managed default master key for DynamoDB table - return sseSpecification; - } - if (SSE_DEFAULT_MASTER_KEY.equals(cmk)) { - LOG.warn("Ignoring default DynamoDB table KMS Master Key {}", - SSE_DEFAULT_MASTER_KEY); - } else { - sseSpecification.setSSEType("KMS"); - sseSpecification.setKMSMasterKeyId(cmk); - } - return sseSpecification; - } - - /** - * Return tags from configuration and the version marker for adding to - * dynamo table during creation. - */ - @Retries.OnceRaw - public List getTableTagsFromConfig() { - List tags = new ArrayList<>(); - - // from configuration - Map tagProperties = - conf.getPropsWithPrefix(S3GUARD_DDB_TABLE_TAG); - for (Map.Entry tagMapEntry : tagProperties.entrySet()) { - Tag tag = new Tag().withKey(tagMapEntry.getKey()) - .withValue(tagMapEntry.getValue()); - tags.add(tag); - } - // add the version marker - tags.add(newVersionMarkerTag()); - return tags; - } - - /** - * Create a new version marker tag. - * @return a new version marker tag - */ - private static Tag newVersionMarkerTag() { - return new Tag().withKey(VERSION_MARKER_TAG_NAME).withValue(valueOf(VERSION)); - } - - /** - * Verify that a table version is compatible with this S3Guard client. - * - * Checks for consistency between the version marker as the item and tag. - * - *

    -   *   1. If the table lacks both version markers AND it's empty,
    -   *      both markers will be added.
    -   *      If the table is not empty the check throws IOException
    -   *   2. If there's no version marker ITEM, the compatibility with the TAG
    -   *      will be checked, and the version marker ITEM will be added if the
    -   *      TAG version is compatible.
    -   *      If the TAG version is not compatible, the check throws OException
    -   *   3. If there's no version marker TAG, the compatibility with the ITEM
    -   *      version marker will be checked, and the version marker ITEM will be
    -   *      added if the ITEM version is compatible.
    -   *      If the ITEM version is not compatible, the check throws IOException
    -   *   4. If the TAG and ITEM versions are both present then both will be checked
    -   *      for compatibility. If the ITEM or TAG version marker is not compatible,
    -   *      the check throws IOException
    -   * 
    - * - * @throws IOException on any incompatibility - */ - @VisibleForTesting - protected void verifyVersionCompatibility() throws IOException { - final Item versionMarkerItem = getVersionMarkerItem(); - Item versionMarkerFromTag = null; - boolean canReadDdbTags = true; - - try { - versionMarkerFromTag = getVersionMarkerFromTags(table, amazonDynamoDB); - } catch (AccessDeniedException e) { - LOG.debug("Can not read tags of table."); - canReadDdbTags = false; - } - - LOG.debug("versionMarkerItem: {}; versionMarkerFromTag: {}", - versionMarkerItem, versionMarkerFromTag); - - if (versionMarkerItem == null && versionMarkerFromTag == null) { - if (!isEmptyTable(tableName, amazonDynamoDB)) { - LOG.error("Table is not empty but missing the version maker. Failing."); - throw new IOException(E_NO_VERSION_MARKER_AND_NOT_EMPTY - + " Table: " + tableName); - } - - if (canReadDdbTags) { - LOG.info("Table {} contains no version marker item and tag. " + - "The table is empty, so the version marker will be added " + - "as TAG and ITEM.", tableName); - putVersionMarkerItemToTable(); - tagTableWithVersionMarker(); - } - - if (!canReadDdbTags) { - LOG.info("Table {} contains no version marker item and the tags are not readable. " + - "The table is empty, so the ITEM version marker will be added .", tableName); - putVersionMarkerItemToTable(); - } - } - - if (versionMarkerItem == null && versionMarkerFromTag != null) { - final int tagVersionMarker = - extractVersionFromMarker(versionMarkerFromTag); - throwExceptionOnVersionMismatch(tagVersionMarker, tableName, - E_INCOMPATIBLE_TAG_VERSION); - - LOG.info("Table {} contains no version marker ITEM but contains " + - "compatible version marker TAG. Restoring the version marker " + - "item from tag.", tableName); - - putVersionMarkerItemToTable(); - } - - if (versionMarkerItem != null && versionMarkerFromTag == null - && canReadDdbTags) { - final int itemVersionMarker = - extractVersionFromMarker(versionMarkerItem); - throwExceptionOnVersionMismatch(itemVersionMarker, tableName, - E_INCOMPATIBLE_ITEM_VERSION); - - LOG.info("Table {} contains no version marker TAG but contains " + - "compatible version marker ITEM. Restoring the version marker " + - "item from item.", tableName); - - tagTableWithVersionMarker(); - } - - if (versionMarkerItem != null && versionMarkerFromTag != null) { - final int tagVersionMarker = - extractVersionFromMarker(versionMarkerFromTag); - final int itemVersionMarker = - extractVersionFromMarker(versionMarkerItem); - - throwExceptionOnVersionMismatch(tagVersionMarker, tableName, - E_INCOMPATIBLE_TAG_VERSION); - throwExceptionOnVersionMismatch(itemVersionMarker, tableName, - E_INCOMPATIBLE_ITEM_VERSION); - - LOG.debug("Table {} contains correct version marker TAG and ITEM.", - tableName); - } - } - - private static boolean isEmptyTable(String tableName, AmazonDynamoDB aadb) { - final ScanRequest req = new ScanRequest().withTableName( - tableName).withLimit(1); - final ScanResult result = aadb.scan(req); - return result.getCount() == 0; - } - - private static void throwExceptionOnVersionMismatch(int actual, - String tableName, - String exMsg) throws IOException { - - if (VERSION != actual) { - throw new IOException(exMsg + " Table " + tableName - + " Expected version: " + VERSION + " actual tag version: " + - actual); - } - } - - /** - * Add version marker to the dynamo table. - */ - @Retries.OnceRaw - private void putVersionMarkerItemToTable() { - final Item marker = createVersionMarker(VERSION_MARKER_ITEM_NAME, VERSION, - System.currentTimeMillis()); - putItem(marker); - } - - /** - * Wait for table being active. - * @param t table to block on. - * @throws IOException IO problems - * @throws InterruptedIOException if the wait was interrupted - * @throws IllegalArgumentException if an exception was raised in the waiter - */ - @Retries.RetryTranslated - private void waitForTableActive(Table t) throws IOException { - invoker.retry("Waiting for active state of table " + tableName, - null, - true, - () -> { - try { - t.waitForActive(); - } catch (IllegalArgumentException ex) { - throw translateTableWaitFailure(tableName, ex); - } catch (InterruptedException e) { - LOG.warn("Interrupted while waiting for table {} in region {}" - + " active", - tableName, region, e); - Thread.currentThread().interrupt(); - throw (InterruptedIOException) - new InterruptedIOException("DynamoDB table '" - + tableName + "' is not active yet in region " + region) - .initCause(e); - } - }); - } - - /** - * Handle a table wait failure by extracting any inner cause and - * converting it, or, if unconvertable by wrapping - * the IllegalArgumentException in an IOE. - * - * @param name name of the table - * @param e exception - * @return an IOE to raise. - */ - @VisibleForTesting - static IOException translateTableWaitFailure( - final String name, IllegalArgumentException e) { - final SdkBaseException ex = extractInnerException(e); - if (ex != null) { - if (ex instanceof WaiterTimedOutException) { - // a timeout waiting for state change: extract the - // message from the outer exception, but translate - // the inner one for the throttle policy. - return new AWSClientIOException(e.getMessage(), ex); - } else { - return translateException(e.getMessage(), name, ex); - } - } else { - return new IOException(e); - } - } - - /** - * Take an {@code IllegalArgumentException} raised by a DDB operation - * and if it contains an inner SDK exception, unwrap it. - * @param ex exception. - * @return the inner AWS exception or null. - */ - public static SdkBaseException extractInnerException( - IllegalArgumentException ex) { - if (ex.getCause() instanceof SdkBaseException) { - return (SdkBaseException) ex.getCause(); - } else { - return null; - } - } - - /** - * Get the version mark item in the existing DynamoDB table. - * - * As the version marker item may be created by another concurrent thread or - * process, we sleep and retry a limited number times if the lookup returns - * with a null value. - * DDB throttling is always retried. - */ - @VisibleForTesting - @Retries.RetryTranslated - protected Item getVersionMarkerItem() throws IOException { - final PrimaryKey versionMarkerKey = - createVersionMarkerPrimaryKey(VERSION_MARKER_ITEM_NAME); - int retryCount = 0; - // look for a version marker, with usual throttling/failure retries. - Item versionMarker = queryVersionMarker(versionMarkerKey); - while (versionMarker == null) { - // The marker was null. - // Two possibilities - // 1. This isn't a S3Guard table. - // 2. This is a S3Guard table in construction; another thread/process - // is about to write/actively writing the version marker. - // So that state #2 is handled, batchWriteRetryPolicy is used to manage - // retries. - // This will mean that if the cause is actually #1, failure will not - // be immediate. As this will ultimately result in a failure to - // init S3Guard and the S3A FS, this isn't going to be a performance - // bottleneck -simply a slightly slower failure report than would otherwise - // be seen. - // "if your settings are broken, performance is not your main issue" - try { - RetryPolicy.RetryAction action = batchWriteRetryPolicy.shouldRetry(null, - retryCount, 0, true); - if (action.action == RetryPolicy.RetryAction.RetryDecision.FAIL) { - break; - } else { - LOG.warn("No version marker found in the DynamoDB table: {}. " + - "Sleeping {} ms before next retry", tableName, action.delayMillis); - Thread.sleep(action.delayMillis); - } - } catch (Exception e) { - throw new IOException("initTable: Unexpected exception " + e, e); - } - retryCount++; - versionMarker = queryVersionMarker(versionMarkerKey); - } - return versionMarker; - } - - /** - * Issue the query to get the version marker, with throttling for overloaded - * DDB tables. - * @param versionMarkerKey key to look up - * @return the marker - * @throws IOException failure - */ - @Retries.RetryTranslated - private Item queryVersionMarker(final PrimaryKey versionMarkerKey) - throws IOException { - return readOp.retry("getVersionMarkerItem", - VERSION_MARKER_ITEM_NAME, true, - () -> table.getItem(versionMarkerKey)); - } - - /** - * PUT a single item to the table. - * @param item item to put - * @return the outcome. - */ - @Retries.OnceRaw - private PutItemOutcome putItem(Item item) { - LOG.debug("Putting item {}", item); - return table.putItem(item); - } - - /** - * Provision the table with given read and write capacity units. - * Call will fail if the table is busy, or the new values match the current - * ones. - *

    - * Until the AWS SDK lets us switch a table to on-demand, an attempt to - * set the I/O capacity to zero will fail. - * @param readCapacity read units: must be greater than zero - * @param writeCapacity write units: must be greater than zero - * @throws IOException on a failure - */ - @Retries.RetryTranslated - void provisionTable(Long readCapacity, Long writeCapacity) - throws IOException { - - if (readCapacity == 0 || writeCapacity == 0) { - // table is pay on demand - throw new IOException(E_ON_DEMAND_NO_SET_CAPACITY); - } - final ProvisionedThroughput toProvision = new ProvisionedThroughput() - .withReadCapacityUnits(readCapacity) - .withWriteCapacityUnits(writeCapacity); - invoker.retry("ProvisionTable", tableName, true, - () -> { - final ProvisionedThroughputDescription p = - table.updateTable(toProvision).getProvisionedThroughput(); - LOG.info("Provision table {} in region {}: readCapacityUnits={}, " - + "writeCapacityUnits={}", - tableName, region, p.getReadCapacityUnits(), - p.getWriteCapacityUnits()); - }); - } - - @Retries.RetryTranslated - public void destroy() throws IOException { - if (table == null) { - LOG.info("In destroy(): no table to delete"); - return; - } - LOG.info("Deleting DynamoDB table {} in region {}", tableName, region); - Preconditions.checkNotNull(dynamoDB, "Not connected to DynamoDB"); - try { - invoker.retry("delete", null, true, - () -> table.delete()); - table.waitForDelete(); - } catch (IllegalArgumentException ex) { - throw new TableDeleteTimeoutException(tableName, - "Timeout waiting for the table " + getTableArn() - + " to be deleted", ex); - } catch (FileNotFoundException rnfe) { - LOG.info("FileNotFoundException while deleting DynamoDB table {} in " - + "region {}. This may indicate that the table does not exist, " - + "or has been deleted by another concurrent thread or process.", - tableName, region); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - LOG.warn("Interrupted while waiting for DynamoDB table {} being deleted", - tableName, ie); - throw new InterruptedIOException("Table " + tableName - + " in region " + region + " has not been deleted"); - } - } - - @Retries.RetryTranslated - @VisibleForTesting - void provisionTableBlocking(Long readCapacity, Long writeCapacity) - throws IOException { - provisionTable(readCapacity, writeCapacity); - waitForTableActive(table); - } - - public Table getTable() { - return table; - } - - public String getTableArn() { - return tableArn; - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/ExpirableMetadata.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/ExpirableMetadata.java deleted file mode 100644 index 5776bd4d345..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/ExpirableMetadata.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -/** - * Expirable Metadata abstract class is for storing the field needed for - * metadata classes in S3Guard that could be expired with TTL. - */ -public abstract class ExpirableMetadata { - private long lastUpdated = 0; - - public long getLastUpdated() { - return lastUpdated; - } - - public void setLastUpdated(long lastUpdated) { - this.lastUpdated = lastUpdated; - } - - public boolean isExpired(long ttl, long now) { - return (lastUpdated + ttl) <= now; - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/ITtlTimeProvider.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/ITtlTimeProvider.java deleted file mode 100644 index aa7fc4721b4..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/ITtlTimeProvider.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -/** - * This interface is defined for handling TTL expiry of metadata in S3Guard. - * - * TTL can be tested by implementing this interface and setting is as - * {@code S3Guard.ttlTimeProvider}. By doing this, getNow() can return any - * value preferred and flaky tests could be avoided. By default getNow() - * returns the EPOCH in runtime. - * - * Time is measured in milliseconds, - */ -public interface ITtlTimeProvider { - - /** - * The current time in milliseconds. - * Assuming this calls System.currentTimeMillis(), this is a native iO call - * and so should be invoked sparingly (i.e. evaluate before any loop, rather - * than inside). - * @return the current time. - */ - long getNow(); - - /** - * The TTL of the metadata. - * @return time in millis after which metadata is considered out of date. - */ - long getMetadataTtl(); -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/ImportOperation.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/ImportOperation.java deleted file mode 100644 index 9c8b7022286..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/ImportOperation.java +++ /dev/null @@ -1,272 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import javax.annotation.Nullable; -import java.io.IOException; -import java.util.HashSet; -import java.util.Set; - -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.RemoteIterator; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.apache.hadoop.fs.s3a.S3ALocatedFileStatus; -import org.apache.hadoop.fs.s3a.impl.ExecutingStoreOperation; -import org.apache.hadoop.util.DurationInfo; - -/** - * Import a directory tree into the metastore. - * This code was moved from S3GuardTool and enhanced to mark - * the destination tree as authoritative. - */ -class ImportOperation extends ExecutingStoreOperation { - - private static final Logger LOG = LoggerFactory.getLogger( - ImportOperation.class); - - /** - * Source file system: must not be guarded. - */ - private final S3AFileSystem filesystem; - - /** - * Destination metadata store. - */ - private final MetadataStore store; - - /** - * Source entry: File or directory. - */ - private final S3AFileStatus status; - - /** - * If importing the directory tree -should it be marked - * authoritative afterwards? - */ - private final boolean authoritative; - - private final boolean verbose; - - /** - * For DDB the BulkOperation tracking eliminates the need for this cache, - * but it is retained here for the local store and to allow for - * ease of moving to operations which may update the store in parallel with - * writing. - */ - private final Set dirCache = new HashSet<>(); - - /** - * Import. - * @param filesystem Unguarded FS to scan. - * @param store store to update - * @param status source status - * @param authoritative should the imported tree be marked as authoritative - * @param verbose Verbose output - */ - ImportOperation(final S3AFileSystem filesystem, - final MetadataStore store, - final S3AFileStatus status, - final boolean authoritative, - final boolean verbose) { - super(filesystem.createStoreContext()); - this.verbose = verbose; - Preconditions.checkState(!filesystem.hasMetadataStore(), - "Source filesystem for import has a metadata store"); - this.filesystem = filesystem; - this.store = store; - this.status = status; - this.authoritative = authoritative; - } - - private S3AFileSystem getFilesystem() { - return filesystem; - } - - private MetadataStore getStore() { - return store; - } - - private FileStatus getStatus() { - return status; - } - - @Override - public Long execute() throws IOException { - final long items; - if (status.isFile()) { - PathMetadata meta = new PathMetadata(status); - getStore().put(meta, null); - items = 1; - } else { - try (DurationInfo ignored = - new DurationInfo(LOG, "Importing %s", getStatus().getPath())) { - items = importDir(); - } - } - return items; - } - - /** - * Recursively import every path under path. - * @return number of items inserted into MetadataStore - * @throws IOException on I/O errors. - */ - private long importDir() throws IOException { - Preconditions.checkArgument(status.isDirectory()); - long totalCountOfEntriesWritten = 0; - final Path basePath = status.getPath(); - final MetadataStore ms = getStore(); - LOG.info("Importing directory {}", basePath); - try (BulkOperationState operationState = ms - .initiateBulkWrite( - BulkOperationState.OperationType.Import, - basePath)) { - long countOfFilesWritten = 0; - long countOfDirsWritten = 0; - RemoteIterator it = getFilesystem() - .listFilesAndEmptyDirectoriesForceNonAuth(basePath, true); - while (it.hasNext()) { - S3ALocatedFileStatus located = it.next(); - S3AFileStatus child; - final Path path = located.getPath(); - final boolean isDirectory = located.isDirectory(); - if (isDirectory) { - child = DynamoDBMetadataStore.makeDirStatus(path, - located.getOwner()); - dirCache.add(path); - // and update the dir count - countOfDirsWritten++; - } else { - child = located.toS3AFileStatus(); - } - - int parentsWritten = putParentsIfNotPresent(child, operationState); - LOG.debug("Wrote {} parent entries", parentsWritten); - - // We don't blindly overwrite any existing file entry in S3Guard with a - // new one, Because that may lose the version information. - // instead we merge them - if (!isDirectory) { - final PathMetadata existingEntry = S3Guard.getWithTtl(ms, path, null, - false, true); - if (existingEntry != null) { - final S3AFileStatus existingStatus = existingEntry.getFileStatus(); - if (existingStatus.isFile()) { - // source is also a file. - // we only worry about an update if the timestamp is different, - final String existingEtag = existingStatus.getETag(); - final String childEtag = child.getETag(); - if (child.getModificationTime() - != existingStatus.getModificationTime() - || existingStatus.getLen() != child.getLen() - || existingEtag == null - || !existingEtag.equals(childEtag)) { - // files are potentially different, though a modtime change - // can just be a clock skew problem - // so if the etag is unchanged, we propagate any versionID - if (childEtag.equals(existingEtag)) { - // copy over any version ID. - child.setVersionId(existingStatus.getVersionId()); - } - } else { - // the entry modtimes match - child = null; - } - } - } - if (child != null) { - countOfFilesWritten++; - } - } - if (child != null) { - // there's an entry to add. - - // log entry spaced to same width - String t = isDirectory ? "Dir " : "File"; - if (verbose) { - LOG.info("{} {}", t, path); - } else { - LOG.debug("{} {}", t, path); - } - S3Guard.putWithTtl( - ms, - new PathMetadata(child), - getFilesystem().getTtlTimeProvider(), - operationState); - totalCountOfEntriesWritten++; - } - } - LOG.info("Updated S3Guard with {} files and {} directory entries", - countOfFilesWritten, countOfDirsWritten); - - // here all entries are imported. - // tell the store that everything should be marked as auth - if (authoritative) { - LOG.info("Marking directory tree {} as authoritative", - basePath); - ms.markAsAuthoritative(basePath, operationState); - } - } - return totalCountOfEntriesWritten; - } - - /** - * Put parents into metastore and cache if the parents are not present. - * - * There's duplication here with S3Guard DDB ancestor state, but this - * is designed to work across implementations. - * @param fileStatus the file or an empty directory. - * @param operationState store's bulk update state. - * @return number of entries written. - * @throws IOException on I/O errors. - */ - private int putParentsIfNotPresent(FileStatus fileStatus, - @Nullable BulkOperationState operationState) throws IOException { - Preconditions.checkNotNull(fileStatus); - Path parent = fileStatus.getPath().getParent(); - int count = 0; - while (parent != null) { - if (dirCache.contains(parent)) { - return count; - } - final ITtlTimeProvider timeProvider - = getFilesystem().getTtlTimeProvider(); - final PathMetadata pmd = S3Guard.getWithTtl(getStore(), parent, - timeProvider, false, true); - if (pmd == null || pmd.isDeleted()) { - S3AFileStatus dir = DynamoDBMetadataStore.makeDirStatus(parent, - fileStatus.getOwner()); - S3Guard.putWithTtl(getStore(), new PathMetadata(dir), - timeProvider, - operationState); - count++; - } - dirCache.add(parent); - parent = parent.getParent(); - } - return count; - } - -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/LocalMetadataEntry.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/LocalMetadataEntry.java deleted file mode 100644 index 54050749758..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/LocalMetadataEntry.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import javax.annotation.Nullable; - -/** - * LocalMetadataEntry is used to store entries in the cache of - * LocalMetadataStore. PathMetadata or dirListingMetadata can be null. The - * entry is not immutable. - */ -public final class LocalMetadataEntry { - @Nullable - private PathMetadata pathMetadata; - @Nullable - private DirListingMetadata dirListingMetadata; - - LocalMetadataEntry() { - } - - LocalMetadataEntry(PathMetadata pmd){ - pathMetadata = pmd; - dirListingMetadata = null; - } - - LocalMetadataEntry(DirListingMetadata dlm){ - pathMetadata = null; - dirListingMetadata = dlm; - } - - public PathMetadata getFileMeta() { - return pathMetadata; - } - - public DirListingMetadata getDirListingMeta() { - return dirListingMetadata; - } - - - public boolean hasPathMeta() { - return this.pathMetadata != null; - } - - public boolean hasDirMeta() { - return this.dirListingMetadata != null; - } - - public void setPathMetadata(PathMetadata pathMetadata) { - this.pathMetadata = pathMetadata; - } - - public void setDirListingMetadata(DirListingMetadata dirListingMetadata) { - this.dirListingMetadata = dirListingMetadata; - } - - @Override public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("LocalMetadataEntry{"); - if(pathMetadata != null) { - sb.append("pathMetadata=" + pathMetadata.getFileStatus().getPath()); - } - if(dirListingMetadata != null){ - sb.append("; dirListingMetadata=" + dirListingMetadata.getPath()); - } - sb.append("}"); - return sb.toString(); - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/LocalMetadataStore.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/LocalMetadataStore.java deleted file mode 100644 index 9d0ba9171ab..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/LocalMetadataStore.java +++ /dev/null @@ -1,651 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import javax.annotation.Nullable; - -import org.apache.hadoop.thirdparty.com.google.common.annotations.VisibleForTesting; -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; - -import org.apache.hadoop.thirdparty.com.google.common.cache.Cache; -import org.apache.hadoop.thirdparty.com.google.common.cache.CacheBuilder; -import org.apache.commons.lang3.StringUtils; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.Tristate; -import org.apache.hadoop.fs.s3a.impl.StoreContext; -import org.apache.hadoop.security.UserGroupInformation; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.net.URI; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; - -import static org.apache.hadoop.fs.s3a.Constants.*; - -/** - * This is a local, in-memory implementation of MetadataStore. - * This is not a coherent cache across processes. It is only - * locally-coherent. - * - * The purpose of this is for unit and integration testing. - * It could also be used to accelerate local-only operations where only one - * process is operating on a given object store, or multiple processes are - * accessing a read-only storage bucket. - * - * This MetadataStore does not enforce filesystem rules such as disallowing - * non-recursive removal of non-empty directories. It is assumed the caller - * already has to perform these sorts of checks. - * - * Contains one cache internally with time based eviction. - */ -public class LocalMetadataStore implements MetadataStore { - - public static final Logger LOG = LoggerFactory.getLogger(MetadataStore.class); - - /** Contains directory and file listings. */ - private Cache localCache; - - private FileSystem fs; - /* Null iff this FS does not have an associated URI host. */ - private String uriHost; - - private String username; - - private ITtlTimeProvider ttlTimeProvider; - - @Override - public void initialize(FileSystem fileSystem, - ITtlTimeProvider ttlTp) throws IOException { - Preconditions.checkNotNull(fileSystem); - fs = fileSystem; - URI fsURI = fs.getUri(); - uriHost = fsURI.getHost(); - if (uriHost != null && uriHost.equals("")) { - uriHost = null; - } - - initialize(fs.getConf(), ttlTp); - } - - @Override - public void initialize(Configuration conf, ITtlTimeProvider ttlTp) - throws IOException { - Preconditions.checkNotNull(conf); - int maxRecords = conf.getInt(S3GUARD_METASTORE_LOCAL_MAX_RECORDS, - DEFAULT_S3GUARD_METASTORE_LOCAL_MAX_RECORDS); - if (maxRecords < 4) { - maxRecords = 4; - } - int ttl = conf.getInt(S3GUARD_METASTORE_LOCAL_ENTRY_TTL, - DEFAULT_S3GUARD_METASTORE_LOCAL_ENTRY_TTL); - - CacheBuilder builder = CacheBuilder.newBuilder().maximumSize(maxRecords); - if (ttl >= 0) { - builder.expireAfterAccess(ttl, TimeUnit.MILLISECONDS); - } - - localCache = builder.build(); - username = UserGroupInformation.getCurrentUser().getShortUserName(); - this.ttlTimeProvider = ttlTp; - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder( - "LocalMetadataStore{"); - sb.append("uriHost='").append(uriHost).append('\''); - sb.append('}'); - return sb.toString(); - } - - @Override - public void delete(Path p, - final BulkOperationState operationState) - throws IOException { - doDelete(p, false, true); - } - - @Override - public void forgetMetadata(Path p) throws IOException { - doDelete(p, false, false); - } - - @Override - public void deleteSubtree(Path path, - final BulkOperationState operationState) - throws IOException { - doDelete(path, true, true); - } - - private synchronized void doDelete(Path p, boolean recursive, - boolean tombstone) { - - Path path = standardize(p); - - // Delete entry from file cache, then from cached parent directory, if any - deleteCacheEntries(path, tombstone); - - if (recursive) { - // Remove all entries that have this dir as path prefix. - deleteEntryByAncestor(path, localCache, tombstone, ttlTimeProvider); - } - } - - @Override - public void deletePaths(final Collection paths, - @Nullable final BulkOperationState operationState) throws IOException { - for (Path path : paths) { - doDelete(path, false, true); - } - } - - @Override - public synchronized PathMetadata get(Path p) throws IOException { - return get(p, false); - } - - @Override - public PathMetadata get(Path p, boolean wantEmptyDirectoryFlag) - throws IOException { - Path path = standardize(p); - synchronized (this) { - PathMetadata m = getFileMeta(path); - - if (wantEmptyDirectoryFlag && m != null && - m.getFileStatus().isDirectory()) { - m.setIsEmptyDirectory(isEmptyDirectory(p)); - } - - if (LOG.isDebugEnabled()) { - LOG.debug("get({}) -> {}", path, m == null ? "null" : m.prettyPrint()); - } - return m; - } - } - - /** - * Determine if directory is empty. - * Call with lock held. - * @param p a Path, already filtered through standardize() - * @return TRUE / FALSE if known empty / not-empty, UNKNOWN otherwise. - */ - private Tristate isEmptyDirectory(Path p) { - DirListingMetadata dlm = getDirListingMeta(p); - return dlm.withoutTombstones().isEmpty(); - } - - @Override - public synchronized DirListingMetadata listChildren(Path p) throws - IOException { - Path path = standardize(p); - DirListingMetadata listing = getDirListingMeta(path); - if (LOG.isDebugEnabled()) { - LOG.debug("listChildren({}) -> {}", path, - listing == null ? "null" : listing.prettyPrint()); - } - - if (listing != null) { - // Make a copy so callers can mutate without affecting our state - return new DirListingMetadata(listing); - } - return null; - } - - @Override - public void move(@Nullable Collection pathsToDelete, - @Nullable Collection pathsToCreate, - @Nullable final BulkOperationState operationState) throws IOException { - LOG.info("Move {} to {}", pathsToDelete, pathsToCreate); - - if (pathsToCreate == null) { - pathsToCreate = Collections.emptyList(); - } - if (pathsToDelete == null) { - pathsToDelete = Collections.emptyList(); - } - - // I feel dirty for using reentrant lock. :-| - synchronized (this) { - - // 1. Delete pathsToDelete - for (Path meta : pathsToDelete) { - LOG.debug("move: deleting metadata {}", meta); - delete(meta, null); - } - - // 2. Create new destination path metadata - for (PathMetadata meta : pathsToCreate) { - LOG.debug("move: adding metadata {}", meta); - put(meta, null); - } - - // 3. We now know full contents of all dirs in destination subtree - for (PathMetadata meta : pathsToCreate) { - FileStatus status = meta.getFileStatus(); - if (status == null || status.isDirectory()) { - continue; - } - DirListingMetadata dir = listChildren(status.getPath()); - if (dir != null) { // could be evicted already - dir.setAuthoritative(true); - } - } - } - } - - @Override - public void put(final PathMetadata meta) throws IOException { - put(meta, null); - } - - @Override - public void put(PathMetadata meta, - final BulkOperationState operationState) throws IOException { - - Preconditions.checkNotNull(meta); - S3AFileStatus status = meta.getFileStatus(); - Path path = standardize(status.getPath()); - synchronized (this) { - - /* Add entry for this file. */ - if (LOG.isDebugEnabled()) { - LOG.debug("put {} -> {}", path, meta.prettyPrint()); - } - LocalMetadataEntry entry = localCache.getIfPresent(path); - if(entry == null){ - entry = new LocalMetadataEntry(meta); - } else { - entry.setPathMetadata(meta); - } - - /* Directory case: - * We also make sure we have an entry in the dirCache, so subsequent - * listStatus(path) at least see the directory. - * - * If we had a boolean flag argument "isNew", we would know whether this - * is an existing directory the client discovered via getFileStatus(), - * or if it is a newly-created directory. In the latter case, we would - * be able to mark the directory as authoritative (fully-cached), - * saving round trips to underlying store for subsequent listStatus() - */ - - // only create DirListingMetadata if the entry does not have one - if (status.isDirectory() && !entry.hasDirMeta()) { - DirListingMetadata dlm = - new DirListingMetadata(path, DirListingMetadata.EMPTY_DIR, false); - entry.setDirListingMetadata(dlm); - } - localCache.put(path, entry); - - /* Update cached parent dir. */ - Path parentPath = path.getParent(); - if (parentPath != null) { - LocalMetadataEntry parentMeta = localCache.getIfPresent(parentPath); - - // Create empty parent LocalMetadataEntry if it doesn't exist - if (parentMeta == null){ - parentMeta = new LocalMetadataEntry(); - localCache.put(parentPath, parentMeta); - } - - // If there is no directory metadata on the parent entry, create - // an empty one - if (!parentMeta.hasDirMeta()) { - DirListingMetadata parentDirMeta = - new DirListingMetadata(parentPath, DirListingMetadata.EMPTY_DIR, - false); - parentDirMeta.setLastUpdated(meta.getLastUpdated()); - parentMeta.setDirListingMetadata(parentDirMeta); - } - - // Add the child pathMetadata to the listing - parentMeta.getDirListingMeta().put(meta); - - // Mark the listing entry as deleted if the meta is set to deleted - if(meta.isDeleted()) { - parentMeta.getDirListingMeta().markDeleted(path, - ttlTimeProvider.getNow()); - } - } - } - } - - @Override - public synchronized void put(DirListingMetadata meta, - final List unchangedEntries, - final BulkOperationState operationState) throws IOException { - if (LOG.isDebugEnabled()) { - LOG.debug("put dirMeta {}", meta.prettyPrint()); - } - LocalMetadataEntry entry = - localCache.getIfPresent(standardize(meta.getPath())); - if (entry == null) { - localCache.put(standardize(meta.getPath()), new LocalMetadataEntry(meta)); - } else { - entry.setDirListingMetadata(meta); - } - put(meta.getListing(), null); - } - - public synchronized void put(Collection metas, - final BulkOperationState operationState) throws - IOException { - for (PathMetadata meta : metas) { - put(meta, operationState); - } - } - - @Override - public void close() throws IOException { - - } - - @Override - public void destroy() throws IOException { - if (localCache != null) { - localCache.invalidateAll(); - } - } - - @Override - public void prune(PruneMode pruneMode, long cutoff) throws IOException{ - prune(pruneMode, cutoff, ""); - } - - @Override - public synchronized long prune(PruneMode pruneMode, long cutoff, - String keyPrefix) { - // prune files - AtomicLong count = new AtomicLong(); - // filter path_metadata (files), filter expired, remove expired - localCache.asMap().entrySet().stream() - .filter(entry -> entry.getValue().hasPathMeta()) - .filter(entry -> expired(pruneMode, - entry.getValue().getFileMeta(), cutoff, keyPrefix)) - .forEach(entry -> { - localCache.invalidate(entry.getKey()); - count.incrementAndGet(); - }); - - - // prune dirs - // filter DIR_LISTING_METADATA, remove expired, remove authoritative bit - localCache.asMap().entrySet().stream() - .filter(entry -> entry.getValue().hasDirMeta()) - .forEach(entry -> { - Path path = entry.getKey(); - DirListingMetadata metadata = entry.getValue().getDirListingMeta(); - Collection oldChildren = metadata.getListing(); - Collection newChildren = new LinkedList<>(); - - for (PathMetadata child : oldChildren) { - if (!expired(pruneMode, child, cutoff, keyPrefix)) { - newChildren.add(child); - } else { - count.incrementAndGet(); - } - } - removeAuthoritativeFromParent(path, oldChildren, newChildren); - }); - return count.get(); - } - - private void removeAuthoritativeFromParent(Path path, - Collection oldChildren, - Collection newChildren) { - if (newChildren.size() != oldChildren.size()) { - DirListingMetadata dlm = - new DirListingMetadata(path, newChildren, false); - localCache.put(path, new LocalMetadataEntry(dlm)); - if (!path.isRoot()) { - DirListingMetadata parent = getDirListingMeta(path.getParent()); - if (parent != null) { - parent.setAuthoritative(false); - } - } - } - } - - private boolean expired(PruneMode pruneMode, PathMetadata metadata, - long cutoff, String keyPrefix) { - final S3AFileStatus status = metadata.getFileStatus(); - final URI statusUri = status.getPath().toUri(); - - // remove the protocol from path string to be able to compare - String bucket = statusUri.getHost(); - String statusTranslatedPath = ""; - if(bucket != null && !bucket.isEmpty()){ - // if there's a bucket, (well defined host in Uri) the pathToParentKey - // can be used to get the path from the status - statusTranslatedPath = - PathMetadataDynamoDBTranslation.pathToParentKey(status.getPath()); - } else { - // if there's no bucket in the path the pathToParentKey will fail, so - // this is the fallback to get the path from status - statusTranslatedPath = statusUri.getPath(); - } - - boolean expired; - switch (pruneMode) { - case ALL_BY_MODTIME: - // Note: S3 doesn't track modification time on directories, so for - // consistency with the DynamoDB implementation we ignore that here - expired = status.getModificationTime() < cutoff && !status.isDirectory() - && statusTranslatedPath.startsWith(keyPrefix); - break; - case TOMBSTONES_BY_LASTUPDATED: - expired = metadata.getLastUpdated() < cutoff && metadata.isDeleted() - && statusTranslatedPath.startsWith(keyPrefix); - break; - default: - throw new UnsupportedOperationException("Unsupported prune mode: " - + pruneMode); - } - - return expired; - } - - @VisibleForTesting - static void deleteEntryByAncestor(Path ancestor, - Cache cache, boolean tombstone, - ITtlTimeProvider ttlTimeProvider) { - - cache.asMap().entrySet().stream() - .filter(entry -> isAncestorOf(ancestor, entry.getKey())) - .forEach(entry -> { - LocalMetadataEntry meta = entry.getValue(); - Path path = entry.getKey(); - if(meta.hasDirMeta()){ - cache.invalidate(path); - } else if(tombstone && meta.hasPathMeta()){ - final PathMetadata pmTombstone = PathMetadata.tombstone(path, - ttlTimeProvider.getNow()); - meta.setPathMetadata(pmTombstone); - } else { - cache.invalidate(path); - } - }); - } - - /** - * @return true if 'ancestor' is ancestor dir in path 'f'. - * All paths here are absolute. Dir does not count as its own ancestor. - */ - private static boolean isAncestorOf(Path ancestor, Path f) { - String aStr = ancestor.toString(); - if (!ancestor.isRoot()) { - aStr += "/"; - } - String fStr = f.toString(); - return (fStr.startsWith(aStr)); - } - - /** - * Update fileCache and dirCache to reflect deletion of file 'f'. Call with - * lock held. - */ - private void deleteCacheEntries(Path path, boolean tombstone) { - LocalMetadataEntry entry = localCache.getIfPresent(path); - // If there's no entry, delete should silently succeed - // (based on MetadataStoreTestBase#testDeleteNonExisting) - if(entry == null){ - LOG.warn("Delete: path {} is missing from cache.", path); - return; - } - - // Remove target file entry - LOG.debug("delete file entry for {}", path); - if(entry.hasPathMeta()){ - if (tombstone) { - PathMetadata pmd = PathMetadata.tombstone(path, - ttlTimeProvider.getNow()); - entry.setPathMetadata(pmd); - } else { - entry.setPathMetadata(null); - } - } - - // If this path is a dir, remove its listing - if(entry.hasDirMeta()) { - LOG.debug("removing listing of {}", path); - entry.setDirListingMetadata(null); - } - - // If the entry is empty (contains no dirMeta or pathMeta) remove it from - // the cache. - if(!entry.hasDirMeta() && !entry.hasPathMeta()){ - localCache.invalidate(entry); - } - - /* Remove this path from parent's dir listing */ - Path parent = path.getParent(); - if (parent != null) { - DirListingMetadata dir = getDirListingMeta(parent); - if (dir != null) { - LOG.debug("removing parent's entry for {} ", path); - if (tombstone) { - dir.markDeleted(path, ttlTimeProvider.getNow()); - } else { - dir.remove(path); - } - } - } - } - - /** - * Return a "standardized" version of a path so we always have a consistent - * hash value. Also asserts the path is absolute, and contains host - * component. - * @param p input Path - * @return standardized version of Path, suitable for hash key - */ - private Path standardize(Path p) { - Preconditions.checkArgument(p.isAbsolute(), "Path must be absolute"); - URI uri = p.toUri(); - if (uriHost != null) { - Preconditions.checkArgument(StringUtils.isNotEmpty(uri.getHost())); - } - return p; - } - - @Override - public Map getDiagnostics() throws IOException { - Map map = new HashMap<>(); - map.put("name", "local://metadata"); - map.put("uriHost", uriHost); - map.put("description", "Local in-VM metadata store for testing"); - map.put(MetadataStoreCapabilities.PERSISTS_AUTHORITATIVE_BIT, - Boolean.toString(true)); - return map; - } - - @Override - public void updateParameters(Map parameters) - throws IOException { - } - - PathMetadata getFileMeta(Path p){ - LocalMetadataEntry entry = localCache.getIfPresent(p); - if(entry != null && entry.hasPathMeta()){ - return entry.getFileMeta(); - } else { - return null; - } - } - - DirListingMetadata getDirListingMeta(Path p){ - LocalMetadataEntry entry = localCache.getIfPresent(p); - if(entry != null && entry.hasDirMeta()){ - return entry.getDirListingMeta(); - } else { - return null; - } - } - - @Override - public RenameTracker initiateRenameOperation(final StoreContext storeContext, - final Path source, - final S3AFileStatus sourceStatus, final Path dest) throws IOException { - return new ProgressiveRenameTracker(storeContext, this, source, dest, - null); - } - - @Override - public synchronized void setTtlTimeProvider(ITtlTimeProvider ttlTimeProvider) { - this.ttlTimeProvider = ttlTimeProvider; - } - - @Override - public synchronized void addAncestors(final Path qualifiedPath, - @Nullable final BulkOperationState operationState) throws IOException { - - Collection newDirs = new ArrayList<>(); - Path parent = qualifiedPath.getParent(); - while (!parent.isRoot()) { - PathMetadata directory = get(parent); - if (directory == null || directory.isDeleted()) { - S3AFileStatus status = new S3AFileStatus(Tristate.FALSE, parent, - username); - PathMetadata meta = new PathMetadata(status, Tristate.FALSE, false, - ttlTimeProvider.getNow()); - newDirs.add(meta); - } else { - break; - } - parent = parent.getParent(); - } - if (!newDirs.isEmpty()) { - put(newDirs, operationState); - } - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/MetadataStore.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/MetadataStore.java deleted file mode 100644 index 94d80635371..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/MetadataStore.java +++ /dev/null @@ -1,438 +0,0 @@ -/** - * 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.hadoop.fs.s3a.s3guard; - -import javax.annotation.Nullable; -import java.io.Closeable; -import java.io.IOException; -import java.util.Collection; -import java.util.List; -import java.util.Map; - -import org.apache.hadoop.thirdparty.com.google.common.annotations.VisibleForTesting; -import org.apache.hadoop.classification.InterfaceAudience; -import org.apache.hadoop.classification.InterfaceStability; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.Retries; -import org.apache.hadoop.fs.s3a.Retries.RetryTranslated; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.impl.StoreContext; - -/** - * {@code MetadataStore} defines the set of operations that any metadata store - * implementation must provide. Note that all {@link Path} objects provided - * to methods must be absolute, not relative paths. - * Implementations must implement any retries needed internally, such that - * transient errors are generally recovered from without throwing exceptions - * from this API. - */ -@InterfaceAudience.Private -@InterfaceStability.Evolving -public interface MetadataStore extends Closeable { - - /** - * Performs one-time initialization of the metadata store. - * - * @param fs {@code FileSystem} associated with the MetadataStore - * @param ttlTimeProvider the time provider to use for metadata expiry - * @throws IOException if there is an error - */ - void initialize(FileSystem fs, ITtlTimeProvider ttlTimeProvider) - throws IOException; - - /** - * Performs one-time initialization of the metadata store via configuration. - * @see #initialize(FileSystem, ITtlTimeProvider) - * @param conf Configuration. - * @param ttlTimeProvider the time provider to use for metadata expiry - * @throws IOException if there is an error - */ - void initialize(Configuration conf, - ITtlTimeProvider ttlTimeProvider) throws IOException; - - /** - * Deletes exactly one path, leaving a tombstone to prevent lingering, - * inconsistent copies of it from being listed. - * - * Deleting an entry with a tombstone needs a - * {@link org.apache.hadoop.fs.s3a.s3guard.S3Guard.TtlTimeProvider} because - * the lastUpdated field of the record has to be updated to

    now
    . - * - * @param path the path to delete - * @param operationState (nullable) operational state for a bulk update - * @throws IOException if there is an error - */ - void delete(Path path, - @Nullable BulkOperationState operationState) - throws IOException; - - /** - * Removes the record of exactly one path. Does not leave a tombstone (see - * {@link MetadataStore#delete(Path, BulkOperationState)}. It is currently - * intended for testing only, and a need to use it as part of normal - * FileSystem usage is not anticipated. - * - * @param path the path to delete - * @throws IOException if there is an error - */ - @VisibleForTesting - void forgetMetadata(Path path) throws IOException; - - /** - * Deletes the entire sub-tree rooted at the given path, leaving tombstones - * to prevent lingering, inconsistent copies of it from being listed. - * - * In addition to affecting future calls to {@link #get(Path)}, - * implementations must also update any stored {@code DirListingMetadata} - * objects which track the parent of this file. - * - * Deleting a subtree with a tombstone needs a - * {@link org.apache.hadoop.fs.s3a.s3guard.S3Guard.TtlTimeProvider} because - * the lastUpdated field of all records have to be updated to
    now
    . - * - * @param path the root of the sub-tree to delete - * @param operationState (nullable) operational state for a bulk update - * @throws IOException if there is an error - */ - @Retries.RetryTranslated - void deleteSubtree(Path path, - @Nullable BulkOperationState operationState) - throws IOException; - - /** - * Delete the paths. - * There's no attempt to order the paths: they are - * deleted in the order passed in. - * @param paths paths to delete. - * @param operationState Nullable operation state - * @throws IOException failure - */ - - @RetryTranslated - void deletePaths(Collection paths, - @Nullable BulkOperationState operationState) - throws IOException; - - /** - * Gets metadata for a path. - * - * @param path the path to get - * @return metadata for {@code path}, {@code null} if not found - * @throws IOException if there is an error - */ - PathMetadata get(Path path) throws IOException; - - /** - * Gets metadata for a path. Alternate method that includes a hint - * whether or not the MetadataStore should do work to compute the value for - * {@link PathMetadata#isEmptyDirectory()}. Since determining emptiness - * may be an expensive operation, this can save wasted work. - * - * @param path the path to get - * @param wantEmptyDirectoryFlag Set to true to give a hint to the - * MetadataStore that it should try to compute the empty directory flag. - * @return metadata for {@code path}, {@code null} if not found - * @throws IOException if there is an error - */ - PathMetadata get(Path path, boolean wantEmptyDirectoryFlag) - throws IOException; - - /** - * Lists metadata for all direct children of a path. - * - * @param path the path to list - * @return metadata for all direct children of {@code path} which are being - * tracked by the MetadataStore, or {@code null} if the path was not found - * in the MetadataStore. - * @throws IOException if there is an error - */ - @Retries.RetryTranslated - DirListingMetadata listChildren(Path path) throws IOException; - - /** - * This adds all new ancestors of a path as directories. - *

    - * Important: to propagate TTL information, any new ancestors added - * must have their last updated timestamps set through - * {@link S3Guard#patchLastUpdated(Collection, ITtlTimeProvider)}. - * @param qualifiedPath path to update - * @param operationState (nullable) operational state for a bulk update - * @throws IOException failure - */ - @RetryTranslated - void addAncestors(Path qualifiedPath, - @Nullable BulkOperationState operationState) throws IOException; - - /** - * Record the effects of a {@link FileSystem#rename(Path, Path)} in the - * MetadataStore. Clients provide explicit enumeration of the affected - * paths (recursively), before and after the rename. - * - * This operation is not atomic, unless specific implementations claim - * otherwise. - * - * On the need to provide an enumeration of directory trees instead of just - * source and destination paths: - * Since a MetadataStore does not have to track all metadata for the - * underlying storage system, and a new MetadataStore may be created on an - * existing underlying filesystem, this move() may be the first time the - * MetadataStore sees the affected paths. Therefore, simply providing src - * and destination paths may not be enough to record the deletions (under - * src path) and creations (at destination) that are happening during the - * rename(). - * - * @param pathsToDelete Collection of all paths that were removed from the - * source directory tree of the move. - * @param pathsToCreate Collection of all PathMetadata for the new paths - * that were created at the destination of the rename(). - * @param operationState Any ongoing state supplied to the rename tracker - * which is to be passed in with each move operation. - * @throws IOException if there is an error - */ - void move(@Nullable Collection pathsToDelete, - @Nullable Collection pathsToCreate, - @Nullable BulkOperationState operationState) throws IOException; - - /** - * Saves metadata for exactly one path. - * - * Implementations may pre-create all the path's ancestors automatically. - * Implementations must update any {@code DirListingMetadata} objects which - * track the immediate parent of this file. - * - * @param meta the metadata to save - * @throws IOException if there is an error - */ - @RetryTranslated - void put(PathMetadata meta) throws IOException; - - /** - * Saves metadata for exactly one path, potentially - * using any bulk operation state to eliminate duplicate work. - * - * Implementations may pre-create all the path's ancestors automatically. - * Implementations must update any {@code DirListingMetadata} objects which - * track the immediate parent of this file. - * - * @param meta the metadata to save - * @param operationState operational state for a bulk update - * @throws IOException if there is an error - */ - @RetryTranslated - void put(PathMetadata meta, - @Nullable BulkOperationState operationState) throws IOException; - - /** - * Saves metadata for any number of paths. - * - * Semantics are otherwise the same as single-path puts. - * - * @param metas the metadata to save - * @param operationState (nullable) operational state for a bulk update - * @throws IOException if there is an error - */ - void put(Collection metas, - @Nullable BulkOperationState operationState) throws IOException; - - /** - * Save directory listing metadata. Callers may save a partial directory - * listing for a given path, or may store a complete and authoritative copy - * of the directory listing. {@code MetadataStore} implementations may - * subsequently keep track of all modifications to the directory contents at - * this path, and return authoritative results from subsequent calls to - * {@link #listChildren(Path)}. See {@link DirListingMetadata}. - * - * Any authoritative results returned are only authoritative for the scope - * of the {@code MetadataStore}: A per-process {@code MetadataStore}, for - * example, would only show results visible to that process, potentially - * missing metadata updates (create, delete) made to the same path by - * another process. - * - * To optimize updates and avoid overwriting existing entries which - * may contain extra data, entries in the list of unchangedEntries may - * be excluded. That is: the listing metadata has the full list of - * what it believes are children, but implementations can opt to ignore - * some. - * @param meta Directory listing metadata. - * @param unchangedEntries list of entries in the dir listing which have - * not changed since the directory was list scanned on s3guard. - * @param operationState operational state for a bulk update - * @throws IOException if there is an error - */ - void put(DirListingMetadata meta, - final List unchangedEntries, - @Nullable BulkOperationState operationState) throws IOException; - - /** - * Destroy all resources associated with the metadata store. - * - * The destroyed resources can be DynamoDB tables, MySQL databases/tables, or - * HDFS directories. Any operations after calling this method may possibly - * fail. - * - * This operation is idempotent. - * - * @throws IOException if there is an error - */ - void destroy() throws IOException; - - /** - * Prune method with two modes of operation: - *

      - *
    • - * {@link PruneMode#ALL_BY_MODTIME} - * Clear any metadata older than a specified mod_time from the store. - * Note that this modification time is the S3 modification time from the - * object's metadata - from the object store. - * Implementations MUST clear file metadata, and MAY clear directory - * metadata (s3a itself does not track modification time for directories). - * Implementations may also choose to throw UnsupportedOperationException - * instead. Note that modification times must be in UTC, as returned by - * System.currentTimeMillis at the time of modification. - *
    • - *
    - * - *
      - *
    • - * {@link PruneMode#TOMBSTONES_BY_LASTUPDATED} - * Clear any tombstone updated earlier than a specified time from the - * store. Note that this last_updated is the time when the metadata - * entry was last updated and maintained by the metadata store. - * Implementations MUST clear file metadata, and MAY clear directory - * metadata (s3a itself does not track modification time for directories). - * Implementations may also choose to throw UnsupportedOperationException - * instead. Note that last_updated must be in UTC, as returned by - * System.currentTimeMillis at the time of modification. - *
    • - *
    - * - * @param pruneMode Prune Mode - * @param cutoff Oldest time to allow (UTC) - * @throws IOException if there is an error - * @throws UnsupportedOperationException if not implemented - */ - void prune(PruneMode pruneMode, long cutoff) throws IOException, - UnsupportedOperationException; - - /** - * Same as {@link MetadataStore#prune(PruneMode, long)}, but with an - * additional keyPrefix parameter to filter the pruned keys with a prefix. - * - * @param pruneMode Prune Mode - * @param cutoff Oldest time in milliseconds to allow (UTC) - * @param keyPrefix The prefix for the keys that should be removed - * @throws IOException if there is an error - * @throws UnsupportedOperationException if not implemented - * @return the number of pruned entries - */ - long prune(PruneMode pruneMode, long cutoff, String keyPrefix) - throws IOException, UnsupportedOperationException; - - /** - * Get any diagnostics information from a store, as a list of (key, value) - * tuples for display. Arbitrary values; no guarantee of stability. - * These are for debugging and testing only. - * @return a map of strings. - * @throws IOException if there is an error - */ - Map getDiagnostics() throws IOException; - - /** - * Tune/update parameters for an existing table. - * @param parameters map of params to change. - * @throws IOException if there is an error - */ - void updateParameters(Map parameters) throws IOException; - - /** - * Mark all directories created/touched in an operation as authoritative. - * The metastore can now update that path with any authoritative - * flags it chooses. - * The store may assume that therefore the operation state is complete. - * This holds for rename and needs to be documented for import. - * @param dest destination path. - * @param operationState active state. - * @throws IOException failure. - * @return the number of directories marked. - */ - default int markAsAuthoritative(Path dest, - BulkOperationState operationState) - throws IOException { - return 0; - } - - /** - * Modes of operation for prune. - * For details see {@link MetadataStore#prune(PruneMode, long)} - */ - enum PruneMode { - ALL_BY_MODTIME, - TOMBSTONES_BY_LASTUPDATED - } - - /** - * Start a rename operation. - * - * @param storeContext store context. - * @param source source path - * @param sourceStatus status of the source file/dir - * @param dest destination path. - * @return the rename tracker - * @throws IOException Failure. - */ - RenameTracker initiateRenameOperation( - StoreContext storeContext, - Path source, - S3AFileStatus sourceStatus, - Path dest) - throws IOException; - - /** - * Initiate a bulk update and create an operation state for it. - * This may then be passed into put operations. - * @param operation the type of the operation. - * @param dest path under which updates will be explicitly put. - * @return null or a store-specific state to pass into the put operations. - * @throws IOException failure - */ - default BulkOperationState initiateBulkWrite( - BulkOperationState.OperationType operation, - Path dest) throws IOException { - return new BulkOperationState(operation); - } - - /** - * The TtlTimeProvider has to be set during the initialization for the - * metadatastore, but this method can be used for testing, and change the - * instance during runtime. - * - * @param ttlTimeProvider - */ - void setTtlTimeProvider(ITtlTimeProvider ttlTimeProvider); - - /** - * Get any S3GuardInstrumentation for this store...must not be null. - * @return any store instrumentation. - */ - default MetastoreInstrumentation getInstrumentation() { - return new MetastoreInstrumentationImpl(); - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/MetadataStoreCapabilities.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/MetadataStoreCapabilities.java deleted file mode 100644 index c1464402246..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/MetadataStoreCapabilities.java +++ /dev/null @@ -1,43 +0,0 @@ -/** - * 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.hadoop.fs.s3a.s3guard; - -import org.apache.hadoop.classification.InterfaceAudience; -import org.apache.hadoop.classification.InterfaceStability; - -/** - * All the capability constants used for the - * {@link MetadataStore} implementations. - */ -@InterfaceAudience.Public -@InterfaceStability.Evolving -public final class MetadataStoreCapabilities { - - private MetadataStoreCapabilities(){ - } - - /** - * This capability tells if the metadata store supports authoritative - * directories. Used in {@link MetadataStore#getDiagnostics()} as a key - * for this capability. The value can be boolean true or false. - * If the Map.get() returns null for this key, that is interpreted as false. - */ - public static final String PERSISTS_AUTHORITATIVE_BIT = - "persist.authoritative.bit"; -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/MetadataStoreListFilesIterator.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/MetadataStoreListFilesIterator.java deleted file mode 100644 index a92aaeb0560..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/MetadataStoreListFilesIterator.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.Queue; -import java.util.Set; - -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.hadoop.classification.InterfaceAudience; -import org.apache.hadoop.classification.InterfaceStability; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.RemoteIterator; -import org.apache.hadoop.fs.s3a.S3AFileStatus; - -/** - * {@code MetadataStoreListFilesIterator} is a {@link RemoteIterator} that - * is similar to {@code DescendantsIterator} but does not return directories - * that have (or may have) children, and will also provide access to the set of - * tombstones to allow recently deleted S3 objects to be filtered out from a - * corresponding request. In other words, it returns tombstones and the same - * set of objects that should exist in S3: empty directories, and files, and not - * other directories whose existence is inferred therefrom. - * - * For example, assume the consistent store contains metadata representing this - * file system structure: - * - *
    - * /dir1
    - * |-- dir2
    - * |   |-- file1
    - * |   `-- file2
    - * `-- dir3
    - *     |-- dir4
    - *     |   `-- file3
    - *     |-- dir5
    - *     |   `-- file4
    - *     `-- dir6
    - * 
    - * - * Consider this code sample: - *
    - * final PathMetadata dir1 = get(new Path("/dir1"));
    - * for (MetadataStoreListFilesIterator files =
    - *     new MetadataStoreListFilesIterator(dir1); files.hasNext(); ) {
    - *   final FileStatus status = files.next().getFileStatus();
    - *   System.out.printf("%s %s%n", status.isDirectory() ? 'D' : 'F',
    - *       status.getPath());
    - * }
    - * 
    - * - * The output is: - *
    - * F /dir1/dir2/file1
    - * F /dir1/dir2/file2
    - * F /dir1/dir3/dir4/file3
    - * F /dir1/dir3/dir5/file4
    - * D /dir1/dir3/dir6
    - * 
    - */ -@InterfaceAudience.Private -@InterfaceStability.Evolving -public class MetadataStoreListFilesIterator implements - RemoteIterator { - public static final Logger LOG = LoggerFactory.getLogger( - MetadataStoreListFilesIterator.class); - - private final boolean allowAuthoritative; - private final MetadataStore metadataStore; - private final Set tombstones = new HashSet<>(); - private final boolean recursivelyAuthoritative; - private Iterator leafNodesIterator = null; - - public MetadataStoreListFilesIterator(MetadataStore ms, PathMetadata meta, - boolean allowAuthoritative) throws IOException { - Preconditions.checkNotNull(ms); - this.metadataStore = ms; - this.allowAuthoritative = allowAuthoritative; - this.recursivelyAuthoritative = prefetch(meta); - } - - /** - * Walks the listing tree, starting from given metadata path. All - * encountered files and empty directories are added to - * {@link leafNodesIterator} unless a directory seems to be empty - * and at least one of the following conditions hold: - *
      - *
    • - * The directory listing is not marked authoritative - *
    • - *
    • - * Authoritative mode is not allowed - *
    • - *
    - * @param meta starting point for tree walk - * @return {@code true} if all encountered directory listings - * are marked as authoritative - * @throws IOException - */ - private boolean prefetch(PathMetadata meta) throws IOException { - final Queue queue = new LinkedList<>(); - final Collection leafNodes = new ArrayList<>(); - - boolean allListingsAuthoritative = true; - if (meta != null) { - final Path path = meta.getFileStatus().getPath(); - if (path.isRoot()) { - DirListingMetadata rootListing = metadataStore.listChildren(path); - if (rootListing != null) { - if (!rootListing.isAuthoritative()) { - allListingsAuthoritative = false; - } - tombstones.addAll(rootListing.listTombstones()); - queue.addAll(rootListing.withoutTombstones().getListing()); - } - } else { - queue.add(meta); - } - } else { - allListingsAuthoritative = false; - } - - while(!queue.isEmpty()) { - PathMetadata nextMetadata = queue.poll(); - S3AFileStatus nextStatus = nextMetadata.getFileStatus(); - if (nextStatus.isFile()) { - // All files are leaf nodes by definition - leafNodes.add(nextStatus); - continue; - } - if (nextStatus.isDirectory()) { - final Path path = nextStatus.getPath(); - DirListingMetadata children = metadataStore.listChildren(path); - if (children != null) { - if (!children.isAuthoritative()) { - allListingsAuthoritative = false; - } - tombstones.addAll(children.listTombstones()); - Collection liveChildren = - children.withoutTombstones().getListing(); - if (!liveChildren.isEmpty()) { - // If it's a directory, has children, not all deleted, then we - // add the children to the queue and move on to the next node - queue.addAll(liveChildren); - continue; - } else if (allowAuthoritative && children.isAuthoritative()) { - leafNodes.add(nextStatus); - } - } else { - // we do not have a listing, so directory definitely non-authoritative - allListingsAuthoritative = false; - } - } - // Directories that *might* be empty are ignored for now, since we - // cannot confirm that they are empty without incurring other costs. - // Users of this class can still discover empty directories via S3's - // fake directories, subject to the same consistency semantics as before. - // The only other possibility is a symlink, which is unsupported on S3A. - } - leafNodesIterator = leafNodes.iterator(); - return allListingsAuthoritative; - } - - @Override - public boolean hasNext() { - return leafNodesIterator.hasNext(); - } - - @Override - public S3AFileStatus next() { - return leafNodesIterator.next(); - } - - public boolean isRecursivelyAuthoritative() { - return recursivelyAuthoritative; - } - - public Set listTombstones() { - return tombstones; - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/MetastoreInstrumentation.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/MetastoreInstrumentation.java deleted file mode 100644 index b5ac00eafe9..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/MetastoreInstrumentation.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -/** - * Instrumentation exported to S3Guard. - */ -public interface MetastoreInstrumentation { - - /** Initialized event. */ - void initialized(); - - /** Store has been closed. */ - void storeClosed(); - - /** - * Throttled request. - */ - void throttled(); - - /** - * S3Guard is retrying after a (retryable) failure. - */ - void retrying(); - - /** - * Records have been deleted. - * @param count the number of records deleted. - */ - void recordsDeleted(int count); - - /** - * Records have been read. - * @param count the number of records read - */ - void recordsRead(int count); - - /** - * records have been written (including tombstones). - * @param count number of records written. - */ - void recordsWritten(int count); - - /** - * A directory has been tagged as authoritative. - */ - void directoryMarkedAuthoritative(); - - /** - * An entry was added. - * @param durationNanos time to add - */ - void entryAdded(long durationNanos); -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/MetastoreInstrumentationImpl.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/MetastoreInstrumentationImpl.java deleted file mode 100644 index 7884d8e830f..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/MetastoreInstrumentationImpl.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -/** - * A no-op implementation of {@link MetastoreInstrumentation} - * which allows metastores to always return an instance - * when requested. - */ -public class MetastoreInstrumentationImpl implements MetastoreInstrumentation { - - @Override - public void initialized() { - - } - - @Override - public void storeClosed() { - - } - - @Override - public void throttled() { - - } - - @Override - public void retrying() { - - } - - @Override - public void recordsDeleted(final int count) { - - } - - @Override - public void recordsRead(final int count) { - - } - - @Override - public void recordsWritten(final int count) { - - } - - @Override - public void directoryMarkedAuthoritative() { - - } - - @Override - public void entryAdded(final long durationNanos) { - - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/NullMetadataStore.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/NullMetadataStore.java deleted file mode 100644 index 722f42176ef..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/NullMetadataStore.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import javax.annotation.Nullable; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.S3ObjectAttributes; -import org.apache.hadoop.fs.s3a.impl.StoreContext; - -import java.io.IOException; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * A no-op implementation of MetadataStore. Clients that use this - * implementation should behave the same as they would without any - * MetadataStore. - */ -public class NullMetadataStore implements MetadataStore { - - @Override - public void initialize(FileSystem fs, ITtlTimeProvider ttlTimeProvider) - throws IOException { - } - - @Override - public void initialize(Configuration conf, ITtlTimeProvider ttlTimeProvider) - throws IOException { - } - - @Override - public void close() throws IOException { - } - - @Override - public void delete(Path path, - final BulkOperationState operationState) - throws IOException { - } - - @Override - public void forgetMetadata(Path path) throws IOException { - } - - @Override - public void deleteSubtree(Path path, - final BulkOperationState operationState) - throws IOException { - } - - @Override - public void deletePaths(final Collection paths, - @Nullable final BulkOperationState operationState) throws IOException { - - } - - @Override - public PathMetadata get(Path path) throws IOException { - return null; - } - - @Override - public PathMetadata get(Path path, boolean wantEmptyDirectoryFlag) - throws IOException { - return null; - } - - @Override - public DirListingMetadata listChildren(Path path) throws IOException { - return null; - } - - @Override - public void move(Collection pathsToDelete, - Collection pathsToCreate, - final BulkOperationState operationState) throws IOException { - } - - @Override - public void put(final PathMetadata meta) throws IOException { - } - - @Override - public void put(PathMetadata meta, - final BulkOperationState operationState) throws IOException { - } - - @Override - public void put(Collection meta, - final BulkOperationState operationState) throws IOException { - } - - @Override - public void put(DirListingMetadata meta, - final List unchangedEntries, - final BulkOperationState operationState) throws IOException { - } - - @Override - public void destroy() throws IOException { - } - - @Override - public void prune(PruneMode pruneMode, long cutoff) { - } - - @Override - public long prune(PruneMode pruneMode, long cutoff, String keyPrefix) { - return 0; - } - - @Override - public String toString() { - return "NullMetadataStore"; - } - - @Override - public Map getDiagnostics() throws IOException { - Map map = new HashMap<>(); - map.put("name", "Null Metadata Store"); - map.put("description", "This is not a real metadata store"); - return map; - } - - @Override - public void updateParameters(Map parameters) - throws IOException { - } - - @Override - public RenameTracker initiateRenameOperation(final StoreContext storeContext, - final Path source, - final S3AFileStatus sourceStatus, - final Path dest) - throws IOException { - return new NullRenameTracker(storeContext, source, dest, this); - } - - @Override - public void setTtlTimeProvider(ITtlTimeProvider ttlTimeProvider) { - } - - @Override - public void addAncestors(final Path qualifiedPath, - @Nullable final BulkOperationState operationState) throws IOException { - } - - private static final class NullRenameTracker extends RenameTracker { - - private NullRenameTracker( - final StoreContext storeContext, - final Path source, - final Path dest, - MetadataStore metadataStore) { - super("NullRenameTracker", storeContext, metadataStore, source, dest, - null); - } - - @Override - public void fileCopied(final Path childSource, - final S3ObjectAttributes sourceAttributes, - final S3ObjectAttributes destAttributes, - final Path destPath, - final long blockSize, - final boolean addAncestors) throws IOException { - - } - - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/PathMetadata.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/PathMetadata.java deleted file mode 100644 index 09297056f65..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/PathMetadata.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; -import org.apache.hadoop.classification.InterfaceAudience; -import org.apache.hadoop.classification.InterfaceStability; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.Tristate; - -/** - * {@code PathMetadata} models path metadata stored in the - * {@link MetadataStore}. The lastUpdated field is implicitly set to 0 in the - * constructors without that parameter to show that it will be initialized - * with 0 if not set otherwise. - */ -@InterfaceAudience.Private -@InterfaceStability.Evolving -public class PathMetadata extends ExpirableMetadata { - - private S3AFileStatus fileStatus; - private Tristate isEmptyDirectory; - private boolean isDeleted; - - /** - * Create a tombstone from the current time. - * It is mandatory to set the lastUpdated field to update when the - * tombstone state has changed to set when the entry got deleted. - * - * @param path path to tombstone - * @param lastUpdated last updated time on which expiration is based. - * @return the entry. - */ - public static PathMetadata tombstone(Path path, long lastUpdated) { - S3AFileStatus s3aStatus = new S3AFileStatus(0, - System.currentTimeMillis(), path, 0, null, - null, null); - return new PathMetadata(s3aStatus, Tristate.UNKNOWN, true, lastUpdated); - } - - /** - * Creates a new {@code PathMetadata} containing given {@code FileStatus}. - * lastUpdated field will be updated to 0 implicitly in this constructor. - * - * @param fileStatus file status containing an absolute path. - */ - public PathMetadata(S3AFileStatus fileStatus) { - this(fileStatus, Tristate.UNKNOWN, false, 0); - } - - /** - * Creates a new {@code PathMetadata} containing given {@code FileStatus}. - * - * @param fileStatus file status containing an absolute path. - * @param lastUpdated last updated time on which expiration is based. - */ - public PathMetadata(S3AFileStatus fileStatus, long lastUpdated) { - this(fileStatus, Tristate.UNKNOWN, false, lastUpdated); - } - - /** - * Creates a new {@code PathMetadata}. - * lastUpdated field will be updated to 0 implicitly in this constructor. - * - * @param fileStatus file status containing an absolute path. - * @param isEmptyDir empty directory {@link Tristate} - */ - public PathMetadata(S3AFileStatus fileStatus, Tristate isEmptyDir) { - this(fileStatus, isEmptyDir, false, 0); - } - - /** - * Creates a new {@code PathMetadata}. - * lastUpdated field will be updated to 0 implicitly in this constructor. - * - * @param fileStatus file status containing an absolute path. - * @param isEmptyDir empty directory {@link Tristate} - * @param isDeleted deleted / tombstoned flag - */ - public PathMetadata(S3AFileStatus fileStatus, Tristate isEmptyDir, - boolean isDeleted) { - this(fileStatus, isEmptyDir, isDeleted, 0); - } - - /** - * Creates a new {@code PathMetadata}. - * - * @param fileStatus file status containing an absolute path. - * @param isEmptyDir empty directory {@link Tristate} - * @param isDeleted deleted / tombstoned flag - * @param lastUpdated last updated time on which expiration is based. - */ - public PathMetadata(S3AFileStatus fileStatus, Tristate isEmptyDir, boolean - isDeleted, long lastUpdated) { - Preconditions.checkNotNull(fileStatus, "fileStatus must be non-null"); - Preconditions.checkNotNull(fileStatus.getPath(), "fileStatus path must be" + - " non-null"); - Preconditions.checkArgument(fileStatus.getPath().isAbsolute(), "path must" + - " be absolute"); - Preconditions.checkArgument(lastUpdated >=0, "lastUpdated parameter must " - + "be greater or equal to 0."); - this.fileStatus = fileStatus; - this.isEmptyDirectory = isEmptyDir; - this.isDeleted = isDeleted; - this.setLastUpdated(lastUpdated); - } - - /** - * @return {@code FileStatus} contained in this {@code PathMetadata}. - */ - public final S3AFileStatus getFileStatus() { - return fileStatus; - } - - /** - * Query if a directory is empty. - * @return Tristate.TRUE if this is known to be an empty directory, - * Tristate.FALSE if known to not be empty, and Tristate.UNKNOWN if the - * MetadataStore does have enough information to determine either way. - */ - public Tristate isEmptyDirectory() { - return isEmptyDirectory; - } - - void setIsEmptyDirectory(Tristate isEmptyDirectory) { - this.isEmptyDirectory = isEmptyDirectory; - fileStatus.setIsEmptyDirectory(isEmptyDirectory); - } - - public boolean isDeleted() { - return isDeleted; - } - - void setIsDeleted(boolean isDeleted) { - this.isDeleted = isDeleted; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof PathMetadata)) { - return false; - } - return this.fileStatus.equals(((PathMetadata)o).fileStatus); - } - - @Override - public int hashCode() { - return fileStatus.hashCode(); - } - - @Override - public String toString() { - return "PathMetadata{" + - "fileStatus=" + fileStatus + - "; isEmptyDirectory=" + isEmptyDirectory + - "; isDeleted=" + isDeleted + - "; lastUpdated=" + super.getLastUpdated() + - '}'; - } - - /** - * Log contents to supplied StringBuilder in a pretty fashion. - * @param sb target StringBuilder - */ - public void prettyPrint(StringBuilder sb) { - sb.append(String.format("%-5s %-20s %-7d %-8s %-6s %-20s %-20s", - fileStatus.isDirectory() ? "dir" : "file", - fileStatus.getPath().toString(), fileStatus.getLen(), - isEmptyDirectory.name(), isDeleted, - fileStatus.getETag(), fileStatus.getVersionId())); - sb.append(fileStatus); - } - - public String prettyPrint() { - StringBuilder sb = new StringBuilder(); - prettyPrint(sb); - return sb.toString(); - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/PathMetadataDynamoDBTranslation.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/PathMetadataDynamoDBTranslation.java deleted file mode 100644 index 413ecf1f422..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/PathMetadataDynamoDBTranslation.java +++ /dev/null @@ -1,425 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.IOException; -import java.net.URI; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import com.amazonaws.services.dynamodbv2.document.Item; -import com.amazonaws.services.dynamodbv2.document.KeyAttribute; -import com.amazonaws.services.dynamodbv2.document.PrimaryKey; -import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; -import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; -import com.amazonaws.services.dynamodbv2.model.KeyType; -import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType; -import org.apache.hadoop.thirdparty.com.google.common.annotations.VisibleForTesting; -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; - -import org.apache.commons.lang3.StringUtils; -import org.apache.hadoop.classification.InterfaceAudience; -import org.apache.hadoop.classification.InterfaceStability; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.Constants; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.Tristate; - -/** - * Defines methods for translating between domain model objects and their - * representations in the DynamoDB schema. - */ -@InterfaceAudience.Private -@InterfaceStability.Evolving -@VisibleForTesting -public final class PathMetadataDynamoDBTranslation { - - /** The HASH key name of each item. */ - @VisibleForTesting - static final String PARENT = "parent"; - /** The RANGE key name of each item. */ - @VisibleForTesting - static final String CHILD = "child"; - @VisibleForTesting - static final String IS_DIR = "is_dir"; - @VisibleForTesting - static final String MOD_TIME = "mod_time"; - @VisibleForTesting - static final String FILE_LENGTH = "file_length"; - @VisibleForTesting - static final String BLOCK_SIZE = "block_size"; - static final String IS_DELETED = "is_deleted"; - static final String IS_AUTHORITATIVE = "is_authoritative"; - static final String LAST_UPDATED = "last_updated"; - static final String ETAG = "etag"; - static final String VERSION_ID = "version_id"; - - /** Used while testing backward compatibility. */ - @VisibleForTesting - static final Set IGNORED_FIELDS = new HashSet<>(); - - /** Table version field {@value} in version marker item. */ - @VisibleForTesting - static final String TABLE_VERSION = "table_version"; - - /** Table creation timestampfield {@value} in version marker item. */ - @VisibleForTesting - static final String TABLE_CREATED = "table_created"; - - /** The version marker field is invalid. */ - static final String E_NOT_VERSION_MARKER = "Not a version marker: "; - - /** - * Returns the key schema for the DynamoDB table. - * - * @return DynamoDB key schema - */ - static Collection keySchema() { - return Arrays.asList( - new KeySchemaElement(PARENT, KeyType.HASH), - new KeySchemaElement(CHILD, KeyType.RANGE)); - } - - /** - * Returns the attribute definitions for the DynamoDB table. - * - * @return DynamoDB attribute definitions - */ - static Collection attributeDefinitions() { - return Arrays.asList( - new AttributeDefinition(PARENT, ScalarAttributeType.S), - new AttributeDefinition(CHILD, ScalarAttributeType.S)); - } - - /** - * Converts a DynamoDB item to a {@link DDBPathMetadata}. - * - * @param item DynamoDB item to convert - * @return {@code item} converted to a {@link DDBPathMetadata} - */ - static DDBPathMetadata itemToPathMetadata(Item item, String username) { - if (item == null) { - return null; - } - - String parentStr = item.getString(PARENT); - Preconditions.checkNotNull(parentStr, "No parent entry in item %s", item); - String childStr = item.getString(CHILD); - Preconditions.checkNotNull(childStr, "No child entry in item %s", item); - - // Skip table version markers, which are only non-absolute paths stored. - Path rawPath = new Path(parentStr, childStr); - if (!rawPath.isAbsoluteAndSchemeAuthorityNull()) { - return null; - } - - Path parent = new Path(Constants.FS_S3A + ":/" + parentStr + "/"); - Path path = new Path(parent, childStr); - - boolean isDir = item.hasAttribute(IS_DIR) && item.getBoolean(IS_DIR); - boolean isAuthoritativeDir = false; - final S3AFileStatus fileStatus; - long lastUpdated = 0; - if (isDir) { - isAuthoritativeDir = !IGNORED_FIELDS.contains(IS_AUTHORITATIVE) - && item.hasAttribute(IS_AUTHORITATIVE) - && item.getBoolean(IS_AUTHORITATIVE); - fileStatus = DynamoDBMetadataStore.makeDirStatus(path, username); - } else { - long len = item.hasAttribute(FILE_LENGTH) ? item.getLong(FILE_LENGTH) : 0; - long modTime = item.hasAttribute(MOD_TIME) ? item.getLong(MOD_TIME) : 0; - long block = item.hasAttribute(BLOCK_SIZE) ? item.getLong(BLOCK_SIZE) : 0; - String eTag = item.getString(ETAG); - String versionId = item.getString(VERSION_ID); - fileStatus = new S3AFileStatus( - len, modTime, path, block, username, eTag, versionId); - } - lastUpdated = - !IGNORED_FIELDS.contains(LAST_UPDATED) - && item.hasAttribute(LAST_UPDATED) - ? item.getLong(LAST_UPDATED) : 0; - - boolean isDeleted = - item.hasAttribute(IS_DELETED) && item.getBoolean(IS_DELETED); - - return new DDBPathMetadata(fileStatus, Tristate.UNKNOWN, isDeleted, - isAuthoritativeDir, lastUpdated); - } - - /** - * Converts a {@link DDBPathMetadata} to a DynamoDB item. - * - * Can ignore {@code IS_AUTHORITATIVE} flag if {@code ignoreIsAuthFlag} is - * true. - * - * @param meta {@link DDBPathMetadata} to convert - * @return {@code meta} converted to DynamoDB item - */ - static Item pathMetadataToItem(DDBPathMetadata meta) { - Preconditions.checkNotNull(meta); - final S3AFileStatus status = meta.getFileStatus(); - final Item item = new Item().withPrimaryKey(pathToKey(status.getPath())); - if (status.isDirectory()) { - item.withBoolean(IS_DIR, true); - if (!IGNORED_FIELDS.contains(IS_AUTHORITATIVE)) { - item.withBoolean(IS_AUTHORITATIVE, meta.isAuthoritativeDir()); - } - } else { - item.withLong(FILE_LENGTH, status.getLen()) - .withLong(MOD_TIME, status.getModificationTime()) - .withLong(BLOCK_SIZE, status.getBlockSize()); - if (status.getETag() != null) { - item.withString(ETAG, status.getETag()); - } - if (status.getVersionId() != null) { - item.withString(VERSION_ID, status.getVersionId()); - } - } - item.withBoolean(IS_DELETED, meta.isDeleted()); - - if(!IGNORED_FIELDS.contains(LAST_UPDATED)) { - item.withLong(LAST_UPDATED, meta.getLastUpdated()); - } - - return item; - } - - /** - * The version marker has a primary key whose PARENT is {@code name}; - * this MUST NOT be a value which represents an absolute path. - * @param name name of the version marker - * @param version version number - * @param timestamp creation timestamp - * @return an item representing a version marker. - */ - static Item createVersionMarker(String name, int version, long timestamp) { - return new Item().withPrimaryKey(createVersionMarkerPrimaryKey(name)) - .withInt(TABLE_VERSION, version) - .withLong(TABLE_CREATED, timestamp); - } - - /** - * Create the primary key of the version marker. - * @param name key name - * @return the key to use when registering or resolving version markers - */ - static PrimaryKey createVersionMarkerPrimaryKey(String name) { - return new PrimaryKey(PARENT, name, CHILD, name); - } - - /** - * Extract the version from a version marker item. - * @param marker version marker item - * @return the extracted version field - * @throws IOException if the item is not a version marker - */ - static int extractVersionFromMarker(Item marker) throws IOException { - if (marker.hasAttribute(TABLE_VERSION)) { - return marker.getInt(TABLE_VERSION); - } else { - throw new IOException(E_NOT_VERSION_MARKER + marker); - } - } - - /** - * Extract the creation time, if present. - * @param marker version marker item - * @return the creation time, or null - * @throws IOException if the item is not a version marker - */ - static Long extractCreationTimeFromMarker(Item marker) { - if (marker.hasAttribute(TABLE_CREATED)) { - return marker.getLong(TABLE_CREATED); - } else { - return null; - } - } - - /** - * Converts a collection {@link DDBPathMetadata} to a collection DynamoDB - * items. - * - * @see #pathMetadataToItem(DDBPathMetadata) - */ - static Item[] pathMetadataToItem(Collection metas) { - if (metas == null) { - return null; - } - - final Item[] items = new Item[metas.size()]; - int i = 0; - for (DDBPathMetadata meta : metas) { - items[i++] = pathMetadataToItem(meta); - } - return items; - } - - /** - * Converts a {@link Path} to a DynamoDB equality condition on that path as - * parent, suitable for querying all direct children of the path. - * - * @param path the path; can not be null - * @return DynamoDB equality condition on {@code path} as parent - */ - static KeyAttribute pathToParentKeyAttribute(Path path) { - return new KeyAttribute(PARENT, pathToParentKey(path)); - } - - /** - * e.g. {@code pathToParentKey(s3a://bucket/path/a) -> /bucket/path/a} - * @param path path to convert - * @return string for parent key - */ - @VisibleForTesting - public static String pathToParentKey(Path path) { - Preconditions.checkNotNull(path); - Preconditions.checkArgument(path.isUriPathAbsolute(), - "Path not absolute: '%s'", path); - URI uri = path.toUri(); - String bucket = uri.getHost(); - Preconditions.checkArgument(!StringUtils.isEmpty(bucket), - "Path missing bucket %s", path); - String pKey = "/" + bucket + uri.getPath(); - - // Strip trailing slash - if (pKey.endsWith("/")) { - pKey = pKey.substring(0, pKey.length() - 1); - } - return pKey; - } - - /** - * Converts a {@link Path} to a DynamoDB key, suitable for getting the item - * matching the path. - * - * @param path the path; can not be null - * @return DynamoDB key for item matching {@code path} - */ - static PrimaryKey pathToKey(Path path) { - Preconditions.checkArgument(!path.isRoot(), - "Root path is not mapped to any PrimaryKey"); - String childName = path.getName(); - PrimaryKey key = new PrimaryKey(PARENT, - pathToParentKey(path.getParent()), CHILD, - childName); - for (KeyAttribute attr : key.getComponents()) { - String name = attr.getName(); - Object v = attr.getValue(); - Preconditions.checkNotNull(v, - "Null value for DynamoDB attribute \"%s\"", name); - Preconditions.checkState(!((String)v).isEmpty(), - "Empty string value for DynamoDB attribute \"%s\"", name); - } - return key; - - } - - /** - * Converts a collection of {@link Path} to a collection of DynamoDB keys. - * - * @see #pathToKey(Path) - */ - static PrimaryKey[] pathToKey(Collection paths) { - if (paths == null) { - return null; - } - - final PrimaryKey[] keys = new PrimaryKey[paths.size()]; - int i = 0; - for (Path p : paths) { - keys[i++] = pathToKey(p); - } - return keys; - } - - /** - * There is no need to instantiate this class. - */ - private PathMetadataDynamoDBTranslation() { - } - - /** - * Convert a collection of metadata entries to a list - * of DDBPathMetadata entries. - * If the sources are already DDBPathMetadata instances, they - * are copied directly into the new list, otherwise new - * instances are created. - * @param pathMetadatas source data - * @return the converted list. - */ - static List pathMetaToDDBPathMeta( - Collection pathMetadatas) { - return pathMetadatas.stream().map(p -> - (p instanceof DDBPathMetadata) - ? (DDBPathMetadata) p - : new DDBPathMetadata(p)) - .collect(Collectors.toList()); - } - - /** - * Convert an item's (parent, child) key to a string value - * for logging. There is no validation of the item. - * @param item item. - * @return an s3a:// prefixed string. - */ - static String itemPrimaryKeyToString(Item item) { - String parent = item.getString(PARENT); - String child = item.getString(CHILD); - return "s3a://" + parent + "/" + child; - } - /** - * Convert an item's (parent, child) key to a string value - * for logging. There is no validation of the item. - * @param item item. - * @return an s3a:// prefixed string. - */ - static String primaryKeyToString(PrimaryKey item) { - Collection c = item.getComponents(); - String parent = ""; - String child = ""; - for (KeyAttribute attr : c) { - switch (attr.getName()) { - case PARENT: - parent = attr.getValue().toString(); - break; - case CHILD: - child = attr.getValue().toString(); - break; - default: - } - } - return "s3a://" + parent + "/" + child; - } - - /** - * Create an empty dir marker which, when passed to the - * DDB metastore, is considered authoritative. - * @param status file status - * @return path metadata. - */ - static PathMetadata authoritativeEmptyDirectoryMarker( - final S3AFileStatus status) { - return new DDBPathMetadata(status, Tristate.TRUE, - false, true, 0); - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/PathOrderComparators.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/PathOrderComparators.java deleted file mode 100644 index cbf41b4bab0..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/PathOrderComparators.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.Serializable; -import java.util.Comparator; - -import org.apache.hadoop.fs.Path; - -/** - * Comparator of path ordering for sorting collections. - * - * The definition of "topmost" is: - *
      - *
    1. The depth of a path is the primary comparator.
    2. - *
    3. Root is topmost, "0"
    4. - *
    5. If two paths are of equal depth, {@link Path#compareTo(Path)}
    6. - * is used. This delegates to URI compareTo. - *
    7. repeated sorts do not change the order
    8. - *
    - */ -final class PathOrderComparators { - - private PathOrderComparators() { - } - - /** - * The shallowest paths come first. - * This is to be used when adding entries. - */ - static final Comparator TOPMOST_PATH_FIRST - = new TopmostFirst(); - - /** - * The leaves come first. - * This is to be used when deleting entries. - */ - static final Comparator TOPMOST_PATH_LAST - = new TopmostLast(); - - /** - * The shallowest paths come first. - * This is to be used when adding entries. - */ - static final Comparator TOPMOST_PM_FIRST - = new PathMetadataComparator(TOPMOST_PATH_FIRST); - - /** - * The leaves come first. - * This is to be used when deleting entries. - */ - static final Comparator TOPMOST_PM_LAST - = new PathMetadataComparator(TOPMOST_PATH_LAST); - - private static class TopmostFirst implements Comparator, Serializable { - - @Override - public int compare(Path pathL, Path pathR) { - // exit fast on equal values. - if (pathL.equals(pathR)) { - return 0; - } - int depthL = pathL.depth(); - int depthR = pathR.depth(); - if (depthL < depthR) { - // left is higher up than the right. - return -1; - } - if (depthR < depthL) { - // right is higher up than the left - return 1; - } - // and if they are of equal depth, use the "classic" comparator - // of paths. - return pathL.compareTo(pathR); - } - } - - /** - * Compare the topmost last. - * For some reason the .reverse() option wasn't giving the - * correct outcome. - */ - private static final class TopmostLast extends TopmostFirst { - - @Override - public int compare(final Path pathL, final Path pathR) { - int compare = super.compare(pathL, pathR); - if (compare < 0) { - return 1; - } - if (compare > 0) { - return -1; - } - return 0; - } - } - - /** - * Compare on path status. - */ - static final class PathMetadataComparator implements - Comparator, Serializable { - - private final Comparator inner; - - PathMetadataComparator(final Comparator inner) { - this.inner = inner; - } - - @Override - public int compare(final PathMetadata o1, final PathMetadata o2) { - return inner.compare(o1.getFileStatus().getPath(), - o2.getFileStatus().getPath()); - } - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/ProgressiveRenameTracker.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/ProgressiveRenameTracker.java deleted file mode 100644 index 700b2ab71a6..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/ProgressiveRenameTracker.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; - -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3ObjectAttributes; -import org.apache.hadoop.fs.s3a.impl.StoreContext; -import org.apache.hadoop.util.DurationInfo; - -import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkArgument; -import static org.apache.hadoop.fs.s3a.s3guard.S3Guard.addMoveAncestors; -import static org.apache.hadoop.fs.s3a.s3guard.S3Guard.addMoveDir; - -/** - * This rename tracker progressively updates the metadata store - * as it proceeds, during the parallelized copy operation. - *

    - * Algorithm - *

      - *
    1. - * As {@code RenameTracker.fileCopied()} callbacks - * are raised, the metastore is updated with the new file entry. - *
    2. - *
    3. - * Including parent entries, as appropriate. - *
    4. - *
    5. - * All directories which have been created are tracked locally, - * to avoid needing to read the store; this is a thread-safe structure. - *
    6. - *
    7. - * The actual update is performed out of any synchronized block. - *
    8. - *
    9. - * When deletes are executed, the store is also updated. - *
    10. - *
    11. - * And at the completion of a successful rename, the source directory - * is also removed. - *
    12. - *
    - *
    - *
    - * 
    - */ -public class ProgressiveRenameTracker extends RenameTracker { - - /** - * The collection of paths to delete; this is added as individual files - * are renamed. - *

    - * The metastore is only updated with these entries after the DELETE - * call containing these paths succeeds. - *

    - * If the DELETE fails; the filesystem will use - * {@code MultiObjectDeleteSupport} to remove all successfully deleted - * entries from the metastore. - */ - private final Collection pathsToDelete = new HashSet<>(); - - public ProgressiveRenameTracker( - final StoreContext storeContext, - final MetadataStore metadataStore, - final Path sourceRoot, - final Path dest, - final BulkOperationState operationState) { - super("ProgressiveRenameTracker", - storeContext, metadataStore, sourceRoot, dest, operationState); - } - - /** - * When a file is copied, any ancestors - * are calculated and then the store is updated with - * the destination entries. - *

    - * The source entries are added to the {@link #pathsToDelete} list. - * @param sourcePath path of source - * @param sourceAttributes status of source. - * @param destAttributes destination attributes - * @param destPath destination path. - * @param blockSize block size. - * @param addAncestors should ancestors be added? - * @throws IOException failure - */ - @Override - public void fileCopied( - final Path sourcePath, - final S3ObjectAttributes sourceAttributes, - final S3ObjectAttributes destAttributes, - final Path destPath, - final long blockSize, - final boolean addAncestors) throws IOException { - - // build the list of entries to add in a synchronized block. - final List entriesToAdd = new ArrayList<>(1); - LOG.debug("Updating store with copied file {}", sourcePath); - MetadataStore store = getMetadataStore(); - synchronized (this) { - checkArgument(!pathsToDelete.contains(sourcePath), - "File being renamed is already processed %s", destPath); - // create the file metadata and update the lists - // the pathsToDelete field is incremented with the new source path, - // for deletion after the DELETE operation succeeds; - // the entriesToAdd variable is filled in with all entries - // to add within this method - S3Guard.addMoveFile( - store, - pathsToDelete, - entriesToAdd, - sourcePath, - destPath, - sourceAttributes.getLen(), - blockSize, - getOwner(), - destAttributes.getETag(), - destAttributes.getVersionId()); - LOG.debug("New metastore entry : {}", entriesToAdd.get(0)); - if (addAncestors) { - // add all new ancestors to the lists - addMoveAncestors( - store, - pathsToDelete, - entriesToAdd, - getSourceRoot(), - sourcePath, - destPath, - getOwner()); - } - } - - // outside the lock, the entriesToAdd variable has all the new entries to - // create. ...so update the store. - // no entries are deleted at this point. - try (DurationInfo ignored = new DurationInfo(LOG, false, - "Adding new metastore entries")) { - store.move(null, entriesToAdd, getOperationState()); - } - } - - /** - * A directory marker has been added. - * Add the new entry and record the source path as another entry to delete. - * @param sourcePath status of source. - * @param destPath destination path. - * @param addAncestors should ancestors be added? - * @throws IOException failure. - */ - @Override - public void directoryMarkerCopied( - final Path sourcePath, - final Path destPath, - final boolean addAncestors) throws IOException { - // this list is created on demand. - final List entriesToAdd = new ArrayList<>(1); - MetadataStore store = getMetadataStore(); - synchronized (this) { - addMoveDir(store, - pathsToDelete, - entriesToAdd, - sourcePath, - destPath, - getOwner()); - // Ancestor directories may not be listed, so we explicitly add them - if (addAncestors) { - addMoveAncestors(store, - pathsToDelete, - entriesToAdd, - getSourceRoot(), - sourcePath, - destPath, - getOwner()); - } - } - // outside the lock, the entriesToAdd list has all new files to create. - // ...so update the store. - try (DurationInfo ignored = new DurationInfo(LOG, false, - "adding %s metastore entries", entriesToAdd.size())) { - store.move(null, entriesToAdd, getOperationState()); - } - } - - @Override - public synchronized void moveSourceDirectory() throws IOException { - // this moves the source directory in the metastore if it has not - // already been processed. - if (!pathsToDelete.contains(getSourceRoot())) { - final List toDelete = new ArrayList<>(1); - final List toAdd = new ArrayList<>(1); - - addMoveDir(getMetadataStore(), pathsToDelete, toAdd, - getSourceRoot(), - getDest(), - getOwner()); - getMetadataStore().move(toDelete, toAdd, getOperationState()); - } - getMetadataStore().markAsAuthoritative( - getDest(), getOperationState()); - } - - /** - * As source objects are deleted, so is the list of entries. - * @param paths path of objects deleted. - * @throws IOException failure. - */ - @Override - public void sourceObjectsDeleted( - final Collection paths) throws IOException { - - // delete the paths from the metastore - try (DurationInfo ignored = new DurationInfo(LOG, false, - "delete %s metastore entries", paths.size())) { - getMetadataStore().move(paths, null, getOperationState()); - getMetadataStore().deletePaths(paths, getOperationState()); - } - } - - @Override - public synchronized void completeRename() throws IOException { - // mark dest tree as authoritative all the way down. - // finish off by deleting source directories. - sourceObjectsDeleted(pathsToDelete); - super.completeRename(); - } - -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/PurgeS3GuardDynamoTable.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/PurgeS3GuardDynamoTable.java deleted file mode 100644 index 746d34fda5c..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/PurgeS3GuardDynamoTable.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import javax.annotation.Nullable; -import java.io.IOException; -import java.net.URI; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.commons.lang3.tuple.Pair; -import org.apache.hadoop.classification.InterfaceAudience; -import org.apache.hadoop.classification.InterfaceStability; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.apache.hadoop.service.Service; -import org.apache.hadoop.service.launcher.LauncherExitCodes; -import org.apache.hadoop.service.launcher.ServiceLaunchException; -import org.apache.hadoop.service.launcher.ServiceLauncher; -import org.apache.hadoop.util.DurationInfo; -import org.apache.hadoop.util.ExitUtil; - -import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkNotNull; -import static org.apache.hadoop.fs.s3a.s3guard.DumpS3GuardDynamoTable.serviceMain; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.PARENT; - -/** - * Purge the S3Guard table of a FileSystem from all entries related to - * that table. - * Will fail if there is no table, or the store is in auth mode. - *

    - *   hadoop org.apache.hadoop.fs.s3a.s3guard.PurgeS3GuardDynamoTable \
    - *   -force s3a://example-bucket/
    - * 
    - * - */ -@InterfaceAudience.Private -@InterfaceStability.Unstable -public class PurgeS3GuardDynamoTable - extends AbstractS3GuardDynamoDBDiagnostic { - - private static final Logger LOG = - LoggerFactory.getLogger(PurgeS3GuardDynamoTable.class); - - public static final String NAME = "PurgeS3GuardDynamoTable"; - - /** - * Name of the force option. - */ - public static final String FORCE = "-force"; - - /** - * Usage message. - */ - private static final String USAGE_MESSAGE = NAME - + " [-force] "; - - /** - * Flag which actually triggers the delete. - */ - private boolean force; - - private long filesFound; - private long filesDeleted; - - public PurgeS3GuardDynamoTable(final String name) { - super(name); - } - - public PurgeS3GuardDynamoTable() { - this(NAME); - } - - public PurgeS3GuardDynamoTable( - final S3AFileSystem filesystem, - final DynamoDBMetadataStore store, - final URI uri, - final boolean force) { - super(NAME, filesystem, store, uri); - this.force = force; - } - - /** - * Bind to the argument list, including validating the CLI. - * @throws Exception failure. - */ - @Override - protected void serviceStart() throws Exception { - if (getStore() == null) { - List arg = getArgumentList(1, 2, USAGE_MESSAGE); - String fsURI = arg.get(0); - if (arg.size() == 2) { - if (!arg.get(0).equals(FORCE)) { - throw new ServiceLaunchException(LauncherExitCodes.EXIT_USAGE, - USAGE_MESSAGE); - } - force = true; - fsURI = arg.get(1); - } - bindFromCLI(fsURI); - } - } - - /** - * Extract the host from the FS URI, then scan and - * delete all entries from that bucket. - * @return the exit code. - * @throws ServiceLaunchException on failure. - * @throws IOException IO failure. - */ - @Override - public int execute() throws ServiceLaunchException, IOException { - - URI uri = getUri(); - String host = uri.getHost(); - String prefix = "/" + host + "/"; - DynamoDBMetadataStore ddbms = getStore(); - S3GuardTableAccess tableAccess = new S3GuardTableAccess(ddbms); - ExpressionSpecBuilder builder = new ExpressionSpecBuilder(); - builder.withKeyCondition( - ExpressionSpecBuilder.S(PARENT).beginsWith(prefix)); - - LOG.info("Scanning for entries with prefix {} to delete from {}", - prefix, ddbms); - - Iterable entries = - ddbms.wrapWithRetries(tableAccess.scanMetadata(builder)); - List list = new ArrayList<>(); - entries.iterator().forEachRemaining(e -> { - if (!(e instanceof S3GuardTableAccess.VersionMarker)) { - Path p = e.getFileStatus().getPath(); - String type = e.getFileStatus().isFile() ? "file" : "directory"; - boolean tombstone = e.isDeleted(); - if (tombstone) { - type = "tombstone " + type; - } - LOG.info("{} {}", type, p); - list.add(p); - } - }); - int count = list.size(); - filesFound = count; - LOG.info("Found {} entries{}", - count, - (count == 0 ? " -nothing to purge": "")); - if (count > 0) { - if (force) { - DurationInfo duration = - new DurationInfo(LOG, - "deleting %s entries from %s", - count, ddbms.toString()); - // sending this in one by one for more efficient retries - for (Path path: list) { - ddbms.getInvoker() - .retry("delete", - prefix, - true, - () -> tableAccess.delete(path)); - } - duration.close(); - long durationMillis = duration.value(); - long timePerEntry = durationMillis / count; - LOG.info("Time per entry: {} ms", timePerEntry); - filesDeleted = count; - } else { - LOG.info("Delete process will only be executed when " - + FORCE + " is set"); - } - } - return LauncherExitCodes.EXIT_SUCCESS; - } - - /** - * This is the Main entry point for the service launcher. - * - * Converts the arguments to a list, instantiates a instance of the class - * then executes it. - * @param args command line arguments. - */ - public static void main(String[] args) { - try { - serviceMain(Arrays.asList(args), new PurgeS3GuardDynamoTable()); - } catch (ExitUtil.ExitException e) { - ExitUtil.terminate(e); - } - } - - /** - * API Entry point to dump the metastore and S3 store world views - *

    - * Both the FS and the store will be dumped: the store is scanned - * before and after the sequence to show what changes were made to - * the store during the list operation. - * @param fs fs to dump. If null a store must be provided. - * @param store store to dump (fallback to FS) - * @param conf configuration to use (fallback to fs) - * @param uri URI of store -only needed if FS is null. - * @param force force the actual delete - * @return (filesFound, filesDeleted) - * @throws ExitUtil.ExitException failure. - */ - @InterfaceAudience.Private - @InterfaceStability.Unstable - public static Pair purgeStore( - @Nullable final S3AFileSystem fs, - @Nullable DynamoDBMetadataStore store, - @Nullable Configuration conf, - @Nullable URI uri, - boolean force) throws ExitUtil.ExitException { - ServiceLauncher serviceLauncher = - new ServiceLauncher<>(NAME); - - if (conf == null) { - conf = checkNotNull(fs, "No filesystem").getConf(); - } - if (store == null) { - store = (DynamoDBMetadataStore) checkNotNull(fs, "No filesystem") - .getMetadataStore(); - } - PurgeS3GuardDynamoTable purge = new PurgeS3GuardDynamoTable(fs, - store, - uri, - force); - ExitUtil.ExitException ex = serviceLauncher.launchService( - conf, - purge, - Collections.emptyList(), - false, - true); - if (ex != null && ex.getExitCode() != 0) { - throw ex; - } - return Pair.of(purge.filesFound, purge.filesDeleted); - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/RenameTracker.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/RenameTracker.java deleted file mode 100644 index 5c112fc1047..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/RenameTracker.java +++ /dev/null @@ -1,275 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.IOException; -import java.util.Collection; -import java.util.List; - -import com.amazonaws.SdkBaseException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3ObjectAttributes; -import org.apache.hadoop.fs.s3a.impl.StoreContext; -import org.apache.hadoop.fs.s3a.impl.AbstractStoreOperation; -import org.apache.hadoop.io.IOUtils; -import org.apache.hadoop.util.DurationInfo; - -import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkNotNull; -import static org.apache.hadoop.fs.s3a.S3AUtils.translateException; - -/** - * A class which manages updating the metastore with the rename process - * as initiated in the S3AFilesystem rename. - *

    - * Subclasses must provide an implementation and return it in - * {@code MetadataStore.initiateRenameOperation()}. - *

    - * The {@link #operationState} field/constructor argument is an opaque state to - * be passed down to the metastore in its move operations; this allows the - * stores to manage ongoing state -while still being able to share - * rename tracker implementations. - *

    - * This is to avoid performance problems wherein the progressive rename - * tracker causes the store to repeatedly create and write duplicate - * ancestor entries for every file added. - */ -public abstract class RenameTracker extends AbstractStoreOperation { - - public static final Logger LOG = LoggerFactory.getLogger( - RenameTracker.class); - - /** source path. */ - private final Path sourceRoot; - - /** destination path. */ - private final Path dest; - - /** - * Track the duration of this operation. - */ - private final DurationInfo durationInfo; - - /** - * Generated name for strings. - */ - private final String name; - - /** - * Any ongoing state supplied to the rename tracker - * which is to be passed in with each move operation. - * This must be closed at the end of the tracker's life. - */ - private final BulkOperationState operationState; - - /** - * The metadata store for this tracker. - * Always non-null. - *

    - * This is passed in separate from the store context to guarantee - * that whichever store creates a tracker is explicitly bound to that - * instance. - */ - private final MetadataStore metadataStore; - - /** - * Constructor. - * @param name tracker name for logs. - * @param storeContext store context. - * @param metadataStore the store - * @param sourceRoot source path. - * @param dest destination path. - * @param operationState ongoing move state. - */ - protected RenameTracker( - final String name, - final StoreContext storeContext, - final MetadataStore metadataStore, - final Path sourceRoot, - final Path dest, - final BulkOperationState operationState) { - super(checkNotNull(storeContext)); - checkNotNull(storeContext.getUsername(), "No username"); - this.metadataStore = checkNotNull(metadataStore); - this.sourceRoot = checkNotNull(sourceRoot); - this.dest = checkNotNull(dest); - this.operationState = operationState; - this.name = String.format("%s (%s, %s)", name, sourceRoot, dest); - durationInfo = new DurationInfo(LOG, false, - name +" (%s, %s)", sourceRoot, dest); - } - - @Override - public String toString() { - return name; - } - - public Path getSourceRoot() { - return sourceRoot; - } - - public Path getDest() { - return dest; - } - - public String getOwner() { - return getStoreContext().getUsername(); - } - - public BulkOperationState getOperationState() { - return operationState; - } - - /** - * Get the metadata store. - * @return a non-null store. - */ - protected MetadataStore getMetadataStore() { - return metadataStore; - } - - /** - * A file has been copied. - * - * @param childSource source of the file. This may actually be different - * from the path of the sourceAttributes. (HOW?) - * @param sourceAttributes status of source. - * @param destAttributes destination attributes - * @param destPath destination path. - * @param blockSize block size. - * @param addAncestors should ancestors be added? - * @throws IOException failure. - */ - public abstract void fileCopied( - Path childSource, - S3ObjectAttributes sourceAttributes, - S3ObjectAttributes destAttributes, - Path destPath, - long blockSize, - boolean addAncestors) throws IOException; - - /** - * A directory marker has been copied. - * @param sourcePath source path. - * @param destPath destination path. - * @param addAncestors should ancestors be added? - * @throws IOException failure. - */ - public void directoryMarkerCopied( - Path sourcePath, - Path destPath, - boolean addAncestors) throws IOException { - } - - /** - * The delete failed. - *

    - * By the time this is called, the metastore will already have - * been updated with the results of any partial delete failure, - * such that all files known to have been deleted will have been - * removed. - * @param e exception - * @param pathsToDelete paths which were to be deleted. - * @param undeletedObjects list of objects which were not deleted. - */ - public IOException deleteFailed( - final Exception e, - final List pathsToDelete, - final List undeletedObjects) { - - return convertToIOException(e); - } - - /** - * Top level directory move. - * This is invoked after all child entries have been copied - * @throws IOException on failure - */ - public void moveSourceDirectory() throws IOException { - } - - /** - * Note that source objects have been deleted. - * The metastore will already have been updated. - * @param paths path of objects deleted. - */ - public void sourceObjectsDeleted( - final Collection paths) throws IOException { - } - - /** - * Complete the operation. - * @throws IOException failure. - */ - public void completeRename() throws IOException { - IOUtils.cleanupWithLogger(LOG, operationState); - noteRenameFinished(); - } - - /** - * Note that the rename has finished by closing the duration info; - * this will log the duration of the operation at debug. - */ - protected void noteRenameFinished() { - durationInfo.close(); - } - - /** - * Rename has failed. - *

    - * The metastore now needs to be updated with its current state - * even though the operation is incomplete. - * Implementations MUST NOT throw exceptions here, as this is going to - * be invoked in an exception handler. - * catch and log or catch and return/wrap. - *

    - * The base implementation returns the IOE passed in and translates - * any AWS exception into an IOE. - * @param ex the exception which caused the failure. - * This is either an IOException or and AWS exception - * @return an IOException to throw in an exception. - */ - public IOException renameFailed(Exception ex) { - LOG.debug("Rename has failed", ex); - IOUtils.cleanupWithLogger(LOG, operationState); - noteRenameFinished(); - return convertToIOException(ex); - } - - /** - * Convert a passed in exception (expected to be an IOE or AWS exception) - * into an IOException. - * @param ex exception caught - * @return the exception to throw in the failure handler. - */ - protected IOException convertToIOException(final Exception ex) { - if (ex instanceof IOException) { - return (IOException) ex; - } else if (ex instanceof SdkBaseException) { - return translateException("rename " + sourceRoot + " to " + dest, - sourceRoot.toString(), - (SdkBaseException) ex); - } else { - // should never happen, but for completeness - return new IOException(ex); - } - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/RetryingCollection.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/RetryingCollection.java deleted file mode 100644 index 108d205f74b..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/RetryingCollection.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.util.Iterator; - -import org.apache.hadoop.fs.s3a.Invoker; -import org.apache.hadoop.fs.s3a.Retries; - -/** - * A collection which wraps the result of a query or scan - * with retries. - * Important: iterate through this only once; the outcome - * of repeating an iteration is "undefined" - * @param type of outcome. - */ -class RetryingCollection implements Iterable { - - /** - * Source iterable. - */ - private final Iterable source; - - /** - * Invoker for retries. - */ - private final Invoker invoker; - - /** - * Operation name for invoker.retry messages. - */ - private final String operation; - - /** - * Constructor. - * @param operation Operation name for invoker.retry messages. - * @param invoker Invoker for retries. - * @param source Source iterable. - */ - RetryingCollection( - final String operation, - final Invoker invoker, - final Iterable source) { - this.operation = operation; - this.source = source; - this.invoker = invoker; - } - - /** - * Demand creates a new iterator which will retry all hasNext/next - * operations through the invoker supplied in the constructor. - * @return a new iterator. - */ - @Override - public Iterator iterator() { - return new RetryingIterator(source.iterator()); - } - - /** - * An iterator which wraps a non-retrying iterator of scan results - * (i.e {@code S3GuardTableAccess.DDBPathMetadataIterator}. - */ - private final class RetryingIterator implements Iterator { - - private final Iterator iterator; - - private RetryingIterator(final Iterator iterator) { - this.iterator = iterator; - } - - /** - * {@inheritDoc}. - * @throws UncheckedIOException for IO failure, including throttling. - */ - @Override - @Retries.RetryTranslated - public boolean hasNext() { - try { - return invoker.retry( - operation, - null, - true, - iterator::hasNext); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - /** - * {@inheritDoc}. - * @throws UncheckedIOException for IO failure, including throttling. - */ - @Override - @Retries.RetryTranslated - public T next() { - try { - return invoker.retry( - "Scan Dynamo", - null, - true, - iterator::next); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - } - -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3Guard.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3Guard.java index 2836887c71f..5e3e5dcab77 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3Guard.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3Guard.java @@ -18,1003 +18,107 @@ package org.apache.hadoop.fs.s3a.s3guard; -import java.io.FileNotFoundException; -import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.TimeUnit; import java.util.function.Function; -import java.util.stream.Collectors; -import javax.annotation.Nullable; - -import org.apache.hadoop.thirdparty.com.google.common.annotations.VisibleForTesting; -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; - -import org.apache.hadoop.fs.RemoteIterator; -import org.apache.hadoop.fs.s3a.S3AFileSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.classification.VisibleForTesting; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.Retries; -import org.apache.hadoop.fs.s3a.Retries.RetryTranslated; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.io.IOUtils; -import org.apache.hadoop.util.ExitUtil; -import org.apache.hadoop.util.ReflectionUtils; +import org.apache.hadoop.fs.PathIOException; +import org.apache.hadoop.fs.s3a.S3AFileSystem; -import static org.apache.hadoop.fs.s3a.Constants.*; +import static org.apache.hadoop.fs.s3a.Constants.AUTHORITATIVE_PATH; import static org.apache.hadoop.fs.s3a.Constants.DEFAULT_AUTHORITATIVE_PATH; -import static org.apache.hadoop.fs.s3a.S3AUtils.createUploadFileStatus; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.authoritativeEmptyDirectoryMarker; -import static org.apache.hadoop.service.launcher.LauncherExitCodes.EXIT_BAD_CONFIGURATION; +import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_METASTORE_DYNAMO; +import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_METASTORE_LOCAL; +import static org.apache.hadoop.fs.s3a.Constants.S3_METADATA_STORE_IMPL; /** * Logic for integrating MetadataStore with S3A. + * Most of the methods here were deleted when the S3Guard feature was removed. */ +@SuppressWarnings("deprecation") @InterfaceAudience.Private @InterfaceStability.Unstable public final class S3Guard { private static final Logger LOG = LoggerFactory.getLogger(S3Guard.class); - @InterfaceAudience.Private - @InterfaceStability.Unstable - @VisibleForTesting - public static final String S3GUARD_DDB_CLIENT_FACTORY_IMPL = - "fs.s3a.s3guard.ddb.client.factory.impl"; - - static final Class - S3GUARD_DDB_CLIENT_FACTORY_IMPL_DEFAULT = - DynamoDBClientFactory.DefaultDynamoDBClientFactory.class; - private static final S3AFileStatus[] EMPTY_LISTING = new S3AFileStatus[0]; - - /** - * Hard-coded policy : {@value}. - * If true, when merging an S3 LIST with S3Guard in non-auth mode, - * only updated entries are added; new entries are left out. - * This policy choice reduces the amount of data stored in Dynamo, - * and hence the complexity of the merge in a non-auth listing. - */ - @VisibleForTesting - public static final boolean DIR_MERGE_UPDATES_ALL_RECORDS_NONAUTH = false; + static final String NULL_METADATA_STORE + = "org.apache.hadoop.fs.s3a.s3guard.NullMetadataStore"; // Utility class. All static functions. - private S3Guard() { } - - /* Utility functions. */ - - /** - * Create a new instance of the configured MetadataStore. - * The returned MetadataStore will have been initialized via - * {@link MetadataStore#initialize(FileSystem, ITtlTimeProvider)} - * by this function before returning it. Callers must clean up by calling - * {@link MetadataStore#close()} when done using the MetadataStore. - * - * @param fs FileSystem whose Configuration specifies which - * implementation to use. - * @param ttlTimeProvider - * @return Reference to new MetadataStore. - * @throws IOException if the metadata store cannot be instantiated - */ - @Retries.OnceTranslated - public static MetadataStore getMetadataStore(FileSystem fs, - ITtlTimeProvider ttlTimeProvider) - throws IOException { - Preconditions.checkNotNull(fs); - Configuration conf = fs.getConf(); - Preconditions.checkNotNull(conf); - MetadataStore msInstance; - try { - Class msClass = getMetadataStoreClass(conf); - msInstance = ReflectionUtils.newInstance(msClass, conf); - LOG.debug("Using {} metadata store for {} filesystem", - msClass.getSimpleName(), fs.getScheme()); - msInstance.initialize(fs, ttlTimeProvider); - return msInstance; - } catch (FileNotFoundException e) { - // Don't log this exception as it means the table doesn't exist yet; - // rely on callers to catch and treat specially - throw e; - } catch (RuntimeException | IOException e) { - String message = "Failed to instantiate metadata store " + - conf.get(S3_METADATA_STORE_IMPL) - + " defined in " + S3_METADATA_STORE_IMPL - + ": " + e; - LOG.error(message, e); - if (e instanceof IOException) { - throw e; - } else { - throw new IOException(message, e); - } - } - } - - static Class getMetadataStoreClass( - Configuration conf) { - if (conf == null) { - return NullMetadataStore.class; - } - if (conf.get(S3_METADATA_STORE_IMPL) != null && LOG.isDebugEnabled()) { - LOG.debug("Metastore option source {}", - (Object)conf.getPropertySources(S3_METADATA_STORE_IMPL)); - } - - Class aClass = conf.getClass( - S3_METADATA_STORE_IMPL, NullMetadataStore.class, - MetadataStore.class); - return aClass; - } - - - /** - * We update the metastore for the specific case of S3 value == S3Guard value - * so as to place a more recent modtime in the store. - * because if not, we will continue to probe S3 whenever we look for this - * object, even we only do this if confident the S3 status is the same - * as the one in the store (i.e. it is not an older version) - * @param metadataStore MetadataStore to {@code put()} into. - * @param pm current data - * @param s3AFileStatus status to store - * @param timeProvider Time provider to use when writing entries - * @return true if the entry was updated. - * @throws IOException if metadata store update failed - */ - @RetryTranslated - public static boolean refreshEntry( - MetadataStore metadataStore, - PathMetadata pm, - S3AFileStatus s3AFileStatus, - ITtlTimeProvider timeProvider) throws IOException { - // the modtime of the data is the same as/older than the s3guard value - // either an old object has been found, or the existing one was retrieved - // in both cases -return s3guard value - S3AFileStatus msStatus = pm.getFileStatus(); - - // first check: size - boolean sizeMatch = msStatus.getLen() == s3AFileStatus.getLen(); - - // etags are expected on all objects, but handle the situation - // that a third party store doesn't serve them. - String s3Etag = s3AFileStatus.getETag(); - String pmEtag = msStatus.getETag(); - boolean etagsMatch = s3Etag != null && s3Etag.equals(pmEtag); - - // version ID: only in some stores, and will be missing in the metastore - // if the entry was created through a list operation. - String s3VersionId = s3AFileStatus.getVersionId(); - String pmVersionId = msStatus.getVersionId(); - boolean versionsMatchOrMissingInMetastore = - pmVersionId == null || pmVersionId.equals(s3VersionId); - if (sizeMatch && etagsMatch && versionsMatchOrMissingInMetastore) { - // update the store, return the new value - LOG.debug("Refreshing the metastore entry/timestamp"); - putAndReturn(metadataStore, s3AFileStatus, timeProvider); - return true; - } - return false; + private S3Guard() { } /** - * Helper function which puts a given S3AFileStatus into the MetadataStore and - * returns the same S3AFileStatus. Instrumentation monitors the put operation. - * @param ms MetadataStore to {@code put()} into. - * @param status status to store - * @param timeProvider Time provider to use when writing entries - * @return The same status as passed in - * @throws IOException if metadata store update failed + * Assert that the FS is not configured to use an unsupported S3Guard + * option. + * the empty/null option is preferred. + * if the local store or null store is requested, the configuration is + * allowed. + * request for a DynamoDB or other store class will raise an exception. + * @param fsURI FileSystem URI + * @param conf configuration + * @return true if an option was set but ignored + * @throws PathIOException if an unsupported metastore was found. */ - @RetryTranslated - public static S3AFileStatus putAndReturn(MetadataStore ms, - S3AFileStatus status, - ITtlTimeProvider timeProvider) throws IOException { - return putAndReturn(ms, status, timeProvider, null); - } + public static boolean checkNoS3Guard(URI fsURI, Configuration conf) throws PathIOException { + final String classname = conf.getTrimmed(S3_METADATA_STORE_IMPL, ""); - /** - * Helper function which puts a given S3AFileStatus into the MetadataStore and - * returns the same S3AFileStatus. Instrumentation monitors the put operation. - * @param ms MetadataStore to {@code put()} into. - * @param status status to store - * @param timeProvider Time provider to use when writing entries - * @param operationState possibly-null metastore state tracker. - * @return The same status as passed in - * @throws IOException if metadata store update failed - */ - @RetryTranslated - public static S3AFileStatus putAndReturn( - final MetadataStore ms, - final S3AFileStatus status, - final ITtlTimeProvider timeProvider, - @Nullable final BulkOperationState operationState) throws IOException { - long startTimeNano = System.nanoTime(); - try { - putWithTtl(ms, new PathMetadata(status), timeProvider, operationState); - } finally { - ms.getInstrumentation().entryAdded((System.nanoTime() - startTimeNano)); + if (classname.isEmpty()) { + // all good. declare nothing was found. + return false; } - return status; - } + // there is a s3guard configuration option + // ignore if harmless; reject if DDB or unknown + final String[] sources = conf.getPropertySources(S3_METADATA_STORE_IMPL); + final String origin = sources == null + ? "unknown" + : sources[0]; + final String fsPath = fsURI.toString(); + switch (classname) { + case NULL_METADATA_STORE: + // harmless + LOG.debug("Ignoring S3Guard store option of {} -no longer needed " + + "Origin {}", + NULL_METADATA_STORE, origin); + break; + case S3GUARD_METASTORE_LOCAL: + // used in some libraries (e.g. hboss) to force a consistent s3 in a test + // run. + // print a message and continue + LOG.warn("Ignoring S3Guard store option of {} -no longer needed or supported. " + + "Origin {}", + S3GUARD_METASTORE_LOCAL, origin); + break; + case S3GUARD_METASTORE_DYNAMO: + // this is the dangerous one, as it is a sign that a config is in use where + // older releases will use DDB for listing metadata, yet this + // client will not update it. + final String message = String.format("S3Guard is no longer needed/supported," + + " yet %s is configured to use DynamoDB as the S3Guard metadata store." + + " This is no longer needed or supported. " + + "Origin of setting is %s", + fsPath, origin); + LOG.error(message); + throw new PathIOException(fsPath, message); - /** - * Creates an authoritative directory marker for the store. - * @param ms MetadataStore to {@code put()} into. - * @param status status to store - * @param timeProvider Time provider to use when writing entries - * @param operationState possibly-null metastore state tracker. - * @throws IOException if metadata store update failed - */ - @RetryTranslated - public static void putAuthDirectoryMarker( - final MetadataStore ms, - final S3AFileStatus status, - final ITtlTimeProvider timeProvider, - @Nullable final BulkOperationState operationState) throws IOException { - long startTimeNano = System.nanoTime(); - try { - final PathMetadata fileMeta = authoritativeEmptyDirectoryMarker(status); - putWithTtl(ms, fileMeta, timeProvider, operationState); - } finally { - ms.getInstrumentation().directoryMarkedAuthoritative(); - ms.getInstrumentation().entryAdded((System.nanoTime() - startTimeNano)); - } - } - - /** - * Initiate a bulk write and create an operation state for it. - * This may then be passed into put operations. - * @param metastore store - * @param operation the type of the operation. - * @param path path under which updates will be explicitly put. - * @return a store-specific state to pass into the put operations, or null - * @throws IOException failure - */ - public static BulkOperationState initiateBulkWrite( - @Nullable final MetadataStore metastore, - final BulkOperationState.OperationType operation, - final Path path) throws IOException { - Preconditions.checkArgument( - operation != BulkOperationState.OperationType.Rename, - "Rename operations cannot be started through initiateBulkWrite"); - if (metastore == null || isNullMetadataStore(metastore)) { - return null; - } else { - return metastore.initiateBulkWrite(operation, path); - } - } - - /** - * Convert the data of a directory listing to an array of {@link FileStatus} - * entries. Tombstones are filtered out at this point. If the listing is null - * an empty array is returned. - * @param dirMeta directory listing -may be null - * @return a possibly-empty array of file status entries - */ - public static S3AFileStatus[] dirMetaToStatuses(DirListingMetadata dirMeta) { - if (dirMeta == null) { - return EMPTY_LISTING; + default: + // an unknown store entirely. + throw new PathIOException(fsPath, + "Filesystem is configured to use unknown S3Guard store " + classname + + " origin " + origin); } - Collection listing = dirMeta.getListing(); - List statuses = new ArrayList<>(); - - for (PathMetadata pm : listing) { - if (!pm.isDeleted()) { - statuses.add(pm.getFileStatus()); - } - } - - return statuses.toArray(new S3AFileStatus[0]); - } - - /** - * Given directory listing metadata from both the backing store and the - * MetadataStore, merge the two sources of truth to create a consistent - * view of the current directory contents, which can be returned to clients. - * - * Also update the MetadataStore to reflect the resulting directory listing. - * - * In not authoritative case: update file metadata if mod_time in listing - * of a file is greater then what is currently in the ms - * - * @param ms MetadataStore to use. - * @param path path to directory - * @param backingStatuses Directory listing from the backing store. - * @param dirMeta Directory listing from MetadataStore. May be null. - * @param isAuthoritative State of authoritative mode - * @param timeProvider Time provider to use when updating entries - * @param toStatusItr function to convert array of file status to - * RemoteIterator. - * @return Final result of directory listing. - * @throws IOException if metadata store update failed - */ - public static RemoteIterator dirListingUnion( - MetadataStore ms, Path path, - RemoteIterator backingStatuses, - DirListingMetadata dirMeta, boolean isAuthoritative, - ITtlTimeProvider timeProvider, - Function> toStatusItr) - throws IOException { - - // Fast-path for NullMetadataStore - if (isNullMetadataStore(ms)) { - return backingStatuses; - } - - assertQualified(path); - - if (dirMeta == null) { - // The metadataStore had zero state for this directory - dirMeta = new DirListingMetadata(path, DirListingMetadata.EMPTY_DIR, - false); - } - - // Since we treat the MetadataStore as a "fresher" or "consistent" view - // of metadata, we always use its metadata first. - - // Since the authoritative case is already handled outside this function, - // we will basically start with the set of directory entries in the - // DirListingMetadata, and add any that only exist in the backingStatuses. - // - // We try to avoid writing any more child entries than need be to :- - // (a) save time and money. - // (b) avoid overwriting the authoritative bit of children (HADOOP-16746). - // For auth mode updates, we supply the full listing and a list of which - // child entries have not been changed; the store gets to optimize its - // update however it chooses. - // - // for non-auth-mode S3Guard, we just build a list of entries to add and - // submit them in a batch; this is more efficient than trickling out the - // updates one-by-one. - - BulkOperationState operationState = ms.initiateBulkWrite( - BulkOperationState.OperationType.Listing, - path); - if (isAuthoritative) { - authoritativeUnion(ms, path, backingStatuses, dirMeta, - timeProvider, operationState); - } else { - nonAuthoritativeUnion(ms, path, backingStatuses, dirMeta, - timeProvider, operationState); - } - IOUtils.cleanupWithLogger(LOG, operationState); - - return toStatusItr.apply(dirMetaToStatuses(dirMeta)); - } - - /** - * Perform the authoritative union operation. - * Here all updated/missing entries are added back; we take care - * not to overwrite unchanged entries as that will lose their - * isAuthoritative bit (HADOOP-16746). - * @param ms MetadataStore to use. - * @param path path to directory - * @param backingStatuses Directory listing from the backing store. - * @param dirMeta Directory listing from MetadataStore. May be null. - * @param timeProvider Time provider to use when updating entries - * @param operationState ongoing operation - * @throws IOException if metadata store update failed - */ - private static void authoritativeUnion( - final MetadataStore ms, - final Path path, - final RemoteIterator backingStatuses, - final DirListingMetadata dirMeta, - final ITtlTimeProvider timeProvider, - final BulkOperationState operationState) throws IOException { - // track all unchanged entries; used so the metastore can identify entries - // it doesn't need to update - List unchangedEntries = new ArrayList<>(dirMeta.getListing().size()); - boolean changed = !dirMeta.isAuthoritative(); - Set deleted = dirMeta.listTombstones(); - final Map dirMetaMap = dirMeta.getListing().stream() - .collect(Collectors.toMap(pm -> pm.getFileStatus().getPath(), pm -> pm)); - while (backingStatuses.hasNext()) { - S3AFileStatus s = backingStatuses.next(); - final Path statusPath = s.getPath(); - if (deleted.contains(statusPath)) { - continue; - } - - // this is built up to be whatever entry is to be added to the dirMeta - // collection - PathMetadata pathMetadata = dirMetaMap.get(statusPath); - - if (pathMetadata == null) { - // there's no entry in the listing, so create one. - pathMetadata = new PathMetadata(s); - } else { - // no change -add the path to the list of unchangedEntries - unchangedEntries.add(statusPath); - } - - // Minor race condition here. Multiple threads could add to this - // mutable DirListingMetadata. Since it is backed by a - // ConcurrentHashMap, the last put() wins. - // More concerning is two threads racing on listStatus() and delete(). - // Any FileSystem has similar race conditions, but we could persist - // a stale entry longer. We could expose an atomic - // DirListingMetadata#putIfNotPresent() - changed |= dirMeta.put(pathMetadata); - } - - if (changed) { - // in an authoritative update, we pass in the full list of entries, - // but do declare which have not changed to avoid needless and potentially - // destructive overwrites. - LOG.debug("Marking the directory {} as authoritative", path); - ms.getInstrumentation().directoryMarkedAuthoritative(); - dirMeta.setAuthoritative(true); // This is the full directory contents - // write the updated dir entry and any changed children. - S3Guard.putWithTtl(ms, dirMeta, unchangedEntries, timeProvider, operationState); - } - } - - /** - * Perform the authoritative union operation. - * @param ms MetadataStore to use. - * @param path path to directory - * @param backingStatuses Directory listing from the backing store. - * @param dirMeta Directory listing from MetadataStore. May be null. - * @param timeProvider Time provider to use when updating entries - * @param operationState ongoing operation - * @throws IOException if metadata store update failed - */ - private static void nonAuthoritativeUnion( - final MetadataStore ms, - final Path path, - final RemoteIterator backingStatuses, - final DirListingMetadata dirMeta, - final ITtlTimeProvider timeProvider, - final BulkOperationState operationState) throws IOException { - List entriesToAdd = new ArrayList<>(); - Set deleted = dirMeta.listTombstones(); - - final Map dirMetaMap = dirMeta.getListing().stream() - .collect(Collectors.toMap(pm -> pm.getFileStatus().getPath(), pm -> pm)); - while (backingStatuses.hasNext()) { - S3AFileStatus s = backingStatuses.next(); - final Path statusPath = s.getPath(); - if (deleted.contains(statusPath)) { - continue; - } - - // this is the record in dynamo - PathMetadata pathMetadata = dirMetaMap.get(statusPath); - - // in non-auth listings, we compare the file status of the metastore - // list with those in the FS, and overwrite the MS entry if - // either of two conditions are met - // - there is no entry in the metastore and - // DIR_MERGE_UPDATES_ALL_RECORDS_NONAUTH is compiled to true - // - there is an entry in the metastore the FS entry is newer. - boolean shouldUpdate; - if (pathMetadata != null) { - // entry is in DDB; check modification time - shouldUpdate = s.getModificationTime() > (pathMetadata.getFileStatus()) - .getModificationTime(); - // create an updated record. - pathMetadata = new PathMetadata(s); - } else { - // entry is not present. Create for insertion into dirMeta - pathMetadata = new PathMetadata(s); - // use hard-coded policy about updating - shouldUpdate = DIR_MERGE_UPDATES_ALL_RECORDS_NONAUTH; - } - if (shouldUpdate) { - // we do want to update DDB and the listing with a new entry. - LOG.debug("Update ms with newer metadata of: {}", s); - // ensure it gets into the dirListing - // add to the list of entries to add later, - entriesToAdd.add(pathMetadata); - } - // add the entry to the union; no-op if it was already there. - dirMeta.put(pathMetadata); - } - - if (!entriesToAdd.isEmpty()) { - // non-auth, just push out the updated entry list - LOG.debug("Adding {} entries under directory {}", entriesToAdd.size(), path); - putWithTtl(ms, entriesToAdd, timeProvider, operationState); - } - } - - /** - * Although NullMetadataStore does nothing, callers may wish to avoid work - * (fast path) when the NullMetadataStore is in use. - * @param ms The MetadataStore to test - * @return true iff the MetadataStore is the null, or no-op, implementation. - */ - public static boolean isNullMetadataStore(MetadataStore ms) { - return (ms instanceof NullMetadataStore); - } - - /** - * Update MetadataStore to reflect creation of the given directories. - * - * If an IOException is raised while trying to update the entry, this - * operation catches the exception, swallows it and returns. - * - * @deprecated this is no longer called by {@code S3AFilesystem.innerMkDirs}. - * See: HADOOP-15079 (January 2018). - * It is currently retained because of its discussion in the method on - * atomicity and in case we need to reinstate it or adapt the current - * process of directory marker creation. - * But it is not being tested and so may age with time...consider - * deleting it in future if it's clear there's no need for it. - * @param ms MetadataStore to update. - * @param dirs null, or an ordered list of directories from leaf to root. - * E.g. if /a/ exists, and mkdirs(/a/b/c/d) is called, this - * list will contain [/a/b/c/d, /a/b/c, /a/b]. /a/b/c/d is - * an empty, dir, and the other dirs only contain their child - * dir. - * @param owner Hadoop user name. - * @param authoritative Whether to mark new directories as authoritative. - * @param timeProvider Time provider. - */ - @Deprecated - @Retries.OnceExceptionsSwallowed - public static void makeDirsOrdered(MetadataStore ms, List dirs, - String owner, boolean authoritative, ITtlTimeProvider timeProvider) { - if (dirs == null) { - return; - } - - /* We discussed atomicity of this implementation. - * The concern is that multiple clients could race to write different - * cached directories to the MetadataStore. Two solutions are proposed: - * 1. Move mkdirs() into MetadataStore interface and let implementations - * ensure they are atomic. - * 2. Specify that the semantics of MetadataStore#putListStatus() is - * always additive, That is, if MetadataStore has listStatus() state - * for /a/b that contains [/a/b/file0, /a/b/file1], and we then call - * putListStatus(/a/b -> [/a/b/file2, /a/b/file3], isAuthoritative=true), - * then we will end up with final state of - * [/a/b/file0, /a/b/file1, /a/b/file2, /a/b/file3], isAuthoritative = - * true - */ - S3AFileStatus prevStatus = null; - - // Use new batched put to reduce round trips. - List pathMetas = new ArrayList<>(dirs.size()); - - try { - // Iterate from leaf to root - for (int i = 0; i < dirs.size(); i++) { - boolean isLeaf = (prevStatus == null); - Path f = dirs.get(i); - assertQualified(f); - S3AFileStatus status = - createUploadFileStatus(f, true, 0, 0, owner, null, null); - - // We only need to put a DirListingMetadata if we are setting - // authoritative bit - DirListingMetadata dirMeta = null; - if (authoritative) { - Collection children; - if (isLeaf) { - children = DirListingMetadata.EMPTY_DIR; - } else { - children = new ArrayList<>(1); - children.add(new PathMetadata(prevStatus)); - } - dirMeta = new DirListingMetadata(f, children, authoritative); - S3Guard.putWithTtl(ms, dirMeta, Collections.emptyList(), timeProvider, null); - } - - pathMetas.add(new PathMetadata(status)); - prevStatus = status; - } - - // Batched put - S3Guard.putWithTtl(ms, pathMetas, timeProvider, null); - } catch (IOException ioe) { - LOG.error("MetadataStore#put() failure:", ioe); - } - } - - /** - * Helper function that records the move of directory paths, adding - * resulting metadata to the supplied lists. - * Does not store in MetadataStore. - * @param ms MetadataStore, used to make this a no-op, when it is - * NullMetadataStore. - * @param srcPaths stores the source path here - * @param dstMetas stores destination metadata here - * @param srcPath source path to store - * @param dstPath destination path to store - * @param owner file owner to use in created records - */ - public static void addMoveDir(MetadataStore ms, Collection srcPaths, - Collection dstMetas, Path srcPath, Path dstPath, - String owner) { - if (isNullMetadataStore(ms)) { - return; - } - assertQualified(srcPath, dstPath); - - S3AFileStatus dstStatus = createUploadFileStatus(dstPath, true, 0, - 0, owner, null, null); - addMoveStatus(srcPaths, dstMetas, srcPath, dstStatus); - } - - /** - * Like {@link #addMoveDir(MetadataStore, Collection, Collection, Path, - * Path, String)} (), but for files. - * @param ms MetadataStore, used to make this a no-op, when it is - * NullMetadataStore. - * @param srcPaths stores the source path here - * @param dstMetas stores destination metadata here - * @param srcPath source path to store - * @param dstPath destination path to store - * @param size length of file moved - * @param blockSize blocksize to associate with destination file - * @param owner file owner to use in created records - * @param eTag the s3 object eTag of file moved - * @param versionId the s3 object versionId of file moved - */ - public static void addMoveFile(MetadataStore ms, Collection srcPaths, - Collection dstMetas, Path srcPath, Path dstPath, - long size, long blockSize, String owner, String eTag, String versionId) { - if (isNullMetadataStore(ms)) { - return; - } - assertQualified(srcPath, dstPath); - S3AFileStatus dstStatus = createUploadFileStatus(dstPath, false, - size, blockSize, owner, eTag, versionId); - addMoveStatus(srcPaths, dstMetas, srcPath, dstStatus); - } - - /** - * Helper method that records the move of all ancestors of a path. - * - * In S3A, an optimization is to delete unnecessary fake directory objects if - * the directory is non-empty. In that case, for a nested child to move, S3A - * is not listing and thus moving all its ancestors (up to source root). So we - * take care of those inferred directories of this path explicitly. - * - * As {@link #addMoveFile} and {@link #addMoveDir}, this method adds resulting - * metadata to the supplied lists. It does not update the MetadataStore. - * - * @param ms MetadataStore, no-op if it is NullMetadataStore - * @param srcPaths stores the source path here - * @param dstMetas stores destination metadata here - * @param srcRoot source root up to which (exclusive) should we add ancestors - * @param srcPath source path of the child to add ancestors - * @param dstPath destination path of the child to add ancestors - * @param owner Hadoop user name - */ - public static void addMoveAncestors(MetadataStore ms, - Collection srcPaths, Collection dstMetas, - Path srcRoot, Path srcPath, Path dstPath, String owner) { - if (isNullMetadataStore(ms)) { - return; - } - - assertQualified(srcRoot, srcPath, dstPath); - - if (srcPath.equals(srcRoot)) { - LOG.debug("Skip moving ancestors of source root directory {}", srcRoot); - return; - } - - Path parentSrc = srcPath.getParent(); - Path parentDst = dstPath.getParent(); - while (parentSrc != null - && !parentSrc.isRoot() - && !parentSrc.equals(srcRoot) - && !srcPaths.contains(parentSrc)) { - LOG.debug("Renaming non-listed parent {} to {}", parentSrc, parentDst); - S3Guard.addMoveDir(ms, srcPaths, dstMetas, parentSrc, parentDst, owner); - parentSrc = parentSrc.getParent(); - parentDst = parentDst.getParent(); - } - } - - /** - * This adds all new ancestors of a path as directories. - * This forwards to - * {@link MetadataStore#addAncestors(Path, BulkOperationState)}. - *

    - * Originally it implemented the logic to probe for an add ancestors, - * but with the addition of a store-specific bulk operation state - * it became unworkable. - * - * @param metadataStore store - * @param qualifiedPath path to update - * @param operationState (nullable) operational state for a bulk update - * @throws IOException failure - */ - @Retries.RetryTranslated - public static void addAncestors( - final MetadataStore metadataStore, - final Path qualifiedPath, - final ITtlTimeProvider timeProvider, - @Nullable final BulkOperationState operationState) throws IOException { - metadataStore.addAncestors(qualifiedPath, operationState); - } - - /** - * Add the fact that a file was moved from a source path to a destination. - * @param srcPaths collection of source paths to update - * @param dstMetas collection of destination meta data entries to update. - * @param srcPath path of the source file. - * @param dstStatus status of the source file after it was copied. - */ - private static void addMoveStatus(Collection srcPaths, - Collection dstMetas, - Path srcPath, - S3AFileStatus dstStatus) { - srcPaths.add(srcPath); - dstMetas.add(new PathMetadata(dstStatus)); - } - - /** - * Assert that the path is qualified with a host and scheme. - * @param p path to check - * @throws NullPointerException if either argument does not hold - */ - public static void assertQualified(Path p) { - URI uri = p.toUri(); - // Paths must include bucket in case MetadataStore is shared between - // multiple S3AFileSystem instances - Preconditions.checkNotNull(uri.getHost(), "Null host in " + uri); - - // This should never fail, but is retained for completeness. - Preconditions.checkNotNull(uri.getScheme(), "Null scheme in " + uri); - } - - /** - * Assert that all paths are valid. - * @param paths path to check - * @throws NullPointerException if either argument does not hold - */ - public static void assertQualified(Path...paths) { - for (Path path : paths) { - assertQualified(path); - } - } - - /** - * Runtime implementation for TTL Time Provider interface. - */ - public static class TtlTimeProvider implements ITtlTimeProvider { - private long authoritativeDirTtl; - - public TtlTimeProvider(long authoritativeDirTtl) { - this.authoritativeDirTtl = authoritativeDirTtl; - } - - public TtlTimeProvider(Configuration conf) { - this.authoritativeDirTtl = - conf.getTimeDuration(METADATASTORE_METADATA_TTL, - DEFAULT_METADATASTORE_METADATA_TTL, TimeUnit.MILLISECONDS); - } - - @Override - public long getNow() { - return System.currentTimeMillis(); - } - - @Override public long getMetadataTtl() { - return authoritativeDirTtl; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { return false; } - final TtlTimeProvider that = (TtlTimeProvider) o; - return authoritativeDirTtl == that.authoritativeDirTtl; - } - - @Override - public int hashCode() { - return Objects.hash(authoritativeDirTtl); - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder( - "TtlTimeProvider{"); - sb.append("authoritativeDirTtl=").append(authoritativeDirTtl); - sb.append(" millis}"); - return sb.toString(); - } - } - - /** - * Put a directory entry, setting the updated timestamp of the - * directory and its children. - * @param ms metastore - * @param dirMeta directory - * @param unchangedEntries list of unchanged entries from the listing - * @param timeProvider nullable time provider - * @throws IOException failure. - */ - public static void putWithTtl(MetadataStore ms, - DirListingMetadata dirMeta, - final List unchangedEntries, - final ITtlTimeProvider timeProvider, - @Nullable final BulkOperationState operationState) - throws IOException { - long now = timeProvider.getNow(); - dirMeta.setLastUpdated(now); - dirMeta.getListing() - .forEach(pm -> pm.setLastUpdated(now)); - ms.put(dirMeta, unchangedEntries, operationState); - } - - /** - * Put an entry, using the time provider to set its timestamp. - * @param ms metastore - * @param fileMeta entry to write - * @param timeProvider nullable time provider - * @param operationState nullable state for a bulk update - * @throws IOException failure. - */ - public static void putWithTtl(MetadataStore ms, PathMetadata fileMeta, - @Nullable ITtlTimeProvider timeProvider, - @Nullable final BulkOperationState operationState) throws IOException { - if (timeProvider != null) { - fileMeta.setLastUpdated(timeProvider.getNow()); - } else { - LOG.debug("timeProvider is null, put {} without setting last_updated", - fileMeta); - } - ms.put(fileMeta, operationState); - } - - /** - * Put entries, using the time provider to set their timestamp. - * @param ms metastore - * @param fileMetas file metadata entries. - * @param timeProvider nullable time provider - * @param operationState nullable state for a bulk update - * @throws IOException failure. - */ - public static void putWithTtl(MetadataStore ms, - Collection fileMetas, - @Nullable ITtlTimeProvider timeProvider, - @Nullable final BulkOperationState operationState) - throws IOException { - patchLastUpdated(fileMetas, timeProvider); - ms.put(fileMetas, operationState); - } - - /** - * Patch any collection of metadata entries with the timestamp - * of a time provider. - * This MUST be used when creating new entries for directories. - * @param fileMetas file metadata entries. - * @param timeProvider nullable time provider - */ - static void patchLastUpdated( - final Collection fileMetas, - @Nullable final ITtlTimeProvider timeProvider) { - if (timeProvider != null) { - final long now = timeProvider.getNow(); - fileMetas.forEach(fileMeta -> fileMeta.setLastUpdated(now)); - } else { - LOG.debug("timeProvider is null, put {} without setting last_updated", - fileMetas); - } - } - - /** - * Get a path entry provided it is not considered expired. - * If the allowAuthoritative flag is true, return without - * checking for TTL expiry. - * @param ms metastore - * @param path path to look up. - * @param timeProvider nullable time provider - * @param needEmptyDirectoryFlag if true, implementation will - * return known state of directory emptiness. - * @param allowAuthoritative if this flag is true, the ttl won't apply to the - * metadata - so it will be returned regardless of it's expiry. - * @return the metadata or null if there as no entry. - * @throws IOException failure. - */ - public static PathMetadata getWithTtl(MetadataStore ms, Path path, - @Nullable ITtlTimeProvider timeProvider, - final boolean needEmptyDirectoryFlag, - final boolean allowAuthoritative) throws IOException { - final PathMetadata pathMetadata = ms.get(path, needEmptyDirectoryFlag); - // if timeProvider is null let's return with what the ms has - if (timeProvider == null) { - LOG.debug("timeProvider is null, returning pathMetadata as is"); - return pathMetadata; - } - - // authoritative mode is enabled for this directory, return what the ms has - if (allowAuthoritative) { - LOG.debug("allowAuthoritative is true, returning pathMetadata as is"); - return pathMetadata; - } - - long ttl = timeProvider.getMetadataTtl(); - - if (pathMetadata != null) { - // Special case: the path metadata's last updated is 0. This can happen - // eg. with an old db using this implementation - if (pathMetadata.getLastUpdated() == 0) { - LOG.debug("PathMetadata TTL for {} is 0, so it will be returned as " - + "not expired.", path); - return pathMetadata; - } - - if (!pathMetadata.isExpired(ttl, timeProvider.getNow())) { - return pathMetadata; - } else { - LOG.debug("PathMetadata TTl for {} is expired in metadata store" - + " -removing entry", path); - // delete the tombstone - ms.forgetMetadata(path); - return null; - } - } - - return null; - } - - /** - * List children; mark the result as non-auth if the TTL has expired. - * If the allowAuthoritative flag is true, return without filtering or - * checking for TTL expiry. - * If false: the expiry scan takes place and the - * TODO: should we always purge tombstones? Even in auth? - * @param ms metastore - * @param path path to look up. - * @param timeProvider nullable time provider - * @param allowAuthoritative if this flag is true, the ttl won't apply to the - * metadata - so it will be returned regardless of it's expiry. - * @return the listing of entries under a path, or null if there as no entry. - * @throws IOException failure. - */ - @RetryTranslated - public static DirListingMetadata listChildrenWithTtl(MetadataStore ms, - Path path, - @Nullable ITtlTimeProvider timeProvider, - boolean allowAuthoritative) - throws IOException { - DirListingMetadata dlm = ms.listChildren(path); - - if (timeProvider == null) { - LOG.debug("timeProvider is null, returning DirListingMetadata as is"); - return dlm; - } - - if (allowAuthoritative) { - LOG.debug("allowAuthoritative is true, returning pathMetadata as is"); - return dlm; - } - - // filter expired entries - if (dlm != null) { - List expired = dlm.removeExpiredEntriesFromListing( - timeProvider.getMetadataTtl(), - timeProvider.getNow()); - // now purge the tombstones - for (PathMetadata metadata : expired) { - if (metadata.isDeleted()) { - ms.forgetMetadata(metadata.getFileStatus().getPath()); - } - } - } - - return dlm; + // an option was set, but it was harmless + return true; } public static Collection getAuthoritativePaths(S3AFileSystem fs) { @@ -1064,16 +168,12 @@ public final class S3Guard { * Is the path for the given FS instance authoritative? * @param p path * @param fs filesystem - * @param authMetadataStore is the MS authoritative. * @param authPaths possibly empty list of authoritative paths * @return true iff the path is authoritative */ public static boolean allowAuthoritative(Path p, S3AFileSystem fs, - boolean authMetadataStore, Collection authPaths) { + Collection authPaths) { String haystack = fs.maybeAddTrailingSlash(fs.qualify(p).toString()); - if (authMetadataStore) { - return true; - } if (!authPaths.isEmpty()) { for (String needle : authPaths) { if (haystack.startsWith(needle)) { @@ -1084,64 +184,4 @@ public final class S3Guard { return false; } - /** - * Format string to use when warning that S3Guard is disabled. - */ - @VisibleForTesting - public static final String DISABLED_LOG_MSG = - "S3Guard is disabled on this bucket: %s"; - - /** - * Error string use in exception raised on an unknown log level. - */ - public static final String UNKNOWN_WARN_LEVEL = - "Unknown " + S3GUARD_DISABLED_WARN_LEVEL + " value: "; - - /** - * Warning levels to use when reporting S3Guard as disabled. - */ - public enum DisabledWarnLevel { - SILENT, - INFORM, - WARN, - FAIL - } - - /** - * Log that S3Guard is disabled -optionally raise an exception. - * @param logger Log to log to - * @param warnLevelStr string value of warn action. - * @param bucket bucket to use in log/error messages - * @throws ExitUtil.ExitException if s3guard was disabled - * and the log level is "fail" - * @throws IllegalArgumentException unknown warning level. - */ - public static void logS3GuardDisabled(Logger logger, String warnLevelStr, - String bucket) - throws ExitUtil.ExitException, IllegalArgumentException { - final DisabledWarnLevel warnLevel; - try { - warnLevel = DisabledWarnLevel.valueOf(warnLevelStr.toUpperCase(Locale.US)); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException(UNKNOWN_WARN_LEVEL + warnLevelStr, e); - } - - String text = String.format(DISABLED_LOG_MSG, bucket); - switch (warnLevel) { - case SILENT: - logger.debug(text); - break; - case INFORM: - logger.info(text); - break; - case WARN: - logger.warn(text); - break; - case FAIL: - logger.error(text); - throw new ExitUtil.ExitException(EXIT_BAD_CONFIGURATION, text); - default: - throw new IllegalArgumentException(UNKNOWN_WARN_LEVEL + warnLevelStr); - } - } } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3GuardDataAccessRetryPolicy.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3GuardDataAccessRetryPolicy.java deleted file mode 100644 index 915b94a0b7b..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3GuardDataAccessRetryPolicy.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.util.concurrent.TimeUnit; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.s3a.S3ARetryPolicy; -import org.apache.hadoop.io.retry.RetryPolicy; - -import static org.apache.hadoop.fs.s3a.Constants.*; -import static org.apache.hadoop.io.retry.RetryPolicies.exponentialBackoffRetry; - -/** - * A Retry policy whose throttling comes from the S3Guard config options. - */ -public class S3GuardDataAccessRetryPolicy extends S3ARetryPolicy { - - public S3GuardDataAccessRetryPolicy(final Configuration conf) { - super(conf); - } - - protected RetryPolicy createThrottleRetryPolicy(final Configuration conf) { - return exponentialBackoffRetry( - conf.getInt(S3GUARD_DDB_MAX_RETRIES, S3GUARD_DDB_MAX_RETRIES_DEFAULT), - conf.getTimeDuration(S3GUARD_DDB_THROTTLE_RETRY_INTERVAL, - S3GUARD_DDB_THROTTLE_RETRY_INTERVAL_DEFAULT, - TimeUnit.MILLISECONDS), - TimeUnit.MILLISECONDS); - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3GuardFsck.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3GuardFsck.java deleted file mode 100644 index 112deca54f1..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3GuardFsck.java +++ /dev/null @@ -1,764 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.security.InvalidParameterException; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import com.amazonaws.services.dynamodbv2.document.Item; -import com.amazonaws.services.dynamodbv2.document.PrimaryKey; -import com.amazonaws.services.dynamodbv2.document.ScanOutcome; -import com.amazonaws.services.dynamodbv2.document.Table; -import com.amazonaws.services.dynamodbv2.document.internal.IteratorSupport; -import com.amazonaws.services.dynamodbv2.document.spec.GetItemSpec; -import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.apache.hadoop.fs.s3a.Tristate; -import org.apache.hadoop.util.StopWatch; - -import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toSet; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.itemToPathMetadata; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.pathToKey; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.pathToParentKey; - -/** - * Main class for the FSCK factored out from S3GuardTool - * The implementation uses fixed DynamoDBMetadataStore as the backing store - * for metadata. - * - * Functions: - *

      - *
    • Checking metadata consistency between S3 and metadatastore
    • - *
    • Checking the internal metadata consistency
    • - *
    - */ -public class S3GuardFsck { - private static final Logger LOG = LoggerFactory.getLogger(S3GuardFsck.class); - public static final String ROOT_PATH_STRING = "/"; - - private final S3AFileSystem rawFS; - private final DynamoDBMetadataStore metadataStore; - - private static final long MOD_TIME_RANGE = 2000L; - - /** - * Creates an S3GuardFsck. - * @param fs the filesystem to compare to - * @param ms metadatastore the metadatastore to compare with (dynamo) - */ - public S3GuardFsck(S3AFileSystem fs, MetadataStore ms) - throws InvalidParameterException { - this.rawFS = fs; - - if (ms == null) { - throw new InvalidParameterException("S3A Bucket " + fs.getBucket() - + " should be guarded by a " - + DynamoDBMetadataStore.class.getCanonicalName()); - } - this.metadataStore = (DynamoDBMetadataStore) ms; - - Preconditions.checkArgument(!rawFS.hasMetadataStore(), - "Raw fs should not have a metadatastore."); - } - - /** - * Compares S3 to MS. - * Iterative breadth first walk on the S3 structure from a given root. - * Creates a list of pairs (metadata in S3 and in the MetadataStore) where - * the consistency or any rule is violated. - * Uses {@link S3GuardFsckViolationHandler} to handle violations. - * The violations are listed in Enums: {@link Violation} - * - * @param p the root path to start the traversal - * @return a list of {@link ComparePair} - * @throws IOException - */ - public List compareS3ToMs(Path p) throws IOException { - StopWatch stopwatch = new StopWatch(); - stopwatch.start(); - int scannedItems = 0; - - final Path rootPath = rawFS.qualify(p); - S3AFileStatus root = (S3AFileStatus) rawFS.getFileStatus(rootPath); - final List comparePairs = new ArrayList<>(); - final Queue queue = new ArrayDeque<>(); - queue.add(root); - - while (!queue.isEmpty()) { - final S3AFileStatus currentDir = queue.poll(); - - - final Path currentDirPath = currentDir.getPath(); - try { - List s3DirListing = Arrays.asList( - rawFS.listStatus(currentDirPath)); - - // Check authoritative directory flag. - compareAuthoritativeDirectoryFlag(comparePairs, currentDirPath, - s3DirListing); - // Add all descendant directory to the queue - s3DirListing.stream().filter(pm -> pm.isDirectory()) - .map(S3AFileStatus.class::cast) - .forEach(pm -> queue.add(pm)); - - // Check file and directory metadata for consistency. - final List children = s3DirListing.stream() - .filter(status -> !status.isDirectory()) - .map(S3AFileStatus.class::cast).collect(toList()); - final List compareResult = - compareS3DirContentToMs(currentDir, children); - comparePairs.addAll(compareResult); - - // Increase the scanned file size. - // One for the directory, one for the children. - scannedItems++; - scannedItems += children.size(); - } catch (FileNotFoundException e) { - LOG.error("The path has been deleted since it was queued: " - + currentDirPath, e); - } - - } - stopwatch.stop(); - - // Create a handler and handle each violated pairs - S3GuardFsckViolationHandler handler = - new S3GuardFsckViolationHandler(rawFS, metadataStore); - for (ComparePair comparePair : comparePairs) { - handler.logError(comparePair); - } - - LOG.info("Total scan time: {}s", stopwatch.now(TimeUnit.SECONDS)); - LOG.info("Scanned entries: {}", scannedItems); - - return comparePairs; - } - - /** - * Compare the directory contents if the listing is authoritative. - * - * @param comparePairs the list of compare pairs to add to - * if it contains a violation - * @param currentDirPath the current directory path - * @param s3DirListing the s3 directory listing to compare with - * @throws IOException - */ - private void compareAuthoritativeDirectoryFlag(List comparePairs, - Path currentDirPath, List s3DirListing) throws IOException { - final DirListingMetadata msDirListing = - metadataStore.listChildren(currentDirPath); - if (msDirListing != null && msDirListing.isAuthoritative()) { - ComparePair cP = new ComparePair(s3DirListing, msDirListing); - - if (s3DirListing.size() != msDirListing.numEntries()) { - cP.violations.add(Violation.AUTHORITATIVE_DIRECTORY_CONTENT_MISMATCH); - } else { - final Set msPaths = msDirListing.getListing().stream() - .map(pm -> pm.getFileStatus().getPath()).collect(toSet()); - final Set s3Paths = s3DirListing.stream() - .map(pm -> pm.getPath()).collect(toSet()); - if (!s3Paths.equals(msPaths)) { - cP.violations.add(Violation.AUTHORITATIVE_DIRECTORY_CONTENT_MISMATCH); - } - } - - if (cP.containsViolation()) { - comparePairs.add(cP); - } - } - } - - /** - * Compares S3 directory content to the metadata store. - * - * @param s3CurrentDir file status of the current directory - * @param children the contents of the directory - * @return the compare pairs with violations of consistency - * @throws IOException - */ - protected List compareS3DirContentToMs( - S3AFileStatus s3CurrentDir, - List children) throws IOException { - final Path path = s3CurrentDir.getPath(); - final PathMetadata pathMetadata = metadataStore.get(path); - List violationComparePairs = new ArrayList<>(); - - final ComparePair rootComparePair = - compareFileStatusToPathMetadata(s3CurrentDir, pathMetadata); - if (rootComparePair.containsViolation()) { - violationComparePairs.add(rootComparePair); - } - - children.forEach(s3ChildMeta -> { - try { - final PathMetadata msChildMeta = - metadataStore.get(s3ChildMeta.getPath()); - final ComparePair comparePair = - compareFileStatusToPathMetadata(s3ChildMeta, msChildMeta); - if (comparePair.containsViolation()) { - violationComparePairs.add(comparePair); - } - } catch (Exception e) { - LOG.error(e.getMessage(), e); - } - }); - - return violationComparePairs; - } - - /** - * Compares a {@link S3AFileStatus} from S3 to a {@link PathMetadata} - * from the metadata store. Finds violated invariants and consistency - * issues. - * - * @param s3FileStatus the file status from S3 - * @param msPathMetadata the path metadata from metadatastore - * @return {@link ComparePair} with the found issues - * @throws IOException - */ - protected ComparePair compareFileStatusToPathMetadata( - S3AFileStatus s3FileStatus, - PathMetadata msPathMetadata) throws IOException { - final Path path = s3FileStatus.getPath(); - - if (msPathMetadata != null) { - LOG.info("Path: {} - Length S3: {}, MS: {} " + - "- Etag S3: {}, MS: {} ", - path, - s3FileStatus.getLen(), msPathMetadata.getFileStatus().getLen(), - s3FileStatus.getETag(), msPathMetadata.getFileStatus().getETag()); - } else { - LOG.info("Path: {} - Length S3: {} - Etag S3: {}, no record in MS.", - path, s3FileStatus.getLen(), s3FileStatus.getETag()); - } - - ComparePair comparePair = new ComparePair(s3FileStatus, msPathMetadata); - - if (!path.equals(path(ROOT_PATH_STRING))) { - final Path parentPath = path.getParent(); - final PathMetadata parentPm = metadataStore.get(parentPath); - - if (parentPm == null) { - comparePair.violations.add(Violation.NO_PARENT_ENTRY); - } else { - if (!parentPm.getFileStatus().isDirectory()) { - comparePair.violations.add(Violation.PARENT_IS_A_FILE); - } - if (parentPm.isDeleted()) { - comparePair.violations.add(Violation.PARENT_TOMBSTONED); - } - } - } else { - LOG.debug("Entry is in the root directory, so there's no parent"); - } - - // If the msPathMetadata is null, we RETURN because - // there is no metadata compare with - if (msPathMetadata == null) { - comparePair.violations.add(Violation.NO_METADATA_ENTRY); - return comparePair; - } - - final S3AFileStatus msFileStatus = msPathMetadata.getFileStatus(); - if (s3FileStatus.isDirectory() && !msFileStatus.isDirectory()) { - comparePair.violations.add(Violation.DIR_IN_S3_FILE_IN_MS); - } - if (!s3FileStatus.isDirectory() && msFileStatus.isDirectory()) { - comparePair.violations.add(Violation.FILE_IN_S3_DIR_IN_MS); - } - - if(msPathMetadata.isDeleted()) { - comparePair.violations.add(Violation.TOMBSTONED_IN_MS_NOT_DELETED_IN_S3); - } - - /** - * Attribute check - */ - if (s3FileStatus.getLen() != msFileStatus.getLen()) { - comparePair.violations.add(Violation.LENGTH_MISMATCH); - } - - // ModTime should be in the accuracy range defined. - long modTimeDiff = Math.abs( - s3FileStatus.getModificationTime() - msFileStatus.getModificationTime() - ); - if (modTimeDiff > MOD_TIME_RANGE) { - comparePair.violations.add(Violation.MOD_TIME_MISMATCH); - } - - if(msPathMetadata.getFileStatus().getVersionId() == null - || s3FileStatus.getVersionId() == null) { - LOG.debug("Missing versionIDs skipped. A HEAD request is " - + "required for each object to get the versionID."); - } else if(!s3FileStatus.getVersionId().equals(msFileStatus.getVersionId())) { - comparePair.violations.add(Violation.VERSIONID_MISMATCH); - } - - // check etag only for files, and not directories - if (!s3FileStatus.isDirectory()) { - if (msPathMetadata.getFileStatus().getETag() == null) { - comparePair.violations.add(Violation.NO_ETAG); - } else if (s3FileStatus.getETag() != null && - !s3FileStatus.getETag().equals(msFileStatus.getETag())) { - comparePair.violations.add(Violation.ETAG_MISMATCH); - } - } - - return comparePair; - } - - private Path path(String s) { - return rawFS.makeQualified(new Path(s)); - } - - /** - * Fix violations found during check. - * - * Currently only supports handling the following violation: - * - Violation.ORPHAN_DDB_ENTRY - * - * @param violations to be handled - * @throws IOException throws the error if there's any during handling - */ - public void fixViolations(List violations) throws IOException { - S3GuardFsckViolationHandler handler = - new S3GuardFsckViolationHandler(rawFS, metadataStore); - - for (ComparePair v : violations) { - if (v.getViolations().contains(Violation.ORPHAN_DDB_ENTRY)) { - try { - handler.doFix(v); - } catch (IOException e) { - LOG.error("Error during handling the violation: ", e); - throw e; - } - } - } - } - - /** - * A compare pair with the pair of metadata and the list of violations. - */ - public static class ComparePair { - private final S3AFileStatus s3FileStatus; - private final PathMetadata msPathMetadata; - - private final List s3DirListing; - private final DirListingMetadata msDirListing; - - private final Path path; - - private final Set violations = new HashSet<>(); - - ComparePair(S3AFileStatus status, PathMetadata pm) { - this.s3FileStatus = status; - this.msPathMetadata = pm; - this.s3DirListing = null; - this.msDirListing = null; - if (status != null) { - this.path = status.getPath(); - } else { - this.path = pm.getFileStatus().getPath(); - } - } - - ComparePair(List s3DirListing, DirListingMetadata msDirListing) { - this.s3DirListing = s3DirListing; - this.msDirListing = msDirListing; - this.s3FileStatus = null; - this.msPathMetadata = null; - this.path = msDirListing.getPath(); - } - - public S3AFileStatus getS3FileStatus() { - return s3FileStatus; - } - - public PathMetadata getMsPathMetadata() { - return msPathMetadata; - } - - public Set getViolations() { - return violations; - } - - public boolean containsViolation() { - return !violations.isEmpty(); - } - - public DirListingMetadata getMsDirListing() { - return msDirListing; - } - - public List getS3DirListing() { - return s3DirListing; - } - - public Path getPath() { - return path; - } - - @Override public String toString() { - return "ComparePair{" + "s3FileStatus=" + s3FileStatus - + ", msPathMetadata=" + msPathMetadata + ", s3DirListing=" + - s3DirListing + ", msDirListing=" + msDirListing + ", path=" - + path + ", violations=" + violations + '}'; - } - } - - /** - * Check the DynamoDB metadatastore internally for consistency. - *
    -   * Tasks to do here:
    -   *  - find orphan entries (entries without a parent).
    -   *  - find if a file's parent is not a directory (so the parent is a file).
    -   *  - find entries where the parent is a tombstone.
    -   *  - warn: no lastUpdated field.
    -   * 
    - */ - public List checkDdbInternalConsistency(Path basePath) - throws IOException { - Preconditions.checkArgument(basePath.isAbsolute(), "path must be absolute"); - - List comparePairs = new ArrayList<>(); - String rootStr = basePath.toString(); - LOG.info("Root for internal consistency check: {}", rootStr); - StopWatch stopwatch = new StopWatch(); - stopwatch.start(); - - final Table table = metadataStore.getTable(); - final String username = metadataStore.getUsername(); - DDBTree ddbTree = new DDBTree(); - - /* - * I. Root node construction - * - If the root node is the real bucket root, a node is constructed instead of - * doing a query to the ddb because the bucket root is not stored. - * - If the root node is not a real bucket root then the entry is queried from - * the ddb and constructed from the result. - */ - - DDBPathMetadata baseMeta; - - if (!basePath.isRoot()) { - PrimaryKey rootKey = pathToKey(basePath); - final GetItemSpec spec = new GetItemSpec() - .withPrimaryKey(rootKey) - .withConsistentRead(true); - final Item baseItem = table.getItem(spec); - baseMeta = itemToPathMetadata(baseItem, username); - - if (baseMeta == null) { - throw new FileNotFoundException( - "Base element metadata is null. " + - "This means the base path element is missing, or wrong path was " + - "passed as base path to the internal ddb consistency checker."); - } - } else { - baseMeta = new DDBPathMetadata( - new S3AFileStatus(Tristate.UNKNOWN, basePath, username) - ); - } - - DDBTreeNode root = new DDBTreeNode(baseMeta); - ddbTree.addNode(root); - ddbTree.setRoot(root); - - /* - * II. Build and check the descendant tree: - * 1. query all nodes where the prefix is the given root, and put it in the tree - * 2. Check connectivity: check if each parent is in the hashmap - * - This is done in O(n): we only need to find the parent based on the - * path with a hashmap lookup. - * - Do a test if the graph is connected - if the parent is not in the - * hashmap, we found an orphan entry. - * - * 3. Do test the elements for errors: - * - File is a parent of a file. - * - Entries where the parent is tombstoned but the entries are not. - * - Warn on no lastUpdated field. - * - */ - ExpressionSpecBuilder builder = new ExpressionSpecBuilder(); - builder.withCondition( - ExpressionSpecBuilder.S("parent") - .beginsWith(pathToParentKey(basePath)) - ); - final IteratorSupport resultIterator = table.scan( - builder.buildForScan()).iterator(); - resultIterator.forEachRemaining(item -> { - final DDBPathMetadata pmd = itemToPathMetadata(item, username); - DDBTreeNode ddbTreeNode = new DDBTreeNode(pmd); - ddbTree.addNode(ddbTreeNode); - }); - - LOG.debug("Root: {}", ddbTree.getRoot()); - - for (Map.Entry entry : ddbTree.getContentMap().entrySet()) { - final DDBTreeNode node = entry.getValue(); - final ComparePair pair = new ComparePair(null, node.val); - // let's skip the root node when checking. - if (node.getVal().getFileStatus().getPath().isRoot()) { - continue; - } - - if(node.getVal().getLastUpdated() == 0) { - pair.violations.add(Violation.NO_LASTUPDATED_FIELD); - } - - // skip further checking the basenode which is not the actual bucket root. - if (node.equals(ddbTree.getRoot())) { - continue; - } - - final Path parent = node.getFileStatus().getPath().getParent(); - final DDBTreeNode parentNode = ddbTree.getContentMap().get(parent); - if (parentNode == null) { - pair.violations.add(Violation.ORPHAN_DDB_ENTRY); - } else { - if (!node.isTombstoned() && !parentNode.isDirectory()) { - pair.violations.add(Violation.PARENT_IS_A_FILE); - } - if(!node.isTombstoned() && parentNode.isTombstoned()) { - pair.violations.add(Violation.PARENT_TOMBSTONED); - } - } - - if (!pair.violations.isEmpty()) { - comparePairs.add(pair); - } - - node.setParent(parentNode); - } - - // Create a handler and handle each violated pairs - S3GuardFsckViolationHandler handler = - new S3GuardFsckViolationHandler(rawFS, metadataStore); - for (ComparePair comparePair : comparePairs) { - handler.logError(comparePair); - } - - stopwatch.stop(); - LOG.info("Total scan time: {}s", stopwatch.now(TimeUnit.SECONDS)); - LOG.info("Scanned entries: {}", ddbTree.contentMap.size()); - - return comparePairs; - } - - /** - * DDBTree is the tree that represents the structure of items in the DynamoDB. - */ - public static class DDBTree { - private final Map contentMap = new HashMap<>(); - private DDBTreeNode root; - - public DDBTree() { - } - - public Map getContentMap() { - return contentMap; - } - - public DDBTreeNode getRoot() { - return root; - } - - public void setRoot(DDBTreeNode root) { - this.root = root; - } - - public void addNode(DDBTreeNode pm) { - contentMap.put(pm.getVal().getFileStatus().getPath(), pm); - } - - @Override - public String toString() { - return "DDBTree{" + - "contentMap=" + contentMap + - ", root=" + root + - '}'; - } - } - - /** - * Tree node for DDBTree. - */ - private static final class DDBTreeNode { - private final DDBPathMetadata val; - private DDBTreeNode parent; - private final List children; - - private DDBTreeNode(DDBPathMetadata pm) { - this.val = pm; - this.parent = null; - this.children = new ArrayList<>(); - } - - public DDBPathMetadata getVal() { - return val; - } - - public DDBTreeNode getParent() { - return parent; - } - - public void setParent(DDBTreeNode parent) { - this.parent = parent; - } - - public List getChildren() { - return children; - } - - public boolean isDirectory() { - return val.getFileStatus().isDirectory(); - } - - public S3AFileStatus getFileStatus() { - return val.getFileStatus(); - } - - public boolean isTombstoned() { - return val.isDeleted(); - } - - @Override - public String toString() { - return "DDBTreeNode{" + - "val=" + val + - ", parent=" + parent + - ", children=" + children + - '}'; - } - } - - /** - * Violation with severity and the handler. - * Defines the severity of the violation between 0-2 - * where 0 is the most severe and 2 is the least severe. - */ - public enum Violation { - /** - * No entry in metadatastore. - */ - NO_METADATA_ENTRY(1, - S3GuardFsckViolationHandler.NoMetadataEntry.class), - /** - * A file or directory entry does not have a parent entry - excluding - * files and directories in the root. - */ - NO_PARENT_ENTRY(0, - S3GuardFsckViolationHandler.NoParentEntry.class), - /** - * An entry’s parent is a file. - */ - PARENT_IS_A_FILE(0, - S3GuardFsckViolationHandler.ParentIsAFile.class), - /** - * A file exists under a path for which there is a - * tombstone entry in the MS. - */ - PARENT_TOMBSTONED(0, - S3GuardFsckViolationHandler.ParentTombstoned.class), - /** - * A directory in S3 is a file entry in the MS. - */ - DIR_IN_S3_FILE_IN_MS(0, - S3GuardFsckViolationHandler.DirInS3FileInMs.class), - /** - * A file in S3 is a directory in the MS. - */ - FILE_IN_S3_DIR_IN_MS(0, - S3GuardFsckViolationHandler.FileInS3DirInMs.class), - AUTHORITATIVE_DIRECTORY_CONTENT_MISMATCH(1, - S3GuardFsckViolationHandler.AuthDirContentMismatch.class), - /** - * An entry in the MS is tombstoned, but the object is not deleted on S3. - */ - TOMBSTONED_IN_MS_NOT_DELETED_IN_S3(0, - S3GuardFsckViolationHandler.TombstonedInMsNotDeletedInS3.class), - /** - * Attribute mismatch. - */ - LENGTH_MISMATCH(0, - S3GuardFsckViolationHandler.LengthMismatch.class), - MOD_TIME_MISMATCH(2, - S3GuardFsckViolationHandler.ModTimeMismatch.class), - /** - * If there's a versionID the mismatch is severe. - */ - VERSIONID_MISMATCH(0, - S3GuardFsckViolationHandler.VersionIdMismatch.class), - /** - * If there's an etag the mismatch is severe. - */ - ETAG_MISMATCH(0, - S3GuardFsckViolationHandler.EtagMismatch.class), - /** - * Don't worry too much if we don't have an etag. - */ - NO_ETAG(2, - S3GuardFsckViolationHandler.NoEtag.class), - /** - * The entry does not have a parent in ddb. - */ - ORPHAN_DDB_ENTRY(0, S3GuardFsckViolationHandler.OrphanDDBEntry.class), - /** - * The entry's lastUpdated field is empty. - */ - NO_LASTUPDATED_FIELD(2, - S3GuardFsckViolationHandler.NoLastUpdatedField.class); - - private final int severity; - private final Class handler; - - Violation(int s, - Class h) { - this.severity = s; - this.handler = h; - } - - public int getSeverity() { - return severity; - } - - public Class getHandler() { - return handler; - } - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3GuardFsckViolationHandler.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3GuardFsckViolationHandler.java deleted file mode 100644 index ec09b4af6b9..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3GuardFsckViolationHandler.java +++ /dev/null @@ -1,425 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.util.Arrays; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.S3AFileSystem; - -/** - * Violation handler for the S3Guard's fsck. - */ -public class S3GuardFsckViolationHandler { - private static final Logger LOG = LoggerFactory.getLogger( - S3GuardFsckViolationHandler.class); - - // The rawFS and metadataStore are here to prepare when the ViolationHandlers - // will not just log, but fix the violations, so they will have access. - private final S3AFileSystem rawFs; - private final DynamoDBMetadataStore metadataStore; - - private static String newLine = System.getProperty("line.separator"); - - public enum HandleMode { - FIX, LOG - } - - public S3GuardFsckViolationHandler(S3AFileSystem fs, - DynamoDBMetadataStore ddbms) { - - this.metadataStore = ddbms; - this.rawFs = fs; - } - - public void logError(S3GuardFsck.ComparePair comparePair) throws IOException { - if (!comparePair.containsViolation()) { - LOG.debug("There is no violation in the compare pair: {}", comparePair); - return; - } - - StringBuilder sB = new StringBuilder(); - sB.append(newLine) - .append("On path: ").append(comparePair.getPath()).append(newLine); - - handleComparePair(comparePair, sB, HandleMode.LOG); - - LOG.error(sB.toString()); - } - - public void doFix(S3GuardFsck.ComparePair comparePair) throws IOException { - if (!comparePair.containsViolation()) { - LOG.debug("There is no violation in the compare pair: {}", comparePair); - return; - } - - StringBuilder sB = new StringBuilder(); - sB.append(newLine) - .append("On path: ").append(comparePair.getPath()).append(newLine); - - handleComparePair(comparePair, sB, HandleMode.FIX); - - LOG.info(sB.toString()); - } - - /** - * Create a new instance of the violation handler for all the violations - * found in the compare pair and use it. - * - * @param comparePair the compare pair with violations - * @param sB StringBuilder to append error strings from violations. - */ - protected void handleComparePair(S3GuardFsck.ComparePair comparePair, - StringBuilder sB, HandleMode handleMode) throws IOException { - - for (S3GuardFsck.Violation violation : comparePair.getViolations()) { - try { - ViolationHandler handler = violation.getHandler() - .getDeclaredConstructor(S3GuardFsck.ComparePair.class) - .newInstance(comparePair); - - switch (handleMode) { - case FIX: - final String errorStr = handler.getError(); - sB.append(errorStr); - break; - case LOG: - final String fixStr = handler.fixViolation(rawFs, metadataStore); - sB.append(fixStr); - break; - default: - throw new UnsupportedOperationException("Unknown handleMode: " + handleMode); - } - - } catch (NoSuchMethodException e) { - LOG.error("Can not find declared constructor for handler: {}", - violation.getHandler()); - } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { - LOG.error("Can not instantiate handler: {}", - violation.getHandler()); - } - sB.append(newLine); - } - } - - /** - * Violation handler abstract class. - * This class should be extended for violation handlers. - */ - public static abstract class ViolationHandler { - private final PathMetadata pathMetadata; - private final S3AFileStatus s3FileStatus; - private final S3AFileStatus msFileStatus; - private final List s3DirListing; - private final DirListingMetadata msDirListing; - - public ViolationHandler(S3GuardFsck.ComparePair comparePair) { - pathMetadata = comparePair.getMsPathMetadata(); - s3FileStatus = comparePair.getS3FileStatus(); - if (pathMetadata != null) { - msFileStatus = pathMetadata.getFileStatus(); - } else { - msFileStatus = null; - } - s3DirListing = comparePair.getS3DirListing(); - msDirListing = comparePair.getMsDirListing(); - } - - public abstract String getError(); - - public PathMetadata getPathMetadata() { - return pathMetadata; - } - - public S3AFileStatus getS3FileStatus() { - return s3FileStatus; - } - - public S3AFileStatus getMsFileStatus() { - return msFileStatus; - } - - public List getS3DirListing() { - return s3DirListing; - } - - public DirListingMetadata getMsDirListing() { - return msDirListing; - } - - public String fixViolation(S3AFileSystem fs, - DynamoDBMetadataStore ddbms) throws IOException { - return String.format("Fixing of violation: %s is not supported yet.", - this.getClass().getSimpleName()); - } - } - - /** - * The violation handler when there's no matching metadata entry in the MS. - */ - public static class NoMetadataEntry extends ViolationHandler { - - public NoMetadataEntry(S3GuardFsck.ComparePair comparePair) { - super(comparePair); - } - - @Override - public String getError() { - return "No PathMetadata for this path in the MS."; - } - } - - /** - * The violation handler when there's no parent entry. - */ - public static class NoParentEntry extends ViolationHandler { - - public NoParentEntry(S3GuardFsck.ComparePair comparePair) { - super(comparePair); - } - - @Override - public String getError() { - return "Entry does not have a parent entry (not root)"; - } - } - - /** - * The violation handler when the parent of an entry is a file. - */ - public static class ParentIsAFile extends ViolationHandler { - - public ParentIsAFile(S3GuardFsck.ComparePair comparePair) { - super(comparePair); - } - - @Override - public String getError() { - return "The entry's parent in the metastore database is a file."; - } - } - - /** - * The violation handler when the parent of an entry is tombstoned. - */ - public static class ParentTombstoned extends ViolationHandler { - - public ParentTombstoned(S3GuardFsck.ComparePair comparePair) { - super(comparePair); - } - - @Override - public String getError() { - return "The entry in the metastore database has a parent entry " + - "which is a tombstone marker"; - } - } - - /** - * The violation handler when there's a directory is a file metadata in MS. - */ - public static class DirInS3FileInMs extends ViolationHandler { - - public DirInS3FileInMs(S3GuardFsck.ComparePair comparePair) { - super(comparePair); - } - - @Override - public String getError() { - return "A directory in S3 is a file entry in the MS"; - } - } - - /** - * The violation handler when a file metadata is a directory in MS. - */ - public static class FileInS3DirInMs extends ViolationHandler { - - public FileInS3DirInMs(S3GuardFsck.ComparePair comparePair) { - super(comparePair); - } - - @Override - public String getError() { - return "A file in S3 is a directory entry in the MS"; - } - } - - /** - * The violation handler when there's a directory listing content mismatch. - */ - public static class AuthDirContentMismatch extends ViolationHandler { - - public AuthDirContentMismatch(S3GuardFsck.ComparePair comparePair) { - super(comparePair); - } - - @Override - public String getError() { - final String str = String.format( - "The content of an authoritative directory listing does " - + "not match the content of the S3 listing. S3: %s, MS: %s", - Arrays.asList(getS3DirListing()), getMsDirListing().getListing()); - return str; - } - } - - /** - * The violation handler when there's a length mismatch. - */ - public static class LengthMismatch extends ViolationHandler { - - public LengthMismatch(S3GuardFsck.ComparePair comparePair) { - super(comparePair); - } - - @Override public String getError() { - return String.format("File length mismatch - S3: %s, MS: %s", - getS3FileStatus().getLen(), getMsFileStatus().getLen()); - } - } - - /** - * The violation handler when there's a modtime mismatch. - */ - public static class ModTimeMismatch extends ViolationHandler { - - public ModTimeMismatch(S3GuardFsck.ComparePair comparePair) { - super(comparePair); - } - - @Override - public String getError() { - return String.format("File timestamp mismatch - S3: %s, MS: %s", - getS3FileStatus().getModificationTime(), - getMsFileStatus().getModificationTime()); - } - } - - /** - * The violation handler when there's a version id mismatch. - */ - public static class VersionIdMismatch extends ViolationHandler { - - public VersionIdMismatch(S3GuardFsck.ComparePair comparePair) { - super(comparePair); - } - - @Override - public String getError() { - return String.format("getVersionId mismatch - S3: %s, MS: %s", - getS3FileStatus().getVersionId(), getMsFileStatus().getVersionId()); - } - } - - /** - * The violation handler when there's an etag mismatch. - */ - public static class EtagMismatch extends ViolationHandler { - - public EtagMismatch(S3GuardFsck.ComparePair comparePair) { - super(comparePair); - } - - @Override - public String getError() { - return String.format("Etag mismatch - S3: %s, MS: %s", - getS3FileStatus().getETag(), getMsFileStatus().getETag()); - } - } - - /** - * The violation handler when there's no etag. - */ - public static class NoEtag extends ViolationHandler { - - public NoEtag(S3GuardFsck.ComparePair comparePair) { - super(comparePair); - } - - @Override - public String getError() { - return "No etag."; - } - } - - /** - * The violation handler when there's a tombstoned entry in the ms is - * present, but the object is not deleted in S3. - */ - public static class TombstonedInMsNotDeletedInS3 extends ViolationHandler { - - public TombstonedInMsNotDeletedInS3(S3GuardFsck.ComparePair comparePair) { - super(comparePair); - } - - @Override - public String getError() { - return "The entry for the path is tombstoned in the MS."; - } - } - - /** - * The violation handler there's no parent in the MetadataStore. - */ - public static class OrphanDDBEntry extends ViolationHandler { - - public OrphanDDBEntry(S3GuardFsck.ComparePair comparePair) { - super(comparePair); - } - - @Override - public String getError() { - return "The DDB entry is orphan - there is no parent in the MS."; - } - - @Override - public String fixViolation(S3AFileSystem fs, DynamoDBMetadataStore ddbms) - throws IOException { - final Path path = getPathMetadata().getFileStatus().getPath(); - ddbms.forgetMetadata(path); - return String.format( - "Fixing violation by removing metadata entry from the " + - "MS on path: %s", path); - } - } - - /** - * The violation handler when there's no last updated field for the entry. - */ - public static class NoLastUpdatedField extends ViolationHandler { - - public NoLastUpdatedField(S3GuardFsck.ComparePair comparePair) { - super(comparePair); - } - - @Override - public String getError() { - return "No lastUpdated field provided for the entry."; - } - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3GuardTableAccess.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3GuardTableAccess.java deleted file mode 100644 index ca88dadeecf..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3GuardTableAccess.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.util.Collection; -import java.util.Iterator; - -import com.amazonaws.services.dynamodbv2.document.Item; -import com.amazonaws.services.dynamodbv2.document.ItemCollection; -import com.amazonaws.services.dynamodbv2.document.QueryOutcome; -import com.amazonaws.services.dynamodbv2.document.ScanOutcome; -import com.amazonaws.services.dynamodbv2.document.Table; -import com.amazonaws.services.dynamodbv2.document.internal.IteratorSupport; -import com.amazonaws.services.dynamodbv2.document.spec.QuerySpec; -import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.commons.lang3.tuple.Pair; -import org.apache.hadoop.classification.InterfaceAudience; -import org.apache.hadoop.classification.InterfaceStability; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.Retries; -import org.apache.hadoop.fs.s3a.S3AFileStatus; - -import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkNotNull; -import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore.VERSION_MARKER_ITEM_NAME; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.CHILD; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.PARENT; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.TABLE_VERSION; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.itemToPathMetadata; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.pathToKey; - -/** - * Package-scoped accessor to table state in S3Guard. - * This is for maintenance, diagnostics and testing: it is not to - * be used otherwise. - *

      - *
    1. - * Some of the operations here may dramatically alter the state of - * a table, so use carefully. - *
    2. - *
    3. - * Operations to assess consistency of a store are best executed - * against a table which is otherwise inactive. - *
    4. - *
    5. - * No retry/throttling or AWS to IOE logic here. - *
    6. - *
    7. - * If a scan or query includes the version marker in the result, it - * is converted to a {@link VersionMarker} instance. - *
    8. - *
    - * - */ -@InterfaceAudience.Private -@InterfaceStability.Unstable -@Retries.OnceRaw -class S3GuardTableAccess { - - private static final Logger LOG = - LoggerFactory.getLogger(S3GuardTableAccess.class); - - /** - * Store instance to work with. - */ - private final DynamoDBMetadataStore store; - - /** - * Table; retrieved from the store. - */ - private final Table table; - - /** - * Construct. - * @param store store to work with. - */ - S3GuardTableAccess(final DynamoDBMetadataStore store) { - this.store = checkNotNull(store); - this.table = checkNotNull(store.getTable()); - } - - /** - * Username of user in store. - * @return a string. - */ - private String getUsername() { - return store.getUsername(); - } - - /** - * Execute a query. - * @param spec query spec. - * @return the outcome. - */ - @Retries.OnceRaw - ItemCollection query(QuerySpec spec) { - return table.query(spec); - } - - /** - * Issue a query where the result is to be an iterator over - * the entries - * of DDBPathMetadata instances. - * @param spec query spec. - * @return an iterator over path entries. - */ - @Retries.OnceRaw - Iterable queryMetadata(QuerySpec spec) { - return new DDBPathMetadataCollection<>(query(spec)); - } - - @Retries.OnceRaw - ItemCollection scan(ExpressionSpecBuilder spec) { - return table.scan(spec.buildForScan()); - } - - @Retries.OnceRaw - Iterable scanMetadata(ExpressionSpecBuilder spec) { - return new DDBPathMetadataCollection<>(scan(spec)); - } - - @Retries.OnceRaw - void delete(Collection paths) { - paths.stream() - .map(PathMetadataDynamoDBTranslation::pathToKey) - .forEach(table::deleteItem); - } - - @Retries.OnceRaw - void delete(Path path) { - table.deleteItem(pathToKey(path)); - } - - /** - * A collection which wraps the result of a query or scan. - * Important: iterate through this only once; the outcome - * of repeating an iteration is "undefined" - * @param type of outcome. - */ - private final class DDBPathMetadataCollection - implements Iterable { - - /** - * Query/scan result. - */ - private final ItemCollection outcome; - - /** - * Instantiate. - * @param outcome query/scan outcome. - */ - private DDBPathMetadataCollection(final ItemCollection outcome) { - this.outcome = outcome; - } - - /** - * Get the iterator. - * @return the iterator. - */ - @Override - public Iterator iterator() { - return new DDBPathMetadataIterator<>(outcome.iterator()); - } - - } - - /** - * An iterator which converts the iterated-over result of - * a query or scan into a {@code DDBPathMetadataIterator} entry. - * @param type of source. - */ - private final class DDBPathMetadataIterator implements - Iterator { - - /** - * Iterator to invoke. - */ - private final IteratorSupport it; - - /** - * Instantiate. - * @param it Iterator to invoke. - */ - private DDBPathMetadataIterator(final IteratorSupport it) { - this.it = it; - } - - @Override - @Retries.OnceRaw - public boolean hasNext() { - return it.hasNext(); - } - - @Override - @Retries.OnceRaw - public DDBPathMetadata next() { - Item item = it.next(); - Pair key = primaryKey(item); - if (VERSION_MARKER_ITEM_NAME.equals(key.getLeft()) && - VERSION_MARKER_ITEM_NAME.equals(key.getRight())) { - // a version marker is found, return the special type - return new VersionMarker(item); - } else { - return itemToPathMetadata(item, getUsername()); - } - } - - } - - /** - * DDBPathMetadata subclass returned when a query returns - * the version marker. - * There is a FileStatus returned where the owner field contains - * the table version; the path is always the unqualified path "/VERSION". - * Because it is unqualified, operations which treat this as a normal - * DDB metadata entry usually fail. - */ - static final class VersionMarker extends DDBPathMetadata { - - /** - * Instantiate. - * @param versionMarker the version marker. - */ - VersionMarker(Item versionMarker) { - super(new S3AFileStatus(true, new Path("/VERSION"), - "" + versionMarker.getString(TABLE_VERSION))); - } - } - - /** - * Given an item, split it to the parent and child fields. - * @param item item to split. - * @return (parent, child). - */ - private static Pair primaryKey(Item item) { - return Pair.of(item.getString(PARENT), item.getString(CHILD)); - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3GuardTool.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3GuardTool.java index b0b3a8c80e5..1f8be586750 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3GuardTool.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/S3GuardTool.java @@ -28,38 +28,28 @@ import java.nio.file.AccessDeniedException; import java.util.Arrays; import java.util.Collection; import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Locale; -import java.util.Map; import java.util.Scanner; -import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import com.amazonaws.services.s3.model.MultipartUpload; -import org.apache.hadoop.thirdparty.com.google.common.annotations.VisibleForTesting; -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.time.DurationFormatUtils; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.classification.VisibleForTesting; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configured; -import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.FilterFileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.s3a.Constants; import org.apache.hadoop.fs.s3a.MultipartUtils; -import org.apache.hadoop.fs.s3a.S3AFileStatus; import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.apache.hadoop.fs.s3a.S3AUtils; import org.apache.hadoop.fs.s3a.WriteOperationHelper; import org.apache.hadoop.fs.s3a.auth.RolePolicies; import org.apache.hadoop.fs.s3a.auth.delegation.S3ADelegationTokens; @@ -82,11 +72,8 @@ import org.apache.hadoop.util.ToolRunner; import static org.apache.hadoop.fs.s3a.Constants.*; import static org.apache.hadoop.fs.s3a.Invoker.LOG_EVENT; -import static org.apache.hadoop.fs.s3a.S3AUtils.clearBucketOption; -import static org.apache.hadoop.fs.s3a.S3AUtils.propagateBucketOptions; import static org.apache.hadoop.fs.s3a.commit.CommitConstants.*; import static org.apache.hadoop.fs.s3a.commit.staging.StagingCommitterConstants.FILESYSTEM_TEMP_PATH; -import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStoreTableManager.SSE_DEFAULT_MASTER_KEY; import static org.apache.hadoop.fs.statistics.IOStatisticsLogging.ioStatisticsToPrettyString; import static org.apache.hadoop.fs.statistics.IOStatisticsSupport.retrieveIOStatistics; import static org.apache.hadoop.fs.statistics.StoreStatisticNames.MULTIPART_UPLOAD_ABORTED; @@ -102,34 +89,39 @@ import static org.apache.hadoop.service.launcher.LauncherExitCodes.*; public abstract class S3GuardTool extends Configured implements Tool, Closeable { private static final Logger LOG = LoggerFactory.getLogger(S3GuardTool.class); + private static final String ENTRY_POINT = "s3guard"; - private static final String NAME = "s3guard"; + private static final String NAME = ENTRY_POINT; private static final String COMMON_USAGE = "When possible and not overridden by more specific options, metadata\n" + - "repository information will be inferred from the S3A URL (if provided)" + - "\n\n" + - "Generic options supported are:\n" + - " -conf - specify an application configuration file\n" + - " -D - define a value for a given property\n"; + "repository information will be inferred from the S3A URL (if provided)" + + "\n\n" + + "Generic options supported are:\n" + + " -conf - specify an application configuration file\n" + + " -D - define a value for a given property\n"; - private static final String USAGE = NAME + + static final List UNSUPPORTED_COMMANDS = Arrays.asList( + "init", + "destroy", + "authoritative", + "diff", + "fsck", + "import", + "prune", + "set-capacity"); + + /** + * Usage includes supported commands, but not the excluded ones. + */ + private static final String USAGE = ENTRY_POINT + " [command] [OPTIONS] [s3a://BUCKET]\n\n" + "Commands: \n" + - "\t" + Init.NAME + " - " + Init.PURPOSE + "\n" + - "\t" + Destroy.NAME + " - " + Destroy.PURPOSE + "\n" + - "\t" + Authoritative.NAME + " - " + Authoritative.PURPOSE + "\n" + "\t" + BucketInfo.NAME + " - " + BucketInfo.PURPOSE + "\n" + - "\t" + Diff.NAME + " - " + Diff.PURPOSE + "\n" + - "\t" + Fsck.NAME + " - " + Fsck.PURPOSE + "\n" + - "\t" + Import.NAME + " - " + Import.PURPOSE + "\n" + "\t" + MarkerTool.MARKERS + " - " + MarkerTool.PURPOSE + "\n" + - "\t" + Prune.NAME + " - " + Prune.PURPOSE + "\n" + - "\t" + SetCapacity.NAME + " - " + SetCapacity.PURPOSE + "\n" + "\t" + SelectTool.NAME + " - " + SelectTool.PURPOSE + "\n" + "\t" + Uploads.NAME + " - " + Uploads.PURPOSE + "\n"; - private static final String DATA_IN_S3_IS_PRESERVED - = "(all data in S3 is preserved)"; + private static final String E_UNSUPPORTED = "This command is no longer supported"; public abstract String getUsage(); @@ -141,6 +133,7 @@ public abstract class S3GuardTool extends Configured implements Tool, static final int ERROR = EXIT_FAIL; static final int E_BAD_STATE = EXIT_NOT_ACCEPTABLE; static final int E_NOT_FOUND = EXIT_NOT_FOUND; + static final int E_S3GUARD_UNSUPPORTED = ERROR; /** Error String when the wrong FS is used for binding: {@value}. **/ @VisibleForTesting @@ -151,12 +144,11 @@ public abstract class S3GuardTool extends Configured implements Tool, */ private FileSystem baseFS; private S3AFileSystem filesystem; - private MetadataStore store; private final CommandFormat commandFormat; public static final String META_FLAG = "meta"; - // These are common to prune, upload subcommands. + // These are common options public static final String DAYS_FLAG = "days"; public static final String HOURS_FLAG = "hours"; public static final String MINUTES_FLAG = "minutes"; @@ -164,13 +156,6 @@ public abstract class S3GuardTool extends Configured implements Tool, public static final String AGE_OPTIONS_USAGE = "[-days ] " + "[-hours ] [-minutes ] [-seconds ]"; - public static final String REGION_FLAG = "region"; - public static final String READ_FLAG = "read"; - public static final String WRITE_FLAG = "write"; - public static final String SSE_FLAG = "sse"; - public static final String CMK_FLAG = "cmk"; - public static final String TAG_FLAG = "tag"; - public static final String VERBOSE = "verbose"; /** @@ -178,19 +163,9 @@ public abstract class S3GuardTool extends Configured implements Tool, * @param conf Configuration. * @param opts any boolean options to support */ - protected S3GuardTool(Configuration conf, String...opts) { + protected S3GuardTool(Configuration conf, String... opts) { super(conf); - - // Set s3guard is off warn level to silent, as the fs is often instantiated - // without s3guard on purpose. - conf.set(S3GUARD_DISABLED_WARN_LEVEL, - S3Guard.DisabledWarnLevel.SILENT.toString()); - commandFormat = new CommandFormat(0, Integer.MAX_VALUE, opts); - // For metadata store URI - commandFormat.addOptionWithValue(META_FLAG); - // DDB region. - commandFormat.addOptionWithValue(REGION_FLAG); } /** @@ -200,66 +175,15 @@ public abstract class S3GuardTool extends Configured implements Tool, public abstract String getName(); /** - * Close the FS and metastore. + * Close the FS. * @throws IOException on failure. */ @Override public void close() throws IOException { IOUtils.cleanupWithLogger(LOG, - baseFS, store); + baseFS); baseFS = null; filesystem = null; - store = null; - } - - /** - * Parse DynamoDB region from either -m option or a S3 path. - * - * Note that as a side effect, if the paths included an S3 path, - * and there is no region set on the CLI, then the S3A FS is - * initialized, after which {@link #filesystem} will no longer - * be null. - * - * @param paths remaining parameters from CLI. - * @throws IOException on I/O errors. - * @throws ExitUtil.ExitException on validation errors - */ - protected void parseDynamoDBRegion(List paths) throws IOException { - Configuration conf = getConf(); - String fromCli = getCommandFormat().getOptValue(REGION_FLAG); - String fromConf = conf.get(S3GUARD_DDB_REGION_KEY); - boolean hasS3Path = !paths.isEmpty(); - - if (fromCli != null) { - if (fromCli.isEmpty()) { - throw invalidArgs("No region provided with -" + REGION_FLAG + " flag"); - } - if (hasS3Path) { - throw invalidArgs("Providing both an S3 path and the" - + " -" + REGION_FLAG - + " flag is not supported. If you need to specify a different " - + "region than the S3 bucket, configure " + S3GUARD_DDB_REGION_KEY); - } - conf.set(S3GUARD_DDB_REGION_KEY, fromCli); - return; - } - - if (fromConf != null) { - if (fromConf.isEmpty()) { - throw invalidArgs("No region provided with config %s", - S3GUARD_DDB_REGION_KEY); - } - return; - } - - if (hasS3Path) { - String s3Path = paths.get(0); - initS3AFileSystem(s3Path); - return; - } - - throw invalidArgs("No region found from -" + REGION_FLAG + " flag, " + - "config, or S3 bucket"); } private long getDeltaComponent(TimeUnit unit, String arg) { @@ -292,111 +216,8 @@ public abstract class S3GuardTool extends Configured implements Tool, format.addOptionWithValue(SECONDS_FLAG); } - protected void checkIfS3BucketIsGuarded(List paths) - throws IOException { - // be sure that path is provided in params, so there's no IOoBE - String s3Path = ""; - if(!paths.isEmpty()) { - s3Path = paths.get(0); - } - - // Check if DynamoDB url is set from arguments. - String metadataStoreUri = getCommandFormat().getOptValue(META_FLAG); - if(metadataStoreUri == null || metadataStoreUri.isEmpty()) { - // If not set, check if filesystem is guarded by creating an - // S3AFileSystem and check if hasMetadataStore is true - try (S3AFileSystem s3AFileSystem = (S3AFileSystem) - S3AFileSystem.newInstance(toUri(s3Path), getConf())){ - Preconditions.checkState(s3AFileSystem.hasMetadataStore(), - "The S3 bucket is unguarded. " + getName() - + " can not be used on an unguarded bucket."); - } - } - } - - /** - * Check if bucket or DDB table name is set. - * @param paths position arguments in which S3 path is provided. - */ - protected void checkBucketNameOrDDBTableNameProvided(List paths) { - String s3Path = null; - if(!paths.isEmpty()) { - s3Path = paths.get(0); - } - - String metadataStoreUri = getCommandFormat().getOptValue(META_FLAG); - - if(metadataStoreUri == null && s3Path == null) { - throw invalidArgs("S3 bucket url or DDB table name have to be provided " - + "explicitly to use " + getName() + " command."); - } - } - - /** - * Parse metadata store from command line option or HDFS configuration. - * - * @param forceCreate override the auto-creation setting to true. - * @return a initialized metadata store. - * @throws IOException on unsupported metadata store. - */ - protected MetadataStore initMetadataStore(boolean forceCreate) - throws IOException { - if (getStore() != null) { - return getStore(); - } - Configuration conf; - if (filesystem == null) { - conf = getConf(); - } else { - conf = filesystem.getConf(); - } - String metaURI = getCommandFormat().getOptValue(META_FLAG); - if (metaURI != null && !metaURI.isEmpty()) { - URI uri = URI.create(metaURI); - LOG.info("Create metadata store: {}", uri + " scheme: " - + uri.getScheme()); - switch (uri.getScheme().toLowerCase(Locale.ENGLISH)) { - case "local": - setStore(new LocalMetadataStore()); - break; - case "dynamodb": - setStore(new DynamoDBMetadataStore()); - conf.set(S3GUARD_DDB_TABLE_NAME_KEY, uri.getAuthority()); - if (forceCreate) { - conf.setBoolean(S3GUARD_DDB_TABLE_CREATE_KEY, true); - } - break; - default: - throw new IOException( - String.format("Metadata store %s is not supported", uri)); - } - } else { - // CLI does not specify metadata store URI, it uses default metadata store - // DynamoDB instead. - setStore(new DynamoDBMetadataStore()); - if (forceCreate) { - conf.setBoolean(S3GUARD_DDB_TABLE_CREATE_KEY, true); - } - } - - if (filesystem == null) { - getStore().initialize(conf, new S3Guard.TtlTimeProvider(conf)); - } else { - getStore().initialize(filesystem, new S3Guard.TtlTimeProvider(conf)); - } - LOG.info("Metadata store {} is initialized.", getStore()); - return getStore(); - } - /** * Create and initialize a new S3A FileSystem instance. - * This instance is always created without S3Guard, so allowing - * a previously created metastore to be patched in. - * - * Note: this is a bit convoluted as it needs to also handle the situation - * of a per-bucket option in core-site.xml, which isn't easily overridden. - * The new config and the setting of the values before any - * {@code Configuration.get()} calls are critical. * * @param path s3a URI * @throws IOException failure to init filesystem @@ -405,45 +226,7 @@ public abstract class S3GuardTool extends Configured implements Tool, protected void initS3AFileSystem(String path) throws IOException { LOG.debug("Initializing S3A FS to {}", path); URI uri = toUri(path); - // Make sure that S3AFileSystem does not hold an actual MetadataStore - // implementation. - Configuration conf = new Configuration(getConf()); - String nullStore = NullMetadataStore.class.getName(); - conf.set(S3_METADATA_STORE_IMPL, nullStore); - String bucket = uri.getHost(); - S3AUtils.setBucketOption(conf, - bucket, - S3_METADATA_STORE_IMPL, S3GUARD_METASTORE_NULL); - String updatedBucketOption = S3AUtils.getBucketOption(conf, bucket, - S3_METADATA_STORE_IMPL); - LOG.debug("updated bucket store option {}", updatedBucketOption); - Preconditions.checkState(S3GUARD_METASTORE_NULL.equals(updatedBucketOption), - "Expected bucket option to be %s but was %s", - S3GUARD_METASTORE_NULL, updatedBucketOption); - - bindFilesystem(FileSystem.newInstance(uri, conf)); - } - - /** - * Initialize the filesystem if there is none bonded to already and - * the command line path list is not empty. - * @param paths path list. - * @return true if at the end of the call, getFilesystem() is not null - * @throws IOException failure to instantiate. - */ - @VisibleForTesting - boolean maybeInitFilesystem(final List paths) - throws IOException { - // is there an S3 FS to create? - if (getFilesystem() == null) { - // none yet -create one - if (!paths.isEmpty()) { - initS3AFileSystem(paths.get(0)); - } else { - LOG.debug("No path on command line, so not instantiating FS"); - } - } - return getFilesystem() != null; + bindFilesystem(FileSystem.newInstance(uri, getConf())); } /** @@ -477,28 +260,16 @@ public abstract class S3GuardTool extends Configured implements Tool, if (!(fs instanceof S3AFileSystem)) { throw new ExitUtil.ExitException(EXIT_SERVICE_UNAVAILABLE, WRONG_FILESYSTEM + "URI " + fs.getUri() + " : " - + fs.getClass().getName()); + + fs.getClass().getName()); } filesystem = (S3AFileSystem) fs; return filesystem; } - @VisibleForTesting - public MetadataStore getStore() { - return store; - } - - @VisibleForTesting - protected void setStore(MetadataStore store) { - Preconditions.checkNotNull(store); - this.store = store; - } - /** * Reset the store and filesystem bindings. */ protected void resetBindings() { - store = null; filesystem = null; } @@ -548,675 +319,6 @@ public abstract class S3GuardTool extends Configured implements Tool, println(stream, ""); } - /** - * Create the metadata store. - */ - static class Init extends S3GuardTool { - public static final String NAME = "init"; - public static final String PURPOSE = "initialize metadata repository"; - private static final String USAGE = NAME + " [OPTIONS] [s3a://BUCKET]\n" + - "\t" + PURPOSE + "\n\n" + - "Common options:\n" + - " -" + META_FLAG + " URL - Metadata repository details " + - "(implementation-specific)\n" + - "\n" + - "Amazon DynamoDB-specific options:\n" + - " -" + REGION_FLAG + " REGION - Service region for connections\n" + - " -" + READ_FLAG + " UNIT - Provisioned read throughput units\n" + - " -" + WRITE_FLAG + " UNIT - Provisioned write through put units\n" + - " -" + SSE_FLAG + " - Enable server side encryption\n" + - " -" + CMK_FLAG + " KEY - Customer managed CMK\n" + - " -" + TAG_FLAG + " key=value; list of tags to tag dynamo table\n" + - "\n" + - " URLs for Amazon DynamoDB are of the form dynamodb://TABLE_NAME.\n" + - " Specifying both the -" + REGION_FLAG + " option and an S3A path\n" + - " is not supported.\n" - + "To create a table with per-request billing, set the read and write\n" - + "capacities to 0"; - - Init(Configuration conf) { - super(conf, SSE_FLAG); - // read capacity. - getCommandFormat().addOptionWithValue(READ_FLAG); - // write capacity. - getCommandFormat().addOptionWithValue(WRITE_FLAG); - // customer managed customer master key (CMK) for server side encryption - getCommandFormat().addOptionWithValue(CMK_FLAG); - // tag - getCommandFormat().addOptionWithValue(TAG_FLAG); - } - - @Override - public String getName() { - return NAME; - } - - @Override - public String getUsage() { - return USAGE; - } - - @Override - public int run(String[] args, PrintStream out) throws Exception { - List paths = parseArgs(args); - try { - checkBucketNameOrDDBTableNameProvided(paths); - } catch (ExitUtil.ExitException e) { - errorln(USAGE); - throw e; - } - CommandFormat commands = getCommandFormat(); - String readCap = commands.getOptValue(READ_FLAG); - if (readCap != null && !readCap.isEmpty()) { - int readCapacity = Integer.parseInt(readCap); - getConf().setInt(S3GUARD_DDB_TABLE_CAPACITY_READ_KEY, readCapacity); - } - String writeCap = commands.getOptValue(WRITE_FLAG); - if (writeCap != null && !writeCap.isEmpty()) { - int writeCapacity = Integer.parseInt(writeCap); - getConf().setInt(S3GUARD_DDB_TABLE_CAPACITY_WRITE_KEY, writeCapacity); - } - if (!paths.isEmpty()) { - String s3path = paths.get(0); - URI fsURI = new URI(s3path); - Configuration bucketConf = propagateBucketOptions(getConf(), - fsURI.getHost()); - setConf(bucketConf); - } - - String cmk = commands.getOptValue(CMK_FLAG); - if (commands.getOpt(SSE_FLAG)) { - getConf().setBoolean(S3GUARD_DDB_TABLE_SSE_ENABLED, true); - LOG.debug("SSE flag is passed to command {}", this.getName()); - if (!StringUtils.isEmpty(cmk)) { - if (SSE_DEFAULT_MASTER_KEY.equals(cmk)) { - LOG.warn("Ignoring default DynamoDB table KMS Master Key " + - "alias/aws/dynamodb in configuration"); - } else { - LOG.debug("Setting customer managed CMK {}", cmk); - getConf().set(S3GUARD_DDB_TABLE_SSE_CMK, cmk); - } - } - } else if (!StringUtils.isEmpty(cmk)) { - throw invalidArgs("Option %s can only be used with option %s", - CMK_FLAG, SSE_FLAG); - } - - String tags = commands.getOptValue(TAG_FLAG); - if (tags != null && !tags.isEmpty()) { - String[] stringList = tags.split(";"); - Map tagsKV = new HashMap<>(); - for(String kv : stringList) { - if(kv.isEmpty() || !kv.contains("=")){ - continue; - } - String[] kvSplit = kv.split("="); - tagsKV.put(kvSplit[0], kvSplit[1]); - } - - for (Map.Entry kv : tagsKV.entrySet()) { - getConf().set(S3GUARD_DDB_TABLE_TAG + kv.getKey(), kv.getValue()); - } - } - - // Validate parameters. - try { - parseDynamoDBRegion(paths); - } catch (ExitUtil.ExitException e) { - errorln(USAGE); - throw e; - } - MetadataStore store = initMetadataStore(true); - printStoreDiagnostics(out, store); - return SUCCESS; - } - } - - /** - * Change the capacity of the metadata store. - */ - static class SetCapacity extends S3GuardTool { - public static final String NAME = "set-capacity"; - public static final String PURPOSE = "Alter metadata store IO capacity"; - public static final String READ_CAP_INVALID = "Read capacity must have " - + "value greater than or equal to 1."; - public static final String WRITE_CAP_INVALID = "Write capacity must have " - + "value greater than or equal to 1."; - private static final String USAGE = NAME + " [OPTIONS] [s3a://BUCKET]\n" + - "\t" + PURPOSE + "\n\n" + - "Common options:\n" + - " -" + META_FLAG + " URL - Metadata repository details " + - "(implementation-specific)\n" + - "\n" + - "Amazon DynamoDB-specific options:\n" + - " -" + READ_FLAG + " UNIT - Provisioned read throughput units\n" + - " -" + WRITE_FLAG + " UNIT - Provisioned write through put units\n" + - "\n" + - " URLs for Amazon DynamoDB are of the form dynamodb://TABLE_NAME.\n" + - " Specifying both the -" + REGION_FLAG + " option and an S3A path\n" + - " is not supported."; - - SetCapacity(Configuration conf) { - super(conf); - // read capacity. - getCommandFormat().addOptionWithValue(READ_FLAG); - // write capacity. - getCommandFormat().addOptionWithValue(WRITE_FLAG); - } - - @Override - public String getName() { - return NAME; - } - - @Override - public String getUsage() { - return USAGE; - } - - @Override - public int run(String[] args, PrintStream out) throws Exception { - List paths = parseArgs(args); - if (paths.isEmpty()) { - errorln(getUsage()); - throw invalidArgs("no arguments"); - } - Map options = new HashMap<>(); - checkIfS3BucketIsGuarded(paths); - - String readCap = getCommandFormat().getOptValue(READ_FLAG); - if (StringUtils.isNotEmpty(readCap)) { - Preconditions.checkArgument(Integer.parseInt(readCap) > 0, - READ_CAP_INVALID); - - S3GuardTool.println(out, "Read capacity set to %s", readCap); - options.put(S3GUARD_DDB_TABLE_CAPACITY_READ_KEY, readCap); - } - String writeCap = getCommandFormat().getOptValue(WRITE_FLAG); - if (StringUtils.isNotEmpty(writeCap)) { - Preconditions.checkArgument(Integer.parseInt(writeCap) > 0, - WRITE_CAP_INVALID); - - S3GuardTool.println(out, "Write capacity set to %s", writeCap); - options.put(S3GUARD_DDB_TABLE_CAPACITY_WRITE_KEY, writeCap); - } - - // Validate parameters. - try { - parseDynamoDBRegion(paths); - maybeInitFilesystem(paths); - } catch (ExitUtil.ExitException e) { - errorln(USAGE); - throw e; - } - MetadataStore store = initMetadataStore(false); - store.updateParameters(options); - printStoreDiagnostics(out, store); - return SUCCESS; - } - } - - - /** - * Destroy a metadata store. - */ - static class Destroy extends S3GuardTool { - public static final String NAME = "destroy"; - public static final String PURPOSE = "destroy the Metadata Store including its" - + " contents" + DATA_IN_S3_IS_PRESERVED; - private static final String USAGE = NAME + " [OPTIONS] [s3a://BUCKET]\n" + - "\t" + PURPOSE + "\n\n" + - "Common options:\n" + - " -" + META_FLAG + " URL - Metadata repository details " + - "(implementation-specific)\n" + - "\n" + - "Amazon DynamoDB-specific options:\n" + - " -" + REGION_FLAG + " REGION - Service region for connections\n" + - "\n" + - " URLs for Amazon DynamoDB are of the form dynamodb://TABLE_NAME.\n" + - " Specifying both the -" + REGION_FLAG + " option and an S3A path\n" + - " is not supported."; - - Destroy(Configuration conf) { - super(conf); - } - - @Override - public String getName() { - return NAME; - } - - @Override - public String getUsage() { - return USAGE; - } - - public int run(String[] args, PrintStream out) throws Exception { - List paths = parseArgs(args); - try { - checkBucketNameOrDDBTableNameProvided(paths); - checkIfS3BucketIsGuarded(paths); - parseDynamoDBRegion(paths); - maybeInitFilesystem(paths); - } catch (ExitUtil.ExitException e) { - errorln(USAGE); - throw e; - } - - try { - initMetadataStore(false); - } catch (FileNotFoundException e) { - // indication that the table was not found - println(out, "Metadata Store does not exist."); - LOG.debug("Failed to bind to store to be destroyed", e); - return SUCCESS; - } - - Preconditions.checkState(getStore() != null, - "Metadata Store is not initialized"); - - try { - getStore().destroy(); - } catch (TableDeleteTimeoutException e) { - LOG.warn("The table is been deleted but it is still (briefly)" - + " listed as present in AWS"); - LOG.debug("Timeout waiting for table disappearing", e); - } - println(out, "Metadata store is deleted."); - return SUCCESS; - } - } - - /** - * Import s3 metadata to the metadata store. - */ - static class Import extends S3GuardTool { - public static final String NAME = "import"; - public static final String PURPOSE = "import metadata from existing S3 " + - "data"; - public static final String AUTH_FLAG = "authoritative"; - private static final String USAGE = NAME + " [OPTIONS] [s3a://PATH]\n" + - "\t" + PURPOSE + "\n\n" + - "Common options:\n" + - " -" + AUTH_FLAG + " - Mark imported directory data as authoritative.\n" + - " -" + VERBOSE + " - Verbose Output.\n" + - " -" + META_FLAG + " URL - Metadata repository details " + - "(implementation-specific)\n" + - "\n" + - "Amazon DynamoDB-specific options:\n" + - " -" + REGION_FLAG + " REGION - Service region for connections\n" + - "\n" + - " URLs for Amazon DynamoDB are of the form dynamodb://TABLE_NAME.\n" + - " Specifying both the -" + REGION_FLAG + " option and an S3A path\n" + - " is not supported."; - - Import(Configuration conf) { - super(conf, AUTH_FLAG, VERBOSE); - } - - @Override - public String getName() { - return NAME; - } - - @Override - public String getUsage() { - return USAGE; - } - - @Override - public int run(String[] args, PrintStream out) throws Exception { - List paths = parseArgs(args); - if (paths.isEmpty()) { - errorln(getUsage()); - throw invalidArgs("no arguments"); - } - String s3Path = paths.get(0); - initS3AFileSystem(s3Path); - - URI uri = toUri(s3Path); - String filePath = uri.getPath(); - if (filePath.isEmpty()) { - // If they specify a naked S3 URI (e.g. s3a://bucket), we'll consider - // root to be the path - filePath = "/"; - } - Path path = new Path(filePath); - S3AFileStatus status = (S3AFileStatus) getFilesystem() - .getFileStatus(path); - - try { - initMetadataStore(false); - } catch (FileNotFoundException e) { - throw storeNotFound(e); - } - - final CommandFormat commandFormat = getCommandFormat(); - - final boolean verbose = commandFormat.getOpt(VERBOSE); - final ImportOperation importer = new ImportOperation( - getFilesystem(), - getStore(), - status, - commandFormat.getOpt(AUTH_FLAG), - verbose); - long items = importer.execute(); - println(out, "Inserted %d items into Metadata Store", items); - if (verbose) { - dumpFileSystemStatistics(out); - } - return SUCCESS; - } - - } - - /** - * Show diffs between the s3 and metadata store. - */ - static class Diff extends S3GuardTool { - public static final String NAME = "diff"; - public static final String PURPOSE = "report on delta between S3 and " + - "repository"; - private static final String USAGE = NAME + " [OPTIONS] s3a://BUCKET\n" + - "\t" + PURPOSE + "\n\n" + - "Common options:\n" + - " -" + META_FLAG + " URL - Metadata repository details " + - "(implementation-specific)\n" + - "\n" + - "Amazon DynamoDB-specific options:\n" + - " -" + REGION_FLAG + " REGION - Service region for connections\n" + - "\n" + - " URLs for Amazon DynamoDB are of the form dynamodb://TABLE_NAME.\n" + - " Specifying both the -" + REGION_FLAG + " option and an S3A path\n" + - " is not supported."; - - private static final String SEP = "\t"; - static final String S3_PREFIX = "S3"; - static final String MS_PREFIX = "MS"; - - Diff(Configuration conf) { - super(conf); - } - - @Override - public String getName() { - return NAME; - } - - @Override - public String getUsage() { - return USAGE; - } - - /** - * Formats the output of printing a FileStatus in S3guard diff tool. - * @param status the status to print. - * @return the string of output. - */ - private static String formatFileStatus(FileStatus status) { - return String.format("%s%s%d%s%s", - status.isDirectory() ? "D" : "F", - SEP, - status.getLen(), - SEP, - status.getPath().toString()); - } - - /** - * Compares metadata from 2 S3 FileStatus's to see if they differ. - * @param thisOne - * @param thatOne - * @return true if the metadata is not identical - */ - private static boolean differ(FileStatus thisOne, FileStatus thatOne) { - Preconditions.checkArgument(!(thisOne == null && thatOne == null)); - return (thisOne == null || thatOne == null) || - (thisOne.getLen() != thatOne.getLen()) || - (thisOne.isDirectory() != thatOne.isDirectory()) || - (!thisOne.isDirectory() && - thisOne.getModificationTime() != thatOne.getModificationTime()); - } - - /** - * Print difference, if any, between two file statuses to the output stream. - * - * @param msStatus file status from metadata store. - * @param s3Status file status from S3. - * @param out output stream. - */ - private static void printDiff(FileStatus msStatus, - FileStatus s3Status, - PrintStream out) { - Preconditions.checkArgument(!(msStatus == null && s3Status == null)); - if (msStatus != null && s3Status != null) { - Preconditions.checkArgument( - msStatus.getPath().equals(s3Status.getPath()), - String.format("The path from metadata store and s3 are different:" + - " ms=%s s3=%s", msStatus.getPath(), s3Status.getPath())); - } - - if (differ(msStatus, s3Status)) { - if (s3Status != null) { - println(out, "%s%s%s", S3_PREFIX, SEP, formatFileStatus(s3Status)); - } - if (msStatus != null) { - println(out, "%s%s%s", MS_PREFIX, SEP, formatFileStatus(msStatus)); - } - } - } - - /** - * Compare the metadata of the directory with the same path, on S3 and - * the metadata store, respectively. If one of them is null, consider the - * metadata of the directory and all its subdirectories are missing from - * the source. - * - * Pass the FileStatus obtained from s3 and metadata store to avoid one - * round trip to fetch the same metadata twice, because the FileStatus - * hve already been obtained from listStatus() / listChildren operations. - * - * @param msDir the directory FileStatus obtained from the metadata store. - * @param s3Dir the directory FileStatus obtained from S3. - * @param out the output stream to generate diff results. - * @throws IOException on I/O errors. - */ - private void compareDir(FileStatus msDir, FileStatus s3Dir, - PrintStream out) throws IOException { - Preconditions.checkArgument(!(msDir == null && s3Dir == null), - "The path does not exist in metadata store and on s3."); - - if (msDir != null && s3Dir != null) { - Preconditions.checkArgument(msDir.getPath().equals(s3Dir.getPath()), - String.format("The path from metadata store and s3 are different:" + - " ms=%s s3=%s", msDir.getPath(), s3Dir.getPath())); - } - - Map s3Children = new HashMap<>(); - if (s3Dir != null && s3Dir.isDirectory()) { - for (FileStatus status : getFilesystem().listStatus(s3Dir.getPath())) { - s3Children.put(status.getPath(), status); - } - } - - Map msChildren = new HashMap<>(); - if (msDir != null && msDir.isDirectory()) { - DirListingMetadata dirMeta = - getStore().listChildren(msDir.getPath()); - - if (dirMeta != null) { - for (PathMetadata meta : dirMeta.getListing()) { - FileStatus status = meta.getFileStatus(); - msChildren.put(status.getPath(), status); - } - } - } - - Set allPaths = new HashSet<>(s3Children.keySet()); - allPaths.addAll(msChildren.keySet()); - - for (Path path : allPaths) { - FileStatus s3Status = s3Children.get(path); - FileStatus msStatus = msChildren.get(path); - printDiff(msStatus, s3Status, out); - if ((s3Status != null && s3Status.isDirectory()) || - (msStatus != null && msStatus.isDirectory())) { - compareDir(msStatus, s3Status, out); - } - } - out.flush(); - } - - /** - * Compare both metadata store and S3 on the same path. - * - * @param path the path to be compared. - * @param out the output stream to display results. - * @throws IOException on I/O errors. - */ - private void compareRoot(Path path, PrintStream out) throws IOException { - Path qualified = getFilesystem().makeQualified(path); - FileStatus s3Status = null; - try { - s3Status = getFilesystem().getFileStatus(qualified); - } catch (FileNotFoundException e) { - /* ignored */ - } - PathMetadata meta = getStore().get(qualified); - FileStatus msStatus = (meta != null && !meta.isDeleted()) ? - meta.getFileStatus() : null; - compareDir(msStatus, s3Status, out); - } - - @VisibleForTesting - public int run(String[] args, PrintStream out) throws IOException { - List paths = parseArgs(args); - if (paths.isEmpty()) { - out.println(USAGE); - throw invalidArgs("no arguments"); - } - String s3Path = paths.get(0); - initS3AFileSystem(s3Path); - initMetadataStore(false); - - URI uri = toUri(s3Path); - Path root; - if (uri.getPath().isEmpty()) { - root = new Path("/"); - } else { - root = new Path(uri.getPath()); - } - root = getFilesystem().makeQualified(root); - compareRoot(root, out); - out.flush(); - return SUCCESS; - } - - } - - /** - * Prune metadata that has not been modified recently. - */ - static class Prune extends S3GuardTool { - public static final String NAME = "prune"; - public static final String PURPOSE = "truncate older metadata from " + - "repository " - + DATA_IN_S3_IS_PRESERVED;; - - public static final String TOMBSTONE = "tombstone"; - - private static final String USAGE = NAME + " [OPTIONS] [s3a://BUCKET]\n" + - "\t" + PURPOSE + "\n\n" + - "Common options:\n" + - " -" + META_FLAG + " URL - Metadata repository details " + - "(implementation-specific)\n" + - "[-" + TOMBSTONE + "]\n" + - "Age options. Any combination of these integer-valued options:\n" + - AGE_OPTIONS_USAGE + "\n" + - "Amazon DynamoDB-specific options:\n" + - " -" + REGION_FLAG + " REGION - Service region for connections\n" + - "\n" + - " URLs for Amazon DynamoDB are of the form dynamodb://TABLE_NAME.\n" + - " Specifying both the -" + REGION_FLAG + " option and an S3A path\n" + - " is not supported."; - - Prune(Configuration conf) { - super(conf, TOMBSTONE); - addAgeOptions(); - } - - @VisibleForTesting - void setMetadataStore(MetadataStore ms) { - Preconditions.checkNotNull(ms); - this.setStore(ms); - } - - @Override - public String getName() { - return NAME; - } - - @Override - public String getUsage() { - return USAGE; - } - - public int run(String[] args, PrintStream out) throws - InterruptedException, IOException { - List paths = parseArgs(args); - try { - checkBucketNameOrDDBTableNameProvided(paths); - parseDynamoDBRegion(paths); - } catch (ExitUtil.ExitException e) { - errorln(USAGE); - throw e; - } - maybeInitFilesystem(paths); - initMetadataStore(false); - - Configuration conf = getConf(); - long confDelta = conf.getLong(S3GUARD_CLI_PRUNE_AGE, 0); - - long cliDelta = ageOptionsToMsec(); - - if (confDelta <= 0 && cliDelta <= 0) { - errorln("You must specify a positive age for metadata to prune."); - } - - // A delta provided on the CLI overrides if one is configured - long delta = confDelta; - if (cliDelta > 0) { - delta = cliDelta; - } - - long now = System.currentTimeMillis(); - long divide = now - delta; - - // remove the protocol from path string to get keyPrefix - // by default the keyPrefix is "/" - unless the s3 URL is provided - String keyPrefix = "/"; - if(paths.size() > 0) { - Path path = new Path(paths.get(0)); - keyPrefix = PathMetadataDynamoDBTranslation.pathToParentKey(path); - } - - MetadataStore.PruneMode mode - = MetadataStore.PruneMode.ALL_BY_MODTIME; - if (getCommandFormat().getOpt(TOMBSTONE)) { - mode = MetadataStore.PruneMode.TOMBSTONES_BY_LASTUPDATED; - } - try { - getStore().prune(mode, divide, - keyPrefix); - } catch (UnsupportedOperationException e){ - errorln("Prune operation not supported in metadata store."); - } - - out.flush(); - return SUCCESS; - } - - } - /** * Get info about a bucket and its S3Guard integration status. */ @@ -1232,21 +334,22 @@ public abstract class S3GuardTool extends Configured implements Tool, public static final String MARKERS_FLAG = "markers"; public static final String MARKERS_AWARE = "aware"; - public static final String PURPOSE = "provide/check S3Guard information" + public static final String PURPOSE = "provide/check information" + " about a specific bucket"; private static final String USAGE = NAME + " [OPTIONS] s3a://BUCKET\n" + "\t" + PURPOSE + "\n\n" + "Common options:\n" - + " -" + GUARDED_FLAG + " - Require S3Guard\n" - + " -" + UNGUARDED_FLAG + " - Force S3Guard to be disabled\n" + " -" + AUTH_FLAG + " - Require the S3Guard mode to be \"authoritative\"\n" + " -" + NONAUTH_FLAG + " - Require the S3Guard mode to be \"non-authoritative\"\n" - + " -" + MAGIC_FLAG + " - Require the S3 filesystem to be support the \"magic\" committer\n" + + " -" + MAGIC_FLAG + + " - Require the S3 filesystem to be support the \"magic\" committer\n" + " -" + ENCRYPTION_FLAG + " (none, sse-s3, sse-kms) - Require encryption policy\n" + " -" + MARKERS_FLAG - + " (aware, keep, delete, authoritative) - directory markers policy\n"; + + " (aware, keep, delete, authoritative) - directory markers policy\n" + + " -" + GUARDED_FLAG + " - Require S3Guard. Will always fail.\n" + + " -" + UNGUARDED_FLAG + " - Force S3Guard to be disabled (always true)\n"; /** * Output when the client cannot get the location of a bucket. @@ -1256,7 +359,6 @@ public abstract class S3GuardTool extends Configured implements Tool, "Location unknown -caller lacks " + RolePolicies.S3_GET_BUCKET_LOCATION + " permission"; - @VisibleForTesting public static final String IS_MARKER_AWARE = "\tThe S3A connector is compatible with buckets where" @@ -1290,20 +392,10 @@ public abstract class S3GuardTool extends Configured implements Tool, CommandFormat commands = getCommandFormat(); URI fsURI = toUri(s3Path); - // check if UNGUARDED_FLAG is passed and use NullMetadataStore in - // config to avoid side effects like creating the table if not exists - Configuration unguardedConf = getConf(); - if (commands.getOpt(UNGUARDED_FLAG)) { - LOG.debug("Unguarded flag is passed to command :" + this.getName()); - clearBucketOption(unguardedConf, fsURI.getHost(), S3_METADATA_STORE_IMPL); - unguardedConf.set(S3_METADATA_STORE_IMPL, S3GUARD_METASTORE_NULL); - } - S3AFileSystem fs = bindFilesystem( - FileSystem.newInstance(fsURI, unguardedConf)); + FileSystem.newInstance(fsURI, getConf())); Configuration conf = fs.getConf(); URI fsUri = fs.getUri(); - MetadataStore store = fs.getMetadataStore(); println(out, "Filesystem %s", fsUri); try { println(out, "Location: %s", fs.getBucketLocation()); @@ -1314,35 +406,17 @@ public abstract class S3GuardTool extends Configured implements Tool, LOG.debug("failed to get bucket location", e); println(out, LOCATION_UNKNOWN); } - boolean usingS3Guard = !(store instanceof NullMetadataStore); - boolean authMode = false; - if (usingS3Guard) { - out.printf("Filesystem %s is using S3Guard with store %s%n", - fsUri, store.toString()); - printOption(out, "Authoritative Metadata Store", - METADATASTORE_AUTHORITATIVE, "false"); - printOption(out, "Authoritative Path", - AUTHORITATIVE_PATH, ""); - final Collection authoritativePaths - = S3Guard.getAuthoritativePaths(fs); - if (!authoritativePaths.isEmpty()) { - println(out, "Qualified Authoritative Paths:"); - for (String path : authoritativePaths) { - println(out, "\t%s", path); - } - println(out, ""); - } - authMode = conf.getBoolean(METADATASTORE_AUTHORITATIVE, false); - final long ttl = conf.getTimeDuration(METADATASTORE_METADATA_TTL, - DEFAULT_METADATASTORE_METADATA_TTL, TimeUnit.MILLISECONDS); - println(out, "\tMetadata time to live: (set in %s) = %s", - METADATASTORE_METADATA_TTL, - DurationFormatUtils.formatDurationHMS(ttl)); - printStoreDiagnostics(out, store); - } else { - println(out, "Filesystem %s is not using S3Guard", fsUri); - } + // print any auth paths for directory marker info + final Collection authoritativePaths + = S3Guard.getAuthoritativePaths(fs); + if (!authoritativePaths.isEmpty()) { + println(out, "Qualified Authoritative Paths:"); + for (String path : authoritativePaths) { + println(out, "\t%s", path); + } + println(out, ""); + } println(out, "%nS3A Client"); printOption(out, "\tSigning Algorithm", SIGNING_ALGORITHM, "(unset)"); String endpoint = conf.getTrimmed(ENDPOINT, ""); @@ -1421,23 +495,8 @@ public abstract class S3GuardTool extends Configured implements Tool, } else { println(out, "\tDelegation token support is disabled"); } - - if (usingS3Guard) { - if (commands.getOpt(UNGUARDED_FLAG)) { - throw badState("S3Guard is enabled for %s", fsUri); - } - if (commands.getOpt(AUTH_FLAG) && !authMode) { - throw badState("S3Guard is enabled for %s," - + " but not in authoritative mode", fsUri); - } - if (commands.getOpt(NONAUTH_FLAG) && authMode) { - throw badState("S3Guard is enabled in authoritative mode for %s", - fsUri); - } - } else { - if (commands.getOpt(GUARDED_FLAG)) { - throw badState("S3Guard is not enabled for %s", fsUri); - } + if (commands.getOpt(GUARDED_FLAG)) { + throw badState("S3Guard is not supported"); } if (commands.getOpt(MAGIC_FLAG) && !magic) { throw badState("The magic committer is not enabled for %s", fsUri); @@ -1448,8 +507,8 @@ public abstract class S3GuardTool extends Configured implements Tool, if (StringUtils.isNotEmpty(desiredEncryption) && !desiredEncryption.equalsIgnoreCase(encryption)) { throw badState("Bucket %s: required encryption is %s" - + " but actual encryption is %s", - fsUri, desiredEncryption, encryption); + + " but actual encryption is %s", + fsUri, desiredEncryption, encryption); } // directory markers @@ -1471,7 +530,7 @@ public abstract class S3GuardTool extends Configured implements Tool, private void processMarkerOption(final PrintStream out, final S3AFileSystem fs, final String marker) { - println(out, "%nSecurity"); + println(out, "%nDirectory Markers"); DirectoryPolicy markerPolicy = fs.getDirectoryMarkerPolicy(); String desc = markerPolicy.describe(); println(out, "\tThe directory marker policy is \"%s\"", desc); @@ -1530,8 +589,8 @@ public abstract class S3GuardTool extends Configured implements Tool, "s3a://BUCKET[/path]\n" + "\t" + PURPOSE + "\n\n" + "Common options:\n" - + " (-" + LIST + " | -" + EXPECT +" | -" + ABORT - + ") [-" + VERBOSE +"] " + + " (-" + LIST + " | -" + EXPECT + " | -" + ABORT + + ") [-" + VERBOSE + "] " + "[] [-force]\n" + "\t - Under given path, list or delete all uploads," + " or only those \n" @@ -1548,7 +607,8 @@ public abstract class S3GuardTool extends Configured implements Tool, public static final String TOTAL = "Total"; /** Runs in one of three modes. */ - private enum Mode { LIST, EXPECT, ABORT }; + private enum Mode {LIST, EXPECT, ABORT} + private Mode mode = null; /** For Mode == EXPECT, expected listing size. */ @@ -1736,228 +796,6 @@ public abstract class S3GuardTool extends Configured implements Tool, } } - /** - * Fsck - check for consistency between S3 and the metadatastore. - */ - static class Fsck extends S3GuardTool { - public static final String CHECK_FLAG = "check"; - public static final String DDB_MS_CONSISTENCY_FLAG = "internal"; - public static final String FIX_FLAG = "fix"; - - public static final String NAME = "fsck"; - public static final String PURPOSE = "Compares S3 with MetadataStore, and " - + "returns a failure status if any rules or invariants are violated. " - + "Only works with DynamoDB metadata stores."; - private static final String USAGE = NAME + " [OPTIONS] [s3a://BUCKET]\n" + - "\t" + PURPOSE + "\n\n" + - "Common options:\n" + - " -" + CHECK_FLAG + " Check the metadata store for errors, but do " - + "not fix any issues.\n" + - " -" + DDB_MS_CONSISTENCY_FLAG + " Check the dynamodb metadata store " - + "for internal consistency.\n" + - " -" + FIX_FLAG + " Fix the errors found in the metadata store. Can " + - "be used with " + CHECK_FLAG + " or " + DDB_MS_CONSISTENCY_FLAG + " flags. " - + "\n\t\tFixes: \n" + - "\t\t\t- Remove orphan entries from DDB." + - "\n"; - - Fsck(Configuration conf) { - super(conf, CHECK_FLAG, DDB_MS_CONSISTENCY_FLAG, FIX_FLAG); - } - - @Override - public String getName() { - return NAME; - } - - @Override - public String getUsage() { - return USAGE; - } - - public int run(String[] args, PrintStream out) throws - InterruptedException, IOException { - List paths = parseArgs(args); - if (paths.isEmpty()) { - out.println(USAGE); - throw invalidArgs("no arguments"); - } - int exitValue = EXIT_SUCCESS; - - final CommandFormat commandFormat = getCommandFormat(); - - // check if there's more than one arguments - // from CHECK and INTERNAL CONSISTENCY - int flags = countTrue(commandFormat.getOpt(CHECK_FLAG), - commandFormat.getOpt(DDB_MS_CONSISTENCY_FLAG)); - if (flags > 1) { - out.println(USAGE); - throw invalidArgs("There should be only one parameter used for checking."); - } - if (flags == 0 && commandFormat.getOpt(FIX_FLAG)) { - errorln(FIX_FLAG + " flag can be used with either " + CHECK_FLAG + " or " + - DDB_MS_CONSISTENCY_FLAG + " flag, but not alone."); - errorln(USAGE); - return ERROR; - } - - String s3Path = paths.get(0); - try { - initS3AFileSystem(s3Path); - } catch (Exception e) { - errorln("Failed to initialize S3AFileSystem from path: " + s3Path); - throw e; - } - - URI uri = toUri(s3Path); - Path root; - if (uri.getPath().isEmpty()) { - root = new Path("/"); - } else { - root = new Path(uri.getPath()); - } - - final S3AFileSystem fs = getFilesystem(); - initMetadataStore(false); - final MetadataStore ms = getStore(); - - if (ms == null || - !(ms instanceof DynamoDBMetadataStore)) { - errorln(s3Path + " path uses metadata store: " + ms); - errorln(NAME + " can be only used with a DynamoDB backed s3a bucket."); - errorln(USAGE); - return ERROR; - } - - List violations; - - if (commandFormat.getOpt(CHECK_FLAG)) { - // do the check - S3GuardFsck s3GuardFsck = new S3GuardFsck(fs, ms); - try { - violations = s3GuardFsck.compareS3ToMs(fs.qualify(root)); - } catch (IOException e) { - throw e; - } - } else if (commandFormat.getOpt(DDB_MS_CONSISTENCY_FLAG)) { - S3GuardFsck s3GuardFsck = new S3GuardFsck(fs, ms); - violations = s3GuardFsck.checkDdbInternalConsistency(fs.qualify(root)); - } else { - errorln("No supported operation is selected."); - errorln(USAGE); - return ERROR; - } - - if (commandFormat.getOpt(FIX_FLAG)) { - S3GuardFsck s3GuardFsck = new S3GuardFsck(fs, ms); - s3GuardFsck.fixViolations(violations); - } - - out.flush(); - - // We fail if there were compare pairs, as the returned compare pairs - // contain issues. - if (violations == null || violations.size() > 0) { - exitValue = EXIT_FAIL; - } - return exitValue; - } - - int countTrue(Boolean... bools) { - return (int) Arrays.stream(bools).filter(p -> p).count(); - } - } - /** - * Audits a DynamoDB S3Guard repository for all the entries being - * 'authoritative'. - * Checks bucket settings if {@link #CHECK_FLAG} is set, then - * treewalk. - */ - static class Authoritative extends S3GuardTool { - - public static final String NAME = "authoritative"; - - public static final String CHECK_FLAG = "check-config"; - public static final String REQUIRE_AUTH = "required"; - - public static final String PURPOSE = "Audits a DynamoDB S3Guard " - + "repository for all the entries being 'authoritative'"; - - private static final String USAGE = NAME + " [OPTIONS] [s3a://PATH]\n" - + "\t" + PURPOSE + "\n\n" - + "Options:\n" - + " -" + REQUIRE_AUTH + " - Require directories under the path to" - + " be authoritative.\n" - + " -" + CHECK_FLAG + " - Check the configuration for the path to" - + " be authoritative\n" - + " -" + VERBOSE + " - Verbose Output.\n"; - - Authoritative(Configuration conf) { - super(conf, CHECK_FLAG, REQUIRE_AUTH, VERBOSE); - } - - @Override - public String getName() { - return NAME; - } - - @Override - public String getUsage() { - return USAGE; - } - - public int run(String[] args, PrintStream out) throws - InterruptedException, IOException { - List paths = parseArgs(args); - if (paths.isEmpty()) { - out.println(USAGE); - throw invalidArgs("no arguments"); - } - maybeInitFilesystem(paths); - initMetadataStore(false); - String s3Path = paths.get(0); - - URI uri = toUri(s3Path); - Path auditPath; - if (uri.getPath().isEmpty()) { - auditPath = new Path("/"); - } else { - auditPath = new Path(uri.getPath()); - } - - final S3AFileSystem fs = getFilesystem(); - final MetadataStore ms = getStore(); - - if (!(ms instanceof DynamoDBMetadataStore)) { - errorln(s3Path + " path uses MS: " + ms); - errorln(NAME + " can be only used with a DynamoDB-backed S3Guard table."); - errorln(USAGE); - return ERROR; - } - - final CommandFormat commandFormat = getCommandFormat(); - if (commandFormat.getOpt(CHECK_FLAG)) { - // check that the path is auth - if (!fs.allowAuthoritative(auditPath)) { - // path isn't considered auth in the S3A bucket info - errorln("Path " + auditPath - + " is not configured to be authoritative"); - return AuthoritativeAuditOperation.ERROR_PATH_NOT_AUTH_IN_FS; - } - } - - final AuthoritativeAuditOperation audit = new AuthoritativeAuditOperation( - fs.createStoreContext(), - (DynamoDBMetadataStore) ms, - commandFormat.getOpt(REQUIRE_AUTH), - commandFormat.getOpt(VERBOSE)); - audit.audit(fs.qualify(auditPath)); - - out.flush(); - return EXIT_SUCCESS; - } - } - private static S3GuardTool command; /** @@ -1980,10 +818,9 @@ public abstract class S3GuardTool extends Configured implements Tool, private static void printHelp() { if (command == null) { errorln("Usage: hadoop " + USAGE); - errorln("\tperform S3Guard metadata store " + - "administrative commands."); + errorln("\tperform S3A connector administrative commands."); } else { - errorln("Usage: hadoop " + command.getUsage()); + errorln("Usage: hadoop " + ENTRY_POINT + command.getUsage()); } errorln(); errorln(COMMON_USAGE); @@ -2010,29 +847,12 @@ public abstract class S3GuardTool extends Configured implements Tool, } /** - * Retrieve and Print store diagnostics. - * @param out output stream - * @param store store - * @throws IOException Failure to retrieve the data. - */ - protected static void printStoreDiagnostics(PrintStream out, - MetadataStore store) - throws IOException { - Map diagnostics = store.getDiagnostics(); - out.println("Metadata Store Diagnostics:"); - for (Map.Entry entry : diagnostics.entrySet()) { - println(out, "\t%s=%s", entry.getKey(), entry.getValue()); - } - } - - - /** - * Handle store not found by converting to an exit exception + * Handle FileNotFoundException by converting to an exit exception * with specific error code. * @param e exception * @return a new exception to throw */ - protected static ExitUtil.ExitException storeNotFound( + protected static ExitUtil.ExitException notFound( FileNotFoundException e) { return new ExitUtil.ExitException( E_NOT_FOUND, e.toString(), e); @@ -2045,7 +865,7 @@ public abstract class S3GuardTool extends Configured implements Tool, * @return a new exception to throw */ protected static ExitUtil.ExitException invalidArgs( - String format, Object...args) { + String format, Object... args) { return exitException(INVALID_ARGUMENT, format, args); } @@ -2056,9 +876,16 @@ public abstract class S3GuardTool extends Configured implements Tool, * @return a new exception to throw */ protected static ExitUtil.ExitException badState( - String format, Object...args) { - int exitCode = E_BAD_STATE; - return exitException(exitCode, format, args); + String format, Object... args) { + return exitException(E_BAD_STATE, format, args); + } + + /** + * Crate an exception declaring S3Guard is unsupported. + * @return an exception raise. + */ + protected static ExitUtil.ExitException s3guardUnsupported() { + throw exitException(E_S3GUARD_UNSUPPORTED, E_UNSUPPORTED); } /** @@ -2068,7 +895,7 @@ public abstract class S3GuardTool extends Configured implements Tool, * @return a new exception to throw */ protected static ExitUtil.ExitException userAborted( - String format, Object...args) { + String format, Object... args) { return exitException(ERROR, format, args); } @@ -2095,7 +922,7 @@ public abstract class S3GuardTool extends Configured implements Tool, * @return exit code. * @throws Exception on I/O errors. */ - public static int run(Configuration conf, String...args) throws + public static int run(Configuration conf, String... args) throws Exception { /* ToolRunner.run does this too, but we must do it before looking at subCommand or instantiating the cmd object below */ @@ -2107,31 +934,17 @@ public abstract class S3GuardTool extends Configured implements Tool, } final String subCommand = otherArgs[0]; LOG.debug("Executing command {}", subCommand); + // if it is no longer supported: raise an exception + if (UNSUPPORTED_COMMANDS.contains(subCommand)) { + throw s3guardUnsupported(); + } switch (subCommand) { - case Init.NAME: - command = new Init(conf); - break; - case Destroy.NAME: - command = new Destroy(conf); - break; - case Import.NAME: - command = new Import(conf); - break; case BucketInfo.NAME: command = new BucketInfo(conf); break; - case Diff.NAME: - command = new Diff(conf); - break; case MarkerTool.MARKERS: command = new MarkerTool(conf); break; - case Prune.NAME: - command = new Prune(conf); - break; - case SetCapacity.NAME: - command = new SetCapacity(conf); - break; case Uploads.NAME: command = new Uploads(conf); break; @@ -2140,12 +953,6 @@ public abstract class S3GuardTool extends Configured implements Tool, // because this is the defacto S3 CLI. command = new SelectTool(conf); break; - case Fsck.NAME: - command = new Fsck(conf); - break; - case Authoritative.NAME: - command = new Authoritative(conf); - break; default: printHelp(); throw new ExitUtil.ExitException(E_USAGE, diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/TableDeleteTimeoutException.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/TableDeleteTimeoutException.java deleted file mode 100644 index 79693321392..00000000000 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/TableDeleteTimeoutException.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import org.apache.hadoop.fs.PathIOException; - -/** - * An exception raised when a table being deleted is still present after - * the wait time is exceeded. - */ -public class TableDeleteTimeoutException extends PathIOException { - - TableDeleteTimeoutException(final String path, - final String error, - final Throwable cause) { - super(path, error, cause); - } -} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/package-info.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/package-info.java index d4303150d1d..89c0300cd91 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/package-info.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/s3guard/package-info.java @@ -17,10 +17,9 @@ */ /** - * This package contains classes related to S3Guard: a feature of S3A to mask - * the eventual consistency behavior of S3 and optimize access patterns by - * coordinating with a strongly consistent external store for file system - * metadata. + * This package contained S3Guard support; now the feature has been removed, + * its contents are limited to the public command line and some + * methods still used by directory marker code. */ @InterfaceAudience.Private @InterfaceStability.Evolving diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/select/SelectInputStream.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/select/SelectInputStream.java index 02d1e53c7ba..e13aa23db3c 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/select/SelectInputStream.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/select/SelectInputStream.java @@ -337,7 +337,7 @@ public class SelectInputStream extends FSInputStream implements /** * Build an exception to raise when an operation is not supported here. - * @param action action which is unsupported. + * @param action action which is Unsupported. * @return an exception to throw. */ protected PathIOException unsupported(final String action) { diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/select/SelectTool.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/select/SelectTool.java index 21c98fd6f8d..73a08750053 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/select/SelectTool.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/select/SelectTool.java @@ -264,7 +264,7 @@ public class SelectTool extends S3GuardTool { stream = FutureIOSupport.awaitFuture(builder.build()); } catch (FileNotFoundException e) { // the source file is missing. - throw storeNotFound(e); + throw notFound(e); } try { if (toConsole) { diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/S3AStatisticsContext.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/S3AStatisticsContext.java index 27f1398d4ea..5378c41edf7 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/S3AStatisticsContext.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/S3AStatisticsContext.java @@ -18,19 +18,11 @@ package org.apache.hadoop.fs.s3a.statistics; -import org.apache.hadoop.fs.s3a.s3guard.MetastoreInstrumentation; - /** * This is the statistics context for ongoing operations in S3A. */ public interface S3AStatisticsContext extends CountersAndGauges { - /** - * Get the metastore instrumentation. - * @return an instance of the metastore statistics tracking. - */ - MetastoreInstrumentation getS3GuardInstrumentation(); - /** * Create a stream input statistics instance. * @return the new instance diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/impl/BondedS3AStatisticsContext.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/impl/BondedS3AStatisticsContext.java index 51bb4afebc4..1ad4f3ff68b 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/impl/BondedS3AStatisticsContext.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/impl/BondedS3AStatisticsContext.java @@ -25,7 +25,6 @@ import java.time.Duration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.s3a.S3AInstrumentation; import org.apache.hadoop.fs.s3a.Statistic; -import org.apache.hadoop.fs.s3a.s3guard.MetastoreInstrumentation; import org.apache.hadoop.fs.s3a.statistics.BlockOutputStreamStatistics; import org.apache.hadoop.fs.s3a.statistics.CommitterStatistics; import org.apache.hadoop.fs.s3a.statistics.DelegationTokenStatistics; @@ -94,16 +93,6 @@ public class BondedS3AStatisticsContext implements S3AStatisticsContext { return statisticsSource.getInstanceStatistics(); } - /** - * Get a MetastoreInstrumentation getInstrumentation() instance for this - * context. - * @return the S3Guard getInstrumentation() point. - */ - @Override - public MetastoreInstrumentation getS3GuardInstrumentation() { - return getInstrumentation().getS3GuardInstrumentation(); - } - /** * Create a stream input statistics instance. * The FileSystem.Statistics instance of the {@link #statisticsSource} diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/impl/EmptyS3AStatisticsContext.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/impl/EmptyS3AStatisticsContext.java index 3a651026a0b..58bf60ec3ab 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/impl/EmptyS3AStatisticsContext.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/statistics/impl/EmptyS3AStatisticsContext.java @@ -22,8 +22,6 @@ import java.io.IOException; import java.time.Duration; import org.apache.hadoop.fs.s3a.Statistic; -import org.apache.hadoop.fs.s3a.s3guard.MetastoreInstrumentation; -import org.apache.hadoop.fs.s3a.s3guard.MetastoreInstrumentationImpl; import org.apache.hadoop.fs.s3a.statistics.BlockOutputStreamStatistics; import org.apache.hadoop.fs.s3a.statistics.ChangeTrackerStatistics; import org.apache.hadoop.fs.s3a.statistics.CommitterStatistics; @@ -49,9 +47,6 @@ import static org.apache.hadoop.fs.statistics.IOStatisticsSupport.stubDurationTr */ public final class EmptyS3AStatisticsContext implements S3AStatisticsContext { - public static final MetastoreInstrumentation - METASTORE_INSTRUMENTATION = new MetastoreInstrumentationImpl(); - public static final S3AInputStreamStatistics EMPTY_INPUT_STREAM_STATISTICS = new EmptyInputStreamStatistics(); @@ -69,11 +64,6 @@ public final class EmptyS3AStatisticsContext implements S3AStatisticsContext { public static final StatisticsFromAwsSdk EMPTY_STATISTICS_FROM_AWS_SDK = new EmptyStatisticsFromAwsSdk(); - @Override - public MetastoreInstrumentation getS3GuardInstrumentation() { - return METASTORE_INSTRUMENTATION; - } - @Override public S3AInputStreamStatistics newInputStreamStatistics() { return EMPTY_INPUT_STREAM_STATISTICS; diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/tools/MarkerTool.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/tools/MarkerTool.java index a4d07c6c5f1..7179c6b2f01 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/tools/MarkerTool.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/tools/MarkerTool.java @@ -73,10 +73,7 @@ import static org.apache.hadoop.service.launcher.LauncherExitCodes.EXIT_SUCCESS; import static org.apache.hadoop.service.launcher.LauncherExitCodes.EXIT_USAGE; /** - * Audit and S3 bucket for directory markers. - *

    - * This tool does not go anywhere near S3Guard; its scan bypasses any - * metastore as we are explicitly looking for marker objects. + * Audit an S3 bucket for directory markers. */ @InterfaceAudience.LimitedPrivate("management tools") @InterfaceStability.Unstable @@ -818,10 +815,9 @@ public final class MarkerTool extends S3GuardTool { int end = Math.min(start + deletePageSize, size); List page = markerKeys.subList(start, end); - List undeleted = new ArrayList<>(); once("Remove S3 Keys", tracker.getBasePath().toString(), () -> - operations.removeKeys(page, true, undeleted, null, false)); + operations.removeKeys(page, true, false)); summary.deleteRequests++; // and move to the start of the next page start = end; diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/tools/MarkerToolOperations.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/tools/MarkerToolOperations.java index 9ab7636d6c9..7d7627dfc03 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/tools/MarkerToolOperations.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/tools/MarkerToolOperations.java @@ -31,7 +31,6 @@ import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.RemoteIterator; import org.apache.hadoop.fs.s3a.Retries; import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.s3guard.BulkOperationState; /** * Operations which must be offered by the store for {@link MarkerTool}. @@ -41,8 +40,7 @@ import org.apache.hadoop.fs.s3a.s3guard.BulkOperationState; public interface MarkerToolOperations { /** - * Create an iterator over objects in S3 only; S3Guard - * is not involved. + * Create an iterator over objects in S3. * The listing includes the key itself, if found. * @param path path of the listing. * @param key object key @@ -56,17 +54,10 @@ public interface MarkerToolOperations { throws IOException; /** - * Remove keys from the store, updating the metastore on a - * partial delete represented as a MultiObjectDeleteException failure by - * deleting all those entries successfully deleted and then rethrowing - * the MultiObjectDeleteException. + * Remove keys from the store. * @param keysToDelete collection of keys to delete on the s3-backend. * if empty, no request is made of the object store. * @param deleteFakeDir indicates whether this is for deleting fake dirs. - * @param undeletedObjectsOnFailure List which will be built up of all - * files that were not deleted. This happens even as an exception - * is raised. - * @param operationState bulk operation state * @param quiet should a bulk query be quiet, or should its result list * all deleted keys * @return the deletion result if a multi object delete was invoked @@ -82,8 +73,6 @@ public interface MarkerToolOperations { DeleteObjectsResult removeKeys( List keysToDelete, boolean deleteFakeDir, - List undeletedObjectsOnFailure, - BulkOperationState operationState, boolean quiet) throws MultiObjectDeleteException, AmazonClientException, IOException; diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/tools/MarkerToolOperationsImpl.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/tools/MarkerToolOperationsImpl.java index d14bb6b1d8e..7ccbc41bbea 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/tools/MarkerToolOperationsImpl.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/tools/MarkerToolOperationsImpl.java @@ -30,7 +30,6 @@ import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.RemoteIterator; import org.apache.hadoop.fs.s3a.S3AFileStatus; import org.apache.hadoop.fs.s3a.impl.OperationCallbacks; -import org.apache.hadoop.fs.s3a.s3guard.BulkOperationState; /** * Implement the marker tool operations by forwarding to the @@ -59,12 +58,10 @@ public class MarkerToolOperationsImpl implements MarkerToolOperations { public DeleteObjectsResult removeKeys( final List keysToDelete, final boolean deleteFakeDir, - final List undeletedObjectsOnFailure, - final BulkOperationState operationState, final boolean quiet) throws MultiObjectDeleteException, AmazonClientException, IOException { return operationCallbacks.removeKeys(keysToDelete, deleteFakeDir, - undeletedObjectsOnFailure, operationState, quiet); + quiet); } } diff --git a/hadoop-tools/hadoop-aws/src/main/shellprofile.d/hadoop-s3guard.sh b/hadoop-tools/hadoop-aws/src/main/shellprofile.d/hadoop-s3guard.sh index 039b0772e76..4a4dc734e78 100644 --- a/hadoop-tools/hadoop-aws/src/main/shellprofile.d/hadoop-s3guard.sh +++ b/hadoop-tools/hadoop-aws/src/main/shellprofile.d/hadoop-s3guard.sh @@ -18,7 +18,7 @@ if ! declare -f hadoop_subcommand_s3guard >/dev/null 2>/dev/null; then if [[ "${HADOOP_SHELL_EXECNAME}" = hadoop ]]; then - hadoop_add_subcommand "s3guard" client "manage metadata on S3" + hadoop_add_subcommand "s3guard" client "S3 Commands" fi # this can't be indented otherwise shelldocs won't get it diff --git a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/assumed_roles.md b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/assumed_roles.md index 96bd64811aa..45170fc339e 100644 --- a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/assumed_roles.md +++ b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/assumed_roles.md @@ -39,7 +39,7 @@ are, how to configure their policies, etc. * You need a role to assume, and know its "ARN". * You need a pair of long-lived IAM User credentials, not the root account set. * Have the AWS CLI installed, and test that it works there. -* Give the role access to S3, and, if using S3Guard, to DynamoDB. +* Give the role access to S3. * For working with data encrypted with SSE-KMS, the role must have access to the appropriate KMS keys. @@ -234,9 +234,6 @@ s3:Get* s3:ListBucket ``` -When using S3Guard, the client needs the appropriate -DynamoDB access permissions - To use SSE-KMS encryption, the client needs the SSE-KMS Permissions to access the KMS key(s). @@ -277,47 +274,6 @@ If the caller doesn't have these permissions, the operation will fail with an `AccessDeniedException`: the S3 Store does not provide the specifics of the cause of the failure. -### S3Guard Permissions - -To use S3Guard, all clients must have a subset of the -[AWS DynamoDB Permissions](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/api-permissions-reference.html). - -To work with buckets protected with S3Guard, the client must have -all the following rights on the DynamoDB Table used to protect that bucket. - -``` -dynamodb:BatchGetItem -dynamodb:BatchWriteItem -dynamodb:DeleteItem -dynamodb:DescribeTable -dynamodb:GetItem -dynamodb:PutItem -dynamodb:Query -dynamodb:UpdateItem -``` - -This is true, *even if the client only has read access to the data*. - -For the `hadoop s3guard` table management commands, _extra_ permissions are required: - -``` -dynamodb:CreateTable -dynamodb:DescribeLimits -dynamodb:DeleteTable -dynamodb:Scan -dynamodb:TagResource -dynamodb:UntagResource -dynamodb:UpdateTable -``` - -Without these permissions, tables cannot be created, destroyed or have their IO capacity -changed through the `s3guard set-capacity` call. -The `dynamodb:Scan` permission is needed for `s3guard prune` - -The `dynamodb:CreateTable` permission is needed by a client when it tries to -create the DynamoDB table on startup, that is -`fs.s3a.s3guard.ddb.table.create` is `true` and the table does not already exist. - ### Mixed Permissions in a single S3 Bucket Mixing permissions down the "directory tree" is limited @@ -348,10 +304,6 @@ file will exist. For a directory copy, only a partial copy of the source data may take place before the permission failure is raised. - -*S3Guard*: if [S3Guard](s3guard.html) is used to manage the directory listings, -then after partial failures of rename/copy the DynamoDB tables can get out of sync. - ### Example: Read access to the base, R/W to the path underneath This example has the base bucket read only, and a directory underneath, @@ -818,29 +770,6 @@ Caused by: com.amazonaws.services.s3.model.AmazonS3Exception: Access Denied (Se Note: the ability to read encrypted data in the store does not guarantee that the caller can encrypt new data. It is a separate permission. - -### `AccessDeniedException` + `AmazonDynamoDBException` - -``` -java.nio.file.AccessDeniedException: bucket1: - com.amazonaws.services.dynamodbv2.model.AmazonDynamoDBException: - User: arn:aws:sts::980678866538:assumed-role/s3guard-test-role/test is not authorized to perform: - dynamodb:DescribeTable on resource: arn:aws:dynamodb:us-west-1:980678866538:table/bucket1 - (Service: AmazonDynamoDBv2; Status Code: 400; -``` - -The caller is trying to access an S3 bucket which uses S3Guard, but the caller -lacks the relevant DynamoDB access permissions. - -The `dynamodb:DescribeTable` operation is the first one used in S3Guard to access, -the DynamoDB table, so it is often the first to fail. It can be a sign -that the role has no permissions at all to access the table named in the exception, -or just that this specific permission has been omitted. - -If the role policy requested for the assumed role didn't ask for any DynamoDB -permissions, this is where all attempts to work with a S3Guarded bucket will -fail. Check the value of `fs.s3a.assumed.role.policy` - ### Error `Unable to execute HTTP request` This is a low-level networking error. Possible causes include: diff --git a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/committer_architecture.md b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/committer_architecture.md index 048f08cf7c0..b69be8ae336 100644 --- a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/committer_architecture.md +++ b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/committer_architecture.md @@ -1818,7 +1818,7 @@ directory on the job commit, so is *very* expensive, and not something which we recommend when working with S3. -To use a S3Guard committer, it must also be identified as the Parquet committer. +To use an S3A committer, it must also be identified as the Parquet committer. The fact that instances are dynamically instantiated somewhat complicates the process. In early tests; we can switch committers for ORC output without making any changes @@ -1928,12 +1928,6 @@ files. ### Security Risks of all committers -#### Visibility - -[Obsolete] If S3Guard is used for storing metadata, then the metadata is visible to -all users with read access. A malicious user with write access could delete -entries of newly generated files, so they would not be visible. - #### Malicious Serialized Data diff --git a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/committers.md b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/committers.md index d09038cd63f..989fdd0fd8e 100644 --- a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/committers.md +++ b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/committers.md @@ -755,10 +755,10 @@ in configuration option fs.s3a.committer.magic.enabled The Job is configured to use the magic committer, but the S3A bucket has not been explicitly declared as supporting it. +The Job is configured to use the magic committer, but the S3A bucket has not been explicitly declared as supporting it. -This can be done for those buckets which are known to be consistent, either -because [S3Guard](s3guard.html) is used to provide consistency, -or because the S3-compatible filesystem is known to be strongly consistent. +As this is now true by default, this error will only surface with a configuration which has explicitly disabled it. +Remove all global/per-bucket declarations of `fs.s3a.bucket.magic.enabled` or set them to `true` ```xml @@ -767,29 +767,35 @@ or because the S3-compatible filesystem is known to be strongly consistent. ``` - Tip: you can verify that a bucket supports the magic committer through the `hadoop s3guard bucket-info` command: ``` > hadoop s3guard bucket-info -magic s3a://landsat-pds/ - -Filesystem s3a://landsat-pds Location: us-west-2 -Filesystem s3a://landsat-pds is not using S3Guard -The "magic" committer is not supported S3A Client - Signing Algorithm: fs.s3a.signing-algorithm=(unset) - Endpoint: fs.s3a.endpoint=s3.amazonaws.com - Encryption: fs.s3a.server-side-encryption-algorithm=none - Input seek policy: fs.s3a.experimental.input.fadvise=normal - Change Detection Source: fs.s3a.change.detection.source=etag - Change Detection Mode: fs.s3a.change.detection.mode=server -Delegation token support is disabled -2019-05-17 13:53:38,245 [main] INFO util.ExitUtil (ExitUtil.java:terminate(210)) - - Exiting with status 46: 46: The magic committer is not enabled for s3a://landsat-pds + Signing Algorithm: fs.s3a.signing-algorithm=(unset) + Endpoint: fs.s3a.endpoint=s3.amazonaws.com + Encryption: fs.s3a.encryption.algorithm=none + Input seek policy: fs.s3a.experimental.input.fadvise=normal + Change Detection Source: fs.s3a.change.detection.source=etag + Change Detection Mode: fs.s3a.change.detection.mode=server + +S3A Committers + The "magic" committer is supported in the filesystem + S3A Committer factory class: mapreduce.outputcommitter.factory.scheme.s3a=org.apache.hadoop.fs.s3a.commit.S3ACommitterFactory + S3A Committer name: fs.s3a.committer.name=magic + Store magic committer integration: fs.s3a.committer.magic.enabled=true + +Security + Delegation token support is disabled + +Directory Markers + The directory marker policy is "delete" + Available Policies: delete, keep, authoritative + Authoritative paths: fs.s3a.authoritative.path=``` ``` ### Error message: "File being created has a magic path, but the filesystem has magic file support disabled" @@ -802,11 +808,6 @@ This message should not appear through the committer itself —it will fail with the error message in the previous section, but may arise if other applications are attempting to create files under the path `/__magic/`. -Make sure the filesystem meets the requirements of the magic committer -(a consistent S3A filesystem through S3Guard or the S3 service itself), -and set the `fs.s3a.committer.magic.enabled` flag to indicate that magic file -writes are supported. - ### `FileOutputCommitter` appears to be still used (from logs or delays in commits) diff --git a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/delegation_token_architecture.md b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/delegation_token_architecture.md index 90e4e5587d0..0ba516313f4 100644 --- a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/delegation_token_architecture.md +++ b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/delegation_token_architecture.md @@ -91,7 +91,7 @@ of: These credentials are obtained from the AWS Secure Token Service (STS) when the the token is issued. * A set of AWS session credentials binding the user to a specific AWS IAM Role, -further restricted to only access the S3 bucket and matching S3Guard DynamoDB table. +further restricted to only access the S3 bucket. Again, these credentials are requested when the token is issued. @@ -404,7 +404,6 @@ Else: as with session delegation tokens, an STS client is created. This time set to restrict access to: * CRUD access the specific bucket a token is being requested for -* CRUD access to the contents of any S3Guard DDB used (not admin rights though). * access to all KMS keys (assumption: AWS KMS is where restrictions are set up) *Example Generated Role Policy* @@ -428,12 +427,7 @@ set to restrict access to: "Effect" : "Allow", "Action" : [ "kms:Decrypt", "kms:GenerateDataKey" ], "Resource" : "arn:aws:kms:*" - }, { - "Sid" : "9", - "Effect" : "Allow", - "Action" : [ "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem", "dynamodb:DeleteItem", "dynamodb:DescribeTable", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query", "dynamodb:UpdateItem" ], - "Resource" : "arn:aws:dynamodb:eu-west-1:980678866fff:table/example-bucket" - } ] + }] } ``` diff --git a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/delegation_tokens.md b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/delegation_tokens.md index aad3f355b2d..f8f9d88d1e7 100644 --- a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/delegation_tokens.md +++ b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/delegation_tokens.md @@ -39,8 +39,7 @@ Clients with this token have the full permissions of the user. *Role Delegation Tokens:* These contain an "STS Session Token" requested by by the STS "Assume Role" API, so grant the caller to interact with S3 as specific AWS -role, *with permissions restricted to purely accessing the S3 bucket -and associated S3Guard data*. +role, *with permissions restricted to purely accessing that specific S3 bucket*. Role Delegation Tokens are the most powerful. By restricting the access rights of the granted STS token, no process receiving the token may perform @@ -121,6 +120,8 @@ Because applications only collect Delegation Tokens in secure clusters, It does mean that to be able to submit delegation tokens in transient cloud-hosted Hadoop clusters, _these clusters must also have Kerberos enabled_. +*Tip*: you should only be deploying Hadoop in public clouds with Kerberos enabled. + ### S3A Session Delegation Tokens @@ -143,8 +144,7 @@ for specifics details on the (current) token lifespan. A Role Delegation Tokens is created by asking the AWS [Security Token Service](http://docs.aws.amazon.com/STS/latest/APIReference/Welcome.html) for set of "Assumed Role" credentials, with a AWS account specific role for a limited duration.. -This role is restricted to only grant access the S3 bucket, the S3Guard table -and all KMS keys, +This role is restricted to only grant access the S3 bucket and all KMS keys, They are marshalled into the S3A Delegation Token. Other S3A connectors can extract these credentials and use them to @@ -389,10 +389,10 @@ There are some further configuration options: | `fs.s3a.assumed.role.sts.endpoint.region` | region for issued tokens | (undefined) | The option `fs.s3a.assumed.role.arn` must be set to a role which the -user can assume. It must have permissions to access the bucket, any -associated S3Guard table and any KMS encryption keys. The actual +user can assume. It must have permissions to access the bucket and +any KMS encryption keys. The actual requested role will be this role, explicitly restricted to the specific -bucket and S3Guard table. +bucket. The XML settings needed to enable session tokens are: @@ -416,14 +416,10 @@ A JSON role policy for the role/session will automatically be generated which wi consist of 1. Full access to the S3 bucket for all operations used by the S3A client (read, write, list, multipart operations, get bucket location, etc). -1. Full user access to any S3Guard DynamoDB table used by the bucket. 1. Full user access to KMS keys. This is to be able to decrypt any data in the bucket encrypted with SSE-KMS, as well as encrypt new data if that is the encryption policy. -If the client doesn't have S3Guard enabled, but the remote application does, -the issued role tokens will not have permission to access the S3Guard table. - ### Enabling Full Delegation Tokens This passes the full credentials in, falling back to any session credentials @@ -663,7 +659,7 @@ for unmarshalling. This identifier must contain all information needed at the far end to authenticate the caller with AWS services used by the S3A client: AWS S3 and -potentially AWS KMS (for SSE-KMS) and AWS DynamoDB (for S3Guard). +potentially AWS KMS (for SSE-KMS). It must have its own unique *Token Kind*, to ensure that it can be distinguished from the other token identifiers when tokens are being unmarshalled. @@ -693,8 +689,7 @@ is needed to keep them out of logs. * Secrets MUST NOT be logged, even at debug level. * Prefer short-lived session secrets over long-term secrets. * Try to restrict the permissions to what a client with the delegated token - may perform to those needed to access data in the S3 bucket. This potentially - includes a DynamoDB table, KMS access, etc. + may perform to those needed to access data in the S3 bucket. * Implementations need to be resistant to attacks which pass in invalid data as their token identifier: validate the types of the unmarshalled data; set limits on the size of all strings and other arrays to read in, etc. @@ -798,7 +793,6 @@ Create the delegation token. If non-empty, the `policy` argument contains an AWS policy model to grant access to: * The target S3 bucket. -* Any S3Guard DDB table it is bonded to. * KMS key `"kms:GenerateDataKey` and `kms:Decrypt`permissions for all KMS keys. This can be converted to a string and passed to the AWS `assumeRole` operation. diff --git a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/directory_markers.md b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/directory_markers.md index 65fcb6502fb..27c337354c4 100644 --- a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/directory_markers.md +++ b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/directory_markers.md @@ -51,16 +51,6 @@ These branches have read-only compatibility. such directories have child entries. * They will open files under directories with such markers. -However, they have limitations when writing/deleting directories. - -Specifically: S3Guard tables may not be correctly updated in -all conditions, especially on the partial failure of delete -operations. Specifically: they may mistakenly add a tombstone in -the dynamoDB table and so future directory/directory tree listings -will consider the directory to be nonexistent. - -_It is not safe for Hadoop releases before Hadoop 3.3.1 to write -to S3 buckets which have directory markers when S3Guard is enabled_ ## Verifying read compatibility. @@ -261,7 +251,7 @@ This is backwards compatible _outside authoritative directories_. Until now, the notion of an "authoritative" directory has only been used as a performance optimization for deployments -where it is known that all Applications are using the same S3Guard metastore +where it is known that all applications were using the same S3Guard metastore when writing and reading data. In such a deployment, if it is also known that all applications are using a compatible version of the s3a connector, then they @@ -272,41 +262,31 @@ every shipping Hadoop releases. ## Directory Markers and S3Guard -Applications which interact with S3A in S3A clients with S3Guard enabled still -create and delete markers. There's no attempt to skip operations, such as by having -`mkdirs() `create entries in the DynamoDB table but not the store. -Having the client always update S3 ensures that other applications and clients -do (eventually) see the changes made by the "guarded" application. +The now-deleted S3Guard feature included the concept of "authoritative paths"; +paths where all clients were required to be using S3Guard and sharing the +same metadata store. +In such a setup, listing authoritative paths would skip all queries of the S3 +store -potentially being much faster. + +In production, authoritative paths were usually only ever for Hive managed +tables, where access was strictly restricted to the Hive services. + +Likewise, the directory marker retention can enabled purely for authoritative +paths. When S3Guard is configured to treat some directories as [Authoritative](s3guard.html#authoritative) then an S3A connector with a retention policy of `fs.s3a.directory.marker.retention` of `authoritative` will omit deleting markers in authoritative directories. -*Note* there may be further changes in directory semantics in "authoritative mode"; -only use in managed applications where all clients are using the same version of -hadoop, and configured consistently. - -After the directory marker feature [HADOOP-13230](https://issues.apache.org/jira/browse/HADOOP-13230) -was added, issues related to S3Guard integration surfaced: - -1. The incremental update of the S3Guard table was inserting tombstones - over directories as the markers were deleted, hiding files underneath. - This happened during directory `rename()` and `delete()`. -1. The update of the S3Guard table after a partial failure of a bulk delete - operation would insert tombstones in S3Guard records of successfully - deleted markers, irrespective of the directory status. - -Issue #1 is unique to Hadoop branch 3.3; however issue #2 is s critical -part of the S3Guard consistency handling. - -Both issues have been fixed in Hadoop 3.3.x, -in [HADOOP-17244](https://issues.apache.org/jira/browse/HADOOP-17244) - -Issue #2, delete failure handling, is not easily backported and is -not likely to be backported. - -Accordingly: Hadoop releases with read-only compatibility must not be used -to rename or delete directories where markers are retained *when S3Guard is enabled.* +```xml + + fs.s3a.bucket.hive.authoritative.path + /tables + +``` +This an option to consider if not 100% confident that all +applications interacting with a store are using an S3A client +which is marker aware. ## Verifying marker policy with `s3guard bucket-info` @@ -334,7 +314,6 @@ Example: `s3guard bucket-info -markers aware` on a compatible release. > hadoop s3guard bucket-info -markers aware s3a://landsat-pds/ Filesystem s3a://landsat-pds Location: us-west-2 - Filesystem s3a://landsat-pds is not using S3Guard ... @@ -354,13 +333,9 @@ is unknown > hadoop s3guard bucket-info -markers aware s3a://landsat-pds/ Illegal option -markers Usage: hadoop bucket-info [OPTIONS] s3a://BUCKET - provide/check S3Guard information about a specific bucket + provide/check information about a specific bucket Common options: - -guarded - Require S3Guard - -unguarded - Force S3Guard to be disabled - -auth - Require the S3Guard mode to be "authoritative" - -nonauth - Require the S3Guard mode to be "non-authoritative" -magic - Require the S3 filesystem to be support the "magic" committer -encryption -require {none, sse-s3, sse-kms} - Require encryption policy @@ -380,7 +355,6 @@ A specific policy check verifies that the connector is configured as desired > hadoop s3guard bucket-info -markers delete s3a://landsat-pds/ Filesystem s3a://landsat-pds Location: us-west-2 -Filesystem s3a://landsat-pds is not using S3Guard ... @@ -394,16 +368,33 @@ does not match that requested: > hadoop s3guard bucket-info -markers keep s3a://landsat-pds/ Filesystem s3a://landsat-pds Location: us-west-2 -Filesystem s3a://landsat-pds is not using S3Guard -... +S3A Client + Signing Algorithm: fs.s3a.signing-algorithm=(unset) + Endpoint: fs.s3a.endpoint=s3.amazonaws.com + Encryption: fs.s3a.encryption.algorithm=none + Input seek policy: fs.s3a.experimental.input.fadvise=normal + Change Detection Source: fs.s3a.change.detection.source=etag + Change Detection Mode: fs.s3a.change.detection.mode=server + +S3A Committers + The "magic" committer is supported in the filesystem + S3A Committer factory class: mapreduce.outputcommitter.factory.scheme.s3a=org.apache.hadoop.fs.s3a.commit.S3ACommitterFactory + S3A Committer name: fs.s3a.committer.name=magic + Store magic committer integration: fs.s3a.committer.magic.enabled=true Security - Delegation token support is disabled + Delegation token support is disabled -The directory marker policy is "delete" +Directory Markers + The directory marker policy is "delete" + Available Policies: delete, keep, authoritative + Authoritative paths: fs.s3a.authoritative.path= + +2021-11-22 16:03:59,175 [main] INFO util.ExitUtil (ExitUtil.java:terminate(210)) + -Exiting with status 46: 46: Bucket s3a://landsat-pds: required marker polic is + "keep" but actual policy is "delete" -2020-08-12 17:14:30,563 [main] INFO util.ExitUtil (ExitUtil.java:terminate(210)) - Exiting with status 46: 46: Bucket s3a://landsat-pds: required marker policy is "keep" but actual policy is "delete" ``` diff --git a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/encryption.md b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/encryption.md index ccdfeeda7e9..ce1286c414a 100644 --- a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/encryption.md +++ b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/encryption.md @@ -601,7 +601,6 @@ clients where S3-CSE has not been enabled. ### Limitations -- S3Guard is not supported with S3-CSE. - Performance will be reduced. All encrypt/decrypt is now being done on the client. - Writing files may be slower, as only a single block can be encrypted and diff --git a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/index.md b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/index.md index 3d08a1a3ea7..9c5091db9e7 100644 --- a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/index.md +++ b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/index.md @@ -84,11 +84,10 @@ schemes. properties, the Hadoop key management store and IAM roles. * Supports per-bucket configuration. * Supports S3 "Server Side Encryption" for both reading and writing: - SSE-S3, SSE-KMS and SSE-C + SSE-S3, SSE-KMS and SSE-C. * Instrumented with Hadoop metrics. * Before S3 was consistent, provided a consistent view of inconsistent storage through [S3Guard](./s3guard.html). - * Actively maintained by the open source community. @@ -148,32 +147,11 @@ Amazon S3 is an example of "an object store". In order to achieve scalability and especially high availability, S3 has —as many other cloud object stores have done— relaxed some of the constraints which classic "POSIX" filesystems promise. -The [S3Guard](./s3guard.html) feature attempts to address some of these, but -it cannot do so completely. Do read these warnings and consider how -they apply. - For further discussion on these topics, please consult [The Hadoop FileSystem API Definition](../../../hadoop-project-dist/hadoop-common/filesystem/index.html). -### Warning #1: S3 Consistency model -Amazon S3 is an example of "an object store". In order to achieve scalability -and especially high availability, S3 has —as many other cloud object stores have -done— relaxed some of the constraints which classic "POSIX" filesystems promise. - -Specifically - -1. Files that are newly created from the Hadoop Filesystem APIs may not be -immediately visible. -2. File delete and update operations may not immediately propagate. Old -copies of the file may exist for an indeterminate time period. -3. Directory operations: `delete()` and `rename()` are implemented by -recursive file-by-file operations. They take time at least proportional to -the number of files, during which time partial updates may be visible. If -the operations are interrupted, the filesystem is left in an intermediate state. - - -### Warning #2: Directories are mimicked +### Warning #1: Directories are mimicked The S3A clients mimics directories by: @@ -213,7 +191,7 @@ to safely save the output of queries directly into S3 object stores through the S3A filesystem. -### Warning #3: Object stores have different authorization models +### Warning #2: Object stores have different authorization models The object authorization model of S3 is much different from the file authorization model of HDFS and traditional file systems. @@ -551,7 +529,7 @@ This means that the default S3A authentication chain can be defined as com.amazonaws.auth.AWSCredentialsProvider. When S3A delegation tokens are not enabled, this list will be used - to directly authenticate with S3 and DynamoDB services. + to directly authenticate with S3 and other AWS services. When S3A Delegation tokens are enabled, depending upon the delegation token binding it may be used to communicate with the STS endpoint to request session/role @@ -778,10 +756,9 @@ All S3A client options are configured with options with the prefix `fs.s3a.`. The client supports Per-bucket configuration to allow different buckets to override the shared settings. This is commonly used to change the endpoint, encryption and authentication mechanisms of buckets. -S3Guard options, various minor options. +and various minor options. -Here are the S3A properties for use in production. The S3Guard options are -documented in the [S3Guard documents](./s3guard.html); some testing-related +Here are the S3A properties for use in production; some testing-related options are covered in [Testing](./testing.md). ```xml @@ -1165,10 +1142,10 @@ be the wrong decision: rebuild the `hadoop-aws` module with the constant -### Throttled requests from S3 and Dynamo DB +### Throttled requests from S3 -When S3A or Dynamo DB returns a response indicating that requests +When AWS S3 returns a response indicating that requests from the caller are being throttled, an exponential back-off with an initial interval and a maximum number of requests. @@ -1235,8 +1212,6 @@ performed by clients. !. Maybe: increase `fs.s3a.readahead.range` to increase the minimum amount of data asked for in every GET request, as well as how much data is skipped in the existing stream before aborting it and creating a new stream. -1. If the DynamoDB tables used by S3Guard are being throttled, increase -the capacity through `hadoop s3guard set-capacity` (and pay more, obviously). 1. KMS: "consult AWS about increasing your capacity". @@ -1312,13 +1287,6 @@ that had already read the first byte. Seeks backward on the other hand can result in new 'Get Object' requests that can trigger the `RemoteFileChangedException`. -Additionally, due to the eventual consistency of S3 in a read-after-overwrite -scenario, visibility of a new write may be delayed, avoiding the -`RemoteFileChangedException` for some readers. That said, if a reader does not -see `RemoteFileChangedException`, they will have at least read a consistent view -of a single version of the file (the version available when they started -reading). - ### Change detection with S3 Versions. It is possible to switch to using the @@ -2025,7 +1993,7 @@ except also allows for a custom SignerInitializer (`org.apache.hadoop.fs.s3a.AwsSignerInitializer`) class to be specified. #### Usage of the Signers -Signers can be set at a per service level(S3, dynamodb, etc) or a common +Signers can be set at a per-service level (S3, etc) or a common signer for all services. ```xml @@ -2035,12 +2003,6 @@ signer for all services. Specify the signer for S3
    - - fs.s3a.ddb.signing-algorithm - ${DdbSignerName} - Specify the signer for DDB - - fs.s3a.signing-algorithm ${SignerName} diff --git a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/performance.md b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/performance.md index ab8b922df2a..f398c4cbcbe 100644 --- a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/performance.md +++ b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/performance.md @@ -55,16 +55,6 @@ it isn't, and some attempts to preserve the metaphor are "aggressively suboptima To make most efficient use of S3, care is needed. -## Speeding up directory listing operations through S3Guard - -[S3Guard](s3guard.html) provides significant speedups for operations which -list files a lot. This includes the setup of all queries against data: -MapReduce, Hive and Spark, as well as DistCP. - - -Experiment with using it to see what speedup it delivers. - - ## Improving data input performance through fadvise The S3A Filesystem client supports the notion of input policies, similar @@ -157,9 +147,7 @@ When using S3 as a destination, this is slow because of the way `rename()` is mimicked with copy and delete. If committing output takes a long time, it is because you are using the standard -`FileOutputCommitter`. If you are doing this on any S3 endpoint which lacks -list consistency (Amazon S3 without [S3Guard](s3guard.html)), this committer -is at risk of losing data! +`FileOutputCommitter`. *Your problem may appear to be performance, but that is a symptom of the underlying problem: the way S3A fakes rename operations means that @@ -448,27 +436,6 @@ If you believe that you are reaching these limits, you may be able to get them increased. Consult [the KMS Rate Limit documentation](http://docs.aws.amazon.com/kms/latest/developerguide/limits.html). -### S3Guard and Throttling - - -S3Guard uses DynamoDB for directory and file lookups; -it is rate limited to the amount of (guaranteed) IO purchased for a -table. - -To see the allocated capacity of a bucket, the `hadoop s3guard bucket-info s3a://bucket` -command will print out the allocated capacity. - - -If significant throttling events/rate is observed here, the pre-allocated -IOPs can be increased with the `hadoop s3guard set-capacity` command, or -through the AWS Console. Throttling events in S3Guard are noted in logs, and -also in the S3A metrics `s3guard_metadatastore_throttle_rate` and -`s3guard_metadatastore_throttled`. - -If you are using DistCP for a large backup to/from a S3Guarded bucket, it is -actually possible to increase the capacity for the duration of the operation. - - ## Best Practises for Code Here are some best practises if you are writing applications to work with @@ -484,10 +451,6 @@ Cache the outcome of `getFileStats()`, rather than repeatedly ask for it. That includes using `isFile()`, `isDirectory()`, which are simply wrappers around `getFileStatus()`. -Don't immediately look for a file with a `getFileStatus()` or listing call -after creating it, or try to read it immediately. -This is where eventual consistency problems surface: the data may not yet be visible. - Rely on `FileNotFoundException` being raised if the source of an operation is missing, rather than implementing your own probe for the file before conditionally calling the operation. diff --git a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/s3guard.md b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/s3guard.md index 27ac10c825f..046717bd5f4 100644 --- a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/s3guard.md +++ b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/s3guard.md @@ -12,14 +12,14 @@ limitations under the License. See accompanying LICENSE file. --> -# S3Guard: Consistency and Metadata Caching for S3A +# S3Guard: Consistency and Metadata Caching for S3A (retired) ## Overview -*S3Guard* is a feature for the S3A client of the S3 object store, -which can use a (consistent) database as the store of metadata about objects +*S3Guard* was a feature for the S3A client of the S3 object store, +which used a consistent database as the store of metadata about objects in an S3 bucket. It was written been 2016 and 2020, *when Amazon S3 was eventually consistent.* @@ -32,7 +32,14 @@ It compensated for the following S3 inconsistencies: It did not compensate for update inconsistency, though by storing the etag values of objects in the database, it could detect and report problems. -Now that S3 is consistent, there is no need for S3Guard at all. +Now that S3 is consistent, _there is no need for S3Guard at all._ +Accordingly, it was removed from the source in 2022 in [HADOOP-17409](https://issues.apache.org/jira/browse/HADOOP-17409), _Remove S3Guard_. + +Attempting to create an S3A connector instance with S3Guard set to anything but the +null or local metastores will now fail. + + +### S3Guard History S3Guard @@ -43,8 +50,6 @@ including those which take place during the partitioning period of query execution, the process where files are listed and the work divided up amongst processes. - - The basic idea was that, for each operation in the Hadoop S3 client (s3a) that reads or modifies metadata, a shadow copy of that metadata is stored in a separate MetadataStore implementation. The store was @@ -59,74 +64,39 @@ separate MetadataStore implementation. The store was For links to early design documents and related patches, see [HADOOP-13345](https://issues.apache.org/jira/browse/HADOOP-13345). -*Important* - -* While all underlying data is persisted in S3, if, for some reason, -the S3Guard-cached metadata becomes inconsistent with that in S3, -queries on the data may become incorrect. -For example, new datasets may be omitted, objects may be overwritten, -or clients may not be aware that some data has been deleted. -It is essential for all clients writing to an S3Guard-enabled -S3 Repository to use the feature. Clients reading the data may work directly -with the S3A data, in which case the normal S3 consistency guarantees apply. ## Moving off S3Guard -How to move off S3Guard, given it is no longer needed. +How to move off S3Guard 1. Unset the option `fs.s3a.metadatastore.impl` globally/for all buckets for which it was selected. -1. If the option `org.apache.hadoop.fs.s3a.s3guard.disabled.warn.level` has been changed from -the default (`SILENT`), change it back. You no longer need to be warned that S3Guard is disabled. 1. Restart all applications. Once you are confident that all applications have been restarted, _Delete the DynamoDB table_. This is to avoid paying for a database you no longer need. -This is best done from the AWS GUI. +This can be done from the AWS GUI. -## Setting up S3Guard +## Removing S3Guard Configurations -### S3A to warn or fail if S3Guard is disabled -A seemingly recurrent problem with S3Guard is that people think S3Guard is -turned on but it isn't. -You can set `org.apache.hadoop.fs.s3a.s3guard.disabled.warn.level` -to avoid this. The property sets what to do when an S3A FS is instantiated -without S3Guard. The following values are available: +The `fs.s3a.metadatastore.impl` option must be one of +* unset +* set to the empty string "" +* set to the "Null" Metadata store `org.apache.hadoop.fs.s3a.s3guard.NullMetadataStore`. -* `SILENT`: Do nothing. -* `INFORM`: Log at info level that FS is instantiated without S3Guard. -* `WARN`: Warn that data may be at risk in workflows. -* `FAIL`: S3AFileSystem instantiation will fail. +To aid the migration of external components which used the Local store for a consistent +view within the test process, the Local Metadata store option is also recognized: +`org.apache.hadoop.fs.s3a.s3guard.LocalMetadataStore`. +When this option is used the S3A connector will warn and continue. -The default setting is `SILENT`. The setting is case insensitive. -The required level can be set in the `core-site.xml`. - ---- -The latest configuration parameters are defined in `core-default.xml`. You -should consult that file for full information, but a summary is provided here. - - -### 1. Choose the Database - -A core concept of S3Guard is that the directory listing data of the object -store, *the metadata* is replicated in a higher-performance, consistent, -database. In S3Guard, this database is called *The Metadata Store* - -By default, S3Guard is not enabled. - -The Metadata Store to use in production is bonded to Amazon's DynamoDB -database service. The following setting will enable this Metadata Store: ```xml fs.s3a.metadatastore.impl - org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore + ``` -Note that the `NullMetadataStore` store can be explicitly requested if desired. -This offers no metadata storage, and effectively disables S3Guard. - ```xml fs.s3a.metadatastore.impl @@ -134,675 +104,55 @@ This offers no metadata storage, and effectively disables S3Guard. ``` -### 2. Configure S3Guard Settings +## Issue: Increased number/cost of S3 IO calls. -More settings will may be added in the future. -Currently the only Metadata Store-independent setting, besides the -implementation class above, are the *allow authoritative* and *fail-on-error* -flags. +More AWS S3 calls may be made once S3Guard is disabled, both for LIST and HEAD operations. -#### Authoritative S3Guard +While this may seem to increase cost, as the DDB table is no longer needed, users will +save on DDB table storage and use costs. -Authoritative S3Guard is a complicated configuration which delivers performance -at the expense of being unsafe for other applications to use the same directory -tree/bucket unless configured consistently. +Some deployments of Apache Hive declared their managed tables to be "authoritative". +The S3 store was no longer checked when listing directories or for updates to +entries. The S3Guard table in DynamoDB was used exclusively. -It can also be used to support [directory marker retention](directory_markers.html) -in higher-performance but non-backwards-compatible modes. +Without S3Guard, listing performance may be slower. However, Hadoop 3.3.0+ has significantly +improved listing performance ([HADOOP-17400](https://issues.apache.org/jira/browse/HADOOP-17400) +_Optimize S3A for maximum performance in directory listings_) so this should not be apparent. -Most deployments do not use this setting -it is ony used in deployments where -specific parts of a bucket (e.g. Apache Hive managed tables) are known to -have exclusive access by a single application (Hive) and other tools/applications -from exactly the same Hadoop release. +We recommend disabling [directory marker deletion](directory_markers.html) to reduce +the number of DELETE operations made when writing files. +this reduces the load on the S3 partition and so the risk of throttling, which can +impact performance. +This is very important when working with versioned S3 buckets, as the tombstone markers +created will slow down subsequent listing operations. -The _authoritative_ expression in S3Guard is present in two different layers, for -two different reasons: - -* Authoritative S3Guard - * S3Guard can be set as authoritative, which means that an S3A client will - avoid round-trips to S3 when **getting file metadata**, and **getting - directory listings** if there is a fully cached version of the directory - stored in metadata store. - * This mode can be set as a configuration property - `fs.s3a.metadatastore.authoritative` - * It can also be set only on specific directories by setting - `fs.s3a.authoritative.path` to one or more prefixes, for example - `s3a://bucket/path` or "/auth1,/auth2". - * All interactions with the S3 bucket(s) must be through S3A clients sharing - the same metadata store. - * This is independent from which metadata store implementation is used. - * In authoritative mode the metadata TTL metadata expiry is not effective. - This means that the metadata entries won't expire on authoritative paths. - -* Authoritative directory listings (isAuthoritative bit) - * Tells if the stored directory listing metadata is complete. - * This is set by the FileSystem client (e.g. s3a) via the `DirListingMetadata` - class (`org.apache.hadoop.fs.s3a.s3guard.DirListingMetadata`). - (The MetadataStore only knows what the FS client tells it.) - * If set to `TRUE`, we know that the directory listing - (`DirListingMetadata`) is full, and complete. - * If set to `FALSE` the listing may not be complete. - * Metadata store may persist the isAuthoritative bit on the metadata store. - * Currently `org.apache.hadoop.fs.s3a.s3guard.LocalMetadataStore` and - `org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore` implementation - supports authoritative bit. - -More on Authoritative S3Guard: - -* This setting is about treating the MetadataStore (e.g. dynamodb) as the source - of truth in general, and also to short-circuit S3 list objects and serve - listings from the MetadataStore in some circumstances. -* For S3A to skip S3's get object metadata, and serve it directly from the -MetadataStore, the following things must all be true: - 1. The S3A client is configured to allow MetadataStore to be authoritative - source of a file metadata (`fs.s3a.metadatastore.authoritative=true`). - 1. The MetadataStore has the file metadata for the path stored in it. -* For S3A to skip S3's list objects on some path, and serve it directly from -the MetadataStore, the following things must all be true: - 1. The MetadataStore implementation persists the bit - `DirListingMetadata.isAuthorititative` set when calling - `MetadataStore#put` (`DirListingMetadata`) - 1. The S3A client is configured to allow MetadataStore to be authoritative - source of a directory listing (`fs.s3a.metadatastore.authoritative=true`). - 1. The MetadataStore has a **full listing for path** stored in it. This only - happens if the FS client (s3a) explicitly has stored a full directory - listing with `DirListingMetadata.isAuthorititative=true` before the said - listing request happens. - -This configuration only enables authoritative mode in the client layer. It is -recommended that you leave the default setting here: - -```xml - - fs.s3a.metadatastore.authoritative - false - -``` - -Note that a MetadataStore MAY persist this bit in the directory listings. (Not -MUST). - -Note that if this is set to true, it may exacerbate or persist existing race -conditions around multiple concurrent modifications and listings of a given -directory tree. - -In particular: **If the Metadata Store is declared as authoritative, -all interactions with the S3 bucket(s) must be through S3A clients sharing -the same Metadata Store** - -#### TTL metadata expiry - -It can be configured how long an entry is valid in the MetadataStore -**if the authoritative mode is turned off**, or the path is not -configured to be authoritative. -If `((lastUpdated + ttl) <= now)` is false for an entry, the entry will -be expired, so the S3 bucket will be queried for fresh metadata. -The time for expiry of metadata can be set as the following: - -```xml - - fs.s3a.metadatastore.metadata.ttl - 15m - -``` - -#### Fail on Error - -By default, S3AFileSystem write operations will fail when updates to -S3Guard metadata fail. S3AFileSystem first writes the file to S3 and then -updates the metadata in S3Guard. If the metadata write fails, -`MetadataPersistenceException` is thrown. The file in S3 **is not** rolled -back. - -If the write operation cannot be programmatically retried, the S3Guard metadata -for the given file can be corrected with a command like the following: - -```bash -hadoop s3guard import [-meta URI] s3a://my-bucket/file-with-bad-metadata -``` - -Programmatic retries of the original operation would require overwrite=true. -Suppose the original operation was `FileSystem.create(myFile, overwrite=false)`. -If this operation failed with `MetadataPersistenceException` a repeat of the -same operation would result in `FileAlreadyExistsException` since the original -operation successfully created the file in S3 and only failed in writing the -metadata to S3Guard. - -Metadata update failures can be downgraded to ERROR logging instead of exception -by setting the following configuration: - -```xml - - fs.s3a.metadatastore.fail.on.write.error - false - -``` - -Setting this false is dangerous as it could result in the type of issue S3Guard -is designed to avoid. For example, a reader may see an inconsistent listing -after a recent write since S3Guard may not contain metadata about the recently -written file due to a metadata write error. - -As with the default setting, the new/updated file is still in S3 and **is not** -rolled back. The S3Guard metadata is likely to be out of sync. - -### 3. Configure the Metadata Store. - -Here are the `DynamoDBMetadataStore` settings. Other Metadata Store -implementations will have their own configuration parameters. - - -### 4. Name Your Table - -First, choose the name of the table you wish to use for the S3Guard metadata -storage in your DynamoDB instance. If you leave it unset/empty, a -separate table will be created for each S3 bucket you access, and that -bucket's name will be used for the name of the DynamoDB table. For example, -this sets the table name to `my-ddb-table-name` - -```xml - - fs.s3a.s3guard.ddb.table - my-ddb-table-name - - The DynamoDB table name to operate. Without this property, the respective - S3 bucket names will be used. - - -``` - -It is good to share a table across multiple buckets for multiple reasons, -especially if you are *not* using on-demand DynamoDB tables, and instead -prepaying for provisioned I/O capacity. - -1. You are billed for the provisioned I/O capacity allocated to the table, -*even when the table is not used*. Sharing capacity can reduce costs. - -1. You can share the "provision burden" across the buckets. That is, rather -than allocating for the peak load on a single bucket, you can allocate for -the peak load *across all the buckets*, which is likely to be significantly -lower. - -1. It's easier to measure and tune the load requirements and cost of -S3Guard, because there is only one table to review and configure in the -AWS management console. - -1. When you don't grant the permission to create DynamoDB tables to users. -A single pre-created table for all buckets avoids the needs for an administrator -to create one for every bucket. - -When wouldn't you want to share a table? - -1. When you are using on-demand DynamoDB and want to keep each table isolated. -1. When you do explicitly want to provision I/O capacity to a specific bucket -and table, isolated from others. - -1. When you are using separate billing for specific buckets allocated -to specific projects. - -1. When different users/roles have different access rights to different buckets. -As S3Guard requires all users to have R/W access to the table, all users will -be able to list the metadata in all buckets, even those to which they lack -read access. - -### 5. Locate your Table - -You may also wish to specify the region to use for DynamoDB. If a region -is not configured, S3A will assume that it is in the same region as the S3 -bucket. A list of regions for the DynamoDB service can be found in -[Amazon's documentation](http://docs.aws.amazon.com/general/latest/gr/rande.html#ddb_region). -In this example, to use the US West 2 region: - -```xml - - fs.s3a.s3guard.ddb.region - us-west-2 - -``` - -When working with S3Guard-managed buckets from EC2 VMs running in AWS -infrastructure, using a local DynamoDB region ensures the lowest latency -and highest reliability, as well as avoiding all long-haul network charges. -The S3Guard tables, and indeed, the S3 buckets, should all be in the same -region as the VMs. - -### 6. Optional: Create your Table - -Next, you can choose whether or not the table will be automatically created -(if it doesn't already exist). If you want this feature, set the -`fs.s3a.s3guard.ddb.table.create` option to `true`. - -```xml - - fs.s3a.s3guard.ddb.table.create - true - - If true, the S3A client will create the table if it does not already exist. - - -``` - -### 7. If creating a table: Choose your billing mode (and perhaps I/O Capacity) - -Next, you need to decide whether to use On-Demand DynamoDB and its -pay-per-request billing (recommended), or to explicitly request a -provisioned IO capacity. - -Before AWS offered pay-per-request billing, the sole billing mechanism, -was "provisioned capacity". This mechanism requires you to choose -the DynamoDB read and write throughput requirements you -expect to need for your expected uses of the S3Guard table. -Setting higher values cost you more money -*even when the table was idle* - *Note* that these settings only affect table creation when -`fs.s3a.s3guard.ddb.table.create` is enabled. To change the throughput for -an existing table, use the AWS console or CLI tool. - -For more details on DynamoDB capacity units, see the AWS page on [Capacity -Unit Calculations](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithTables.html#CapacityUnitCalculations). - -Provisioned IO capacity is billed per hour for the life of the table, *even when the -table and the underlying S3 buckets are not being used*. - -There are also charges incurred for data storage and for data I/O outside of the -region of the DynamoDB instance. S3Guard only stores metadata in DynamoDB: path names -and summary details of objects —the actual data is stored in S3, so billed at S3 -rates. - -With provisioned I/O capacity, attempting to perform more I/O than the capacity -requested throttles the operation and may result in operations failing. -Larger I/O capacities cost more. - -With the introduction of On-Demand DynamoDB, you can now avoid paying for -provisioned capacity by creating an on-demand table. -With an on-demand table you are not throttled if your DynamoDB requests exceed -any pre-provisioned limit, nor do you pay per hour even when a table is idle. - -You do, however, pay more per DynamoDB operation. -Even so, the ability to cope with sudden bursts of read or write requests, combined -with the elimination of charges for idle tables, suit the use patterns made of -S3Guard tables by applications interacting with S3. That is: periods when the table -is rarely used, with intermittent high-load operations when directory trees -are scanned (query planning and similar), or updated (rename and delete operations). - - -We recommending using On-Demand DynamoDB for maximum performance in operations -such as query planning, and lowest cost when S3 buckets are not being accessed. - -This is the default, as configured in the default configuration options. - -```xml - - fs.s3a.s3guard.ddb.table.capacity.read - 0 - - Provisioned throughput requirements for read operations in terms of capacity - units for the DynamoDB table. This config value will only be used when - creating a new DynamoDB table. - If set to 0 (the default), new tables are created with "per-request" capacity. - If a positive integer is provided for this and the write capacity, then - a table with "provisioned capacity" will be created. - You can change the capacity of an existing provisioned-capacity table - through the "s3guard set-capacity" command. - - - - - fs.s3a.s3guard.ddb.table.capacity.write - 0 - - Provisioned throughput requirements for write operations in terms of - capacity units for the DynamoDB table. - If set to 0 (the default), new tables are created with "per-request" capacity. - Refer to related configuration option fs.s3a.s3guard.ddb.table.capacity.read - - -``` - -### 8. If creating a table: Enable server side encryption (SSE) - -Encryption at rest can help you protect sensitive data in your DynamoDB table. -When creating a new table, you can set server side encryption on the table -using the default AWS owned customer master key (CMK), AWS managed CMK, or -customer managed CMK. S3Guard code accessing the table is all the same whether -SSE is enabled or not. For more details on DynamoDB table server side -encryption, see the AWS page on [Encryption at Rest: How It Works](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/encryption.howitworks.html). - -These are the default configuration options, as configured in `core-default.xml`. - -```xml - - fs.s3a.s3guard.ddb.table.sse.enabled - false - - Whether server-side encryption (SSE) is enabled or disabled on the table. - By default it's disabled, meaning SSE is set to AWS owned CMK. - - - - - fs.s3a.s3guard.ddb.table.sse.cmk - - - The KMS Customer Master Key (CMK) used for the KMS encryption on the table. - To specify a CMK, this config value can be its key ID, Amazon Resource Name - (ARN), alias name, or alias ARN. Users only need to provide this config if - the key is different from the default DynamoDB KMS Master Key, which is - alias/aws/dynamodb. - - -``` - -## Authenticating with S3Guard - -The DynamoDB metadata store takes advantage of the fact that the DynamoDB -service uses the same authentication mechanisms as S3. S3Guard -gets all its credentials from the S3A client that is using it. - -All existing S3 authentication mechanisms can be used. - -## Per-bucket S3Guard configuration - -In production, it is likely only some buckets will have S3Guard enabled; -those which are read-only may have disabled, for example. Equally importantly, -buckets in different regions should have different tables, each -in the relevant region. - -These options can be managed through S3A's [per-bucket configuration -mechanism](./index.html#Configuring_different_S3_buckets). -All options with the under `fs.s3a.bucket.BUCKETNAME.KEY` are propagated -to the options `fs.s3a.KEY` *for that bucket only*. - -As an example, here is a configuration to use different metadata stores -and tables for different buckets - -First, we define shortcuts for the metadata store classnames: - - -```xml - - s3guard.null - org.apache.hadoop.fs.s3a.s3guard.NullMetadataStore - - - - s3guard.dynamo - org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore - -``` - -Next, Amazon's public landsat database is configured with no -metadata store: - -```xml - - fs.s3a.bucket.landsat-pds.metadatastore.impl - ${s3guard.null} - The read-only landsat-pds repository isn't - managed by S3Guard - -``` - -Next the `ireland-2` and `ireland-offline` buckets are configured with -DynamoDB as the store, and a shared table `production-table`: - - -```xml - - fs.s3a.bucket.ireland-2.metadatastore.impl - ${s3guard.dynamo} - - - - fs.s3a.bucket.ireland-offline.metadatastore.impl - ${s3guard.dynamo} - - - - fs.s3a.bucket.ireland-2.s3guard.ddb.table - production-table - -``` - -The region of this table is automatically set to be that of the buckets, -here `eu-west-1`; the same table name may actually be used in different -regions. - -Together then, this configuration enables the DynamoDB Metadata Store -for two buckets with a shared table, while disabling it for the public -bucket. - - -### Out-of-band operations with S3Guard - -We call an operation out-of-band (OOB) when a bucket is used by a client with - S3Guard, and another client runs a write (e.g delete, move, rename, - overwrite) operation on an object in the same bucket without S3Guard. - -The definition of behaviour in S3AFileSystem/MetadataStore in case of OOBs: -* A client with S3Guard -* B client without S3Guard (Directly to S3) - - -* OOB OVERWRITE, authoritative mode: - * A client creates F1 file - * B client overwrites F1 file with F2 (Same, or different file size) - * A client's getFileStatus returns F1 metadata - -* OOB OVERWRITE, NOT authoritative mode: - * A client creates F1 file - * B client overwrites F1 file with F2 (Same, or different file size) - * A client's getFileStatus returns F2 metadata. In not authoritative mode we - check S3 for the file. If the modification time of the file in S3 is greater - than in S3Guard, we can safely return the S3 file metadata and update the - cache. - -* OOB DELETE, authoritative mode: - * A client creates F file - * B client deletes F file - * A client's getFileStatus returns that the file is still there - -* OOB DELETE, NOT authoritative mode: - * A client creates F file - * B client deletes F file - * A client's getFileStatus returns that the file is still there - -Note: authoritative and NOT authoritative mode behaves the same at -OOB DELETE case. - -The behaviour in case of getting directory listings: -* File status in metadata store gets updated during the listing the same way -as in getFileStatus. +Finally, the S3A [auditing](auditing.html) feature adds information to the S3 server logs +about which jobs, users and filesystem operations have been making S3 requests. +This auditing information can be used to identify opportunities to reduce load. ## S3Guard Command Line Interface (CLI) -Note that in some cases an AWS region or `s3a://` URI can be provided. - -Metadata store URIs include a scheme that designates the backing store. For -example (e.g. `dynamodb://table_name`;). As documented above, the -AWS region can be inferred if the URI to an existing bucket is provided. - - -The S3A URI must also be provided for per-bucket configuration options -to be picked up. That is: when an s3a URL is provided on the command line, -all its "resolved" per-bucket settings are used to connect to, authenticate -with and configure the S3Guard table. If no such URL is provided, then -the base settings are picked up. - - -### Create a table: `s3guard init` - -```bash -hadoop s3guard init -meta URI ( -region REGION | s3a://BUCKET ) -``` - -Creates and initializes an empty metadata store. - -A DynamoDB metadata store can be initialized with additional parameters -pertaining to capacity. - -If these values are both zero, then an on-demand DynamoDB table is created; -if positive values then they set the -[Provisioned Throughput](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ProvisionedThroughput.html) -of the table. - - -```bash -[-write PROVISIONED_WRITES] [-read PROVISIONED_READS] -``` - -Server side encryption (SSE) can be enabled with AWS managed customer master key -(CMK), or customer managed CMK. By default the DynamoDB table will be encrypted -with AWS owned CMK. To use a customer managed CMK, you can specify its KMS key -ID, ARN, alias name, or alias ARN. If not specified, the default AWS managed CMK -for DynamoDB "alias/aws/dynamodb" will be used. - -```bash -[-sse [-cmk KMS_CMK_ID]] -``` - -Tag argument can be added with a key=value list of tags. The table for the -metadata store will be created with these tags in DynamoDB. - -```bash -[-tag key=value;] -``` - - -Example 1 - -```bash -hadoop s3guard init -meta dynamodb://ireland-team -write 0 -read 0 s3a://ireland-1 -``` - -Creates an on-demand table "ireland-team", -in the same location as the S3 bucket "ireland-1". - - -Example 2 - -```bash -hadoop s3guard init -meta dynamodb://ireland-team -region eu-west-1 --read 0 --write 0 -``` - -Creates a table "ireland-team" in the region "eu-west-1.amazonaws.com" - - -Example 3 - -```bash -hadoop s3guard init -meta dynamodb://ireland-team -tag tag1=first;tag2=second; -``` - -Creates a table "ireland-team" with tags "first" and "second". The read and -write capacity will be those of the site configuration's values of -`fs.s3a.s3guard.ddb.table.capacity.read` and `fs.s3a.s3guard.ddb.table.capacity.write`; -if these are both zero then it will be an on-demand table. - - -Example 4 - -```bash -hadoop s3guard init -meta dynamodb://ireland-team -sse -``` - -Creates a table "ireland-team" with server side encryption enabled. The CMK will -be using the default AWS managed "alias/aws/dynamodb". - - -### Import a bucket: `s3guard import` - -```bash -hadoop s3guard import [-meta URI] [-authoritative] [-verbose] s3a://PATH -``` - -Pre-populates a metadata store according to the current contents of an S3 -bucket/path. If the `-meta` option is omitted, the binding information is taken -from the `core-site.xml` configuration. - -Usage - -``` -hadoop s3guard import - -import [OPTIONS] [s3a://PATH] - import metadata from existing S3 data - -Common options: - -authoritative - Mark imported directory data as authoritative. - -verbose - Verbose Output. - -meta URL - Metadata repository details (implementation-specific) - -Amazon DynamoDB-specific options: - -region REGION - Service region for connections - - URLs for Amazon DynamoDB are of the form dynamodb://TABLE_NAME. - Specifying both the -region option and an S3A path - is not supported. -``` - -Example - -Import all files and directories in a bucket into the S3Guard table. - -```bash -hadoop s3guard import s3a://ireland-1 -``` - -Import a directory tree, marking directories as authoritative. - -```bash -hadoop s3guard import -authoritative -verbose s3a://ireland-1/fork-0008 - -2020-01-03 12:05:18,321 [main] INFO - Metadata store DynamoDBMetadataStore{region=eu-west-1, - tableName=s3guard-metadata, tableArn=arn:aws:dynamodb:eu-west-1:980678866538:table/s3guard-metadata} is initialized. -2020-01-03 12:05:18,324 [main] INFO - Starting: Importing s3a://ireland-1/fork-0008 -2020-01-03 12:05:18,324 [main] INFO - Importing directory s3a://ireland-1/fork-0008 -2020-01-03 12:05:18,537 [main] INFO - Dir s3a://ireland-1/fork-0008/test/doTestListFiles-0-0-0-false -2020-01-03 12:05:18,630 [main] INFO - Dir s3a://ireland-1/fork-0008/test/doTestListFiles-0-0-0-true -2020-01-03 12:05:19,142 [main] INFO - Dir s3a://ireland-1/fork-0008/test/doTestListFiles-2-0-0-false/dir-0 -2020-01-03 12:05:19,191 [main] INFO - Dir s3a://ireland-1/fork-0008/test/doTestListFiles-2-0-0-false/dir-1 -2020-01-03 12:05:19,240 [main] INFO - Dir s3a://ireland-1/fork-0008/test/doTestListFiles-2-0-0-true/dir-0 -2020-01-03 12:05:19,289 [main] INFO - Dir s3a://ireland-1/fork-0008/test/doTestListFiles-2-0-0-true/dir-1 -2020-01-03 12:05:19,314 [main] INFO - Updated S3Guard with 0 files and 6 directory entries -2020-01-03 12:05:19,315 [main] INFO - Marking directory tree s3a://ireland-1/fork-0008 as authoritative -2020-01-03 12:05:19,342 [main] INFO - Importing s3a://ireland-1/fork-0008: duration 0:01.018s -Inserted 6 items into Metadata Store -``` - -### Compare a S3Guard table and the S3 Store: `s3guard diff` - -```bash -hadoop s3guard diff [-meta URI] s3a://BUCKET -``` - -Lists discrepancies between a metadata store and bucket. Note that depending on -how S3Guard is used, certain discrepancies are to be expected. - -Example - -```bash -hadoop s3guard diff s3a://ireland-1 -``` ### Display information about a bucket, `s3guard bucket-info` -Prints and optionally checks the s3guard and encryption status of a bucket. +Prints and optionally checks the status of a bucket. ```bash -hadoop s3guard bucket-info [-guarded] [-unguarded] [-auth] [-nonauth] [-magic] [-encryption ENCRYPTION] s3a://BUCKET +hadoop s3guard bucket-info [-guarded] [-unguarded] [-auth] [-nonauth] [-magic] [-encryption ENCRYPTION] [-markers MARKER] s3a://BUCKET ``` Options | argument | meaning | |-----------|-------------| -| `-guarded` | Require S3Guard to be enabled | -| `-unguarded` | Force S3Guard to be disabled | -| `-auth` | Require the S3Guard mode to be "authoritative" | -| `-nonauth` | Require the S3Guard mode to be "non-authoritative" | +| `-guarded` | Require S3Guard to be enabled. This will now always fail | +| `-unguarded` | Require S3Guard to be disabled. This will now always succeed | +| `-auth` | Require the S3Guard mode to be "authoritative". This will now always fail | +| `-nonauth` | Require the S3Guard mode to be "non-authoritative". This will now always fail | | `-magic` | Require the S3 filesystem to be support the "magic" committer | -| `-encryption ` | Require a specific server-side encryption algorithm | +| `-markers` | Directory marker status: `aware`, `keep`, `delete`, `authoritative` | +| `-encryption ` | Require a specific encryption algorithm | The server side encryption options are not directly related to S3Guard, but it is often convenient to check them at the same time. @@ -810,79 +160,36 @@ it is often convenient to check them at the same time. Example ```bash -hadoop s3guard bucket-info -guarded -magic s3a://ireland-1 -``` +> hadoop s3guard bucket-info -magic -markers keep s3a://test-london/ -List the details of bucket `s3a://ireland-1`, mandating that it must have S3Guard enabled -("-guarded") and that support for the magic S3A committer is enabled ("-magic") - -``` -Filesystem s3a://ireland-1 -Location: eu-west-1 -Filesystem s3a://ireland-1 is using S3Guard with store DynamoDBMetadataStore{region=eu-west-1, tableName=ireland-1} -Authoritative S3Guard: fs.s3a.metadatastore.authoritative=false -Metadata Store Diagnostics: - ARN=arn:aws:dynamodb:eu-west-1:00000000:table/ireland-1 - billing-mode=provisioned - description=S3Guard metadata store in DynamoDB - name=ireland-1 - read-capacity=20 - region=eu-west-1 - retryPolicy=ExponentialBackoffRetry(maxRetries=9, sleepTime=100 MILLISECONDS) - size=12812 - status=ACTIVE - table={AttributeDefinitions: [{AttributeName: child,AttributeType: S}, - {AttributeName: parent,AttributeType: S}],TableName: ireland-1, - KeySchema: [{AttributeName: parent,KeyType: HASH}, {AttributeName: child,KeyType: RANGE}], - TableStatus: ACTIVE, - CreationDateTime: Fri Aug 25 19:07:25 BST 2017, - ProvisionedThroughput: {LastIncreaseDateTime: Tue Aug 29 11:45:18 BST 2017, - LastDecreaseDateTime: Wed Aug 30 15:37:51 BST 2017, - NumberOfDecreasesToday: 1, - ReadCapacityUnits: 20,WriteCapacityUnits: 20}, - TableSizeBytes: 12812,ItemCount: 91, - TableArn: arn:aws:dynamodb:eu-west-1:00000000:table/ireland-1,} - write-capacity=20 -The "magic" committer is supported +2021-11-22 15:21:00,289 [main] INFO impl.DirectoryPolicyImpl (DirectoryPolicyImpl.java:getDirectoryPolicy(189)) - Directory markers will be kept +Filesystem s3a://test-london +Location: eu-west-2 S3A Client - Signing Algorithm: fs.s3a.signing-algorithm=(unset) - Endpoint: fs.s3a.endpoint=s3-eu-west-1.amazonaws.com - Encryption: fs.s3a.server-side-encryption-algorithm=none - Input seek policy: fs.s3a.experimental.input.fadvise=normal - Change Detection Source: fs.s3a.change.detection.source=etag - Change Detection Mode: fs.s3a.change.detection.mode=server -Delegation token support is disabled -``` + Signing Algorithm: fs.s3a.signing-algorithm=(unset) + Endpoint: fs.s3a.endpoint=(unset) + Encryption: fs.s3a.encryption.algorithm=none + Input seek policy: fs.s3a.experimental.input.fadvise=normal + Change Detection Source: fs.s3a.change.detection.source=etag + Change Detection Mode: fs.s3a.change.detection.mode=server -This listing includes all the information about the table supplied from +S3A Committers + The "magic" committer is supported in the filesystem + S3A Committer factory class: mapreduce.outputcommitter.factory.scheme.s3a=org.apache.hadoop.fs.s3a.commit.S3ACommitterFactory + S3A Committer name: fs.s3a.committer.name=magic + Store magic committer integration: fs.s3a.committer.magic.enabled=true -```bash -hadoop s3guard bucket-info -unguarded -encryption none s3a://landsat-pds -``` - -List the S3Guard status of clients of the public `landsat-pds` bucket, -and verifies that the data is neither tracked with S3Guard nor encrypted. +Security + Delegation token support is disabled +Directory Markers + The directory marker policy is "keep" + Available Policies: delete, keep, authoritative + Authoritative paths: fs.s3a.authoritative.path= -``` -Filesystem s3a://landsat-pdsLocation: us-west-2 -Filesystem s3a://landsat-pds is not using S3Guard -Endpoint: fs.s3a.endpoints3.amazonaws.com -Encryption: fs.s3a.server-side-encryption-algorithm=none -Input seek policy: fs.s3a.experimental.input.fadvise=normal ``` -Note that other clients may have a S3Guard table set up to store metadata -on this bucket; the checks are all done from the perspective of the configuration -settings of the current client. - -```bash -hadoop s3guard bucket-info -guarded -auth s3a://landsat-pds -``` - -Require the bucket to be using S3Guard in authoritative mode. This will normally -fail against this specific bucket. ### List or Delete Leftover Multipart Uploads: `s3guard uploads` @@ -918,975 +225,3 @@ default, and terminates with a success or failure exit code depending on whether or not the supplied number matches the number of uploads found that match the given options (path, age). - -### Delete a table: `s3guard destroy` - -Deletes a metadata store. With DynamoDB as the store, this means -the specific DynamoDB table use to store the metadata. - -```bash -hadoop s3guard destroy [-meta URI] ( -region REGION | s3a://BUCKET ) -``` - -This *does not* delete the bucket, only the S3Guard table which it is bound -to. - - -Examples - -```bash -hadoop s3guard destroy s3a://ireland-1 -``` - -Deletes the table which the bucket ireland-1 is configured to use -as its MetadataStore. - -```bash -hadoop s3guard destroy -meta dynamodb://ireland-team -region eu-west-1 -``` - - -### Clean up a table, `s3guard prune` - -Delete all file entries in the MetadataStore table whose object "modification -time" is older than the specified age. - -```bash -hadoop s3guard prune [-days DAYS] [-hours HOURS] [-minutes MINUTES] - [-seconds SECONDS] [-tombstone] [-meta URI] ( -region REGION | s3a://BUCKET ) -``` - -A time value of hours, minutes and/or seconds must be supplied. - -1. This does not delete the entries in the bucket itself. -1. The modification time is effectively the creation time of the objects -in the S3 Bucket. -1. If an S3A URI is supplied, only the entries in the table specified by the -URI and older than a specific age are deleted. - - -The `-tombstone` option instructs the operation to only purge "tombstones", -markers of deleted files. These tombstone markers are only used briefly, -to indicate that a recently deleted file should not be found in listings. -As a result, there is no adverse consequences in regularly pruning old -tombstones. - -Example - -```bash -hadoop s3guard prune -days 7 s3a://ireland-1 -``` - -Deletes all entries in the S3Guard table for files older than seven days from -the table associated with `s3a://ireland-1`. - -```bash -hadoop s3guard prune -tombstone -days 7 s3a://ireland-1/path_prefix/ -``` - -Deletes all entries in the S3Guard table for tombstones older than seven days from -the table associated with `s3a://ireland-1` and with the prefix `path_prefix` - -```bash -hadoop s3guard prune -hours 1 -minutes 30 -meta dynamodb://ireland-team -region eu-west-1 -``` - -Delete all file entries more than 90 minutes old from the table "`ireland-team"` in -the region `eu-west-1`. - - -### Audit the "authoritative state of a DynamoDB Table, `s3guard authoritative` - -This recursively checks a S3Guard table to verify that all directories -underneath are marked as "authoritative", and/or that the configuration -is set for the S3A client to treat files and directories urnder the path -as authoritative. - -``` -hadoop s3guard authoritative - -authoritative [OPTIONS] [s3a://PATH] - Audits a DynamoDB S3Guard repository for all the entries being 'authoritative' - -Options: - -required Require directories under the path to be authoritative. - -check-config Check the configuration for the path to be authoritative - -verbose Verbose Output. -``` - -Verify that a path under an object store is declared to be authoritative -in the cluster configuration -and therefore that file entries will not be -validated against S3, and that directories marked as "authoritative" in the -S3Guard table will be treated as complete. - -```bash -hadoop s3guard authoritative -check-config s3a:///ireland-1/fork-0003/test/ - -2020-01-03 11:42:29,147 [main] INFO Metadata store DynamoDBMetadataStore{ - region=eu-west-1, tableName=s3guard-metadata, tableArn=arn:aws:dynamodb:eu-west-1:980678866538:table/s3guard-metadata} is initialized. -Path /fork-0003/test is not configured to be authoritative -``` - -Scan a store and report which directories are not marked as authoritative. - -```bash -hadoop s3guard authoritative s3a://ireland-1/ - -2020-01-03 11:51:58,416 [main] INFO - Metadata store DynamoDBMetadataStore{region=eu-west-1, tableName=s3guard-metadata, tableArn=arn:aws:dynamodb:eu-west-1:980678866538:table/s3guard-metadata} is initialized. -2020-01-03 11:51:58,419 [main] INFO - Starting: audit s3a://ireland-1/ -2020-01-03 11:51:58,422 [main] INFO - Root directory s3a://ireland-1/ -2020-01-03 11:51:58,469 [main] INFO - files 4; directories 12 -2020-01-03 11:51:58,469 [main] INFO - Directory s3a://ireland-1/Users -2020-01-03 11:51:58,521 [main] INFO - files 0; directories 1 -2020-01-03 11:51:58,522 [main] INFO - Directory s3a://ireland-1/fork-0007 -2020-01-03 11:51:58,573 [main] INFO - Directory s3a://ireland-1/fork-0001 -2020-01-03 11:51:58,626 [main] INFO - files 0; directories 1 -2020-01-03 11:51:58,626 [main] INFO - Directory s3a://ireland-1/fork-0006 -2020-01-03 11:51:58,676 [main] INFO - Directory s3a://ireland-1/path -2020-01-03 11:51:58,734 [main] INFO - files 0; directories 1 -2020-01-03 11:51:58,735 [main] INFO - Directory s3a://ireland-1/fork-0008 -2020-01-03 11:51:58,802 [main] INFO - files 0; directories 1 -2020-01-03 11:51:58,802 [main] INFO - Directory s3a://ireland-1/fork-0004 -2020-01-03 11:51:58,854 [main] INFO - files 0; directories 1 -2020-01-03 11:51:58,855 [main] WARN - Directory s3a://ireland-1/fork-0003 is not authoritative -2020-01-03 11:51:58,905 [main] INFO - files 0; directories 1 -2020-01-03 11:51:58,906 [main] INFO - Directory s3a://ireland-1/fork-0005 -2020-01-03 11:51:58,955 [main] INFO - Directory s3a://ireland-1/customsignerpath2 -2020-01-03 11:51:59,006 [main] INFO - Directory s3a://ireland-1/fork-0002 -2020-01-03 11:51:59,063 [main] INFO - files 0; directories 1 -2020-01-03 11:51:59,064 [main] INFO - Directory s3a://ireland-1/customsignerpath1 -2020-01-03 11:51:59,121 [main] INFO - Directory s3a://ireland-1/Users/stevel -2020-01-03 11:51:59,170 [main] INFO - files 0; directories 1 -2020-01-03 11:51:59,171 [main] INFO - Directory s3a://ireland-1/fork-0001/test -2020-01-03 11:51:59,233 [main] INFO - Directory s3a://ireland-1/path/style -2020-01-03 11:51:59,282 [main] INFO - files 0; directories 1 -2020-01-03 11:51:59,282 [main] INFO - Directory s3a://ireland-1/fork-0008/test -2020-01-03 11:51:59,338 [main] INFO - files 15; directories 10 -2020-01-03 11:51:59,339 [main] INFO - Directory s3a://ireland-1/fork-0004/test -2020-01-03 11:51:59,394 [main] WARN - Directory s3a://ireland-1/fork-0003/test is not authoritative -2020-01-03 11:51:59,451 [main] INFO - files 35; directories 1 -2020-01-03 11:51:59,451 [main] INFO - Directory s3a://ireland-1/fork-0002/test -2020-01-03 11:51:59,508 [main] INFO - Directory s3a://ireland-1/Users/stevel/Projects -2020-01-03 11:51:59,558 [main] INFO - files 0; directories 1 -2020-01-03 11:51:59,559 [main] INFO - Directory s3a://ireland-1/path/style/access -2020-01-03 11:51:59,610 [main] INFO - Directory s3a://ireland-1/fork-0008/test/doTestListFiles-0-2-0-false -2020-01-03 11:51:59,660 [main] INFO - Directory s3a://ireland-1/fork-0008/test/doTestListFiles-0-2-1-false -2020-01-03 11:51:59,719 [main] INFO - Directory s3a://ireland-1/fork-0008/test/doTestListFiles-0-0-0-true -2020-01-03 11:51:59,773 [main] INFO - Directory s3a://ireland-1/fork-0008/test/doTestListFiles-2-0-0-true -2020-01-03 11:51:59,824 [main] INFO - files 0; directories 2 -2020-01-03 11:51:59,824 [main] INFO - Directory s3a://ireland-1/fork-0008/test/doTestListFiles-0-2-1-true -2020-01-03 11:51:59,879 [main] INFO - Directory s3a://ireland-1/fork-0008/test/doTestListFiles-0-0-1-false -2020-01-03 11:51:59,939 [main] INFO - Directory s3a://ireland-1/fork-0008/test/doTestListFiles-0-0-0-false -2020-01-03 11:51:59,990 [main] INFO - Directory s3a://ireland-1/fork-0008/test/doTestListFiles-0-2-0-true -2020-01-03 11:52:00,042 [main] INFO - Directory s3a://ireland-1/fork-0008/test/doTestListFiles-2-0-0-false -2020-01-03 11:52:00,094 [main] INFO - files 0; directories 2 -2020-01-03 11:52:00,094 [main] INFO - Directory s3a://ireland-1/fork-0008/test/doTestListFiles-0-0-1-true -2020-01-03 11:52:00,144 [main] WARN - Directory s3a://ireland-1/fork-0003/test/ancestor is not authoritative -2020-01-03 11:52:00,197 [main] INFO - Directory s3a://ireland-1/Users/stevel/Projects/hadoop-trunk -2020-01-03 11:52:00,245 [main] INFO - files 0; directories 1 -2020-01-03 11:52:00,245 [main] INFO - Directory s3a://ireland-1/fork-0008/test/doTestListFiles-2-0-0-true/dir-0 -2020-01-03 11:52:00,296 [main] INFO - Directory s3a://ireland-1/fork-0008/test/doTestListFiles-2-0-0-true/dir-1 -2020-01-03 11:52:00,346 [main] INFO - Directory s3a://ireland-1/fork-0008/test/doTestListFiles-2-0-0-false/dir-0 -2020-01-03 11:52:00,397 [main] INFO - Directory s3a://ireland-1/fork-0008/test/doTestListFiles-2-0-0-false/dir-1 -2020-01-03 11:52:00,479 [main] INFO - Directory s3a://ireland-1/Users/stevel/Projects/hadoop-trunk/hadoop-tools -2020-01-03 11:52:00,530 [main] INFO - files 0; directories 1 -2020-01-03 11:52:00,530 [main] INFO - Directory s3a://ireland-1/Users/stevel/Projects/hadoop-trunk/hadoop-tools/hadoop-aws -2020-01-03 11:52:00,582 [main] INFO - files 0; directories 1 -2020-01-03 11:52:00,582 [main] INFO - Directory s3a://ireland-1/Users/stevel/Projects/hadoop-trunk/hadoop-tools/hadoop-aws/target -2020-01-03 11:52:00,636 [main] INFO - files 0; directories 1 -2020-01-03 11:52:00,637 [main] INFO - Directory s3a://ireland-1/Users/stevel/Projects/hadoop-trunk/hadoop-tools/hadoop-aws/target/test-dir -2020-01-03 11:52:00,691 [main] INFO - files 0; directories 3 -2020-01-03 11:52:00,691 [main] INFO - Directory s3a://ireland-1/Users/stevel/Projects/hadoop-trunk/hadoop-tools/hadoop-aws/target/test-dir/2 -2020-01-03 11:52:00,752 [main] INFO - Directory s3a://ireland-1/Users/stevel/Projects/hadoop-trunk/hadoop-tools/hadoop-aws/target/test-dir/5 -2020-01-03 11:52:00,807 [main] INFO - Directory s3a://ireland-1/Users/stevel/Projects/hadoop-trunk/hadoop-tools/hadoop-aws/target/test-dir/8 -2020-01-03 11:52:00,862 [main] INFO - Scanned 45 directories - 3 were not marked as authoritative -2020-01-03 11:52:00,863 [main] INFO - audit s3a://ireland-1/: duration 0:02.444s -``` - -Scan the path/bucket and fail if any entry is non-authoritative. - -```bash -hadoop s3guard authoritative -verbose -required s3a://ireland-1/ - -2020-01-03 11:47:40,288 [main] INFO - Metadata store DynamoDBMetadataStore{region=eu-west-1, tableName=s3guard-metadata, tableArn=arn:aws:dynamodb:eu-west-1:980678866538:table/s3guard-metadata} is initialized. -2020-01-03 11:47:40,291 [main] INFO - Starting: audit s3a://ireland-1/ -2020-01-03 11:47:40,295 [main] INFO - Root directory s3a://ireland-1/ -2020-01-03 11:47:40,336 [main] INFO - files 4; directories 12 -2020-01-03 11:47:40,336 [main] INFO - Directory s3a://ireland-1/Users -2020-01-03 11:47:40,386 [main] INFO - files 0; directories 1 -2020-01-03 11:47:40,386 [main] INFO - Directory s3a://ireland-1/fork-0007 -2020-01-03 11:47:40,435 [main] INFO - files 1; directories 0 -2020-01-03 11:47:40,435 [main] INFO - Directory s3a://ireland-1/fork-0001 -2020-01-03 11:47:40,486 [main] INFO - files 0; directories 1 -2020-01-03 11:47:40,486 [main] INFO - Directory s3a://ireland-1/fork-0006 -2020-01-03 11:47:40,534 [main] INFO - files 1; directories 0 -2020-01-03 11:47:40,535 [main] INFO - Directory s3a://ireland-1/path -2020-01-03 11:47:40,587 [main] INFO - files 0; directories 1 -2020-01-03 11:47:40,588 [main] INFO - Directory s3a://ireland-1/fork-0008 -2020-01-03 11:47:40,641 [main] INFO - files 0; directories 1 -2020-01-03 11:47:40,642 [main] INFO - Directory s3a://ireland-1/fork-0004 -2020-01-03 11:47:40,692 [main] INFO - files 0; directories 1 -2020-01-03 11:47:40,693 [main] WARN - Directory s3a://ireland-1/fork-0003 is not authoritative -2020-01-03 11:47:40,693 [main] INFO - audit s3a://ireland-1/: duration 0:00.402s -2020-01-03 11:47:40,698 [main] INFO - Exiting with status 46: `s3a://ireland-1/fork-0003': Directory is not marked as authoritative in the S3Guard store -``` - -This command is primarily for testing. - -### Tune the I/O capacity of the DynamoDB Table, `s3guard set-capacity` - -Alter the read and/or write capacity of a s3guard table created with provisioned -I/O capacity. - -```bash -hadoop s3guard set-capacity [--read UNIT] [--write UNIT] ( -region REGION | s3a://BUCKET ) -``` - -The `--read` and `--write` units are those of `s3guard init`. - -It cannot be used to change the I/O capacity of an on demand table (there is -no need), and nor can it be used to convert an existing table to being -on-demand. For that the AWS console must be used. - -Example - -``` -hadoop s3guard set-capacity -read 20 -write 20 s3a://ireland-1 -``` - -Set the capacity of the table used by bucket `s3a://ireland-1` to 20 read -and 20 write. (This is a low number, incidentally) - -``` -2017-08-30 16:21:26,343 [main] INFO s3guard.S3GuardTool (S3GuardTool.java:initMetadataStore(229)) - Metadata store DynamoDBMetadataStore{region=eu-west-1, tableName=ireland-1} is initialized. -2017-08-30 16:21:26,344 [main] INFO s3guard.DynamoDBMetadataStore (DynamoDBMetadataStore.java:updateParameters(1084)) - Current table capacity is read: 25, write: 25 -2017-08-30 16:21:26,344 [main] INFO s3guard.DynamoDBMetadataStore (DynamoDBMetadataStore.java:updateParameters(1086)) - Changing capacity of table to read: 20, write: 20 -Metadata Store Diagnostics: - ARN=arn:aws:dynamodb:eu-west-1:00000000000:table/ireland-1 - billing-mode=provisioned - description=S3Guard metadata store in DynamoDB - name=ireland-1 - read-capacity=25 - region=eu-west-1 - retryPolicy=ExponentialBackoffRetry(maxRetries=9, sleepTime=100 MILLISECONDS) - size=12812 - status=UPDATING - table={ ... } - write-capacity=25 -``` - -After the update, the table status changes to `UPDATING`; this is a sign that -the capacity has been changed. - -Repeating the same command will not change the capacity, as both read and -write values match that already in use. - -``` -2017-08-30 16:24:35,337 [main] INFO s3guard.DynamoDBMetadataStore (DynamoDBMetadataStore.java:updateParameters(1090)) - Table capacity unchanged at read: 20, write: 20 -Metadata Store Diagnostics: - ARN=arn:aws:dynamodb:eu-west-1:00000000000:table/ireland-1 - billing-mode=provisioned - description=S3Guard metadata store in DynamoDB - name=ireland-1 - read-capacity=20 - region=eu-west-1 - retryPolicy=ExponentialBackoffRetry(maxRetries=9, sleepTime=100 MILLISECONDS) - size=12812 - status=ACTIVE - table={ ... } - write-capacity=20 -``` -*Note*: There is a limit to how many times in a 24 hour period the capacity -of a bucket can be changed, either through this command or the AWS console. - -### Check the consistency of the metadata store, `s3guard fsck` - -Compares S3 with MetadataStore, and returns a failure status if any -rules or invariants are violated. Only works with DynamoDB metadata stores. - -```bash -hadoop s3guard fsck [-check | -internal] [-fix] (s3a://BUCKET | s3a://PATH_PREFIX) -``` - -`-check` operation checks the metadata store from the S3 perspective, but -does not fix any issues. -The consistency issues will be logged in ERROR loglevel. - -`-internal` operation checks the internal consistency of the metadata store, -but does not fix any issues. - -`-fix` operation fixes consistency issues between the metadatastore and the S3 -bucket. This parameter is optional, and can be used together with check or -internal parameters, but not alone. -The following fix is implemented: -- Remove orphan entries from DDB - -The errors found will be logged at the ERROR log level. - -*Note*: `-check` and `-internal` operations can be used only as separate -commands. Running `fsck` with both will result in an error. - -Example - -```bash -hadoop s3guard fsck -check s3a://ireland-1/path_prefix/ -``` - -Checks the metadata store while iterating through the S3 bucket. -The path_prefix will be used as the root element of the check. - -```bash -hadoop s3guard fsck -internal s3a://ireland-1/path_prefix/ -``` - -Checks the metadata store internal consistency. -The path_prefix will be used as the root element of the check. - - -## Debugging and Error Handling - -If you run into network connectivity issues, or have a machine failure in the -middle of an operation, you may end up with your metadata store having state -that differs from S3. The S3Guard CLI commands, covered in the CLI section -above, can be used to diagnose and repair these issues. - -There are some logs whose log level can be increased to provide more -information. - -```properties -# Log S3Guard classes -log4j.logger.org.apache.hadoop.fs.s3a.s3guard=DEBUG - -# Log all S3A classes -log4j.logger.org.apache.hadoop.fs.s3a=DEBUG - -# Enable debug logging of AWS DynamoDB client -log4j.logger.com.amazonaws.services.dynamodbv2.AmazonDynamoDB - -# Log all HTTP requests made; includes S3 interaction. This may -# include sensitive information such as account IDs in HTTP headers. -log4j.logger.com.amazonaws.request=DEBUG -``` - -If all else fails, S3Guard is designed to allow for easy recovery by deleting -the metadata store data. In DynamoDB, this can be accomplished by simply -deleting the table, and allowing S3Guard to recreate it from scratch. Note -that S3Guard tracks recent changes to file metadata to implement consistency. -Deleting the metadata store table will simply result in a period of eventual -consistency for any file modifications that were made right before the table -was deleted. - -### Enabling a log message whenever S3Guard is *disabled* - -When dealing with support calls related to the S3A connector, "is S3Guard on?" -is the usual opening question. This can be determined by looking at the application logs for -messages about S3Guard starting -the absence of S3Guard can only be inferred by the absence -of such messages. - -There is a another strategy: have the S3A Connector log whenever *S3Guard is not enabled* - -This can be done in the configuration option `fs.s3a.s3guard.disabled.warn.level` - -```xml - - fs.s3a.s3guard.disabled.warn.level - silent - - Level to print a message when S3Guard is disabled. - Values: - "warn": log at WARN level - "inform": log at INFO level - "silent": log at DEBUG level - "fail": raise an exception - - -``` - -The `fail` option is clearly more than logging; it exists as an extreme debugging -tool. Use with care. - -### Failure Semantics - -Operations which modify metadata will make changes to S3 first. If, and only -if, those operations succeed, the equivalent changes will be made to the -Metadata Store. - -These changes to S3 and Metadata Store are not fully-transactional: If the S3 -operations succeed, and the subsequent Metadata Store updates fail, the S3 -changes will *not* be rolled back. In this case, an error message will be -logged. - -### Versioning - -S3Guard tables are created with a version marker entry and table tag. -The entry is created with the primary key and child entry of `../VERSION`; -the use of a relative path guarantees that it will not be resolved. -Table tag key is named `s3guard_version`. - -When the table is initialized by S3Guard, the table will be tagged during the -creating and the version marker entry will be created in the table. -If the table lacks the version marker entry or tag, S3Guard will try to create -it according to the following rules: - -1. If the table lacks both version markers AND it's empty, both markers will be added. -If the table is not empty the check throws IOException -1. If there's no version marker ITEM, the compatibility with the TAG -will be checked, and the version marker ITEM will be added if the -TAG version is compatible. -If the TAG version is not compatible, the check throws OException -1. If there's no version marker TAG, the compatibility with the ITEM -version marker will be checked, and the version marker ITEM will be -added if the ITEM version is compatible. -If the ITEM version is not compatible, the check throws IOException -1. If the TAG and ITEM versions are both present then both will be checked -for compatibility. If the ITEM or TAG version marker is not compatible, -the check throws IOException - -*Note*: If the user does not have sufficient rights to tag the table the -initialization of S3Guard will not fail, but there will be no version marker tag -on the dynamo table. - -*Versioning policy* - -1. The version number of an S3Guard table will only be incremented when -an incompatible change is made to the table structure —that is, the structure -has changed so that it is no longer readable by older versions, or because -it has added new mandatory fields which older versions do not create. -1. The version number of S3Guard tables will only be changed by incrementing -the value. -1. Updated versions of S3Guard MAY continue to support older version tables. -1. If an incompatible change is made such that existing tables are not compatible, -then a means shall be provided to update existing tables. For example: -an option in the Command Line Interface, or an option to upgrade tables -during S3Guard initialization. - -*Note*: this policy does not indicate any intent to upgrade table structures -in an incompatible manner. The version marker in tables exists to support -such an option if it ever becomes necessary, by ensuring that all S3Guard -client can recognise any version mismatch. - -## Security - -All users of the DynamoDB table must have write access to it. This -effectively means they must have write access to the entire object store. - -There's not been much testing of using a S3Guard Metadata Store -with a read-only S3 Bucket. It *should* work, provided all users -have write access to the DynamoDB table. And, as updates to the Metadata Store -are only made after successful file creation, deletion and rename, the -store is *unlikely* to get out of sync, it is still something which -merits more testing before it could be considered reliable. - -## Managing DynamoDB I/O Capacity - -Historically, DynamoDB has been not only billed on use (data and I/O requests) --but on provisioned I/O Capacity. - -With Provisioned IO, when an application makes more requests than -the allocated capacity permits, the request is rejected; it is up to -the calling application to detect when it is being so throttled and -react. S3Guard does this, but as a result: when the client is being -throttled, operations are slower. This capacity throttling is averaged -over a few minutes: a briefly overloaded table will not be throttled, -but the rate cannot be sustained. - -The load on a table is visible in the AWS console: go to the -DynamoDB page for the table and select the "metrics" tab. -If the graphs of throttled read or write -requests show that a lot of throttling has taken place, then there is not -enough allocated capacity for the applications making use of the table. - -Similarly, if the capacity graphs show that the read or write loads are -low compared to the allocated capacities, then the table *may* be overprovisioned -for the current workload. - -The S3Guard connector to DynamoDB can be configured to make -multiple attempts to repeat a throttled request, with an exponential -backoff between them. - -The relevant settings for managing retries in the connector are: - -```xml - - - fs.s3a.s3guard.ddb.max.retries - 9 - - Max retries on throttled/incompleted DynamoDB operations - before giving up and throwing an IOException. - Each retry is delayed with an exponential - backoff timer which starts at 100 milliseconds and approximately - doubles each time. The minimum wait before throwing an exception is - sum(100, 200, 400, 800, .. 100*2^N-1 ) == 100 * ((2^N)-1) - - - - - fs.s3a.s3guard.ddb.throttle.retry.interval - 100ms - - Initial interval to retry after a request is throttled events; - the back-off policy is exponential until the number of retries of - fs.s3a.s3guard.ddb.max.retries is reached. - - - - - fs.s3a.s3guard.ddb.background.sleep - 25ms - - Length (in milliseconds) of pause between each batch of deletes when - pruning metadata. Prevents prune operations (which can typically be low - priority background operations) from overly interfering with other I/O - operations. - - -``` - -Having a large value for `fs.s3a.s3guard.ddb.max.retries` will ensure -that clients of an overloaded table will not fail immediately. However -queries may be unexpectedly slow. - -If operations, especially directory operations, are slow, check the AWS -console. It is also possible to set up AWS alerts for capacity limits -being exceeded. - -### On-Demand Dynamo Capacity - -[Amazon DynamoDB On-Demand](https://aws.amazon.com/blogs/aws/amazon-dynamodb-on-demand-no-capacity-planning-and-pay-per-request-pricing/) -removes the need to pre-allocate I/O capacity for S3Guard tables. -Instead the caller is _only_ charged per I/O Operation. - -* There are no SLA capacity guarantees. This is generally not an issue -for S3Guard applications. -* There's no explicit limit on I/O capacity, so operations which make -heavy use of S3Guard tables (for example: SQL query planning) do not -get throttled. -* You are charged more per DynamoDB API call, in exchange for paying nothing -when you are not interacting with DynamoDB. -* There's no way put a limit on the I/O; you may unintentionally run up -large bills through sustained heavy load. -* The `s3guard set-capacity` command fails: it does not make sense any more. - -When idle, S3Guard tables are only billed for the data stored, not for -any unused capacity. For this reason, there is no performance benefit -from sharing a single S3Guard table across multiple buckets. - -*Creating a S3Guard Table with On-Demand Tables* - -The default settings for S3Guard are to create on-demand tables; this -can also be done explicitly in the `s3guard init` command by setting the -read and write capacities to zero. - - -```bash -hadoop s3guard init -meta dynamodb://ireland-team -write 0 -read 0 s3a://ireland-1 -``` - -*Enabling DynamoDB On-Demand for an existing S3Guard table* - -You cannot currently convert an existing S3Guard table to being an on-demand -table through the `s3guard` command. - -It can be done through the AWS console or [the CLI](https://docs.aws.amazon.com/cli/latest/reference/dynamodb/update-table.html). -From the Web console or the command line, switch the billing to pay-per-request. - -Once enabled, the read and write capacities of the table listed in the -`hadoop s3guard bucket-info` command become "0", and the "billing-mode" -attribute changes to "per-request": - -``` -> hadoop s3guard bucket-info s3a://example-bucket/ - -Filesystem s3a://example-bucket -Location: eu-west-1 -Filesystem s3a://example-bucket is using S3Guard with store - DynamoDBMetadataStore{region=eu-west-1, tableName=example-bucket, - tableArn=arn:aws:dynamodb:eu-west-1:11111122223333:table/example-bucket} -Authoritative S3Guard: fs.s3a.metadatastore.authoritative=false -Metadata Store Diagnostics: - ARN=arn:aws:dynamodb:eu-west-1:11111122223333:table/example-bucket - billing-mode=per-request - description=S3Guard metadata store in DynamoDB - name=example-bucket - persist.authoritative.bit=true - read-capacity=0 - region=eu-west-1 - retryPolicy=ExponentialBackoffRetry(maxRetries=9, sleepTime=250 MILLISECONDS) - size=66797 - status=ACTIVE - table={AttributeDefinitions: - [{AttributeName: child,AttributeType: S}, - {AttributeName: parent,AttributeType: S}], - TableName: example-bucket, - KeySchema: [{ - AttributeName: parent,KeyType: HASH}, - {AttributeName: child,KeyType: RANGE}], - TableStatus: ACTIVE, - CreationDateTime: Thu Oct 11 18:51:14 BST 2018, - ProvisionedThroughput: { - LastIncreaseDateTime: Tue Oct 30 16:48:45 GMT 2018, - LastDecreaseDateTime: Tue Oct 30 18:00:03 GMT 2018, - NumberOfDecreasesToday: 0, - ReadCapacityUnits: 0, - WriteCapacityUnits: 0}, - TableSizeBytes: 66797, - ItemCount: 415, - TableArn: arn:aws:dynamodb:eu-west-1:11111122223333:table/example-bucket, - TableId: a7b0728a-f008-4260-b2a0-aaaaabbbbb,} - write-capacity=0 -The "magic" committer is supported -``` - -### Autoscaling (Provisioned Capacity) S3Guard tables. - -[DynamoDB Auto Scaling](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/AutoScaling.html) -can automatically increase and decrease the allocated capacity. - -Before DynamoDB On-Demand was introduced, autoscaling was the sole form -of dynamic scaling. - -Experiments with S3Guard and DynamoDB Auto Scaling have shown that any Auto Scaling -operation will only take place after callers have been throttled for a period of -time. The clients will still need to be configured to retry when overloaded -until any extra capacity is allocated. Furthermore, as this retrying will -block the threads from performing other operations -including more I/O, the -the autoscale may not scale fast enough. - -This is why the DynamoDB On-Demand appears is a better option for -workloads with Hadoop, Spark, Hive and other applications. - -If autoscaling is to be used, we recommend experimenting with the option, -based on usage information collected from previous days, and choosing a -combination of retry counts and an interval which allow for the clients to cope with -some throttling, but not to time-out other applications. - -## Read-After-Overwrite Consistency - -S3Guard provides read-after-overwrite consistency through ETags (default) or -object versioning checked either on the server (default) or client. This works -such that a reader reading a file after an overwrite either sees the new version -of the file or an error. Without S3Guard, new readers may see the original -version. Once S3 reaches eventual consistency, new readers will see the new -version. - -Readers using S3Guard will usually see the new file version, but may -in rare cases see `RemoteFileChangedException` instead. This would occur if -an S3 object read cannot provide the version tracked in S3Guard metadata. - -S3Guard achieves this behavior by storing ETags and object version IDs in the -S3Guard metadata store (e.g. DynamoDB). On opening a file, S3AFileSystem -will look in S3 for the version of the file indicated by the ETag or object -version ID stored in the metadata store. If that version is unavailable, -`RemoteFileChangedException` is thrown. Whether ETag or version ID and -server or client mode is used is determed by the -[fs.s3a.change.detection configuration options](./index.html#Handling_Read-During-Overwrite). - -### No Versioning Metadata Available - -When the first S3AFileSystem clients are upgraded to a version of -`S3AFileSystem` that contains these change tracking features, any existing -S3Guard metadata will not contain ETags or object version IDs. Reads of files -tracked in such S3Guard metadata will access whatever version of the file is -available in S3 at the time of read. Only if the file is subsequently updated -will S3Guard start tracking ETag and object version ID and as such generating -`RemoteFileChangedException` if an inconsistency is detected. - -Similarly, when S3Guard metadata is pruned, S3Guard will no longer be able to -detect an inconsistent read. S3Guard metadata should be retained for at least -as long as the perceived possible read-after-overwrite temporary inconsistency -window. That window is expected to be short, but there are no guarantees so it -is at the administrator's discretion to weigh the risk. - -### Known Limitations - -#### S3 Select - -S3 Select does not provide a capability for server-side ETag or object -version ID qualification. Whether `fs.s3a.change.detection.mode` is "client" or -"server", S3Guard will cause a client-side check of the file version before -opening the file with S3 Select. If the current version does not match the -version tracked in S3Guard, `RemoteFileChangedException` is thrown. - -It is still possible that the S3 Select read will access a different version of -the file, if the visible file version changes between the version check and -the opening of the file. This can happen due to eventual consistency or -an overwrite of the file between the version check and the open of the file. - -#### Rename - -Rename is implemented via copy in S3. With `fs.s3a.change.detection.mode` set -to "client", a fully reliable mechansim for ensuring the copied content is the expected -content is not possible. This is the case since there isn't necessarily a way -to know the expected ETag or version ID to appear on the object resulting from -the copy. - -Furthermore, if `fs.s3a.change.detection.mode` is "server" and a third-party S3 -implementation is used that doesn't honor the provided ETag or version ID, -S3AFileSystem and S3Guard cannot detect it. - -When `fs.s3.change.detection.mode` is "client", a client-side check -will be performed before the copy to ensure the current version of the file -matches S3Guard metadata. If not, `RemoteFileChangedException` is thrown. -Similar to as discussed with regard to S3 Select, this is not sufficient to -guarantee that same version is the version copied. - -When `fs.s3.change.detection.mode` server, the expected version is also specified -in the underlying S3 `CopyObjectRequest`. As long as the server honors it, the -copied object will be correct. - -All this said, with the defaults of `fs.s3.change.detection.mode` of "server" and -`fs.s3.change.detection.source` of "etag", when working with Amazon's S3, copy should in fact -either copy the expected file version or, in the case of an eventual consistency -anomaly, generate `RemoteFileChangedException`. The same should be true when -`fs.s3.change.detection.source` = "versionid". - -#### Out of Sync Metadata - -The S3Guard version tracking metadata (ETag or object version ID) could become -out of sync with the true current object metadata in S3. For example, S3Guard -is still tracking v1 of some file after v2 has been written. This could occur -for reasons such as a writer writing without utilizing S3Guard and/or -S3AFileSystem or simply due to a write with S3AFileSystem and S3Guard that wrote -successfully to S3, but failed in communication with S3Guard's metadata store -(e.g. DynamoDB). - -If this happens, reads of the affected file(s) will result in -`RemoteFileChangedException` until one of: - -* the S3Guard metadata is corrected out-of-band -* the file is overwritten (causing an S3Guard metadata update) -* the S3Guard metadata is pruned - -The S3Guard metadata for a file can be corrected with the `s3guard import` -command as discussed above. The command can take a file URI instead of a -bucket URI to correct the metadata for a single file. For example: - -```bash -hadoop s3guard import [-meta URI] s3a://my-bucket/file-with-bad-metadata -``` - -## Troubleshooting - -### Error: `S3Guard table lacks version marker.` - -The table which was intended to be used as a S3guard metadata store -does not have any version marker indicating that it is a S3Guard table. - -It may be that this is not a S3Guard table. - -* Make sure that this is the correct table name. -* Delete the table, so it can be rebuilt. - -### Error: `Database table is from an incompatible S3Guard version` - -This indicates that the version of S3Guard which created (or possibly updated) -the database table is from a different version that that expected by the S3A -client. - -This error will also include the expected and actual version numbers. - -If the expected version is lower than the actual version, then the version -of the S3A client library is too old to interact with this S3Guard-managed -bucket. Upgrade the application/library. - -If the expected version is higher than the actual version, then the table -itself will need upgrading. - -### Error `"DynamoDB table TABLE does not exist in region REGION; auto-creation is turned off"` - -S3Guard could not find the DynamoDB table for the Metadata Store, -and it was not configured to create it. Either the table was missing, -or the configuration is preventing S3Guard from finding the table. - -1. Verify that the value of `fs.s3a.s3guard.ddb.table` is correct. -1. If the region for an existing table has been set in -`fs.s3a.s3guard.ddb.region`, verify that the value is correct. -1. If the region is not set, verify that the table exists in the same -region as the bucket being used. -1. Create the table if necessary. - - -### Error `"The level of configured provisioned throughput for the table was exceeded"` - -``` -org.apache.hadoop.fs.s3a.AWSServiceThrottledException: listFiles on s3a://bucket/10/d1/d2/d3: -com.amazonaws.services.dynamodbv2.model.ProvisionedThroughputExceededException: -The level of configured provisioned throughput for the table was exceeded. -Consider increasing your provisioning level with the UpdateTable API. -(Service: AmazonDynamoDBv2; Status Code: 400; -Error Code: ProvisionedThroughputExceededException; -``` -The I/O load of clients of the (shared) DynamoDB table was exceeded. - -1. Switch to On-Demand Dynamo DB tables (AWS console) -1. Increase the capacity of the DynamoDB table (AWS console or `s3guard set-capacity`)/ -1. Increase the retry count and/or sleep time of S3Guard on throttle events (Hadoop configuration). - -### Error `Max retries exceeded` - -The I/O load of clients of the (shared) DynamoDB table was exceeded, and -the number of attempts to retry the operation exceeded the configured amount. - -1. Switch to On-Demand Dynamo DB tables (AWS console). -1. Increase the capacity of the DynamoDB table. -1. Increase the retry count and/or sleep time of S3Guard on throttle events. - - -### Error when running `set-capacity`: `org.apache.hadoop.fs.s3a.AWSServiceThrottledException: ProvisionTable` - -``` -org.apache.hadoop.fs.s3a.AWSServiceThrottledException: ProvisionTable on s3guard-example: -com.amazonaws.services.dynamodbv2.model.LimitExceededException: -Subscriber limit exceeded: Provisioned throughput decreases are limited within a given UTC day. -After the first 4 decreases, each subsequent decrease in the same UTC day can be performed at most once every 3600 seconds. -Number of decreases today: 6. -Last decrease at Wednesday, July 25, 2018 8:48:14 PM UTC. -Next decrease can be made at Wednesday, July 25, 2018 9:48:14 PM UTC -``` - -There's are limit on how often you can change the capacity of an DynamoDB table; -if you call `set-capacity` too often, it fails. Wait until the after the time indicated -and try again. - -### Error `Invalid region specified` - -``` -java.io.IOException: Invalid region specified "iceland-2": - Region can be configured with fs.s3a.s3guard.ddb.region: - us-gov-west-1, us-east-1, us-east-2, us-west-1, us-west-2, - eu-west-1, eu-west-2, eu-west-3, eu-central-1, ap-south-1, - ap-southeast-1, ap-southeast-2, ap-northeast-1, ap-northeast-2, - sa-east-1, cn-north-1, cn-northwest-1, ca-central-1 - at org.apache.hadoop.fs.s3a.s3guard.DynamoDBClientFactory$DefaultDynamoDBClientFactory.getRegion - at org.apache.hadoop.fs.s3a.s3guard.DynamoDBClientFactory$DefaultDynamoDBClientFactory.createDynamoDBClient -``` - -The region specified in `fs.s3a.s3guard.ddb.region` is invalid. - -### "Neither ReadCapacityUnits nor WriteCapacityUnits can be specified when BillingMode is PAY_PER_REQUEST" - -``` -ValidationException; One or more parameter values were invalid: - Neither ReadCapacityUnits nor WriteCapacityUnits can be specified when - BillingMode is PAY_PER_REQUEST - (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ValidationException) -``` - -On-Demand DynamoDB tables do not have any fixed capacity -it is an error -to try to change it with the `set-capacity` command. - -### `MetadataPersistenceException` - -A filesystem write operation failed to persist metadata to S3Guard. The file was -successfully written to S3 and now the S3Guard metadata is likely to be out of -sync. - -See [Fail on Error](#fail-on-error) for more detail. - -### Error `RemoteFileChangedException` - -An exception like the following could occur for a couple of reasons: - -* the S3Guard metadata is out of sync with the true S3 metadata. For -example, the S3Guard DynamoDB table is tracking a different ETag than the ETag -shown in the exception. This may suggest the object was updated in S3 without -involvement from S3Guard or there was a transient failure when S3Guard tried to -write to DynamoDB. - -* S3 is exhibiting read-after-overwrite temporary inconsistency. The S3Guard -metadata was updated with a new ETag during a recent write, but the current read -is not seeing that ETag due to S3 eventual consistency. This exception prevents -the reader from an inconsistent read where the reader sees an older version of -the file. - -``` -org.apache.hadoop.fs.s3a.RemoteFileChangedException: open 's3a://my-bucket/test/file.txt': - Change reported by S3 while reading at position 0. - ETag 4e886e26c072fef250cfaf8037675405 was unavailable - at org.apache.hadoop.fs.s3a.impl.ChangeTracker.processResponse(ChangeTracker.java:167) - at org.apache.hadoop.fs.s3a.S3AInputStream.reopen(S3AInputStream.java:207) - at org.apache.hadoop.fs.s3a.S3AInputStream.lambda$lazySeek$1(S3AInputStream.java:355) - at org.apache.hadoop.fs.s3a.Invoker.lambda$retry$2(Invoker.java:195) - at org.apache.hadoop.fs.s3a.Invoker.once(Invoker.java:109) - at org.apache.hadoop.fs.s3a.Invoker.lambda$retry$3(Invoker.java:265) - at org.apache.hadoop.fs.s3a.Invoker.retryUntranslated(Invoker.java:322) - at org.apache.hadoop.fs.s3a.Invoker.retry(Invoker.java:261) - at org.apache.hadoop.fs.s3a.Invoker.retry(Invoker.java:193) - at org.apache.hadoop.fs.s3a.Invoker.retry(Invoker.java:215) - at org.apache.hadoop.fs.s3a.S3AInputStream.lazySeek(S3AInputStream.java:348) - at org.apache.hadoop.fs.s3a.S3AInputStream.read(S3AInputStream.java:381) - at java.io.FilterInputStream.read(FilterInputStream.java:83) -``` - -### Error `AWSClientIOException: copyFile` caused by `NullPointerException` - -The AWS SDK has an [issue](https://github.com/aws/aws-sdk-java/issues/1644) -where it will throw a relatively generic `AmazonClientException` caused by -`NullPointerException` when copying a file and specifying a precondition -that cannot be met. This can bubble up from `S3AFileSystem.rename()`. It -suggests that the file in S3 is inconsistent with the metadata in S3Guard. - -``` -org.apache.hadoop.fs.s3a.AWSClientIOException: copyFile(test/rename-eventually2.dat, test/dest2.dat) on test/rename-eventually2.dat: com.amazonaws.AmazonClientException: Unable to complete transfer: null: Unable to complete transfer: null - at org.apache.hadoop.fs.s3a.S3AUtils.translateException(S3AUtils.java:201) - at org.apache.hadoop.fs.s3a.Invoker.once(Invoker.java:111) - at org.apache.hadoop.fs.s3a.Invoker.lambda$retry$4(Invoker.java:314) - at org.apache.hadoop.fs.s3a.Invoker.retryUntranslated(Invoker.java:406) - at org.apache.hadoop.fs.s3a.Invoker.retry(Invoker.java:310) - at org.apache.hadoop.fs.s3a.Invoker.retry(Invoker.java:285) - at org.apache.hadoop.fs.s3a.S3AFileSystem.copyFile(S3AFileSystem.java:3034) - at org.apache.hadoop.fs.s3a.S3AFileSystem.innerRename(S3AFileSystem.java:1258) - at org.apache.hadoop.fs.s3a.S3AFileSystem.rename(S3AFileSystem.java:1119) - at org.apache.hadoop.fs.s3a.ITestS3ARemoteFileChanged.lambda$testRenameEventuallyConsistentFile2$6(ITestS3ARemoteFileChanged.java:556) - at org.apache.hadoop.test.LambdaTestUtils.intercept(LambdaTestUtils.java:498) - at org.apache.hadoop.fs.s3a.ITestS3ARemoteFileChanged.testRenameEventuallyConsistentFile2(ITestS3ARemoteFileChanged.java:554) - at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) - at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) - at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) - at java.lang.reflect.Method.invoke(Method.java:498) - at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) - at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) - at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) - at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) - at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26) - at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27) - at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55) - at org.junit.internal.runners.statements.FailOnTimeout$CallableStatement.call(FailOnTimeout.java:298) - at org.junit.internal.runners.statements.FailOnTimeout$CallableStatement.call(FailOnTimeout.java:292) - at java.util.concurrent.FutureTask.run(FutureTask.java:266) - at java.lang.Thread.run(Thread.java:748) -Caused by: com.amazonaws.AmazonClientException: Unable to complete transfer: null - at com.amazonaws.services.s3.transfer.internal.AbstractTransfer.unwrapExecutionException(AbstractTransfer.java:286) - at com.amazonaws.services.s3.transfer.internal.AbstractTransfer.rethrowExecutionException(AbstractTransfer.java:265) - at com.amazonaws.services.s3.transfer.internal.CopyImpl.waitForCopyResult(CopyImpl.java:67) - at org.apache.hadoop.fs.s3a.impl.CopyOutcome.waitForCopy(CopyOutcome.java:72) - at org.apache.hadoop.fs.s3a.S3AFileSystem.lambda$copyFile$14(S3AFileSystem.java:3047) - at org.apache.hadoop.fs.s3a.Invoker.once(Invoker.java:109) - ... 25 more -Caused by: java.lang.NullPointerException - at com.amazonaws.services.s3.transfer.internal.CopyCallable.copyInOneChunk(CopyCallable.java:154) - at com.amazonaws.services.s3.transfer.internal.CopyCallable.call(CopyCallable.java:134) - at com.amazonaws.services.s3.transfer.internal.CopyMonitor.call(CopyMonitor.java:132) - at com.amazonaws.services.s3.transfer.internal.CopyMonitor.call(CopyMonitor.java:43) - at java.util.concurrent.FutureTask.run(FutureTask.java:266) - at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) - at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) - ... 1 more -``` - -### Error `Attempt to change a resource which is still in use: Table is being deleted` - -``` -com.amazonaws.services.dynamodbv2.model.ResourceInUseException: - Attempt to change a resource which is still in use: Table is being deleted: - s3guard.test.testDynamoDBInitDestroy351245027 - (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ResourceInUseException;) -``` - -You have attempted to call `hadoop s3guard destroy` on a table which is already -being destroyed. - -## Other Topics - -For details on how to test S3Guard, see [Testing S3Guard](./testing.html#s3guard) diff --git a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/testing.md b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/testing.md index a8ac64b7193..3218af870cf 100644 --- a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/testing.md +++ b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/testing.md @@ -12,7 +12,7 @@ limitations under the License. See accompanying LICENSE file. --> -# Testing the S3A filesystem client and its features, including S3Guard +# Testing the S3A filesystem client and its features @@ -309,7 +309,7 @@ mvn surefire-report:failsafe-report-only ## Testing Versioned Stores Some tests (specifically some in `ITestS3ARemoteFileChanged`) require -a versioned bucket for full test coverage as well as S3Guard being enabled. +a versioned bucket for full test coverage. To enable versioning in a bucket. @@ -379,11 +379,6 @@ By their very nature they are slow. And, as their execution time is often limited by bandwidth between the computer running the tests and the S3 endpoint, parallel execution does not speed these tests up. -***Note: Running scale tests with `-Ds3guard` and `-Ddynamo` requires that -you use a private, testing-only DynamoDB table.*** The tests do disruptive -things such as deleting metadata and setting the provisioned throughput -to very low values. - ### Enabling the Scale Tests The tests are enabled if the `scale` property is set in the maven build @@ -882,28 +877,6 @@ This is invaluable for debugging test failures. How to set test options in your hadoop configuration rather than on the maven command line: -As an example let's assume you want to run S3Guard integration tests using IDE. -Please add the following properties in -`hadoop-tools/hadoop-aws/src/test/resources/auth-keys.xml` file. - Local configuration is stored in auth-keys.xml. The changes to this file won't be committed, - so it's safe to store local config here. -```xml - - fs.s3a.s3guard.test.enabled - true - -``` - -```xml - - fs.s3a.s3guard.test.implementation - dynamo - -``` - -Warning : Although this is easier for IDE debugging setups, once you do this, -you cannot change configurations on the mvn command line, such as testing without s3guard. - ### Keeping AWS Costs down Most of the base S3 tests are designed to use public AWS data @@ -914,7 +887,6 @@ so will cost more as well as generally take more time to execute. You are however billed for 1. Data left in S3 after test runs. -2. DynamoDB capacity reserved by S3Guard tables. 3. HTTP operations on files (HEAD, LIST, GET). 4. In-progress multipart uploads from bulk IO or S3A committer tests. 5. Encryption/decryption using AWS KMS keys. @@ -923,8 +895,6 @@ The GET/decrypt costs are incurred on each partial read of a file, so random IO can cost more than sequential IO; the speedup of queries with columnar data usually justifies this. -The DynamoDB costs come from the number of entries stores and the allocated capacity. - How to keep costs down * Don't run the scale tests with large datasets; keep `fs.s3a.scale.test.huge.filesize` unset, or a few MB (minimum: 5). @@ -935,19 +905,7 @@ it can be manually done: * Abort all outstanding uploads: hadoop s3guard uploads -abort -force s3a://test-bucket/ -* If you don't need it, destroy the S3Guard DDB table. - hadoop s3guard destroy s3a://test-bucket/ - -The S3Guard tests will automatically create the Dynamo DB table in runs with -`-Ds3guard -Ddynamo` set; default capacity of these buckets -tests is very small; it keeps costs down at the expense of IO performance -and, for test runs in or near the S3/DDB stores, throttling events. - -If you want to manage capacity, use `s3guard set-capacity` to increase it -(performance) or decrease it (costs). -For remote `hadoop-aws` test runs, the read/write capacities of "0" each should suffice; -increase it if parallel test run logs warn of throttling. ## Tips @@ -974,25 +932,21 @@ using an absolute XInclude reference to it. **Warning do not enable any type of failure injection in production. The following settings are for testing only.** -One of the challenges with S3A integration tests is the fact that S3 was an +One of the challenges with S3A integration tests was the fact that S3 was an eventually-consistent storage system. To simulate inconsistencies more frequently than they would normally surface, S3A supports a shim layer on top of the `AmazonS3Client` class which artificially delays certain paths from appearing in listings. This is implemented in the class `InconsistentAmazonS3Client`. -Now that S3 is consistent, injecting failures during integration and -functional testing is less important. -There's no need to enable it to verify that S3Guard can recover -from consistencies, given that in production such consistencies -will never surface. +Now that S3 is consistent, injecting inconsistency is no longer needed +during testing. +However, it is stil useful to use the other feature of the client: +throttling simulation. ## Simulating List Inconsistencies ### Enabling the InconsistentAmazonS3CClient -There are two ways of enabling the `InconsistentAmazonS3Client`: at -config-time, or programmatically. For an example of programmatic test usage, -see `ITestS3GuardListConsistency`. To enable the fault-injecting client via configuration, switch the S3A client to use the "Inconsistent S3 Client Factory" when connecting to @@ -1005,387 +959,39 @@ S3: ``` -The inconsistent client works by: - -1. Choosing which objects will be "inconsistent" at the time the object is -created or deleted. -2. When `listObjects()` is called, any keys that we have marked as -inconsistent above will not be returned in the results (until the -configured delay has elapsed). Similarly, deleted items may be *added* to -missing results to delay the visibility of the delete. - -There are two ways of choosing which keys (filenames) will be affected: By -substring, and by random probability. +The inconsistent client will, on every AWS SDK request, +generate a random number, and if less than the probability, +raise a 503 exception. ```xml - - fs.s3a.failinject.inconsistency.key.substring - DELAY_LISTING_ME - - fs.s3a.failinject.inconsistency.probability - 1.0 + fs.s3a.failinject.throttle.probability + 0.05 ``` -By default, any object which has the substring "DELAY_LISTING_ME" in its key -will subject to delayed visibility. For example, the path -`s3a://my-bucket/test/DELAY_LISTING_ME/file.txt` would match this condition. -To match all keys use the value "\*" (a single asterisk). This is a special -value: *We don't support arbitrary wildcards.* +These exceptions are returned to S3; they do not test the +AWS SDK retry logic. -The default probability of delaying an object is 1.0. This means that *all* -keys that match the substring will get delayed visibility. Note that we take -the logical *and* of the two conditions (substring matches *and* probability -random chance occurs). Here are some example configurations: - -``` -| substring | probability | behavior | -|-----------|-------------|--------------------------------------------| -| | 0.001 | An empty tag in .xml config will | -| | | be interpreted as unset and revert to the | -| | | default value, "DELAY_LISTING_ME" | -| | | | -| * | 0.001 | 1/1000 chance of *any* key being delayed. | -| | | | -| delay | 0.01 | 1/100 chance of any key containing "delay" | -| | | | -| delay | 1.0 | All keys containing substring "delay" .. | -``` - -You can also configure how long you want the delay in visibility to last. -The default is 5000 milliseconds (five seconds). - -```xml - - fs.s3a.failinject.inconsistency.msec - 5000 - -``` - - -### Limitations of Inconsistency Injection - -Although `InconsistentAmazonS3Client` can delay the visibility of an object -or parent directory, it does not prevent the key of that object from -appearing in all prefix searches. For example, if we create the following -object with the default configuration above, in an otherwise empty bucket: - -``` -s3a://bucket/a/b/c/DELAY_LISTING_ME -``` - -Then the following paths will still be visible as directories (ignoring -possible real-world inconsistencies): - -``` -s3a://bucket/a -s3a://bucket/a/b -``` - -Whereas `getFileStatus()` on the following *will* be subject to delayed -visibility (`FileNotFoundException` until delay has elapsed): - -``` -s3a://bucket/a/b/c -s3a://bucket/a/b/c/DELAY_LISTING_ME -``` - -In real-life S3 inconsistency, however, we expect that all the above paths -(including `a` and `b`) will be subject to delayed visibility. ### Using the `InconsistentAmazonS3CClient` in downstream integration tests The inconsistent client is shipped in the `hadoop-aws` JAR, so it can -be used in applications which work with S3 to see how they handle -inconsistent directory listings. +be used in integration tests. ## Testing S3Guard -[S3Guard](./s3guard.html) is an extension to S3A which added consistent metadata -listings to the S3A client. +As part of the removal of S3Guard from the production code, the tests have been updated +so that -It has not been needed for applications to work safely with AWS S3 since November -2020. However, it is currently still part of the codebase, and so something which -needs to be tested. +* All S3Guard-specific tests have been deleted. +* All tests parameterized on S3Guard settings have had those test configurations removed. +* The maven profiles option to run tests with S3Guard have been removed. -The basic strategy for testing S3Guard correctness consists of: +There is no need to test S3Guard -and so tests are lot faster. +(We developers are all happy) -1. MetadataStore Contract tests. - - The MetadataStore contract tests are inspired by the Hadoop FileSystem and - `FileContext` contract tests. Each implementation of the `MetadataStore` interface - subclasses the `MetadataStoreTestBase` class and customizes it to initialize - their MetadataStore. This test ensures that the different implementations - all satisfy the semantics of the MetadataStore API. - -2. Running existing S3A unit and integration tests with S3Guard enabled. - - You can run the S3A integration tests on top of S3Guard by configuring your - `MetadataStore` in your - `hadoop-tools/hadoop-aws/src/test/resources/core-site.xml` or - `hadoop-tools/hadoop-aws/src/test/resources/auth-keys.xml` files. - Next run the S3A integration tests as outlined in the *Running the Tests* section - of the [S3A documentation](./index.html) - -3. Running fault-injection tests that test S3Guard's consistency features. - - The `ITestS3GuardListConsistency` uses failure injection to ensure - that list consistency logic is correct even when the underlying storage is - eventually consistent. - - The integration test adds a shim above the Amazon S3 Client layer that injects - delays in object visibility. - - All of these tests will be run if you follow the steps listed in step 2 above. - - No charges are incurred for using this store, and its consistency - guarantees are that of the underlying object store instance. - -### Testing S3A with S3Guard Enabled - -All the S3A tests which work with a private repository can be configured to -run with S3Guard by using the `s3guard` profile. When set, this will run -all the tests with local memory for the metadata set to "non-authoritative" mode. - -```bash -mvn -T 1C verify -Dparallel-tests -DtestsThreadCount=6 -Ds3guard -``` - -When the `s3guard` profile is enabled, following profiles can be specified: - -* `dynamo`: use an AWS-hosted DynamoDB table; creating the table if it does - not exist. You will have to pay the bills for DynamoDB web service. -* `auth`: treat the S3Guard metadata as authoritative. - -```bash -mvn -T 1C verify -Dparallel-tests -DtestsThreadCount=6 -Ds3guard -Ddynamo -Dauth -``` - -When experimenting with options, it is usually best to run a single test suite -at a time until the operations appear to be working. - -```bash -mvn -T 1C verify -Dtest=skip -Dit.test=ITestS3AMiscOperations -Ds3guard -Ddynamo -``` - -### Notes - -1. If the `s3guard` profile is not set, then the S3Guard properties are those -of the test configuration set in s3a contract xml file or `auth-keys.xml` - -If the `s3guard` profile *is* set: -1. The S3Guard options from maven (the dynamo and authoritative flags) - overwrite any previously set in the configuration files. -1. DynamoDB will be configured to create any missing tables. -1. When using DynamoDB and running `ITestDynamoDBMetadataStore`, - the `fs.s3a.s3guard.ddb.test.table` -property MUST be configured, and the name of that table MUST be different - than what is used for `fs.s3a.s3guard.ddb.table`. The test table is destroyed - and modified multiple times during the test. - 1. Several of the tests create and destroy DynamoDB tables. The table names - are prefixed with the value defined by - `fs.s3a.s3guard.test.dynamo.table.prefix` (default="s3guard.test."). The user - executing the tests will need sufficient privilege to create and destroy such - tables. If the tests abort uncleanly, these tables may be left behind, - incurring AWS charges. - - -### How to Dump the Table and Metastore State - -There's an unstable entry point to list the contents of a table -and S3 filesystem ot a set of Tab Separated Value files: - -``` -hadoop org.apache.hadoop.fs.s3a.s3guard.DumpS3GuardDynamoTable s3a://bucket/ dir/out -``` - -This generates a set of files prefixed `dir/out-` with different views of the -world which can then be viewed on the command line or editor: - -``` -"type" "deleted" "path" "is_auth_dir" "is_empty_dir" "len" "updated" "updated_s" "last_modified" "last_modified_s" "etag" "version" -"file" "true" "s3a://bucket/fork-0001/test/ITestS3AContractDistCp/testDirectWrite/remote" "false" "UNKNOWN" 0 1562171244451 "Wed Jul 03 17:27:24 BST 2019" 1562171244451 "Wed Jul 03 17:27:24 BST 2019" "" "" -"file" "true" "s3a://bucket/Users/stevel/Projects/hadoop-trunk/hadoop-tools/hadoop-aws/target/test-dir/1/5xlPpalRwv/test/new/newdir/file1" "false" "UNKNOWN" 0 1562171518435 "Wed Jul 03 17:31:58 BST 2019" 1562171518435 "Wed Jul 03 17:31:58 BST 2019" "" "" -"file" "true" "s3a://bucket/Users/stevel/Projects/hadoop-trunk/hadoop-tools/hadoop-aws/target/test-dir/1/5xlPpalRwv/test/new/newdir/subdir" "false" "UNKNOWN" 0 1562171518535 "Wed Jul 03 17:31:58 BST 2019" 1562171518535 "Wed Jul 03 17:31:58 BST 2019" "" "" -"file" "true" "s3a://bucket/test/DELAY_LISTING_ME/testMRJob" "false" "UNKNOWN" 0 1562172036299 "Wed Jul 03 17:40:36 BST 2019" 1562172036299 "Wed Jul 03 17:40:36 BST 2019" "" "" -``` - -This is unstable: the output format may change without warning. -To understand the meaning of the fields, consult the documentation. -They are, currently: - -| field | meaning | source | -|-------|---------| -------| -| `type` | type | filestatus | -| `deleted` | tombstone marker | metadata | -| `path` | path of an entry | filestatus | -| `is_auth_dir` | directory entry authoritative status | metadata | -| `is_empty_dir` | does the entry represent an empty directory | metadata | -| `len` | file length | filestatus | -| `last_modified` | file status last modified | filestatus | -| `last_modified_s` | file status last modified as string | filestatus | -| `updated` | time (millis) metadata was updated | metadata | -| `updated_s` | updated time as a string | metadata | -| `etag` | any etag | filestatus | -| `version` | any version| filestatus | - -Files generated - -| suffix | content | -|---------------|---------| -| `-scan.csv` | Full scan/dump of the metastore | -| `-store.csv` | Recursive walk through the metastore | -| `-tree.csv` | Treewalk through filesystem `listStatus("/")` calls | -| `-flat.csv` | Flat listing through filesystem `listFiles("/", recursive)` | -| `-s3.csv` | Dump of the S3 Store *only* | -| `-scan-2.csv` | Scan of the store after the previous operations | - -Why the two scan entries? The S3A listing and treewalk operations -may add new entries to the metastore/DynamoDB table. - -Note 1: this is unstable; entry list and meaning may change, sorting of output, -the listing algorithm, representation of types, etc. It's expected -uses are: diagnostics, support calls and helping us developers -work out what we've just broken. - -Note 2: This *is* safe to use against an active store; the tables may appear -to be inconsistent due to changes taking place during the dump sequence. - -### Resetting the Metastore: `PurgeS3GuardDynamoTable` - -The `PurgeS3GuardDynamoTable` entry point -`org.apache.hadoop.fs.s3a.s3guard.PurgeS3GuardDynamoTable` can -list all entries in a store for a specific filesystem, and delete them. -It *only* deletes those entries in the store for that specific filesystem, -even if the store is shared. - -```bash -hadoop org.apache.hadoop.fs.s3a.s3guard.PurgeS3GuardDynamoTable \ - -force s3a://bucket/ -``` - -Without the `-force` option the table is scanned, but no entries deleted; -with it then all entries for that filesystem are deleted. -No attempt is made to order the deletion; while the operation is under way -the store is not fully connected (i.e. there may be entries whose parent has -already been deleted). - -Needless to say: *it is not safe to use this against a table in active use.* - -### Scale Testing MetadataStore Directly - -There are some scale tests that exercise Metadata Store implementations -directly. These ensure that S3Guard is are robust to things like DynamoDB -throttling, and compare performance for different implementations. These -are included in the scale tests executed when `-Dscale` is passed to -the maven command line. - -The two S3Guard scale tests are `ITestDynamoDBMetadataStoreScale` and -`ITestLocalMetadataStoreScale`. - -To run these tests, your DynamoDB table needs to be of limited capacity; -the values in `ITestDynamoDBMetadataStoreScale` currently require a read capacity -of 10 or less. a write capacity of 15 or more. - -The following settings allow us to run `ITestDynamoDBMetadataStoreScale` with -artificially low read and write capacity provisioned, so we can judge the -effects of being throttled by the DynamoDB service: - -```xml - - scale.test.operation.count - 10 - - - scale.test.directory.count - 3 - - - fs.s3a.scale.test.enabled - true - - - fs.s3a.s3guard.ddb.table - my-scale-test - - - fs.s3a.s3guard.ddb.table.create - true - - - fs.s3a.s3guard.ddb.table.capacity.read - 5 - - - fs.s3a.s3guard.ddb.table.capacity.write - 5 - -``` - -These tests verify that the invoked operations can trigger retries in the -S3Guard code, rather than just in the AWS SDK level, so showing that if -SDK operations fail, they get retried. They also verify that the filesystem -statistics are updated to record that throttling took place. - -*Do not panic if these tests fail to detect throttling!* - -These tests are unreliable as they need certain conditions to be met -to repeatedly fail: - -1. You must have a low-enough latency connection to the DynamoDB store that, -for the capacity allocated, you can overload it. -1. The AWS Console can give you a view of what is happening here. -1. Running a single test on its own is less likely to trigger an overload -than trying to run the whole test suite. -1. And running the test suite more than once, back-to-back, can also help -overload the cluster. -1. Stepping through with a debugger will reduce load, so may not trigger -failures. - -If the tests fail, it *probably* just means you aren't putting enough load -on the table. - -These tests do not verify that the entire set of DynamoDB calls made -during the use of a S3Guarded S3A filesystem are wrapped by retry logic. - -*The best way to verify resilience is to run the entire `hadoop-aws` test suite, -or even a real application, with throttling enabled. - -### Testing encrypted DynamoDB tables - -By default, a DynamoDB table is encrypted using AWS owned customer master key -(CMK). You can enable server side encryption (SSE) using AWS managed CMK or -customer managed CMK in KMS before running the S3Guard tests. -1. To enable AWS managed CMK, set the config -`fs.s3a.s3guard.ddb.table.sse.enabled` to true in `auth-keys.xml`. -1. To enable customer managed CMK, you need to create a KMS key and set the -config in `auth-keys.xml`. The value can be the key ARN or alias. Example: -``` - - fs.s3a.s3guard.ddb.table.sse.enabled - true - - - fs.s3a.s3guard.ddb.table.sse.cmk - arn:aws:kms:us-west-2:360379543683:key/071a86ff-8881-4ba0-9230-95af6d01ca01 - -``` -For more details about SSE on DynamoDB table, please see [S3Guard doc](./s3guard.html). - -### Testing only: Local Metadata Store - -There is an in-memory Metadata Store for testing. - -```xml - - fs.s3a.metadatastore.impl - org.apache.hadoop.fs.s3a.s3guard.LocalMetadataStore - -``` - -This is not for use in production. ## Testing Assumed Roles @@ -1404,11 +1010,8 @@ The specific tests an Assumed Role ARN is required for are To run these tests you need: 1. A role in your AWS account will full read and write access rights to -the S3 bucket used in the tests, DynamoDB, for S3Guard, and KMS for any -SSE-KMS tests. +the S3 bucket used in the tests, and KMS for any SSE-KMS tests. -If your bucket is set up by default to use S3Guard, the role must have access -to that service. 1. Your IAM User to have the permissions to "assume" that role. @@ -1436,7 +1039,7 @@ thorough test, by switching to the credentials provider. ``` The usual credentials needed to log in to the bucket will be used, but now -the credentials used to interact with S3 and DynamoDB will be temporary +the credentials used to interact with S3 will be temporary role credentials, rather than the full credentials. ## Qualifying an AWS SDK Update @@ -1455,7 +1058,7 @@ picked and reverted easily. 1. Do not mix in an SDK update with any other piece of work, for the same reason. 1. Plan for an afternoon's work, including before/after testing, log analysis and any manual tests. -1. Make sure all the integration tests are running (including s3guard, ARN, encryption, scale) +1. Make sure all the integration tests are running (including ARN, encryption, scale) *before you start the upgrade*. 1. Create a JIRA for updating the SDK. Don't include the version (yet), as it may take a couple of SDK updates before it is ready. @@ -1463,7 +1066,7 @@ as it may take a couple of SDK updates before it is ready. 1. Create a private git branch of trunk for JIRA, and in `hadoop-project/pom.xml` update the `aws-java-sdk.version` to the new SDK version. 1. Update AWS SDK versions in NOTICE.txt. -1. Do a clean build and rerun all the `hadoop-aws` tests, with and without the `-Ds3guard -Ddynamo` options. +1. Do a clean build and rerun all the `hadoop-aws` tests. This includes the `-Pscale` set, with a role defined for the assumed role tests. in `fs.s3a.assumed.role.arn` for testing assumed roles, and `fs.s3a.encryption.key` for encryption, for full coverage. @@ -1490,7 +1093,7 @@ as it may take a couple of SDK updates before it is ready. We need a run through of the CLI to see if there have been changes there which cause problems, especially whether new log messages have surfaced, -or whether some packaging change breaks that CLI +or whether some packaging change breaks that CLI. From the root of the project, create a command line release `mvn package -Pdist -DskipTests -Dmaven.javadoc.skip=true -DskipShade`; @@ -1502,7 +1105,7 @@ From the root of the project, create a command line release `mvn package -Pdist export HADOOP_OPTIONAL_TOOLS="hadoop-aws" ``` -Run some basic s3guard commands as well as file operations. +Run some basic s3guard CLI as well as file operations. ```bash export BUCKETNAME=example-bucket-name @@ -1514,21 +1117,6 @@ bin/hadoop s3guard uploads $BUCKET # repeat twice, once with "no" and once with "yes" as responses bin/hadoop s3guard uploads -abort $BUCKET -# --------------------------------------------------- -# assuming s3guard is enabled -# if on pay-by-request, expect an error message and exit code of -1 -bin/hadoop s3guard set-capacity $BUCKET - -# skip for PAY_PER_REQUEST -bin/hadoop s3guard set-capacity -read 15 -write 15 $BUCKET - -bin/hadoop s3guard bucket-info -guarded $BUCKET - -bin/hadoop s3guard diff $BUCKET/ -bin/hadoop s3guard prune -minutes 10 $BUCKET/ -bin/hadoop s3guard import -verbose $BUCKET/ -bin/hadoop s3guard authoritative -verbose $BUCKET - # --------------------------------------------------- # root filesystem operatios # --------------------------------------------------- @@ -1556,7 +1144,6 @@ bin/hadoop fs -rm -r $BUCKET/\* # --------------------------------------------------- bin/hadoop fs -mkdir $BUCKET/dir-no-trailing -# used to fail with S3Guard bin/hadoop fs -mkdir $BUCKET/dir-trailing/ bin/hadoop fs -touchz $BUCKET/file bin/hadoop fs -ls $BUCKET/ diff --git a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/troubleshooting_s3a.md b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/troubleshooting_s3a.md index d55e5229108..3019b8525db 100644 --- a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/troubleshooting_s3a.md +++ b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/troubleshooting_s3a.md @@ -336,13 +336,6 @@ spark.sql.hive.metastore.sharedPrefixes com.amazonaws. You are trying to use session/temporary credentials and the session token supplied is considered invalid. -``` -org.apache.hadoop.fs.s3a.AWSBadRequestException: initTable on bucket: - com.amazonaws.services.dynamodbv2.model.AmazonDynamoDBException: - The security token included in the request is invalid - (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: UnrecognizedClientException) -``` - This can surface if your configuration is setting the `fs.s3a.secret.key`, `fs.s3a.access.key` and `fs.s3a.session.key` correctly, but the AWS credential provider list set in `AWS_CREDENTIALS_PROVIDER` does not include @@ -1030,32 +1023,6 @@ before versioning was enabled. See [Handling Read-During-Overwrite](./index.html#handling_read-during-overwrite) for more information. -### `RemoteFileChangedException`: "File to rename not found on guarded S3 store after repeated attempts" - -A file being renamed and listed in the S3Guard table could not be found -in the S3 bucket even after multiple attempts. - -Now that S3 is consistent, this is sign that the S3Guard table is out of sync with -the S3 Data. - -Fix: disable S3Guard: it is no longer needed. - -``` -org.apache.hadoop.fs.s3a.RemoteFileChangedException: copyFile(/sourcedir/missing, /destdir/) - `s3a://example/sourcedir/missing': File not found on S3 after repeated attempts: `s3a://example/sourcedir/missing' -at org.apache.hadoop.fs.s3a.S3AFileSystem.copyFile(S3AFileSystem.java:3231) -at org.apache.hadoop.fs.s3a.S3AFileSystem.access$700(S3AFileSystem.java:177) -at org.apache.hadoop.fs.s3a.S3AFileSystem$RenameOperationCallbacksImpl.copyFile(S3AFileSystem.java:1368) -at org.apache.hadoop.fs.s3a.impl.RenameOperation.copySourceAndUpdateTracker(RenameOperation.java:448) -at org.apache.hadoop.fs.s3a.impl.RenameOperation.lambda$initiateCopy$0(RenameOperation.java:412) -``` - -If error occurs and the file is on S3, consider increasing the value of -`fs.s3a.s3guard.consistency.retry.limit`. - -We also recommend using applications/application -options which do not rename files when committing work or when copying data -to S3, but instead write directly to the final destination. ### Rename not behaving as "expected" @@ -1453,30 +1420,6 @@ The user trying to use the KMS Key ID should have the right permissions to acces If not, then add permission(or IAM role) in "Key users" section by selecting the AWS-KMS CMK Key on AWS console. -### S3-CSE cannot be used with S3Guard - -S3-CSE not supported for S3Guard enabled buckets. -``` -org.apache.hadoop.fs.PathIOException: `s3a://test-bucket': S3-CSE cannot be used with S3Guard - at org.apache.hadoop.fs.s3a.S3AFileSystem.initialize(S3AFileSystem.java:543) - at org.apache.hadoop.fs.FileSystem.createFileSystem(FileSystem.java:3460) - at org.apache.hadoop.fs.FileSystem.access$300(FileSystem.java:172) - at org.apache.hadoop.fs.FileSystem$Cache.getInternal(FileSystem.java:3565) - at org.apache.hadoop.fs.FileSystem$Cache.get(FileSystem.java:3512) - at org.apache.hadoop.fs.FileSystem.get(FileSystem.java:539) - at org.apache.hadoop.fs.Path.getFileSystem(Path.java:366) - at org.apache.hadoop.fs.shell.PathData.expandAsGlob(PathData.java:342) - at org.apache.hadoop.fs.shell.Command.expandArgument(Command.java:252) - at org.apache.hadoop.fs.shell.Command.expandArguments(Command.java:235) - at org.apache.hadoop.fs.shell.FsCommand.processRawArguments(FsCommand.java:105) - at org.apache.hadoop.fs.shell.Command.run(Command.java:179) - at org.apache.hadoop.fs.FsShell.run(FsShell.java:327) - at org.apache.hadoop.util.ToolRunner.run(ToolRunner.java:81) - at org.apache.hadoop.util.ToolRunner.run(ToolRunner.java:95) - at org.apache.hadoop.fs.FsShell.main(FsShell.java:390) -``` -If you want to use S3Guard then disable S3-CSE or disable S3Guard if you want -to use S3-CSE. ### Message appears in logs "Not all bytes were read from the S3ObjectInputStream" @@ -1553,6 +1496,56 @@ The specified bucket does not exist at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeOneRequest(AmazonHttpClient.java:1367) ``` + +## S3Guard Errors + +S3Guard has been completely cut from the s3a connector +[HADOOP-17409 Remove S3Guard - no longer needed](HADOOP-17409 Remove S3Guard - no longer needed). + +To avoid consistency problems with older releases, if an S3A filesystem is configured to use DynamoDB the filesystem +will fail to initialize. + +### S3Guard is no longer needed/supported + +The option `fs.s3a.metadatastore.impl` or the per-bucket version has a value of +`org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore` + +``` +org.apache.hadoop.fs.PathIOException: `s3a://production-london': S3Guard is no longer needed/supported, + yet s3a://production-london is configured to use DynamoDB as the S3Guard metadata store. + This is no longer needed or supported. + Origin of setting is fs.s3a.bucket.production-london.metadatastore.impl via [core-site.xml] + at org.apache.hadoop.fs.s3a.s3guard.S3Guard.checkNoS3Guard(S3Guard.java:111) + at org.apache.hadoop.fs.s3a.S3AFileSystem.initialize(S3AFileSystem.java:540) + at org.apache.hadoop.fs.FileSystem.createFileSystem(FileSystem.java:3459) + at org.apache.hadoop.fs.FileSystem.access$300(FileSystem.java:171) + at org.apache.hadoop.fs.FileSystem$Cache.getInternal(FileSystem.java:3564) + at org.apache.hadoop.fs.FileSystem$Cache.get(FileSystem.java:3511) + at org.apache.hadoop.fs.FileSystem.get(FileSystem.java:538) + at org.apache.hadoop.fs.Path.getFileSystem(Path.java:366) + at org.apache.hadoop.fs.shell.PathData.expandAsGlob(PathData.java:342) + at org.apache.hadoop.fs.shell.Command.expandArgument(Command.java:252) + at org.apache.hadoop.fs.shell.Command.expandArguments(Command.java:235) + at org.apache.hadoop.fs.shell.FsCommand.processRawArguments(FsCommand.java:105) + at org.apache.hadoop.fs.shell.Command.run(Command.java:179) + at org.apache.hadoop.fs.FsShell.run(FsShell.java:327) + at org.apache.hadoop.util.ToolRunner.run(ToolRunner.java:81) + at org.apache.hadoop.util.ToolRunner.run(ToolRunner.java:95) + at org.apache.hadoop.fs.FsShell.main(FsShell.java:390) + +ls: `s3a://production-london': S3Guard is no longer needed/supported, + yet s3a://production-london is configured to use DynamoDB as the S3Guard metadata store. + This is no longer needed or supported. + Origin of setting is fs.s3a.bucket.production-london.metadatastore.impl via [core-site.xml] +``` + +The error message will state the property from where it came, here `fs.s3a.bucket.production-london.metadatastore.impl` and which +file the option was set if known, here `core-site.xml`. + +Fix: remove the configuration options enabling S3Guard. + +Consult the [S3Guard documentation](s3guard.html) for more details. + ## Other Errors ### `SdkClientException` Unable to verify integrity of data upload @@ -1926,44 +1919,6 @@ Please don't do that. Given that the emulated directory rename and delete operat are not atomic, even without retries, multiple S3 clients working with the same paths can interfere with each other -### Tuning S3Guard open/rename Retry Policies - -When the S3A connector attempts to open a file for which it has an entry in -its database, it will retry if the desired file is not found. This is -done if: - -* No file is found in S3. -* There is a file but its version or etag is not consistent with S3Guard table. - -These can be symptoms of S3's eventual consistency, hence the retries. -They can also be caused by changes having been made to the S3 Store without -SGuard being kept up to date. - -For this reason, the number of retry events are limited. - -```xml - - fs.s3a.s3guard.consistency.retry.limit - 7 - - Number of times to retry attempts to read/open/copy files when - S3Guard believes a specific version of the file to be available, - but the S3 request does not find any version of a file, or a different - version. - - - - - fs.s3a.s3guard.consistency.retry.interval - 2s - - Initial interval between attempts to retry operations while waiting for S3 - to become consistent with the S3Guard data. - An exponential back-off is used here: every failure doubles the delay. - - -``` - ### Tuning AWS request timeouts It is possible to configure a global timeout for AWS service calls using following property: diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractCreate.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractCreate.java index fd9497ba3ff..d2a858f615e 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractCreate.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractCreate.java @@ -22,25 +22,11 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.contract.AbstractContractCreateTest; import org.apache.hadoop.fs.contract.AbstractFSContract; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.maybeEnableS3Guard; - /** * S3A contract tests creating files. */ public class ITestS3AContractCreate extends AbstractContractCreateTest { - /** - * Create a configuration, possibly patching in S3Guard options. - * @return a configuration - */ - @Override - protected Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - // patch in S3Guard options - maybeEnableS3Guard(conf); - return conf; - } - @Override protected AbstractFSContract createContract(Configuration conf) { return new S3AContract(conf); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractDelete.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractDelete.java index 95ea410fa66..a47dcaef61e 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractDelete.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractDelete.java @@ -22,25 +22,11 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.contract.AbstractContractDeleteTest; import org.apache.hadoop.fs.contract.AbstractFSContract; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.maybeEnableS3Guard; - /** * S3A contract tests covering deletes. */ public class ITestS3AContractDelete extends AbstractContractDeleteTest { - /** - * Create a configuration, possibly patching in S3Guard options. - * @return a configuration - */ - @Override - protected Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - // patch in S3Guard options - maybeEnableS3Guard(conf); - return conf; - } - @Override protected AbstractFSContract createContract(Configuration conf) { return new S3AContract(conf); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractDistCp.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractDistCp.java index f4f5c176b37..e761e0d14bf 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractDistCp.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractDistCp.java @@ -20,7 +20,6 @@ package org.apache.hadoop.fs.contract.s3a; import static org.apache.hadoop.fs.s3a.Constants.*; import static org.apache.hadoop.fs.s3a.S3ATestConstants.SCALE_TEST_TIMEOUT_MILLIS; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.maybeEnableS3Guard; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.StorageStatistics; @@ -41,7 +40,7 @@ public class ITestS3AContractDistCp extends AbstractContractDistCpTest { } /** - * Create a configuration, possibly patching in S3Guard options. + * Create a configuration. * @return a configuration */ @Override @@ -49,8 +48,6 @@ public class ITestS3AContractDistCp extends AbstractContractDistCpTest { Configuration newConf = super.createConfiguration(); newConf.setLong(MULTIPART_SIZE, MULTIPART_SETTING); newConf.set(FAST_UPLOAD_BUFFER, FAST_UPLOAD_BUFFER_DISK); - // patch in S3Guard options - maybeEnableS3Guard(newConf); return newConf; } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractGetFileStatus.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractGetFileStatus.java index 638a22786b8..a68b0ea12af 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractGetFileStatus.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractGetFileStatus.java @@ -24,8 +24,6 @@ import org.apache.hadoop.fs.s3a.Constants; import org.apache.hadoop.fs.s3a.S3ATestConstants; import org.apache.hadoop.fs.s3a.S3ATestUtils; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.maybeEnableS3Guard; - /** * S3A contract tests covering getFileStatus. * Some of the tests can take too long when the fault injection rate is high, @@ -51,8 +49,6 @@ public class ITestS3AContractGetFileStatus S3ATestUtils.disableFilesystemCaching(conf); // aggressively low page size forces tests to go multipage conf.setInt(Constants.MAX_PAGING_KEYS, 2); - // patch in S3Guard options - maybeEnableS3Guard(conf); return conf; } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractMkdir.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractMkdir.java index dba52e128d4..d953e7eb6ae 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractMkdir.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractMkdir.java @@ -22,25 +22,11 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.contract.AbstractContractMkdirTest; import org.apache.hadoop.fs.contract.AbstractFSContract; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.maybeEnableS3Guard; - /** * Test dir operations on S3A. */ public class ITestS3AContractMkdir extends AbstractContractMkdirTest { - /** - * Create a configuration, possibly patching in S3Guard options. - * @return a configuration - */ - @Override - protected Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - // patch in S3Guard options - maybeEnableS3Guard(conf); - return conf; - } - @Override protected AbstractFSContract createContract(Configuration conf) { return new S3AContract(conf); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractMultipartUploader.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractMultipartUploader.java index 8222fff6145..48f5982bed6 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractMultipartUploader.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractMultipartUploader.java @@ -31,7 +31,6 @@ import static org.apache.hadoop.fs.s3a.S3ATestConstants.SCALE_TEST_TIMEOUT_MILLI import static org.apache.hadoop.fs.s3a.S3ATestUtils.assume; import static org.apache.hadoop.fs.s3a.S3ATestUtils.getTestPropertyBool; import static org.apache.hadoop.fs.s3a.S3ATestUtils.getTestPropertyBytes; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.maybeEnableS3Guard; import static org.apache.hadoop.fs.s3a.scale.AbstractSTestS3AHugeFiles.DEFAULT_HUGE_PARTITION_SIZE; /** @@ -65,17 +64,6 @@ public class ITestS3AContractMultipartUploader extends return (S3AFileSystem) super.getFileSystem(); } - /** - * Create a configuration, possibly patching in S3Guard options. - * @return a configuration - */ - @Override - protected Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - maybeEnableS3Guard(conf); - return conf; - } - @Override protected AbstractFSContract createContract(Configuration conf) { return new S3AContract(conf); @@ -96,15 +84,6 @@ public class ITestS3AContractMultipartUploader extends return true; } - /** - * Provide a pessimistic time to become consistent. - * @return a time in milliseconds - */ - @Override - protected int timeToBecomeConsistentMillis() { - return 30 * 1000; - } - @Override protected boolean finalizeConsumesUploadIdImmediately() { return false; @@ -130,7 +109,7 @@ public class ITestS3AContractMultipartUploader extends * S3 has no concept of directories, so this test does not apply. */ public void testDirectoryInTheWay() throws Exception { - skip("unsupported"); + skip("Unsupported"); } @Override diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractOpen.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractOpen.java index d78273b1471..4765fa8e8d7 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractOpen.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractOpen.java @@ -22,25 +22,11 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.contract.AbstractContractOpenTest; import org.apache.hadoop.fs.contract.AbstractFSContract; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.maybeEnableS3Guard; - /** * S3A contract tests opening files. */ public class ITestS3AContractOpen extends AbstractContractOpenTest { - /** - * Create a configuration, possibly patching in S3Guard options. - * @return a configuration - */ - @Override - protected Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - // patch in S3Guard options - maybeEnableS3Guard(conf); - return conf; - } - @Override protected AbstractFSContract createContract(Configuration conf) { return new S3AContract(conf); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractRename.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractRename.java index e44df5facd2..d3ba7373cc9 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractRename.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractRename.java @@ -18,21 +18,15 @@ package org.apache.hadoop.fs.contract.s3a; -import java.util.Arrays; -import java.util.Collection; - -import org.junit.Assume; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.contract.AbstractContractRenameTest; -import org.apache.hadoop.fs.contract.AbstractFSContract; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.contract.AbstractContractRenameTest; +import org.apache.hadoop.fs.contract.AbstractFSContract; import org.apache.hadoop.fs.s3a.S3AFileSystem; import org.apache.hadoop.fs.s3a.S3ATestUtils; import org.apache.hadoop.fs.s3a.Statistic; @@ -41,70 +35,26 @@ import static org.apache.hadoop.fs.contract.ContractTestUtils.dataset; import static org.apache.hadoop.fs.contract.ContractTestUtils.skip; import static org.apache.hadoop.fs.contract.ContractTestUtils.verifyFileContents; import static org.apache.hadoop.fs.contract.ContractTestUtils.writeDataset; -import static org.apache.hadoop.fs.s3a.Constants.METADATASTORE_AUTHORITATIVE; import static org.apache.hadoop.fs.s3a.S3ATestConstants.S3A_TEST_TIMEOUT; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.maybeEnableS3Guard; /** * S3A contract tests covering rename. - * Parameterized for auth mode as testRenameWithNonEmptySubDir was failing - * during HADOOP-16697 development; this lets us ensure that when S3Guard - * is enabled, both auth and nonauth paths work */ -@RunWith(Parameterized.class) public class ITestS3AContractRename extends AbstractContractRenameTest { public static final Logger LOG = LoggerFactory.getLogger( ITestS3AContractRename.class); - private final boolean authoritative; - - /** - * Parameterization. - */ - @Parameterized.Parameters(name = "auth={0}") - public static Collection params() { - return Arrays.asList(new Object[][]{ - {false}, - {true} - }); - } - - public ITestS3AContractRename(boolean authoritative) { - this.authoritative = authoritative; - } - @Override protected int getTestTimeoutMillis() { return S3A_TEST_TIMEOUT; } - /** - * Create a configuration, possibly patching in S3Guard options. - * @return a configuration - */ - @Override - protected Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - // patch in S3Guard options - maybeEnableS3Guard(conf); - conf.setBoolean(METADATASTORE_AUTHORITATIVE, authoritative); - return conf; - } - @Override protected AbstractFSContract createContract(Configuration conf) { return new S3AContract(conf); } - @Override - public void setup() throws Exception { - super.setup(); - Assume.assumeTrue( - "Skipping auth mode tests when the FS doesn't have a metastore", - !authoritative || ((S3AFileSystem) getFileSystem()).hasMetadataStore()); - } - @Override public void testRenameDirIntoExistingDir() throws Throwable { describe("S3A rename into an existing directory returns false"); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractRootDir.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractRootDir.java index 6377b65f713..5335de1b324 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractRootDir.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractRootDir.java @@ -18,20 +18,15 @@ package org.apache.hadoop.fs.contract.s3a; -import java.io.FileNotFoundException; -import java.io.IOException; +import org.junit.Ignore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.contract.AbstractContractRootDirectoryTest; import org.apache.hadoop.fs.contract.AbstractFSContract; import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.junit.Ignore; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static org.apache.hadoop.fs.s3a.S3ATestUtils.maybeEnableS3Guard; - /** * root dir operations against an S3 bucket. */ @@ -41,18 +36,6 @@ public class ITestS3AContractRootDir extends private static final Logger LOG = LoggerFactory.getLogger(ITestS3AContractRootDir.class); - /** - * Create a configuration, possibly patching in S3Guard options. - * @return a configuration - */ - @Override - protected Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - // patch in S3Guard options - maybeEnableS3Guard(conf); - return conf; - } - @Override protected AbstractFSContract createContract(Configuration conf) { return new S3AContract(conf); @@ -67,40 +50,4 @@ public class ITestS3AContractRootDir extends @Ignore("S3 always return false when non-recursively remove root dir") public void testRmNonEmptyRootDirNonRecursive() throws Throwable { } - - /** - * This is overridden to allow for eventual consistency on listings, - * but only if the store does not have S3Guard protecting it. - */ - @Override - public void testListEmptyRootDirectory() throws IOException { - int maxAttempts = 10; - describe("Listing root directory; for consistency allowing " - + maxAttempts + " attempts"); - for (int attempt = 1; attempt <= maxAttempts; ++attempt) { - try { - super.testListEmptyRootDirectory(); - break; - } catch (AssertionError | FileNotFoundException e) { - if (attempt < maxAttempts) { - LOG.info("Attempt {} of {} for empty root directory test failed. " - + "This is likely caused by eventual consistency of S3 " - + "listings. Attempting retry.", attempt, maxAttempts, - e); - try { - Thread.sleep(1000); - } catch (InterruptedException e2) { - Thread.currentThread().interrupt(); - fail("Test interrupted."); - break; - } - } else { - LOG.error( - "Empty root directory test failed {} attempts. Failing test.", - maxAttempts); - throw e; - } - } - } - } } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractSeek.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractSeek.java index 329b940a66a..2410827cb39 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractSeek.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractSeek.java @@ -52,7 +52,6 @@ import static org.apache.hadoop.fs.s3a.Constants.INPUT_FADV_SEQUENTIAL; import static org.apache.hadoop.fs.s3a.Constants.READAHEAD_RANGE; import static org.apache.hadoop.fs.s3a.Constants.SSL_CHANNEL_MODE; import static org.apache.hadoop.fs.s3a.S3ATestConstants.FS_S3A_IMPL_DISABLE_CACHE; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.maybeEnableS3Guard; import static org.apache.hadoop.security.ssl.DelegatingSSLSocketFactory. SSLChannelMode.Default_JSSE; import static org.apache.hadoop.security.ssl.DelegatingSSLSocketFactory. @@ -105,7 +104,7 @@ public class ITestS3AContractSeek extends AbstractContractSeekTest { } /** - * Create a configuration, possibly patching in S3Guard options. + * Create a configuration. * The FS is set to be uncached and the readahead and seek policies * of the bucket itself are removed, so as to guarantee that the * parameterized and test settings are @@ -114,8 +113,6 @@ public class ITestS3AContractSeek extends AbstractContractSeekTest { @Override protected Configuration createConfiguration() { Configuration conf = super.createConfiguration(); - // patch in S3Guard options - maybeEnableS3Guard(conf); // purge any per-bucket overrides. try { URI bucketURI = new URI(checkNotNull(conf.get("fs.contract.test.fs.s3a"))); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractUnbuffer.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractUnbuffer.java index d6dbce9233b..2c7149ff5cb 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractUnbuffer.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/ITestS3AContractUnbuffer.java @@ -22,18 +22,8 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.contract.AbstractContractUnbufferTest; import org.apache.hadoop.fs.contract.AbstractFSContract; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.maybeEnableS3Guard; - public class ITestS3AContractUnbuffer extends AbstractContractUnbufferTest { - @Override - protected Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - // patch in S3Guard options - maybeEnableS3Guard(conf); - return conf; - } - @Override protected AbstractFSContract createContract(Configuration conf) { return new S3AContract(conf); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/S3AContract.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/S3AContract.java index 695a4a2b682..0d3dd4c2f66 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/S3AContract.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/contract/s3a/S3AContract.java @@ -18,17 +18,12 @@ package org.apache.hadoop.fs.contract.s3a; -import java.io.IOException; - import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.PathIOException; import org.apache.hadoop.fs.contract.AbstractBondedFSContract; import org.apache.hadoop.fs.s3a.S3AFileSystem; import org.apache.hadoop.fs.s3a.S3ATestUtils; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.maybeSkipIfS3GuardAndS3CSEIOE; - /** * The contract of S3A: only enabled if the test bucket is provided. */ @@ -68,20 +63,6 @@ public class S3AContract extends AbstractBondedFSContract { } } - /** - * Skip S3AFS initialization if S3-CSE and S3Guard are enabled. - * - */ - @Override - public void init() throws IOException { - try { - super.init(); - } catch (PathIOException ioe) { - // Skip the tests if S3-CSE and S3-Guard are enabled. - maybeSkipIfS3GuardAndS3CSEIOE(ioe); - } - } - @Override public String getScheme() { return "s3a"; diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/AbstractS3AMockTest.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/AbstractS3AMockTest.java index 5765fe471c2..7cd60cdd4da 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/AbstractS3AMockTest.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/AbstractS3AMockTest.java @@ -26,8 +26,6 @@ import com.amazonaws.services.s3.AmazonS3; import java.net.URI; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.NullMetadataStore; import org.junit.After; import org.junit.Before; @@ -68,10 +66,7 @@ public abstract class AbstractS3AMockTest { Configuration conf = new Configuration(); conf.setClass(S3_CLIENT_FACTORY_IMPL, MockS3ClientFactory.class, S3ClientFactory.class); - // We explicitly disable MetadataStore. For unit - // test we don't issue request to AWS DynamoDB service. - conf.setClass(S3_METADATA_STORE_IMPL, NullMetadataStore.class, - MetadataStore.class); + // use minimum multipart size for faster triggering conf.setLong(Constants.MULTIPART_SIZE, MULTIPART_MIN_SIZE); conf.setInt(Constants.S3A_BUCKET_PROBE, 1); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/AbstractS3ATestBase.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/AbstractS3ATestBase.java index 242919192f1..213f94432c4 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/AbstractS3ATestBase.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/AbstractS3ATestBase.java @@ -41,7 +41,6 @@ import java.io.IOException; import static org.apache.hadoop.fs.contract.ContractTestUtils.dataset; import static org.apache.hadoop.fs.contract.ContractTestUtils.writeDataset; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.getTestDynamoTablePrefix; import static org.apache.hadoop.fs.s3a.S3ATestUtils.getTestPropertyBool; import static org.apache.hadoop.fs.s3a.S3AUtils.E_FS_CLOSED; import static org.apache.hadoop.fs.s3a.tools.MarkerTool.UNLIMITED_LISTING; @@ -175,7 +174,7 @@ public abstract class AbstractS3ATestBase extends AbstractFSContractTestBase } /** - * Create a configuration, possibly patching in S3Guard options. + * Create a configuration. * @return a configuration */ @Override @@ -235,10 +234,6 @@ public abstract class AbstractS3ATestBase extends AbstractFSContractTestBase ContractTestUtils.verifyFileContents(getFileSystem(), path, data); } - protected String getTestTableName(String suffix) { - return getTestDynamoTablePrefix(getConfiguration()) + suffix; - } - /** * Create a span from the source; returns a no-op if * creation fails or the source is null. diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestAuthoritativePath.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestAuthoritativePath.java deleted file mode 100644 index b1d742a4005..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestAuthoritativePath.java +++ /dev/null @@ -1,335 +0,0 @@ -/* - * 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.hadoop.fs.s3a; - -import java.io.IOException; -import java.net.URI; -import java.util.Collection; - -import org.apache.hadoop.fs.s3a.s3guard.NullMetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.S3Guard; -import org.apache.hadoop.io.IOUtils; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; - -import org.junit.Before; -import org.junit.Test; - -import static org.apache.hadoop.fs.contract.ContractTestUtils.touch; -import static org.apache.hadoop.fs.s3a.Constants.METADATASTORE_AUTHORITATIVE; -import static org.apache.hadoop.fs.s3a.Constants.AUTHORITATIVE_PATH; -import static org.apache.hadoop.fs.s3a.Constants.S3_METADATA_STORE_IMPL; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.checkListingContainsPath; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.checkListingDoesNotContainPath; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.metadataStorePersistsAuthoritativeBit; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.removeBaseAndBucketOverrides; -import static org.junit.Assume.assumeTrue; - -public class ITestAuthoritativePath extends AbstractS3ATestBase { - - public Path testRoot; - - private S3AFileSystem fullyAuthFS; - private S3AFileSystem rawFS; - - private MetadataStore ms; - - @Before - public void setup() throws Exception { - super.setup(); - - long timestamp = System.currentTimeMillis(); - testRoot = path("" + timestamp); - - S3AFileSystem fs = getFileSystem(); - // These test will fail if no ms - assumeTrue("FS needs to have a metadatastore.", - fs.hasMetadataStore()); - assumeTrue("Metadatastore should persist authoritative bit", - metadataStorePersistsAuthoritativeBit(fs.getMetadataStore())); - - // This test setup shares a single metadata store across instances, - // so that test runs with a local FS work. - // but this needs to be addressed in teardown, where the Auth fs - // needs to be detached from the metadata store before it is closed, - ms = fs.getMetadataStore(); - - fullyAuthFS = createFullyAuthFS(); - assertTrue("No S3Guard store for fullyAuthFS", - fullyAuthFS.hasMetadataStore()); - assertTrue("Authoritative mode off in fullyAuthFS", - fullyAuthFS.hasAuthoritativeMetadataStore()); - - rawFS = createRawFS(); - assertFalse("UnguardedFS still has S3Guard", - rawFS.hasMetadataStore()); - } - - private void cleanUpFS(S3AFileSystem fs) { - // detach from the (shared) metadata store. - if (fs != null) { - fs.setMetadataStore(new NullMetadataStore()); - } - - IOUtils.cleanupWithLogger(LOG, fs); - } - - @Override - public void teardown() throws Exception { - if (fullyAuthFS != null) { - fullyAuthFS.delete(testRoot, true); - } - - cleanUpFS(fullyAuthFS); - cleanUpFS(rawFS); - super.teardown(); - } - - private S3AFileSystem createFullyAuthFS() - throws Exception { - S3AFileSystem testFS = getFileSystem(); - Configuration config = new Configuration(testFS.getConf()); - URI uri = testFS.getUri(); - - removeBaseAndBucketOverrides(uri.getHost(), config, - METADATASTORE_AUTHORITATIVE, - AUTHORITATIVE_PATH); - config.setBoolean(METADATASTORE_AUTHORITATIVE, true); - final S3AFileSystem newFS = createFS(uri, config); - // set back the same metadata store instance - newFS.setMetadataStore(ms); - return newFS; - } - - private S3AFileSystem createSinglePathAuthFS(String authPath) - throws Exception { - S3AFileSystem testFS = getFileSystem(); - Configuration config = new Configuration(testFS.getConf()); - URI uri = testFS.getUri(); - - removeBaseAndBucketOverrides(uri.getHost(), config, - METADATASTORE_AUTHORITATIVE, - AUTHORITATIVE_PATH); - config.set(AUTHORITATIVE_PATH, authPath.toString()); - final S3AFileSystem newFS = createFS(uri, config); - // set back the same metadata store instance - newFS.setMetadataStore(ms); - return newFS; - } - - private S3AFileSystem createMultiPathAuthFS(String first, String middle, String last) - throws Exception { - S3AFileSystem testFS = getFileSystem(); - Configuration config = new Configuration(testFS.getConf()); - URI uri = testFS.getUri(); - - removeBaseAndBucketOverrides(uri.getHost(), config, - METADATASTORE_AUTHORITATIVE, - AUTHORITATIVE_PATH); - config.set(AUTHORITATIVE_PATH, first + "," + middle + "," + last); - final S3AFileSystem newFS = createFS(uri, config); - // set back the same metadata store instance - newFS.setMetadataStore(ms); - return newFS; - } - - private S3AFileSystem createRawFS() throws Exception { - S3AFileSystem testFS = getFileSystem(); - Configuration config = new Configuration(testFS.getConf()); - URI uri = testFS.getUri(); - - removeBaseAndBucketOverrides(uri.getHost(), config, - S3_METADATA_STORE_IMPL); - removeBaseAndBucketOverrides(uri.getHost(), config, - METADATASTORE_AUTHORITATIVE, - AUTHORITATIVE_PATH); - return createFS(uri, config); - } - - /** - * Create and initialize a new filesystem. - * This filesystem MUST be closed in test teardown. - * @param uri FS URI - * @param config config. - * @return new instance - * @throws IOException failure - */ - private S3AFileSystem createFS(final URI uri, final Configuration config) - throws IOException { - S3AFileSystem fs2 = new S3AFileSystem(); - fs2.initialize(uri, config); - return fs2; - } - - private void runTestOutsidePath(S3AFileSystem partiallyAuthFS, Path nonAuthPath) throws Exception { - Path inBandPath = new Path(nonAuthPath, "out-of-path-in-band"); - Path outOfBandPath = new Path(nonAuthPath, "out-of-path-out-of-band"); - - touch(fullyAuthFS, inBandPath); - - // trigger an authoritative write-back - fullyAuthFS.listStatus(inBandPath.getParent()); - - touch(rawFS, outOfBandPath); - - // listing lacks outOfBandPath => short-circuited by auth mode - checkListingDoesNotContainPath(fullyAuthFS, outOfBandPath); - - // partiallyAuthFS differs from fullyAuthFS because we're outside the path - checkListingContainsPath(partiallyAuthFS, outOfBandPath); - - // sanity check that in-band operations are always visible - checkListingContainsPath(fullyAuthFS, inBandPath); - checkListingContainsPath(partiallyAuthFS, inBandPath); - - } - - private void runTestInsidePath(S3AFileSystem partiallyAuthFS, Path authPath) throws Exception { - Path inBandPath = new Path(authPath, "in-path-in-band"); - Path outOfBandPath = new Path(authPath, "in-path-out-of-band"); - - touch(fullyAuthFS, inBandPath); - - // trigger an authoritative write-back - fullyAuthFS.listStatus(inBandPath.getParent()); - - touch(rawFS, outOfBandPath); - - // listing lacks outOfBandPath => short-circuited by auth mode - checkListingDoesNotContainPath(fullyAuthFS, outOfBandPath); - checkListingDoesNotContainPath(partiallyAuthFS, outOfBandPath); - - // sanity check that in-band operations are always successful - checkListingContainsPath(fullyAuthFS, inBandPath); - checkListingContainsPath(partiallyAuthFS, inBandPath); - } - - @Test - public void testSingleAuthPath() throws Exception { - Path authPath = new Path(testRoot, "testSingleAuthPath-auth"); - Path nonAuthPath = new Path(testRoot, "testSingleAuthPath"); - S3AFileSystem fs = createSinglePathAuthFS(authPath.toString()); - try { - assertTrue("No S3Guard store for partially authoritative FS", - fs.hasMetadataStore()); - - runTestInsidePath(fs, authPath); - runTestOutsidePath(fs, nonAuthPath); - } finally { - cleanUpFS(fs); - } - } - - @Test - public void testAuthPathWithOtherBucket() throws Exception { - Path authPath; - Path nonAuthPath; - S3AFileSystem fs = null; - String landsat = "s3a://landsat-pds/data"; - String decoy2 = "/decoy2"; - - try { - authPath = new Path(testRoot, "testMultiAuthPath-first"); - nonAuthPath = new Path(testRoot, "nonAuth-1"); - fs = createMultiPathAuthFS(authPath.toString(), landsat, decoy2); - assertTrue("No S3Guard store for partially authoritative FS", - fs.hasMetadataStore()); - - runTestInsidePath(fs, authPath); - runTestOutsidePath(fs, nonAuthPath); - } finally { - cleanUpFS(fs); - } - } - - @Test - public void testMultiAuthPath() throws Exception { - Path authPath; - Path nonAuthPath; - S3AFileSystem fs = null; - String decoy1 = "/decoy1"; - String decoy2 = "/decoy2"; - - try { - authPath = new Path(testRoot, "testMultiAuthPath-first"); - nonAuthPath = new Path(testRoot, "nonAuth-1"); - fs = createMultiPathAuthFS(authPath.toString(), decoy1, decoy2); - assertTrue("No S3Guard store for partially authoritative FS", - fs.hasMetadataStore()); - - runTestInsidePath(fs, authPath); - runTestOutsidePath(fs, nonAuthPath); - } finally { - cleanUpFS(fs); - } - - try { - authPath = new Path(testRoot, "testMultiAuthPath-middle"); - nonAuthPath = new Path(testRoot, "nonAuth-2"); - fs = createMultiPathAuthFS(decoy1, authPath.toString(), decoy2); - assertTrue("No S3Guard store for partially authoritative FS", - fs.hasMetadataStore()); - - runTestInsidePath(fs, authPath); - runTestOutsidePath(fs, nonAuthPath); - } finally { - cleanUpFS(fs); - } - - try { - authPath = new Path(testRoot, "testMultiAuthPath-last"); - nonAuthPath = new Path(testRoot, "nonAuth-3"); - fs = createMultiPathAuthFS(decoy1, decoy2, authPath.toString()); - assertTrue("No S3Guard store for partially authoritative FS", - fs.hasMetadataStore()); - - runTestInsidePath(fs, authPath); - runTestOutsidePath(fs, nonAuthPath); - } finally { - cleanUpFS(fs); - } - } - - @Test - public void testPrefixVsDirectory() throws Exception { - S3AFileSystem fs = createSinglePathAuthFS("/auth"); - Collection authPaths = S3Guard.getAuthoritativePaths(fs); - - try{ - Path totalMismatch = new Path(testRoot, "/non-auth"); - assertFalse(S3Guard.allowAuthoritative(totalMismatch, fs, - false, authPaths)); - - Path prefixMatch = new Path(testRoot, "/authoritative"); - assertFalse(S3Guard.allowAuthoritative(prefixMatch, fs, - false, authPaths)); - - Path directoryMatch = new Path(testRoot, "/auth/oritative"); - assertTrue(S3Guard.allowAuthoritative(directoryMatch, fs, - false, authPaths)); - - Path unqualifiedMatch = new Path(testRoot.toUri().getPath(), "/auth/oritative"); - assertTrue(S3Guard.allowAuthoritative(unqualifiedMatch, fs, - false, authPaths)); - } finally { - cleanUpFS(fs); - } - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestBlockingThreadPoolExecutorService.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestBlockingThreadPoolExecutorService.java index 55423273b95..cf9ad877add 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestBlockingThreadPoolExecutorService.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestBlockingThreadPoolExecutorService.java @@ -43,7 +43,7 @@ import static org.junit.Assert.assertEquals; public class ITestBlockingThreadPoolExecutorService { private static final Logger LOG = LoggerFactory.getLogger( - BlockingThreadPoolExecutorService.class); + ITestBlockingThreadPoolExecutorService.class); private static final int NUM_ACTIVE_TASKS = 4; private static final int NUM_WAITING_TASKS = 2; @@ -57,7 +57,7 @@ public class ITestBlockingThreadPoolExecutorService { private static BlockingThreadPoolExecutorService tpe; @Rule - public Timeout testTimeout = new Timeout(60 * 1000); + public Timeout testTimeout = new Timeout(60, TimeUnit.SECONDS); @AfterClass public static void afterClass() throws Exception { diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestDowngradeSyncable.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestDowngradeSyncable.java index 0bcb11a823d..3ad679e6728 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestDowngradeSyncable.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestDowngradeSyncable.java @@ -44,7 +44,7 @@ public class ITestDowngradeSyncable extends AbstractS3ACostTest { public ITestDowngradeSyncable() { - super(false, true, false); + super(true); } @Override diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestLocatedFileStatusFetcher.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestLocatedFileStatusFetcher.java index 5b6e634a631..fcf412dac8a 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestLocatedFileStatusFetcher.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestLocatedFileStatusFetcher.java @@ -19,13 +19,9 @@ package org.apache.hadoop.fs.s3a; import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Collection; import org.assertj.core.api.Assertions; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,13 +35,6 @@ import org.apache.hadoop.mapred.LocatedFileStatusFetcher; import static org.apache.hadoop.fs.contract.ContractTestUtils.createFile; import static org.apache.hadoop.fs.contract.ContractTestUtils.touch; -import static org.apache.hadoop.fs.s3a.Constants.AUTHORITATIVE_PATH; -import static org.apache.hadoop.fs.s3a.Constants.METADATASTORE_AUTHORITATIVE; -import static org.apache.hadoop.fs.s3a.Constants.S3_METADATA_STORE_IMPL; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.disableFilesystemCaching; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.getTestBucketName; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.removeBaseAndBucketOverrides; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.removeBucketOverrides; import static org.apache.hadoop.fs.statistics.IOStatisticAssertions.assertThatStatisticCounter; import static org.apache.hadoop.fs.statistics.IOStatisticAssertions.extractStatistics; import static org.apache.hadoop.fs.statistics.StoreStatisticNames.OBJECT_LIST_REQUEST; @@ -61,24 +50,11 @@ import static org.apache.hadoop.mapreduce.lib.input.FileInputFormat.LIST_STATUS_ * but whereas that tests failure paths, this looks at the performance * of successful invocations. */ -@RunWith(Parameterized.class) public class ITestLocatedFileStatusFetcher extends AbstractS3ATestBase { private static final Logger LOG = LoggerFactory.getLogger(ITestLocatedFileStatusFetcher.class); - - /** - * Parameterization. - */ - @Parameterized.Parameters(name = "{0}") - public static Collection params() { - return Arrays.asList(new Object[][]{ - {"raw", false}, - {"nonauth", true} - }); - } - /** Filter to select everything. */ private static final PathFilter EVERYTHING = t -> true; @@ -115,10 +91,6 @@ public class ITestLocatedFileStatusFetcher extends AbstractS3ATestBase { */ private static final int EXPECTED_LIST_COUNT = 4; - private final String name; - - private final boolean s3guard; - private Path basePath; private Path emptyDir; @@ -137,40 +109,13 @@ public class ITestLocatedFileStatusFetcher extends AbstractS3ATestBase { private Configuration listConfig; - public ITestLocatedFileStatusFetcher(final String name, - final boolean s3guard) { - this.name = name; - this.s3guard = s3guard; - } - - @Override - public Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - String bucketName = getTestBucketName(conf); - - removeBaseAndBucketOverrides(bucketName, conf, - METADATASTORE_AUTHORITATIVE, - AUTHORITATIVE_PATH); - removeBucketOverrides(bucketName, conf, - S3_METADATA_STORE_IMPL); - if (!s3guard) { - removeBaseAndBucketOverrides(bucketName, conf, - S3_METADATA_STORE_IMPL); - } - conf.setBoolean(METADATASTORE_AUTHORITATIVE, false); - disableFilesystemCaching(conf); - return conf; - } @Override public void setup() throws Exception { super.setup(); S3AFileSystem fs = getFileSystem(); - // avoiding the parameterization to steer clear of accidentally creating - // patterns; a timestamp is used to ensure tombstones from previous runs - // do not interfere - basePath = path("ITestLocatedFileStatusFetcher-" + name - + "-" + System.currentTimeMillis() / 1000); + + basePath = methodPath(); // define the paths and create them. describe("Creating test directories and files"); @@ -197,7 +142,6 @@ public class ITestLocatedFileStatusFetcher extends AbstractS3ATestBase { listConfig = new Configuration(getConfiguration()); } - /** * Assert that the fetcher stats logs the expected number of calls. * @param fetcher fetcher diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3ABucketExistence.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3ABucketExistence.java index 934ad29ed25..7ed8083eaf6 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3ABucketExistence.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3ABucketExistence.java @@ -34,8 +34,6 @@ import static org.apache.hadoop.fs.contract.ContractTestUtils.dataset; import static org.apache.hadoop.fs.contract.ContractTestUtils.writeDataset; import static org.apache.hadoop.fs.s3a.Constants.FS_S3A; import static org.apache.hadoop.fs.s3a.Constants.S3A_BUCKET_PROBE; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_METASTORE_NULL; -import static org.apache.hadoop.fs.s3a.Constants.S3_METADATA_STORE_IMPL; import static org.apache.hadoop.test.LambdaTestUtils.intercept; /** @@ -55,8 +53,6 @@ public class ITestS3ABucketExistence extends AbstractS3ATestBase { public void testNoBucketProbing() throws Exception { describe("Disable init-time probes and expect FS operations to fail"); Configuration conf = createConfigurationWithProbe(0); - // metastores can bypass S3 checks, so disable S3Guard, always - conf.set(S3_METADATA_STORE_IMPL, S3GUARD_METASTORE_NULL); fs = FileSystem.get(uri, conf); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AConfiguration.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AConfiguration.java index dce99a6144f..696f2c4ced7 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AConfiguration.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AConfiguration.java @@ -527,7 +527,7 @@ public class ITestS3AConfiguration { Assert.assertEquals(s3SignerOverride, clientConfiguration.getSignerOverride()); clientConfiguration = S3AUtils - .createAwsConf(config, "dontcare", AWS_SERVICE_IDENTIFIER_DDB); + .createAwsConf(config, "dontcare", AWS_SERVICE_IDENTIFIER_STS); Assert.assertNull(clientConfiguration.getSignerOverride()); // Configured base SIGNING_ALGORITHM, overridden for S3 only @@ -539,41 +539,9 @@ public class ITestS3AConfiguration { Assert.assertEquals(s3SignerOverride, clientConfiguration.getSignerOverride()); clientConfiguration = S3AUtils - .createAwsConf(config, "dontcare", AWS_SERVICE_IDENTIFIER_DDB); + .createAwsConf(config, "dontcare", AWS_SERVICE_IDENTIFIER_STS); Assert .assertEquals(signerOverride, clientConfiguration.getSignerOverride()); } - @Test(timeout = 10_000L) - public void testDdbSpecificSignerOverride() throws IOException { - ClientConfiguration clientConfiguration = null; - Configuration config; - - String signerOverride = "testSigner"; - String ddbSignerOverride = "testDdbSigner"; - - // Default SIGNING_ALGORITHM, overridden for S3 - config = new Configuration(); - config.set(SIGNING_ALGORITHM_DDB, ddbSignerOverride); - clientConfiguration = S3AUtils - .createAwsConf(config, "dontcare", AWS_SERVICE_IDENTIFIER_DDB); - Assert.assertEquals(ddbSignerOverride, - clientConfiguration.getSignerOverride()); - clientConfiguration = S3AUtils - .createAwsConf(config, "dontcare", AWS_SERVICE_IDENTIFIER_S3); - Assert.assertNull(clientConfiguration.getSignerOverride()); - - // Configured base SIGNING_ALGORITHM, overridden for S3 - config = new Configuration(); - config.set(SIGNING_ALGORITHM, signerOverride); - config.set(SIGNING_ALGORITHM_DDB, ddbSignerOverride); - clientConfiguration = S3AUtils - .createAwsConf(config, "dontcare", AWS_SERVICE_IDENTIFIER_DDB); - Assert.assertEquals(ddbSignerOverride, - clientConfiguration.getSignerOverride()); - clientConfiguration = S3AUtils - .createAwsConf(config, "dontcare", AWS_SERVICE_IDENTIFIER_S3); - Assert - .assertEquals(signerOverride, clientConfiguration.getSignerOverride()); - } } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AContractGetFileStatusV1List.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AContractGetFileStatusV1List.java index 42e905e416d..6d950d9bfaa 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AContractGetFileStatusV1List.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AContractGetFileStatusV1List.java @@ -25,7 +25,6 @@ import org.apache.hadoop.fs.contract.s3a.S3AContract; import static org.apache.hadoop.fs.s3a.Constants.LIST_VERSION; import static org.apache.hadoop.fs.s3a.S3ATestUtils.disableFilesystemCaching; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.maybeEnableS3Guard; /** * S3A contract tests for getFileStatus, using the v1 List Objects API. @@ -50,7 +49,6 @@ public class ITestS3AContractGetFileStatusV1List Configuration conf = super.createConfiguration(); disableFilesystemCaching(conf); conf.setInt(Constants.MAX_PAGING_KEYS, 2); - maybeEnableS3Guard(conf); // Use v1 List Objects API conf.setInt(LIST_VERSION, 1); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3ADelayedFNF.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3ADelayedFNF.java index 7b9e79e4753..ca9d185c3e9 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3ADelayedFNF.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3ADelayedFNF.java @@ -33,7 +33,6 @@ import java.io.FileNotFoundException; import static org.apache.hadoop.fs.s3a.Constants.CHANGE_DETECT_MODE; import static org.apache.hadoop.fs.s3a.Constants.CHANGE_DETECT_SOURCE; -import static org.apache.hadoop.fs.s3a.Constants.METADATASTORE_AUTHORITATIVE; import static org.apache.hadoop.fs.s3a.Constants.RETRY_INTERVAL; import static org.apache.hadoop.fs.s3a.Constants.RETRY_LIMIT; import static org.apache.hadoop.fs.s3a.S3ATestUtils.removeBaseAndBucketOverrides; @@ -53,8 +52,7 @@ public class ITestS3ADelayedFNF extends AbstractS3ATestBase { CHANGE_DETECT_SOURCE, CHANGE_DETECT_MODE, RETRY_LIMIT, - RETRY_INTERVAL, - METADATASTORE_AUTHORITATIVE); + RETRY_INTERVAL); conf.setInt(RETRY_LIMIT, 2); conf.set(RETRY_INTERVAL, "1ms"); return conf; diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AEncryptionSSEC.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AEncryptionSSEC.java index ff46e981eab..64e37bf832b 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AEncryptionSSEC.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AEncryptionSSEC.java @@ -41,19 +41,21 @@ import static org.apache.hadoop.fs.s3a.Constants.DIRECTORY_MARKER_POLICY; import static org.apache.hadoop.fs.s3a.Constants.DIRECTORY_MARKER_POLICY_DELETE; import static org.apache.hadoop.fs.s3a.Constants.DIRECTORY_MARKER_POLICY_KEEP; import static org.apache.hadoop.fs.s3a.Constants.ETAG_CHECKSUM_ENABLED; -import static org.apache.hadoop.fs.s3a.Constants.S3_METADATA_STORE_IMPL; import static org.apache.hadoop.fs.s3a.Constants.S3_ENCRYPTION_ALGORITHM; import static org.apache.hadoop.fs.s3a.Constants.S3_ENCRYPTION_KEY; import static org.apache.hadoop.fs.s3a.Constants.SERVER_SIDE_ENCRYPTION_ALGORITHM; import static org.apache.hadoop.fs.s3a.Constants.SERVER_SIDE_ENCRYPTION_KEY; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.*; +import static org.apache.hadoop.fs.s3a.S3ATestUtils.createTestPath; +import static org.apache.hadoop.fs.s3a.S3ATestUtils.disableFilesystemCaching; +import static org.apache.hadoop.fs.s3a.S3ATestUtils.getTestBucketName; +import static org.apache.hadoop.fs.s3a.S3ATestUtils.removeBaseAndBucketOverrides; import static org.apache.hadoop.test.LambdaTestUtils.intercept; /** * Concrete class that extends {@link AbstractTestS3AEncryption} * and tests SSE-C encryption. * HEAD requests against SSE-C-encrypted data will fail if the wrong key - * is presented, so the tests are very brittle to S3Guard being on vs. off. + * is presented -this used to cause problems with S3Guard. * Equally "vexing" has been the optimizations of getFileStatus(), wherein * LIST comes before HEAD path + / */ @@ -78,18 +80,11 @@ public class ITestS3AEncryptionSSEC extends AbstractTestS3AEncryption { @Parameterized.Parameters(name = "{0}") public static Collection params() { return Arrays.asList(new Object[][]{ - {"raw-keep-markers", false, true}, - {"raw-delete-markers", false, false}, - {"guarded-keep-markers", true, true}, - {"guarded-delete-markers", true, false} + {"keep-markers", true}, + {"delete-markers", false} }); } - /** - * Parameter: should the stores be guarded? - */ - private final boolean s3guard; - /** * Parameter: should directory markers be retained? */ @@ -101,9 +96,7 @@ public class ITestS3AEncryptionSSEC extends AbstractTestS3AEncryption { private S3AFileSystem fsKeyB; public ITestS3AEncryptionSSEC(final String name, - final boolean s3guard, final boolean keepMarkers) { - this.s3guard = s3guard; this.keepMarkers = keepMarkers; } @@ -113,13 +106,6 @@ public class ITestS3AEncryptionSSEC extends AbstractTestS3AEncryption { Configuration conf = super.createConfiguration(); disableFilesystemCaching(conf); String bucketName = getTestBucketName(conf); - removeBucketOverrides(bucketName, conf, - S3_METADATA_STORE_IMPL); - if (!s3guard) { - // in a raw run remove all s3guard settings - removeBaseAndBucketOverrides(bucketName, conf, - S3_METADATA_STORE_IMPL); - } // directory marker options removeBaseAndBucketOverrides(bucketName, conf, DIRECTORY_MARKER_POLICY, @@ -335,7 +321,7 @@ public class ITestS3AEncryptionSSEC extends AbstractTestS3AEncryption { * @return true if check for a path being a file will issue a HEAD request. */ private boolean statusProbesCheckS3(S3AFileSystem fs, Path path) { - return !fs.hasMetadataStore() || !fs.allowAuthoritative(path); + return true; } /** @@ -370,9 +356,7 @@ public class ITestS3AEncryptionSSEC extends AbstractTestS3AEncryption { } /** - * getFileChecksum always goes to S3, so when - * the caller lacks permissions, it fails irrespective - * of guard. + * getFileChecksum always goes to S3. */ @Test public void testChecksumRequiresReadAccess() throws Throwable { diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AFailureHandling.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AFailureHandling.java index 71a57943711..c34ba22b105 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AFailureHandling.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AFailureHandling.java @@ -19,16 +19,11 @@ package org.apache.hadoop.fs.s3a; import com.amazonaws.services.s3.model.DeleteObjectsRequest; -import com.amazonaws.services.s3.model.MultiObjectDeleteException; -import org.apache.hadoop.thirdparty.com.google.common.collect.Lists; -import org.assertj.core.api.Assertions; + import org.junit.Assume; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.hadoop.fs.s3a.impl.MultiObjectDeleteSupport; import org.apache.hadoop.fs.statistics.StoreStatisticNames; import org.apache.hadoop.fs.store.audit.AuditSpan; @@ -44,7 +39,6 @@ import java.nio.file.AccessDeniedException; import static org.apache.hadoop.fs.contract.ContractTestUtils.*; import static org.apache.hadoop.fs.s3a.test.ExtraAssertions.failIf; import static org.apache.hadoop.fs.s3a.impl.MultiObjectDeleteSupport.*; -import static org.apache.hadoop.fs.s3a.impl.TestPartialDeleteFailures.keysToDelete; import static org.apache.hadoop.test.LambdaTestUtils.*; /** @@ -81,7 +75,7 @@ public class ITestS3AFailureHandling extends AbstractS3ATestBase { private void removeKeys(S3AFileSystem fileSystem, String... keys) throws IOException { try (AuditSpan span = span()) { - fileSystem.removeKeys(buildDeleteRequest(keys), false, null); + fileSystem.removeKeys(buildDeleteRequest(keys), false); } } @@ -132,51 +126,6 @@ public class ITestS3AFailureHandling extends AbstractS3ATestBase { fs.pathToKey(csvPath), "missing-key.csv" }); - MultiObjectDeleteException ex = intercept( - MultiObjectDeleteException.class, - () -> fs.removeKeys(keys, false, null)); - - final List undeleted - = extractUndeletedPaths(ex, fs::keyToQualifiedPath); - String undeletedFiles = join(undeleted); - failIf(undeleted.size() != 2, - "undeleted list size wrong: " + undeletedFiles, - ex); - assertTrue("no CSV in " +undeletedFiles, undeleted.contains(csvPath)); - - // and a full split, after adding a new key - String marker = "/marker"; - Path markerPath = fs.keyToQualifiedPath(marker); - keys.add(new DeleteObjectsRequest.KeyVersion(marker)); - - Pair, List> pair = - new MultiObjectDeleteSupport(fs.createStoreContext(), null) - .splitUndeletedKeys(ex, keys); - assertEquals(undeleted, toPathList(pair.getLeft())); - List right = pair.getRight(); - Assertions.assertThat(right) - .hasSize(1); - assertEquals(markerPath, right.get(0).getPath()); - } - - /** - * See what happens when you delete two entries which do not exist. - * It must not raise an exception. - */ - @Test - public void testMultiObjectDeleteMissingEntriesSucceeds() throws Throwable { - describe("Delete keys which don't exist"); - Path base = path("missing"); - S3AFileSystem fs = getFileSystem(); - List keys = keysToDelete( - Lists.newArrayList(new Path(base, "1"), new Path(base, "2"))); - try (AuditSpan span = span()) { - fs.removeKeys(keys, false, null); - } - } - - private String join(final Iterable iterable) { - return "[" + StringUtils.join(iterable, ",") + "]"; } /** diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AFileOperationCost.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AFileOperationCost.java index ca8e49cc33b..27c70b2b214 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AFileOperationCost.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AFileOperationCost.java @@ -25,7 +25,6 @@ import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.s3a.impl.StatusProbeEnum; import org.apache.hadoop.fs.s3a.performance.AbstractS3ACostTest; - import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -43,14 +42,13 @@ import java.util.EnumSet; import static org.apache.hadoop.fs.contract.ContractTestUtils.*; import static org.apache.hadoop.fs.s3a.Statistic.*; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.*; import static org.apache.hadoop.fs.s3a.performance.OperationCost.*; import static org.apache.hadoop.test.GenericTestUtils.getTestDir; import static org.apache.hadoop.test.LambdaTestUtils.intercept; /** * Use metrics to assert about the cost of file API calls. - * Parameterized on guarded vs raw. and directory marker keep vs delete + * Parameterized on directory marker keep vs delete. */ @RunWith(Parameterized.class) public class ITestS3AFileOperationCost extends AbstractS3ACostTest { @@ -64,25 +62,19 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { @Parameterized.Parameters(name = "{0}") public static Collection params() { return Arrays.asList(new Object[][]{ - {"raw-keep-markers", false, true, false}, - {"raw-delete-markers", false, false, false}, - {"nonauth-keep-markers", true, true, false}, - {"auth-delete-markers", true, false, true} + {"keep-markers", true}, + {"delete-markers", false}, }); } - public ITestS3AFileOperationCost(final String name, - final boolean s3guard, - final boolean keepMarkers, - final boolean authoritative) { - super(s3guard, keepMarkers, authoritative); + public ITestS3AFileOperationCost( + final String name, + final boolean keepMarkers) { + super(keepMarkers); } /** * Test the cost of {@code listLocatedStatus(file)}. - * There's a minor inefficiency in that calling this on - * a file in S3Guard still executes a LIST call, even - * though the file record is in the store. */ @Test public void testCostOfLocatedFileStatusOnFile() throws Throwable { @@ -90,11 +82,8 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { Path file = file(methodPath()); S3AFileSystem fs = getFileSystem(); verifyMetrics(() -> fs.listLocatedStatus(file), - whenRaw(FILE_STATUS_FILE_PROBE - .plus(LIST_LOCATED_STATUS_LIST_OP)), - whenAuthoritative(LIST_LOCATED_STATUS_LIST_OP), - whenNonauth(LIST_LOCATED_STATUS_LIST_OP - .plus(S3GUARD_NONAUTH_FILE_STATUS_PROBE))); + always(FILE_STATUS_FILE_PROBE + .plus(LIST_LOCATED_STATUS_LIST_OP))); } @Test @@ -104,10 +93,8 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { S3AFileSystem fs = getFileSystem(); verifyMetrics(() -> fs.listLocatedStatus(dir), - whenRaw(LIST_LOCATED_STATUS_LIST_OP - .plus(GET_FILE_STATUS_ON_EMPTY_DIR)), - whenAuthoritative(NO_IO), - whenNonauth(LIST_LOCATED_STATUS_LIST_OP)); + always(LIST_LOCATED_STATUS_LIST_OP + .plus(GET_FILE_STATUS_ON_EMPTY_DIR))); } @Test @@ -118,9 +105,7 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { Path file = file(new Path(dir, "file.txt")); verifyMetrics(() -> fs.listLocatedStatus(dir), - whenRaw(LIST_LOCATED_STATUS_LIST_OP), - whenAuthoritative(NO_IO), - whenNonauth(LIST_LOCATED_STATUS_LIST_OP)); + always(LIST_LOCATED_STATUS_LIST_OP)); } @Test @@ -131,10 +116,8 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { touch(fs, file); verifyMetrics(() -> fs.listFiles(file, true), - whenRaw(LIST_LOCATED_STATUS_LIST_OP - .plus(GET_FILE_STATUS_ON_FILE)), - whenAuthoritative(NO_IO), - whenNonauth(LIST_LOCATED_STATUS_LIST_OP)); + always(LIST_LOCATED_STATUS_LIST_OP + .plus(GET_FILE_STATUS_ON_FILE))); } @Test @@ -146,10 +129,8 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { fs.mkdirs(dir); verifyMetrics(() -> fs.listFiles(dir, true), - whenRaw(LIST_FILES_LIST_OP - .plus(GET_FILE_STATUS_ON_EMPTY_DIR)), - whenAuthoritative(NO_IO), - whenNonauth(LIST_FILES_LIST_OP)); + always(LIST_FILES_LIST_OP + .plus(GET_FILE_STATUS_ON_EMPTY_DIR))); } @Test @@ -162,9 +143,7 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { touch(fs, file); verifyMetrics(() -> fs.listFiles(dir, true), - whenRaw(LIST_FILES_LIST_OP), - whenAuthoritative(NO_IO), - whenNonauth(LIST_FILES_LIST_OP)); + always(LIST_FILES_LIST_OP)); } @Test @@ -174,7 +153,7 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { S3AFileSystem fs = getFileSystem(); verifyMetricsIntercepting(FileNotFoundException.class, "", () -> fs.listFiles(dir, true), - whenRaw(LIST_FILES_LIST_OP + always(LIST_FILES_LIST_OP .plus(GET_FILE_STATUS_FNFE))); } @@ -186,11 +165,8 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { touch(fs, file); verifyMetrics(() -> fs.listStatus(file), - whenRaw(LIST_STATUS_LIST_OP - .plus(GET_FILE_STATUS_ON_FILE)), - whenAuthoritative(LIST_STATUS_LIST_OP), - whenNonauth(LIST_STATUS_LIST_OP - .plus(S3GUARD_NONAUTH_FILE_STATUS_PROBE))); + always(LIST_STATUS_LIST_OP + .plus(GET_FILE_STATUS_ON_FILE))); } @Test @@ -201,10 +177,8 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { fs.mkdirs(dir); verifyMetrics(() -> fs.listStatus(dir), - whenRaw(LIST_STATUS_LIST_OP - .plus(GET_FILE_STATUS_ON_EMPTY_DIR)), - whenAuthoritative(NO_IO), - whenNonauth(LIST_STATUS_LIST_OP)); + always(LIST_STATUS_LIST_OP + .plus(GET_FILE_STATUS_ON_EMPTY_DIR))); } @Test @@ -217,16 +191,14 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { touch(fs, file); verifyMetrics(() -> fs.listStatus(dir), - whenRaw(LIST_STATUS_LIST_OP), - whenAuthoritative(NO_IO), - whenNonauth(LIST_STATUS_LIST_OP)); + always(LIST_STATUS_LIST_OP)); } @Test public void testCostOfGetFileStatusOnFile() throws Throwable { describe("performing getFileStatus on a file"); Path simpleFile = file(methodPath()); - S3AFileStatus status = verifyRawInnerGetFileStatus(simpleFile, true, + S3AFileStatus status = verifyInnerGetFileStatus(simpleFile, true, StatusProbeEnum.ALL, GET_FILE_STATUS_ON_FILE); assertTrue("not a file: " + status, status.isFile()); @@ -236,13 +208,13 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { public void testCostOfGetFileStatusOnEmptyDir() throws Throwable { describe("performing getFileStatus on an empty directory"); Path dir = dir(methodPath()); - S3AFileStatus status = verifyRawInnerGetFileStatus(dir, true, + S3AFileStatus status = verifyInnerGetFileStatus(dir, true, StatusProbeEnum.ALL, GET_FILE_STATUS_ON_DIR_MARKER); assertSame("not empty: " + status, Tristate.TRUE, status.isEmptyDirectory()); // but now only ask for the directories and the file check is skipped. - verifyRawInnerGetFileStatus(dir, false, + verifyInnerGetFileStatus(dir, false, StatusProbeEnum.DIRECTORIES, FILE_STATUS_DIR_PROBE); @@ -254,7 +226,7 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { @Test public void testCostOfGetFileStatusOnMissingFile() throws Throwable { describe("performing getFileStatus on a missing file"); - interceptRawGetFileStatusFNFE(methodPath(), false, + interceptGetFileStatusFNFE(methodPath(), false, StatusProbeEnum.ALL, GET_FILE_STATUS_FNFE); } @@ -262,7 +234,7 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { @Test public void testCostOfRootFileStatus() throws Throwable { Path root = path("/"); - S3AFileStatus rootStatus = verifyRawInnerGetFileStatus( + S3AFileStatus rootStatus = verifyInnerGetFileStatus( root, false, StatusProbeEnum.ALL, @@ -275,7 +247,7 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { Assertions.assertThat(rootStatus.isEmptyDirectory()) .isEqualTo(Tristate.UNKNOWN); - rootStatus = verifyRawInnerGetFileStatus( + rootStatus = verifyInnerGetFileStatus( root, true, StatusProbeEnum.ALL, @@ -305,7 +277,7 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { describe("performing getFileStatus on a non-empty directory"); Path dir = dir(methodPath()); file(new Path(dir, "simple.txt")); - S3AFileStatus status = verifyRawInnerGetFileStatus(dir, true, + S3AFileStatus status = verifyInnerGetFileStatus(dir, true, StatusProbeEnum.ALL, GET_FILE_STATUS_ON_DIR); assertEmptyDirStatus(status, Tristate.FALSE); @@ -349,24 +321,23 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { @Test public void testDirProbes() throws Throwable { describe("Test directory probe cost"); - assumeUnguarded(); S3AFileSystem fs = getFileSystem(); // Create the empty directory. Path emptydir = dir(methodPath()); // head probe fails - interceptRawGetFileStatusFNFE(emptydir, false, + interceptGetFileStatusFNFE(emptydir, false, StatusProbeEnum.HEAD_ONLY, FILE_STATUS_FILE_PROBE); // a LIST will find it and declare as empty - S3AFileStatus status = verifyRawInnerGetFileStatus(emptydir, true, + S3AFileStatus status = verifyInnerGetFileStatus(emptydir, true, StatusProbeEnum.LIST_ONLY, FILE_STATUS_DIR_PROBE); assertEmptyDirStatus(status, Tristate.TRUE); // skip all probes and expect no operations to take place - interceptRawGetFileStatusFNFE(emptydir, false, + interceptGetFileStatusFNFE(emptydir, false, EnumSet.noneOf(StatusProbeEnum.class), NO_IO); @@ -375,17 +346,16 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { String emptyDirTrailingSlash = fs.pathToKey(emptydir.getParent()) + "/" + emptydir.getName() + "/"; // A HEAD request does not probe for keys with a trailing / - interceptRaw(FileNotFoundException.class, "", + interceptOperation(FileNotFoundException.class, "", NO_IO, () -> fs.s3GetFileStatus(emptydir, emptyDirTrailingSlash, - StatusProbeEnum.HEAD_ONLY, null, false)); + StatusProbeEnum.HEAD_ONLY, false)); // but ask for a directory marker and you get the entry - status = verifyRaw(FILE_STATUS_DIR_PROBE, () -> + status = verify(FILE_STATUS_DIR_PROBE, () -> fs.s3GetFileStatus(emptydir, emptyDirTrailingSlash, StatusProbeEnum.LIST_ONLY, - null, true)); assertEquals(emptydir, status.getPath()); assertEmptyDirStatus(status, Tristate.TRUE); @@ -397,12 +367,11 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { intercept(IllegalArgumentException.class, "", () -> fs.s3GetFileStatus(new Path("/something"), "/something", - StatusProbeEnum.HEAD_ONLY, null, true)); + StatusProbeEnum.HEAD_ONLY, true)); } @Test public void testCreateCost() throws Throwable { - describe("Test file creation cost -raw only"); - assumeUnguarded(); + describe("Test file creation cost"); Path testFile = methodPath(); // when overwrite is false, the path is checked for existence. create(testFile, false, @@ -414,12 +383,11 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { @Test public void testCreateCostFileExists() throws Throwable { describe("Test cost of create file failing with existing file"); - assumeUnguarded(); Path testFile = file(methodPath()); // now there is a file there, an attempt with overwrite == false will // fail on the first HEAD. - interceptRaw(FileAlreadyExistsException.class, "", + interceptOperation(FileAlreadyExistsException.class, "", FILE_STATUS_FILE_PROBE, () -> file(testFile, false)); } @@ -427,12 +395,11 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { @Test public void testCreateCostDirExists() throws Throwable { describe("Test cost of create file failing with existing dir"); - assumeUnguarded(); Path testFile = dir(methodPath()); // now there is a file there, an attempt with overwrite == false will // fail on the first HEAD. - interceptRaw(FileAlreadyExistsException.class, "", + interceptOperation(FileAlreadyExistsException.class, "", GET_FILE_STATUS_ON_DIR_MARKER, () -> file(testFile, false)); } @@ -443,8 +410,7 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { */ @Test public void testCreateBuilder() throws Throwable { - describe("Test builder file creation cost -raw only"); - assumeUnguarded(); + describe("Test builder file creation cost"); Path testFile = methodPath(); dir(testFile.getParent()); @@ -459,7 +425,7 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { // now there is a file there, an attempt with overwrite == false will // fail on the first HEAD. - interceptRaw(FileAlreadyExistsException.class, "", + interceptOperation(FileAlreadyExistsException.class, "", GET_FILE_STATUS_ON_FILE, () -> buildFile(testFile, false, true, GET_FILE_STATUS_ON_FILE)); @@ -469,8 +435,6 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { public void testCostOfGlobStatus() throws Throwable { describe("Test globStatus has expected cost"); S3AFileSystem fs = getFileSystem(); - assume("Unguarded FS only", !fs.hasMetadataStore()); - Path basePath = path("testCostOfGlobStatus/nextFolder/"); // create a bunch of files @@ -482,7 +446,7 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { fs.globStatus(basePath.suffix("/*")); // 2 head + 1 list from getFileStatus on path, // plus 1 list to match the glob pattern - verifyRaw(LIST_STATUS_LIST_OP, + verify(LIST_STATUS_LIST_OP, () -> fs.globStatus(basePath.suffix("/*"))); } @@ -490,8 +454,6 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { public void testCostOfGlobStatusNoSymlinkResolution() throws Throwable { describe("Test globStatus does not attempt to resolve symlinks"); S3AFileSystem fs = getFileSystem(); - assume("Unguarded FS only", !fs.hasMetadataStore()); - Path basePath = path("testCostOfGlobStatusNoSymlinkResolution/f/"); // create a single file, globStatus returning a single file on a pattern @@ -501,7 +463,7 @@ public class ITestS3AFileOperationCost extends AbstractS3ACostTest { // unguarded: 2 head + 1 list from getFileStatus on path, // plus 1 list to match the glob pattern // no additional operations from symlink resolution - verifyRaw(LIST_STATUS_LIST_OP, + verify(LIST_STATUS_LIST_OP, () -> fs.globStatus(basePath.suffix("/*"))); } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AInconsistency.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AInconsistency.java deleted file mode 100644 index a49998d50d5..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AInconsistency.java +++ /dev/null @@ -1,238 +0,0 @@ -/* - * 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.hadoop.fs.s3a; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.contract.ContractTestUtils; -import org.apache.hadoop.fs.s3a.impl.ChangeDetectionPolicy; -import org.apache.hadoop.fs.s3a.impl.ChangeDetectionPolicy.Source; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.NullMetadataStore; -import org.apache.hadoop.test.LambdaTestUtils; - -import org.junit.Assume; -import org.junit.Before; -import org.junit.Test; - -import java.io.FileNotFoundException; -import java.io.InputStream; - -import static org.apache.hadoop.fs.contract.ContractTestUtils.touch; -import static org.apache.hadoop.fs.contract.ContractTestUtils.writeTextFile; -import static org.apache.hadoop.fs.s3a.Constants.*; -import static org.apache.hadoop.fs.s3a.FailureInjectionPolicy.*; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.removeBaseAndBucketOverrides; -import static org.apache.hadoop.test.LambdaTestUtils.eventually; -import static org.apache.hadoop.test.LambdaTestUtils.intercept; - -/** - * Tests S3A behavior under forced inconsistency via {@link - * InconsistentAmazonS3Client}. - * - * These tests are for validating expected behavior *without* S3Guard, but - * may also run with S3Guard enabled. For tests that validate S3Guard's - * consistency features, see {@link ITestS3GuardListConsistency}. - */ -public class ITestS3AInconsistency extends AbstractS3ATestBase { - - private static final int OPEN_READ_ITERATIONS = 10; - - public static final int INCONSISTENCY_MSEC = 800; - - private static final int INITIAL_RETRY = 128; - - private static final int RETRIES = 4; - - /** By using a power of 2 for the initial time, the total is a shift left. */ - private static final int TOTAL_RETRY_DELAY = INITIAL_RETRY << RETRIES; - - /** - * S3 Client side encryption when enabled should skip this test. - */ - @Before - public void setUp() { - skipIfClientSideEncryption(); - } - - @Override - protected Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - // reduce retry limit so FileNotFoundException cases timeout faster, - // speeding up the tests - removeBaseAndBucketOverrides(conf, - CHANGE_DETECT_SOURCE, - CHANGE_DETECT_MODE, - RETRY_LIMIT, - RETRY_INTERVAL, - METADATASTORE_AUTHORITATIVE, - S3_CLIENT_FACTORY_IMPL); - conf.setClass(S3_CLIENT_FACTORY_IMPL, InconsistentS3ClientFactory.class, - S3ClientFactory.class); - conf.set(FAIL_INJECT_INCONSISTENCY_KEY, DEFAULT_DELAY_KEY_SUBSTRING); - // the reads are always inconsistent - conf.setFloat(FAIL_INJECT_INCONSISTENCY_PROBABILITY, 1.0f); - // but the inconsistent time is less than exponential growth of the - // retry interval (128 -> 256 -> 512 -> 1024 - conf.setLong(FAIL_INJECT_INCONSISTENCY_MSEC, INCONSISTENCY_MSEC); - conf.setInt(RETRY_LIMIT, RETRIES); - conf.set(RETRY_INTERVAL, String.format("%dms", INITIAL_RETRY)); - return conf; - } - - @Test - public void testGetFileStatus() throws Exception { - S3AFileSystem fs = getFileSystem(); - - // 1. Make sure no ancestor dirs exist - Path dir = path("ancestor"); - fs.delete(dir, true); - waitUntilDeleted(dir); - - // 2. Create a descendant file, which implicitly creates ancestors - // This file has delayed visibility. - touch(getFileSystem(), - path("ancestor/file-" + DEFAULT_DELAY_KEY_SUBSTRING)); - - // 3. Assert expected behavior. If S3Guard is enabled, we should be able - // to get status for ancestor. If S3Guard is *not* enabled, S3A will - // fail to infer the existence of the ancestor since visibility of the - // child file is delayed, and its key prefix search will return nothing. - try { - FileStatus status = fs.getFileStatus(dir); - if (fs.hasMetadataStore()) { - assertTrue("Ancestor is dir", status.isDirectory()); - } else { - fail("getFileStatus should fail due to delayed visibility."); - } - } catch (FileNotFoundException e) { - if (fs.hasMetadataStore()) { - fail("S3Guard failed to list parent of inconsistent child."); - } - LOG.info("File not found, as expected."); - } - } - - - /** - * Ensure that deleting a file with an open read stream does eventually cause - * readers to get a FNFE, even with S3Guard and its retries enabled. - * In real usage, S3Guard should be enabled for all clients that modify the - * file, so the delete would be immediately recorded in the MetadataStore. - * Here, however, we test deletion from under S3Guard to make sure it still - * eventually propagates the FNFE after any retry policies are exhausted. - */ - @Test - public void testOpenDeleteRead() throws Exception { - S3AFileSystem fs = getFileSystem(); - ChangeDetectionPolicy changeDetectionPolicy = - fs.getChangeDetectionPolicy(); - Assume.assumeFalse("FNF not expected when using a bucket with" - + " object versioning", - changeDetectionPolicy.getSource() == Source.VersionId); - - Path p = path("testOpenDeleteRead.txt"); - writeTextFile(fs, p, "1337c0d3z", true); - try (InputStream s = fs.open(p)) { - // Disable s3guard, delete file underneath it, re-enable s3guard - MetadataStore metadataStore = fs.getMetadataStore(); - fs.setMetadataStore(new NullMetadataStore()); - fs.delete(p, false); - fs.setMetadataStore(metadataStore); - eventually(TOTAL_RETRY_DELAY * 2, INITIAL_RETRY * 2, () -> { - intercept(FileNotFoundException.class, () -> s.read()); - }); - } - } - - /** - * Test read() path behavior when getFileStatus() succeeds but subsequent - * read() on the input stream fails due to eventual consistency. - * There are many points in the InputStream codepaths that can fail. We set - * a probability of failure and repeat the test multiple times to achieve - * decent coverage. - */ - @Test - public void testOpenFailOnRead() throws Exception { - - S3AFileSystem fs = getFileSystem(); - - // 1. Patch in a different failure injection policy with <1.0 probability - Configuration conf = fs.getConf(); - conf.setFloat(FAIL_INJECT_INCONSISTENCY_PROBABILITY, 0.5f); - InconsistentAmazonS3Client.setFailureInjectionPolicy(fs, - new FailureInjectionPolicy(conf)); - - // 2. Make sure no ancestor dirs exist - Path dir = path("ancestor"); - fs.delete(dir, true); - waitUntilDeleted(dir); - - // 3. Create a descendant file, which implicitly creates ancestors - // This file has delayed visibility. - describe("creating test file"); - Path path = path("ancestor/file-to-read-" + DEFAULT_DELAY_KEY_SUBSTRING); - writeTextFile(getFileSystem(), path, "Reading is fun", false); - - // 4. Clear inconsistency so the first getFileStatus() can succeed, if we - // are not using S3Guard. If we are using S3Guard, it should tolerate the - // delayed visibility. - if (!fs.hasMetadataStore()) { - InconsistentAmazonS3Client.clearInconsistency(fs); - } - - // ? Do we need multiple iterations when S3Guard is disabled? For now, - // leaving it in - for (int i = 0; i < OPEN_READ_ITERATIONS; i++) { - doOpenFailOnReadTest(fs, path, i); - } - } - - private void doOpenFailOnReadTest(S3AFileSystem fs, Path path, int iteration) - throws Exception { - - // 4. Open the file - describe(String.format("i=%d: opening test file", iteration)); - try(InputStream in = fs.open(path)) { - // 5. Assert expected behavior on read() failure. - int l = 4; - byte[] buf = new byte[l]; - describe("reading test file"); - // Use both read() variants - if ((iteration % 2) == 0) { - assertEquals(l, in.read(buf, 0, l)); - } else { - in.read(); - } - } catch (FileNotFoundException e) { - if (fs.hasMetadataStore()) { - LOG.error("Error:", e); - ContractTestUtils.fail("S3Guard failed to handle fail-on-read", e); - } else { - LOG.info("File not found on read(), as expected."); - } - } - } - - private void waitUntilDeleted(final Path p) throws Exception { - LambdaTestUtils.eventually(30 * 1000, 1000, - () -> assertPathDoesNotExist("Dir should be deleted", p)); - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AMetadataPersistenceException.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AMetadataPersistenceException.java deleted file mode 100644 index 7a980a39eff..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3AMetadataPersistenceException.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * 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.hadoop.fs.s3a; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; - -import org.apache.hadoop.fs.s3a.s3guard.S3Guard; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FSDataOutputStream; -import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3ATestUtils.MetricDiff; -import org.apache.hadoop.fs.s3a.s3guard.BulkOperationState; -import org.apache.hadoop.fs.s3a.s3guard.LocalMetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.NullMetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.PathMetadata; -import org.apache.hadoop.io.IOUtils; - -import static org.apache.hadoop.test.LambdaTestUtils.intercept; - -/** - * Tests failed writes to metadata store generate the expected - * MetadataPersistenceException. - */ -@RunWith(Parameterized.class) -public class ITestS3AMetadataPersistenceException extends AbstractS3ATestBase { - private static final Logger LOG = - LoggerFactory.getLogger(ITestS3AMetadataPersistenceException.class); - - private S3AFileSystem fs; - private IOException ioException; - private final boolean failOnError; - - public ITestS3AMetadataPersistenceException(boolean failOnError) { - this.failOnError = failOnError; - } - - @Parameterized.Parameters - public static Collection params() { - return Arrays.asList(new Object[][]{ - {true}, - {false} - }); - } - - @Override - protected Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - conf.set(Constants.FAIL_ON_METADATA_WRITE_ERROR, - Boolean.toString(failOnError)); - // replaced in setup() by IOExceptionMetadataStore - conf.setClass(Constants.S3_METADATA_STORE_IMPL, - NullMetadataStore.class, - MetadataStore.class); - return conf; - } - - @Override - public void setup() throws Exception { - super.setup(); - S3AFileSystem contractFs = getFileSystem(); - fs = (S3AFileSystem) FileSystem.newInstance( - contractFs.getUri(), contractFs.getConf()); - ioException = new IOException(); - IOExceptionMetadataStore metadataStore = - new IOExceptionMetadataStore(ioException); - metadataStore.initialize(getConfiguration(), - new S3Guard.TtlTimeProvider(getConfiguration())); - fs.setMetadataStore(metadataStore); - } - - @Override - public void teardown() throws Exception { - IOUtils.cleanupWithLogger(LOG, fs); - super.teardown(); - } - - @Test - public void testFailedMetadataUpdate() throws Throwable { - // write a trivial file - Path testFile = path("testFailedMetadataUpdate"); - try { - FSDataOutputStream outputStream = fs.create(testFile); - outputStream.write(1); - - if (failOnError) { - // close should throw the expected exception - MetadataPersistenceException thrown = - intercept( - MetadataPersistenceException.class, - outputStream::close); - assertEquals("cause didn't match original exception", - ioException, thrown.getCause()); - } else { - MetricDiff ignoredCount = new MetricDiff(fs, Statistic.IGNORED_ERRORS); - - // close should merely log and increment the statistic - outputStream.close(); - ignoredCount.assertDiffEquals("ignored errors", 1); - } - } finally { - // turn off the store and forcibly delete from the raw bucket. - fs.setMetadataStore(new NullMetadataStore()); - fs.delete(testFile, false); - } - } - - private static class IOExceptionMetadataStore extends LocalMetadataStore { - private final IOException ioException; - - private IOExceptionMetadataStore(IOException ioException) { - this.ioException = ioException; - } - - @Override - public void put(PathMetadata meta, - final BulkOperationState operationState) throws IOException { - throw ioException; - } - - @Override - public void put(final PathMetadata meta) throws IOException { - put(meta, null); - } - - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3ARemoteFileChanged.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3ARemoteFileChanged.java deleted file mode 100644 index 770a99599f5..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3ARemoteFileChanged.java +++ /dev/null @@ -1,1549 +0,0 @@ -/* - * 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.hadoop.fs.s3a; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Collection; -import java.util.Optional; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.model.CopyObjectRequest; -import com.amazonaws.services.s3.model.CopyObjectResult; -import com.amazonaws.services.s3.model.GetObjectMetadataRequest; -import com.amazonaws.services.s3.model.GetObjectRequest; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.S3Object; -import org.apache.hadoop.thirdparty.com.google.common.base.Charsets; -import org.junit.Assume; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.mockito.ArgumentMatchers; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FSDataInputStream; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.impl.ChangeDetectionPolicy.Mode; -import org.apache.hadoop.fs.s3a.impl.ChangeDetectionPolicy.Source; -import org.apache.hadoop.fs.s3a.s3guard.LocalMetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.NullMetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.PathMetadata; - -import static org.apache.hadoop.fs.contract.ContractTestUtils.dataset; -import static org.apache.hadoop.fs.contract.ContractTestUtils.readUTF8; -import static org.apache.hadoop.fs.contract.ContractTestUtils.writeDataset; -import static org.apache.hadoop.fs.s3a.Constants.*; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.removeBaseAndBucketOverrides; -import static org.apache.hadoop.fs.s3a.impl.ChangeDetectionPolicy.CHANGE_DETECTED; -import static org.apache.hadoop.fs.s3a.select.SelectConstants.S3_SELECT_CAPABILITY; -import static org.apache.hadoop.fs.s3a.select.SelectConstants.SELECT_SQL; -import static org.apache.hadoop.test.LambdaTestUtils.eventually; -import static org.apache.hadoop.test.LambdaTestUtils.intercept; -import static org.apache.hadoop.test.LambdaTestUtils.interceptFuture; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.when; - -/** - * Test S3A remote file change detection. - * This is a very parameterized test; the first three parameters - * define configuration options for the tests, while the final one - * declares the expected outcomes given those options. - * - * This test uses mocking to insert transient failures into the S3 client, - * underneath the S3A Filesystem instance. - * - * This is used to simulate eventual consistency, so force the change policy - * failure modes to be encountered. - * - * If changes are made to the filesystem such that the number of calls to - * operations such as {@link S3AFileSystem#getObjectMetadata(Path)} are - * changed, the number of failures which the mock layer must generate may - * change. - * - * As the S3Guard auth mode flag does control whether or not a HEAD is issued - * in a call to {@code getFileStatus()}; the test parameter {@link #authMode} - * is used to help predict this count. - * - * Important: if you are seeing failures in this test after changing - * one of the rename/copy/open operations, it may be that an increase, - * decrease or change in the number of low-level S3 HEAD/GET operations is - * triggering the failures. - * Please review the changes to see that you haven't unintentionally done this. - * If it is intentional, please update the parameters here. - * - * If you are seeing failures without such a change, and nobody else is, - * it is likely that you have a different bucket configuration option which - * is somehow triggering a regression. If you can work out which option - * this is, then extend {@link #createConfiguration()} to reset that parameter - * too. - * - * Note: to help debug these issues, set the log for this to DEBUG: - *
    - *   log4j.logger.org.apache.hadoop.fs.s3a.ITestS3ARemoteFileChanged=DEBUG
    - * 
    - * The debug information printed will include a trace of where operations - * are being called from, to help understand why the test is failing. - */ -@RunWith(Parameterized.class) -public class ITestS3ARemoteFileChanged extends AbstractS3ATestBase { - - private static final Logger LOG = - LoggerFactory.getLogger(ITestS3ARemoteFileChanged.class); - - private static final String TEST_DATA = "Some test data"; - - private static final byte[] TEST_DATA_BYTES = TEST_DATA.getBytes( - Charsets.UTF_8); - private static final int TEST_MAX_RETRIES = 4; - private static final String TEST_RETRY_INTERVAL = "1ms"; - private static final String QUOTED_TEST_DATA = - "\"" + TEST_DATA + "\""; - - private Optional originalS3Client = Optional.empty(); - - private static final String INCONSISTENT = "inconsistent"; - - private static final String CONSISTENT = "consistent"; - - private enum InteractionType { - READ, - READ_AFTER_DELETE, - EVENTUALLY_CONSISTENT_READ, - COPY, - EVENTUALLY_CONSISTENT_COPY, - EVENTUALLY_CONSISTENT_METADATA, - SELECT, - EVENTUALLY_CONSISTENT_SELECT - } - - private final String changeDetectionSource; - private final String changeDetectionMode; - private final boolean authMode; - private final Collection expectedExceptionInteractions; - private S3AFileSystem fs; - - /** - * Test parameters. - *
      - *
    1. Change detection source: etag or version.
    2. - *
    3. Change detection policy: server, client, client+warn, none
    4. - *
    5. Whether to enable auth mode on the filesystem.
    6. - *
    7. Expected outcomes.
    8. - *
    - * @return the test configuration. - */ - @Parameterized.Parameters(name = "{0}-{1}-auth-{2}") - public static Collection params() { - return Arrays.asList(new Object[][]{ - // make sure it works with invalid config - {"bogus", "bogus", - true, - Arrays.asList( - InteractionType.READ, - InteractionType.READ_AFTER_DELETE, - InteractionType.EVENTUALLY_CONSISTENT_READ, - InteractionType.COPY, - InteractionType.EVENTUALLY_CONSISTENT_COPY, - InteractionType.EVENTUALLY_CONSISTENT_METADATA, - InteractionType.SELECT, - InteractionType.EVENTUALLY_CONSISTENT_SELECT)}, - - // test with etag - {CHANGE_DETECT_SOURCE_ETAG, CHANGE_DETECT_MODE_SERVER, - true, - Arrays.asList( - InteractionType.READ, - InteractionType.READ_AFTER_DELETE, - InteractionType.EVENTUALLY_CONSISTENT_READ, - InteractionType.COPY, - InteractionType.EVENTUALLY_CONSISTENT_COPY, - InteractionType.EVENTUALLY_CONSISTENT_METADATA, - InteractionType.SELECT, - InteractionType.EVENTUALLY_CONSISTENT_SELECT)}, - {CHANGE_DETECT_SOURCE_ETAG, CHANGE_DETECT_MODE_CLIENT, - false, - Arrays.asList( - InteractionType.READ, - InteractionType.EVENTUALLY_CONSISTENT_READ, - InteractionType.READ_AFTER_DELETE, - InteractionType.COPY, - // not InteractionType.EVENTUALLY_CONSISTENT_COPY as copy change - // detection can't really occur client-side. The eTag of - // the new object can't be expected to match. - InteractionType.EVENTUALLY_CONSISTENT_METADATA, - InteractionType.SELECT, - InteractionType.EVENTUALLY_CONSISTENT_SELECT)}, - {CHANGE_DETECT_SOURCE_ETAG, CHANGE_DETECT_MODE_WARN, - false, - Arrays.asList( - InteractionType.READ_AFTER_DELETE)}, - {CHANGE_DETECT_SOURCE_ETAG, CHANGE_DETECT_MODE_NONE, - false, - Arrays.asList( - InteractionType.READ_AFTER_DELETE)}, - - // test with versionId - // when using server-side versionId, the exceptions - // shouldn't happen since the previous version will still be available - {CHANGE_DETECT_SOURCE_VERSION_ID, CHANGE_DETECT_MODE_SERVER, - true, - Arrays.asList( - InteractionType.EVENTUALLY_CONSISTENT_READ, - InteractionType.EVENTUALLY_CONSISTENT_COPY, - InteractionType.EVENTUALLY_CONSISTENT_METADATA, - InteractionType.EVENTUALLY_CONSISTENT_SELECT)}, - - // with client-side versionId it will behave similar to client-side eTag - {CHANGE_DETECT_SOURCE_VERSION_ID, CHANGE_DETECT_MODE_CLIENT, - false, - Arrays.asList( - InteractionType.READ, - InteractionType.READ_AFTER_DELETE, - InteractionType.EVENTUALLY_CONSISTENT_READ, - InteractionType.COPY, - // not InteractionType.EVENTUALLY_CONSISTENT_COPY as copy change - // detection can't really occur client-side. The versionId of - // the new object can't be expected to match. - InteractionType.EVENTUALLY_CONSISTENT_METADATA, - InteractionType.SELECT, - InteractionType.EVENTUALLY_CONSISTENT_SELECT)}, - - {CHANGE_DETECT_SOURCE_VERSION_ID, CHANGE_DETECT_MODE_WARN, - true, - Arrays.asList( - InteractionType.READ_AFTER_DELETE)}, - {CHANGE_DETECT_SOURCE_VERSION_ID, CHANGE_DETECT_MODE_NONE, - false, - Arrays.asList( - InteractionType.READ_AFTER_DELETE)} - }); - } - - public ITestS3ARemoteFileChanged(String changeDetectionSource, - String changeDetectionMode, - boolean authMode, - Collection expectedExceptionInteractions) { - this.changeDetectionSource = changeDetectionSource; - this.changeDetectionMode = changeDetectionMode; - this.authMode = authMode; - this.expectedExceptionInteractions = expectedExceptionInteractions; - } - - @Override - public void setup() throws Exception { - super.setup(); - // skip all versioned checks if the remote FS doesn't do - // versions. - fs = getFileSystem(); - skipIfVersionPolicyAndNoVersionId(); - // cache the original S3 client for teardown. - originalS3Client = Optional.of( - fs.getAmazonS3ClientForTesting("caching")); - } - - @Override - public void teardown() throws Exception { - // restore the s3 client so there's no mocking interfering with the teardown - if (fs != null) { - originalS3Client.ifPresent(fs::setAmazonS3Client); - } - super.teardown(); - } - - @Override - protected Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - removeBaseAndBucketOverrides(conf, - CHANGE_DETECT_SOURCE, - CHANGE_DETECT_MODE, - RETRY_LIMIT, - RETRY_INTERVAL, - S3GUARD_CONSISTENCY_RETRY_LIMIT, - S3GUARD_CONSISTENCY_RETRY_INTERVAL, - METADATASTORE_AUTHORITATIVE, - AUTHORITATIVE_PATH); - conf.set(CHANGE_DETECT_SOURCE, changeDetectionSource); - conf.set(CHANGE_DETECT_MODE, changeDetectionMode); - conf.setBoolean(METADATASTORE_AUTHORITATIVE, authMode); - conf.set(AUTHORITATIVE_PATH, ""); - - // reduce retry limit so FileNotFoundException cases timeout faster, - // speeding up the tests - conf.setInt(RETRY_LIMIT, TEST_MAX_RETRIES); - conf.set(RETRY_INTERVAL, TEST_RETRY_INTERVAL); - conf.setInt(S3GUARD_CONSISTENCY_RETRY_LIMIT, TEST_MAX_RETRIES); - conf.set(S3GUARD_CONSISTENCY_RETRY_INTERVAL, TEST_RETRY_INTERVAL); - - if (conf.getClass(S3_METADATA_STORE_IMPL, MetadataStore.class) == - NullMetadataStore.class) { - LOG.debug("Enabling local S3Guard metadata store"); - // favor LocalMetadataStore over NullMetadataStore - conf.setClass(S3_METADATA_STORE_IMPL, - LocalMetadataStore.class, MetadataStore.class); - } - S3ATestUtils.disableFilesystemCaching(conf); - return conf; - } - - /** - * Get the path of this method, including parameterized values. - * @return a path unique to this method and parameters - * @throws IOException failure. - */ - protected Path path() throws IOException { - return super.path(getMethodName()); - } - - /** - * How many HEAD requests are made in a call to - * {@link S3AFileSystem#getFileStatus(Path)}? - * @return a number >= 0. - */ - private int getFileStatusHeadCount() { - return authMode ? 0 : 1; - } - - /** - * Tests reading a file that is changed while the reader's InputStream is - * open. - */ - @Test - public void testReadFileChangedStreamOpen() throws Throwable { - describe("Tests reading a file that is changed while the reader's " - + "InputStream is open."); - final int originalLength = 8192; - final byte[] originalDataset = dataset(originalLength, 'a', 32); - final int newLength = originalLength + 1; - final byte[] newDataset = dataset(newLength, 'A', 32); - final Path testpath = path("readFileToChange.txt"); - // initial write - writeDataset(fs, testpath, originalDataset, originalDataset.length, - 1024, false); - - try(FSDataInputStream instream = fs.open(testpath)) { - // seek forward and read successfully - instream.seek(1024); - assertTrue("no data to read", instream.read() >= 0); - - // overwrite - writeDataset(fs, testpath, newDataset, newDataset.length, 1024, true); - // here the new file length is larger. Probe the file to see if this is - // true, with a spin and wait - eventually(30 * 1000, 1000, - () -> { - assertEquals(newLength, fs.getFileStatus(testpath).getLen()); - }); - - // With the new file version in place, any subsequent S3 read by - // eTag/versionId will fail. A new read by eTag/versionId will occur in - // reopen() on read after a seek() backwards. We verify seek backwards - // results in the expected exception and seek() forward works without - // issue. - - // first check seek forward - instream.seek(2048); - assertTrue("no data to read", instream.read() >= 0); - - // now check seek backward - instream.seek(instream.getPos() - 100); - - if (expectedExceptionInteractions.contains(InteractionType.READ)) { - expectReadFailure(instream); - } else { - instream.read(); - } - - byte[] buf = new byte[256]; - - // seek backward - instream.seek(0); - - if (expectedExceptionInteractions.contains(InteractionType.READ)) { - expectReadFailure(instream); - intercept(RemoteFileChangedException.class, "", "read", - () -> instream.read(0, buf, 0, buf.length)); - intercept(RemoteFileChangedException.class, "", "readfully", - () -> instream.readFully(0, buf)); - } else { - instream.read(buf); - instream.read(0, buf, 0, buf.length); - instream.readFully(0, buf); - } - - // delete the file. Reads must fail - fs.delete(testpath, false); - - // seek backward - instream.seek(0); - - if (expectedExceptionInteractions.contains( - InteractionType.READ_AFTER_DELETE)) { - intercept(FileNotFoundException.class, "", "read()", - () -> instream.read()); - intercept(FileNotFoundException.class, "", "readfully", - () -> instream.readFully(2048, buf)); - } else { - instream.read(); - instream.readFully(2048, buf); - } - } - } - - /** - * Tests reading a file where the version visible in S3 does not match the - * version tracked in the metadata store. - */ - @Test - public void testReadFileChangedOutOfSyncMetadata() throws Throwable { - final Path testpath = writeOutOfSyncFileVersion("fileChangedOutOfSync.dat"); - - try (FSDataInputStream instream = fs.open(testpath)) { - if (expectedExceptionInteractions.contains(InteractionType.READ)) { - expectReadFailure(instream); - } else { - instream.read(); - } - } - } - - /** - * Verifies that when the openFile builder is passed in a status, - * then that is used to eliminate the getFileStatus call in open(); - * thus the version and etag passed down are still used. - */ - @Test - public void testOpenFileWithStatus() throws Throwable { - final Path testpath = path("testOpenFileWithStatus.dat"); - final byte[] dataset = TEST_DATA_BYTES; - S3AFileStatus originalStatus = - writeFile(testpath, dataset, dataset.length, true); - - // forge a file status with a different etag - // no attempt is made to change the versionID as it will - // get rejected by S3 as an invalid version - S3AFileStatus forgedStatus = - S3AFileStatus.fromFileStatus(originalStatus, Tristate.FALSE, - originalStatus.getETag() + "-fake", - originalStatus.getVersionId() + ""); - fs.getMetadataStore().put( - new PathMetadata(forgedStatus, Tristate.FALSE, false)); - - // verify the bad etag gets picked up. - LOG.info("Opening stream with s3guard's (invalid) status."); - try (FSDataInputStream instream = fs.openFile(testpath) - .build() - .get()) { - try { - instream.read(); - // No exception only if we don't enforce change detection as exception - assertTrue( - "Read did not raise an exception even though the change detection " - + "mode was " + changeDetectionMode - + " and the inserted file status was invalid", - changeDetectionMode.equals(CHANGE_DETECT_MODE_NONE) - || changeDetectionMode.equals(CHANGE_DETECT_MODE_WARN) - || changeDetectionSource.equals(CHANGE_DETECT_SOURCE_VERSION_ID)); - } catch (RemoteFileChangedException ignored) { - // Ignored. - } - } - - // By passing in the status open() doesn't need to check s3guard - // And hence the existing file is opened - LOG.info("Opening stream with the original status."); - try (FSDataInputStream instream = fs.openFile(testpath) - .withFileStatus(originalStatus) - .build() - .get()) { - instream.read(); - } - - // and this holds for S3A Located Status - LOG.info("Opening stream with S3ALocatedFileStatus."); - try (FSDataInputStream instream = fs.openFile(testpath) - .withFileStatus(new S3ALocatedFileStatus(originalStatus, null)) - .build() - .get()) { - instream.read(); - } - - // if you pass in a status of a dir, it will be rejected - S3AFileStatus s2 = new S3AFileStatus(true, testpath, "alice"); - assertTrue("not a directory " + s2, s2.isDirectory()); - LOG.info("Open with directory status"); - interceptFuture(FileNotFoundException.class, "", - fs.openFile(testpath) - .withFileStatus(s2) - .build()); - - // now, we delete the file from the store and s3guard - // when we pass in the status, there's no HEAD request, so it's only - // in the read call where the 404 surfaces. - // and there, when versionID is passed to the GET, the data is returned - LOG.info("Testing opening a deleted file"); - fs.delete(testpath, false); - try (FSDataInputStream instream = fs.openFile(testpath) - .withFileStatus(originalStatus) - .build() - .get()) { - if (changeDetectionSource.equals(CHANGE_DETECT_SOURCE_VERSION_ID) - && changeDetectionMode.equals(CHANGE_DETECT_MODE_SERVER)) { - // the deleted file is still there if you know the version ID - // and the check is server-side - instream.read(); - } else { - // all other cases, the read will return 404. - intercept(FileNotFoundException.class, - () -> instream.read()); - } - - } - - // whereas without that status, you fail in the get() when a HEAD is - // issued - interceptFuture(FileNotFoundException.class, "", - fs.openFile(testpath).build()); - - } - - /** - * Ensures a file can be read when there is no version metadata - * (ETag, versionId). - */ - @Test - public void testReadWithNoVersionMetadata() throws Throwable { - final Path testpath = writeFileWithNoVersionMetadata("readnoversion.dat"); - - assertEquals("Contents of " + testpath, - TEST_DATA, - readUTF8(fs, testpath, -1)); - } - - /** - * Tests using S3 Select on a file where the version visible in S3 does not - * match the version tracked in the metadata store. - */ - @Test - public void testSelectChangedFile() throws Throwable { - requireS3Select(); - final Path testpath = writeOutOfSyncFileVersion("select.dat"); - - if (expectedExceptionInteractions.contains(InteractionType.SELECT)) { - interceptFuture(RemoteFileChangedException.class, "select", - fs.openFile(testpath) - .must(SELECT_SQL, "SELECT * FROM S3OBJECT").build()); - } else { - fs.openFile(testpath) - .must(SELECT_SQL, "SELECT * FROM S3OBJECT") - .build() - .get() - .close(); - } - } - - /** - * Tests using S3 Select on a file where the version visible in S3 does not - * initially match the version tracked in the metadata store, but eventually - * (after retries) does. - */ - @Test - public void testSelectEventuallyConsistentFile() throws Throwable { - describe("Eventually Consistent S3 Select"); - requireS3Guard(); - requireS3Select(); - AmazonS3 s3ClientSpy = spyOnFilesystem(); - - final Path testpath1 = writeEventuallyConsistentFileVersion( - "select1.dat", s3ClientSpy, 0, TEST_MAX_RETRIES, 0); - - // should succeed since the inconsistency doesn't last longer than the - // configured retry limit - fs.openFile(testpath1) - .must(SELECT_SQL, "SELECT * FROM S3OBJECT") - .build() - .get() - .close(); - - // select() makes a getFileStatus() call before the consistency checking - // that will match the stub. As such, we need an extra inconsistency here - // to cross the threshold - int getMetadataInconsistencyCount = TEST_MAX_RETRIES + 2; - final Path testpath2 = writeEventuallyConsistentFileVersion( - "select2.dat", s3ClientSpy, 0, getMetadataInconsistencyCount, 0); - - if (expectedExceptionInteractions.contains( - InteractionType.EVENTUALLY_CONSISTENT_SELECT)) { - // should fail since the inconsistency lasts longer than the configured - // retry limit - interceptFuture(RemoteFileChangedException.class, "select", - fs.openFile(testpath2) - .must(SELECT_SQL, "SELECT * FROM S3OBJECT").build()); - } else { - fs.openFile(testpath2) - .must(SELECT_SQL, "SELECT * FROM S3OBJECT") - .build() - .get() - .close(); - } - } - - /** - * Ensures a file can be read via S3 Select when there is no version metadata - * (ETag, versionId). - */ - @Test - public void testSelectWithNoVersionMetadata() throws Throwable { - requireS3Select(); - final Path testpath = - writeFileWithNoVersionMetadata("selectnoversion.dat"); - - try (FSDataInputStream instream = fs.openFile(testpath) - .must(SELECT_SQL, "SELECT * FROM S3OBJECT") - .build() - .get()) { - assertEquals(QUOTED_TEST_DATA, - IOUtils.toString(instream, StandardCharsets.UTF_8).trim()); - } - } - - /** - * Tests doing a rename() on a file where the version visible in S3 does not - * match the version tracked in the metadata store. - * @throws Throwable failure - */ - @Test - public void testRenameChangedFile() throws Throwable { - final Path testpath = writeOutOfSyncFileVersion("rename.dat"); - - final Path dest = path("dest.dat"); - if (expectedExceptionInteractions.contains(InteractionType.COPY)) { - intercept(RemoteFileChangedException.class, "", - "expected copy() failure", - () -> fs.rename(testpath, dest)); - } else { - fs.rename(testpath, dest); - } - } - - /** - * Inconsistent response counts for getObjectMetadata() and - * copyObject() for a rename. - * @param metadataCallsExpectedBeforeRetryLoop number of getObjectMetadata - * calls expected before the consistency checking retry loop - * @return the inconsistencies for (metadata, copy) - */ - private Pair renameInconsistencyCounts( - int metadataCallsExpectedBeforeRetryLoop) { - int metadataInconsistencyCount = TEST_MAX_RETRIES - + metadataCallsExpectedBeforeRetryLoop; - int copyInconsistencyCount = - versionCheckingIsOnServer() ? TEST_MAX_RETRIES : 0; - - return Pair.of(metadataInconsistencyCount, copyInconsistencyCount); - } - - /** - * Tests doing a rename() on a file where the version visible in S3 does not - * match the version in the metadata store until a certain number of retries - * has been met. - */ - @Test - public void testRenameEventuallyConsistentFile() throws Throwable { - requireS3Guard(); - AmazonS3 s3ClientSpy = spyOnFilesystem(); - - // Total inconsistent response count across getObjectMetadata() and - // copyObject(). - // The split of inconsistent responses between getObjectMetadata() and - // copyObject() is arbitrary. - Pair counts = renameInconsistencyCounts( - getFileStatusHeadCount()); - int metadataInconsistencyCount = counts.getLeft(); - int copyInconsistencyCount = counts.getRight(); - final Path testpath1 = - writeEventuallyConsistentFileVersion("rename-eventually1.dat", - s3ClientSpy, - 0, - metadataInconsistencyCount, - copyInconsistencyCount); - - final Path dest1 = path("dest1.dat"); - // shouldn't fail since the inconsistency doesn't last through the - // configured retry limit - fs.rename(testpath1, dest1); - } - - /** - * Tests doing a rename() on a file where the version visible in S3 does not - * match the version in the metadata store until a certain number of retries - * has been met. - * The test expects failure by AWSClientIOException caused by NPE due to - * https://github.com/aws/aws-sdk-java/issues/1644 - */ - @Test - public void testRenameEventuallyConsistentFileNPE() throws Throwable { - requireS3Guard(); - skipIfVersionPolicyAndNoVersionId(); - AmazonS3 s3ClientSpy = spyOnFilesystem(); - - Pair counts = renameInconsistencyCounts( - getFileStatusHeadCount()); - int metadataInconsistencyCount = counts.getLeft(); - int copyInconsistencyCount = counts.getRight(); - // giving copyInconsistencyCount + 1 here should trigger the failure, - // exceeding the retry limit - final Path testpath2 = - writeEventuallyConsistentFileVersion("rename-eventuallyNPE.dat", - s3ClientSpy, - 0, - metadataInconsistencyCount, - copyInconsistencyCount + 1); - final Path dest2 = path("destNPE.dat"); - if (expectedExceptionInteractions.contains( - InteractionType.EVENTUALLY_CONSISTENT_COPY)) { - // should fail since the inconsistency is set up to persist longer than - // the configured retry limit - // the expected exception is not RemoteFileChangedException due to - // https://github.com/aws/aws-sdk-java/issues/1644 - // If this test is failing after an AWS SDK update, - // then it means the SDK bug is fixed. - // Please update this test to match the new behavior. - AWSClientIOException exception = - intercept(AWSClientIOException.class, - "Unable to complete transfer: null", - "expected copy() failure", - () -> fs.rename(testpath2, dest2)); - AmazonClientException cause = exception.getCause(); - if (cause == null) { - // no cause; something else went wrong: throw. - throw new AssertionError("No inner cause", - exception); - } - Throwable causeCause = cause.getCause(); - if (!(causeCause instanceof NullPointerException)) { - // null causeCause or it is the wrong type: throw - throw new AssertionError("Innermost cause is not NPE", - exception); - } - } else { - fs.rename(testpath2, dest2); - } - } - - /** - * Tests doing a rename() on a file where the version visible in S3 does not - * match the version in the metadata store until a certain number of retries - * has been met. - * The test expects failure by RemoteFileChangedException. - */ - @Test - public void testRenameEventuallyConsistentFileRFCE() throws Throwable { - requireS3Guard(); - skipIfVersionPolicyAndNoVersionId(); - AmazonS3 s3ClientSpy = spyOnFilesystem(); - - Pair counts = renameInconsistencyCounts( - getFileStatusHeadCount()); - int metadataInconsistencyCount = counts.getLeft(); - int copyInconsistencyCount = counts.getRight(); - // giving metadataInconsistencyCount + 1 here should trigger the failure, - // exceeding the retry limit - final Path testpath2 = - writeEventuallyConsistentFileVersion("rename-eventuallyRFCE.dat", - s3ClientSpy, - 0, - metadataInconsistencyCount + 1, - copyInconsistencyCount); - final Path dest2 = path("destRFCE.dat"); - if (expectedExceptionInteractions.contains( - InteractionType.EVENTUALLY_CONSISTENT_METADATA)) { - // should fail since the inconsistency is set up to persist longer than - // the configured retry limit - intercept(RemoteFileChangedException.class, - CHANGE_DETECTED, - "expected copy() failure", - () -> fs.rename(testpath2, dest2)); - } else { - fs.rename(testpath2, dest2); - } - } - - /** - * Tests doing a rename() on a directory containing - * an file which is eventually consistent. - * There is no call to getFileStatus on the source file whose - * inconsistency is simulated; the state of S3Guard auth mode is not - * relevant. - */ - @Test - public void testRenameEventuallyConsistentDirectory() throws Throwable { - requireS3Guard(); - AmazonS3 s3ClientSpy = spyOnFilesystem(); - Path basedir = path(); - Path sourcedir = new Path(basedir, "sourcedir"); - fs.mkdirs(sourcedir); - Path destdir = new Path(basedir, "destdir"); - Path inconsistentFile = new Path(sourcedir, INCONSISTENT); - Path consistentFile = new Path(sourcedir, CONSISTENT); - - // write the consistent data - writeDataset(fs, consistentFile, TEST_DATA_BYTES, TEST_DATA_BYTES.length, - 1024, true, true); - - Pair counts = renameInconsistencyCounts(0); - int metadataInconsistencyCount = counts.getLeft(); - int copyInconsistencyCount = counts.getRight(); - - writeEventuallyConsistentData( - s3ClientSpy, - inconsistentFile, - TEST_DATA_BYTES, - 0, - metadataInconsistencyCount, - copyInconsistencyCount); - - // must not fail since the inconsistency doesn't last through the - // configured retry limit - fs.rename(sourcedir, destdir); - } - - /** - * Tests doing a rename() on a file which is eventually visible. - */ - @Test - public void testRenameEventuallyVisibleFile() throws Throwable { - requireS3Guard(); - AmazonS3 s3ClientSpy = spyOnFilesystem(); - Path basedir = path(); - Path sourcedir = new Path(basedir, "sourcedir"); - fs.mkdirs(sourcedir); - Path destdir = new Path(basedir, "destdir"); - Path inconsistentFile = new Path(sourcedir, INCONSISTENT); - Path consistentFile = new Path(sourcedir, CONSISTENT); - - // write the consistent data - writeDataset(fs, consistentFile, TEST_DATA_BYTES, TEST_DATA_BYTES.length, - 1024, true, true); - - Pair counts = renameInconsistencyCounts(0); - int metadataInconsistencyCount = counts.getLeft(); - - writeDataset(fs, inconsistentFile, TEST_DATA_BYTES, TEST_DATA_BYTES.length, - 1024, true, true); - - stubTemporaryNotFound(s3ClientSpy, metadataInconsistencyCount, - inconsistentFile); - - // must not fail since the inconsistency doesn't last through the - // configured retry limit - fs.rename(sourcedir, destdir); - } - - /** - * Tests doing a rename() on a file which never quite appears will - * fail with a RemoteFileChangedException rather than have the exception - * downgraded to a failure. - */ - @Test - public void testRenameMissingFile() - throws Throwable { - requireS3Guard(); - AmazonS3 s3ClientSpy = spyOnFilesystem(); - Path basedir = path(); - Path sourcedir = new Path(basedir, "sourcedir"); - fs.mkdirs(sourcedir); - Path destdir = new Path(basedir, "destdir"); - Path inconsistentFile = new Path(sourcedir, INCONSISTENT); - Path consistentFile = new Path(sourcedir, CONSISTENT); - - // write the consistent data - writeDataset(fs, consistentFile, TEST_DATA_BYTES, TEST_DATA_BYTES.length, - 1024, true, true); - - Pair counts = renameInconsistencyCounts(0); - int metadataInconsistencyCount = counts.getLeft(); - - writeDataset(fs, inconsistentFile, TEST_DATA_BYTES, TEST_DATA_BYTES.length, - 1024, true, true); - - stubTemporaryNotFound(s3ClientSpy, metadataInconsistencyCount + 1, - inconsistentFile); - - String expected = fs.hasMetadataStore() - ? RemoteFileChangedException.FILE_NEVER_FOUND - : RemoteFileChangedException.FILE_NOT_FOUND_SINGLE_ATTEMPT; - RemoteFileChangedException ex = intercept( - RemoteFileChangedException.class, - expected, - () -> fs.rename(sourcedir, destdir)); - assertEquals("Path in " + ex, - inconsistentFile, ex.getPath()); - if (!(ex.getCause() instanceof FileNotFoundException)) { - throw ex; - } - } - - /** - * Ensures a file can be renamed when there is no version metadata - * (ETag, versionId). - */ - @Test - public void testRenameWithNoVersionMetadata() throws Throwable { - final Path testpath = - writeFileWithNoVersionMetadata("renamenoversion.dat"); - - final Path dest = path("noversiondest.dat"); - fs.rename(testpath, dest); - assertEquals("Contents of " + dest, - TEST_DATA, - readUTF8(fs, dest, -1)); - } - - /** - * Ensures S3Guard and retries allow an eventually consistent read. - */ - @Test - public void testReadAfterEventuallyConsistentWrite() throws Throwable { - requireS3Guard(); - AmazonS3 s3ClientSpy = spyOnFilesystem(); - final Path testpath1 = - writeEventuallyConsistentFileVersion("eventually1.dat", - s3ClientSpy, TEST_MAX_RETRIES, 0 , 0); - - try (FSDataInputStream instream1 = fs.open(testpath1)) { - // succeeds on the last retry - instream1.read(); - } - } - - /** - * Ensures S3Guard and retries allow an eventually consistent read. - */ - @Test - public void testReadAfterEventuallyConsistentWrite2() throws Throwable { - requireS3Guard(); - AmazonS3 s3ClientSpy = spyOnFilesystem(); - final Path testpath2 = - writeEventuallyConsistentFileVersion("eventually2.dat", - s3ClientSpy, TEST_MAX_RETRIES + 1, 0, 0); - - try (FSDataInputStream instream2 = fs.open(testpath2)) { - if (expectedExceptionInteractions.contains( - InteractionType.EVENTUALLY_CONSISTENT_READ)) { - // keeps retrying and eventually gives up with RemoteFileChangedException - expectReadFailure(instream2); - } else { - instream2.read(); - } - } - } - - /** - * Ensures read on re-open (after seek backwards) when S3 does not return the - * version of the file tracked in the metadata store fails immediately. No - * retries should happen since a retry is not expected to recover. - */ - @Test - public void testEventuallyConsistentReadOnReopen() throws Throwable { - requireS3Guard(); - AmazonS3 s3ClientSpy = spyOnFilesystem(); - String filename = "eventually-reopen.dat"; - final Path testpath = - writeEventuallyConsistentFileVersion(filename, - s3ClientSpy, 0, 0, 0); - - try (FSDataInputStream instream = fs.open(testpath)) { - instream.read(); - // overwrite the file, returning inconsistent version for - // (effectively) infinite retries - writeEventuallyConsistentFileVersion(filename, s3ClientSpy, - Integer.MAX_VALUE, 0, 0); - instream.seek(0); - if (expectedExceptionInteractions.contains(InteractionType.READ)) { - // if it retries at all, it will retry forever, which should fail - // the test. The expected behavior is immediate - // RemoteFileChangedException. - expectReadFailure(instream); - } else { - instream.read(); - } - } - } - - /** - * Writes a file with old ETag and versionId in the metadata store such - * that the metadata is out of sync with S3. Attempts to read such a file - * should result in {@link RemoteFileChangedException}. - */ - private Path writeOutOfSyncFileVersion(String filename) throws IOException { - final Path testpath = path(filename); - final byte[] dataset = TEST_DATA_BYTES; - S3AFileStatus originalStatus = - writeFile(testpath, dataset, dataset.length, false); - - // overwrite with half the content - S3AFileStatus newStatus = writeFile(testpath, dataset, dataset.length / 2, - true); - - // put back the original etag, versionId - S3AFileStatus forgedStatus = - S3AFileStatus.fromFileStatus(newStatus, Tristate.FALSE, - originalStatus.getETag(), originalStatus.getVersionId()); - fs.getMetadataStore().put( - new PathMetadata(forgedStatus, Tristate.FALSE, false)); - - return testpath; - } - - /** - * Write data to a file; return the status from the filesystem. - * @param path file path - * @param dataset dataset to write from - * @param length number of bytes from the dataset to write. - * @param overwrite overwrite flag - * @return the retrieved file status. - */ - private S3AFileStatus writeFile(final Path path, - final byte[] dataset, - final int length, - final boolean overwrite) throws IOException { - writeDataset(fs, path, dataset, length, - 1024, overwrite); - return (S3AFileStatus) fs.getFileStatus(path); - } - - /** - * Writes {@link #TEST_DATA} to a file where the file will be inconsistent - * in S3 for a set of operations. - * The duration of the inconsistency is controlled by the - * getObjectInconsistencyCount, getMetadataInconsistencyCount, and - * copyInconsistentCallCount parameters. - * The inconsistency manifests in AmazonS3#getObject, - * AmazonS3#getObjectMetadata, and AmazonS3#copyObject. - * This method sets up the provided s3ClientSpy to return a response to each - * of these methods indicating an inconsistency where the requested object - * version (eTag or versionId) is not available until a certain retry - * threshold is met. - * Providing inconsistent call count values above or - * below the overall retry limit allows a test to simulate a condition that - * either should or should not result in an overall failure from retry - * exhaustion. - * @param filename name of file (will be under test path) - * @param s3ClientSpy s3 client to patch - * @param getObjectInconsistencyCount number of GET inconsistencies - * @param getMetadataInconsistencyCount number of HEAD inconsistencies - * @param copyInconsistencyCount number of COPY inconsistencies. - * @return the path written - * @throws IOException failure to write the test data. - */ - private Path writeEventuallyConsistentFileVersion(String filename, - AmazonS3 s3ClientSpy, - int getObjectInconsistencyCount, - int getMetadataInconsistencyCount, - int copyInconsistencyCount) - throws IOException { - return writeEventuallyConsistentData(s3ClientSpy, - path(filename), - TEST_DATA_BYTES, - getObjectInconsistencyCount, - getMetadataInconsistencyCount, - copyInconsistencyCount); - } - - /** - * Writes data to a path and configures the S3 client for inconsistent - * HEAD, GET or COPY operations. - * @param testpath absolute path of file - * @param s3ClientSpy s3 client to patch - * @param dataset bytes to write. - * @param getObjectInconsistencyCount number of GET inconsistencies - * @param getMetadataInconsistencyCount number of HEAD inconsistencies - * @param copyInconsistencyCount number of COPY inconsistencies. - * @return the path written - * @throws IOException failure to write the test data. - */ - private Path writeEventuallyConsistentData(final AmazonS3 s3ClientSpy, - final Path testpath, - final byte[] dataset, - final int getObjectInconsistencyCount, - final int getMetadataInconsistencyCount, - final int copyInconsistencyCount) - throws IOException { - writeDataset(fs, testpath, dataset, dataset.length, - 1024, true); - S3AFileStatus originalStatus = (S3AFileStatus) fs.getFileStatus(testpath); - - // overwrite with half the content - writeDataset(fs, testpath, dataset, dataset.length / 2, - 1024, true); - - LOG.debug("Original file info: {}: version={}, etag={}", testpath, - originalStatus.getVersionId(), originalStatus.getETag()); - - S3AFileStatus newStatus = (S3AFileStatus) fs.getFileStatus(testpath); - LOG.debug("Updated file info: {}: version={}, etag={}", testpath, - newStatus.getVersionId(), newStatus.getETag()); - - LOG.debug("File {} will be inconsistent for {} HEAD and {} GET requests", - testpath, getMetadataInconsistencyCount, getObjectInconsistencyCount); - - stubTemporaryUnavailable(s3ClientSpy, getObjectInconsistencyCount, - testpath, newStatus); - - stubTemporaryWrongVersion(s3ClientSpy, getObjectInconsistencyCount, - testpath, originalStatus); - - if (versionCheckingIsOnServer()) { - // only stub inconsistency when mode is server since no constraints that - // should trigger inconsistency are passed in any other mode - LOG.debug("File {} will be inconsistent for {} COPY operations", - testpath, copyInconsistencyCount); - stubTemporaryCopyInconsistency(s3ClientSpy, testpath, newStatus, - copyInconsistencyCount); - } - - stubTemporaryMetadataInconsistency(s3ClientSpy, testpath, originalStatus, - newStatus, getMetadataInconsistencyCount); - - return testpath; - } - - /** - * Log the call hierarchy at debug level, helps track down - * where calls to operations are coming from. - */ - private void logLocationAtDebug() { - if (LOG.isDebugEnabled()) { - LOG.debug("Call hierarchy", new Exception("here")); - } - } - - /** - * Stubs {@link AmazonS3#getObject(GetObjectRequest)} - * within s3ClientSpy to return null until inconsistentCallCount calls have - * been made. The null response simulates what occurs when an object - * matching the specified ETag or versionId is not available. - * @param s3ClientSpy the spy to stub - * @param inconsistentCallCount the number of calls that should return the - * null response - * @param testpath the path of the object the stub should apply to - */ - private void stubTemporaryUnavailable(AmazonS3 s3ClientSpy, - int inconsistentCallCount, Path testpath, - S3AFileStatus newStatus) { - Answer temporarilyUnavailableAnswer = new Answer() { - private int callCount = 0; - - @Override - public S3Object answer(InvocationOnMock invocation) throws Throwable { - // simulates ETag or versionId constraint not met until - // inconsistentCallCount surpassed - callCount++; - if (callCount <= inconsistentCallCount) { - LOG.info("Temporarily unavailable {} count {} of {}", - testpath, callCount, inconsistentCallCount); - logLocationAtDebug(); - return null; - } - return (S3Object) invocation.callRealMethod(); - } - }; - - // match the requests that would be made in either server-side change - // detection mode - doAnswer(temporarilyUnavailableAnswer).when(s3ClientSpy) - .getObject( - matchingGetObjectRequest( - testpath, newStatus.getETag(), null)); - doAnswer(temporarilyUnavailableAnswer).when(s3ClientSpy) - .getObject( - matchingGetObjectRequest( - testpath, null, newStatus.getVersionId())); - } - - /** - * Stubs {@link AmazonS3#getObject(GetObjectRequest)} - * within s3ClientSpy to return an object modified to contain metadata - * from originalStatus until inconsistentCallCount calls have been made. - * @param s3ClientSpy the spy to stub - * @param testpath the path of the object the stub should apply to - * @param inconsistentCallCount the number of calls that should return the - * null response - * @param originalStatus the status metadata to inject into the - * inconsistentCallCount responses - */ - private void stubTemporaryWrongVersion(AmazonS3 s3ClientSpy, - int inconsistentCallCount, Path testpath, - S3AFileStatus originalStatus) { - Answer temporarilyWrongVersionAnswer = new Answer() { - private int callCount = 0; - - @Override - public S3Object answer(InvocationOnMock invocation) throws Throwable { - // simulates old ETag or versionId until inconsistentCallCount surpassed - callCount++; - S3Object s3Object = (S3Object) invocation.callRealMethod(); - if (callCount <= inconsistentCallCount) { - LOG.info("Temporary Wrong Version {} count {} of {}", - testpath, callCount, inconsistentCallCount); - logLocationAtDebug(); - S3Object objectSpy = Mockito.spy(s3Object); - ObjectMetadata metadataSpy = - Mockito.spy(s3Object.getObjectMetadata()); - when(objectSpy.getObjectMetadata()).thenReturn(metadataSpy); - when(metadataSpy.getETag()).thenReturn(originalStatus.getETag()); - when(metadataSpy.getVersionId()) - .thenReturn(originalStatus.getVersionId()); - return objectSpy; - } - return s3Object; - } - }; - - // match requests that would be made in client-side change detection - doAnswer(temporarilyWrongVersionAnswer).when(s3ClientSpy).getObject( - matchingGetObjectRequest(testpath, null, null)); - } - - /** - * Stubs {@link AmazonS3#copyObject(CopyObjectRequest)} - * within s3ClientSpy to return null (indicating preconditions not met) until - * copyInconsistentCallCount calls have been made. - * @param s3ClientSpy the spy to stub - * @param testpath the path of the object the stub should apply to - * @param newStatus the status metadata containing the ETag and versionId - * that should be matched in order for the stub to apply - * @param copyInconsistentCallCount how many times to return the - * precondition failed error - */ - private void stubTemporaryCopyInconsistency(AmazonS3 s3ClientSpy, - Path testpath, S3AFileStatus newStatus, - int copyInconsistentCallCount) { - Answer temporarilyPreconditionsNotMetAnswer = - new Answer() { - private int callCount = 0; - - @Override - public CopyObjectResult answer(InvocationOnMock invocation) - throws Throwable { - callCount++; - if (callCount <= copyInconsistentCallCount) { - String message = "preconditions not met on call " + callCount - + " of " + copyInconsistentCallCount; - LOG.info("Copying {}: {}", testpath, message); - logLocationAtDebug(); - return null; - } - return (CopyObjectResult) invocation.callRealMethod(); - } - }; - - // match requests made during copy - doAnswer(temporarilyPreconditionsNotMetAnswer).when(s3ClientSpy).copyObject( - matchingCopyObjectRequest(testpath, newStatus.getETag(), null)); - doAnswer(temporarilyPreconditionsNotMetAnswer).when(s3ClientSpy).copyObject( - matchingCopyObjectRequest(testpath, null, newStatus.getVersionId())); - } - - /** - * Stubs {@link AmazonS3#getObjectMetadata(GetObjectMetadataRequest)} - * within s3ClientSpy to return metadata from originalStatus until - * metadataInconsistentCallCount calls have been made. - * @param s3ClientSpy the spy to stub - * @param testpath the path of the object the stub should apply to - * @param originalStatus the inconsistent status metadata to return - * @param newStatus the status metadata to return after - * metadataInconsistentCallCount is met - * @param metadataInconsistentCallCount how many times to return the - * inconsistent metadata - */ - private void stubTemporaryMetadataInconsistency(AmazonS3 s3ClientSpy, - Path testpath, S3AFileStatus originalStatus, - S3AFileStatus newStatus, int metadataInconsistentCallCount) { - Answer temporarilyOldMetadataAnswer = - new Answer() { - private int callCount = 0; - - @Override - public ObjectMetadata answer(InvocationOnMock invocation) - throws Throwable { - ObjectMetadata objectMetadata = - (ObjectMetadata) invocation.callRealMethod(); - callCount++; - if (callCount <= metadataInconsistentCallCount) { - LOG.info("Inconsistent metadata {} count {} of {}", - testpath, callCount, metadataInconsistentCallCount); - logLocationAtDebug(); - ObjectMetadata metadataSpy = - Mockito.spy(objectMetadata); - when(metadataSpy.getETag()).thenReturn(originalStatus.getETag()); - when(metadataSpy.getVersionId()) - .thenReturn(originalStatus.getVersionId()); - return metadataSpy; - } - return objectMetadata; - } - }; - - // match requests made during select - doAnswer(temporarilyOldMetadataAnswer).when(s3ClientSpy).getObjectMetadata( - matchingMetadataRequest(testpath, null)); - doAnswer(temporarilyOldMetadataAnswer).when(s3ClientSpy).getObjectMetadata( - matchingMetadataRequest(testpath, newStatus.getVersionId())); - } - - /** - * Writes a file with null ETag and versionId in the metadata store. - */ - private Path writeFileWithNoVersionMetadata(String filename) - throws IOException { - final Path testpath = path(filename); - S3AFileStatus originalStatus = writeFile(testpath, TEST_DATA_BYTES, - TEST_DATA_BYTES.length, false); - - // remove ETag and versionId - S3AFileStatus newStatus = S3AFileStatus.fromFileStatus(originalStatus, - Tristate.FALSE, null, null); - fs.getMetadataStore().put(new PathMetadata(newStatus, Tristate.FALSE, - false)); - - return testpath; - } - - /** - * The test is invalid if the policy uses versionId but the bucket doesn't - * have versioning enabled. - * - * Tests the given file for a versionId to detect whether bucket versioning - * is enabled. - */ - private void skipIfVersionPolicyAndNoVersionId(Path testpath) - throws IOException { - if (fs.getChangeDetectionPolicy().getSource() == Source.VersionId) { - // skip versionId tests if the bucket doesn't have object versioning - // enabled - Assume.assumeTrue( - "Target filesystem does not support versioning", - fs.getObjectMetadata(testpath).getVersionId() != null); - } - } - - /** - * Like {@link #skipIfVersionPolicyAndNoVersionId(Path)} but generates a new - * file to test versionId against. - */ - private void skipIfVersionPolicyAndNoVersionId() throws IOException { - if (fs.getChangeDetectionPolicy().getSource() == Source.VersionId) { - Path versionIdFeatureTestFile = path("versionIdTest"); - writeDataset(fs, versionIdFeatureTestFile, TEST_DATA_BYTES, - TEST_DATA_BYTES.length, 1024, true, true); - skipIfVersionPolicyAndNoVersionId(versionIdFeatureTestFile); - } - } - - private GetObjectRequest matchingGetObjectRequest(Path path, String eTag, - String versionId) { - return ArgumentMatchers.argThat(request -> { - if (request.getBucketName().equals(fs.getBucket()) - && request.getKey().equals(fs.pathToKey(path))) { - if (eTag == null && !request.getMatchingETagConstraints().isEmpty()) { - return false; - } - if (eTag != null && - !request.getMatchingETagConstraints().contains(eTag)) { - return false; - } - if (versionId == null && request.getVersionId() != null) { - return false; - } - if (versionId != null && !versionId.equals(request.getVersionId())) { - return false; - } - return true; - } - return false; - }); - } - - private CopyObjectRequest matchingCopyObjectRequest(Path path, String eTag, - String versionId) { - return ArgumentMatchers.argThat(request -> { - if (request.getSourceBucketName().equals(fs.getBucket()) - && request.getSourceKey().equals(fs.pathToKey(path))) { - if (eTag == null && !request.getMatchingETagConstraints().isEmpty()) { - return false; - } - if (eTag != null && - !request.getMatchingETagConstraints().contains(eTag)) { - return false; - } - if (versionId == null && request.getSourceVersionId() != null) { - return false; - } - if (versionId != null && - !versionId.equals(request.getSourceVersionId())) { - return false; - } - return true; - } - return false; - }); - } - - private GetObjectMetadataRequest matchingMetadataRequest(Path path, - String versionId) { - return ArgumentMatchers.argThat(request -> { - if (request.getBucketName().equals(fs.getBucket()) - && request.getKey().equals(fs.pathToKey(path))) { - if (versionId == null && request.getVersionId() != null) { - return false; - } - if (versionId != null && - !versionId.equals(request.getVersionId())) { - return false; - } - return true; - } - return false; - }); - } - - /** - * Match any getObjectMetadata request against a given path. - * @param path path to to match. - * @return the matching request. - */ - private GetObjectMetadataRequest matchingMetadataRequest(Path path) { - return ArgumentMatchers.argThat(request -> { - return request.getBucketName().equals(fs.getBucket()) - && request.getKey().equals(fs.pathToKey(path)); - }); - } - - /** - * Skip a test case if it needs S3Guard and the filesystem does - * not have it. - */ - private void requireS3Guard() { - Assume.assumeTrue("S3Guard must be enabled", fs.hasMetadataStore()); - } - - /** - * Skip a test case if S3 Select is not supported on this store. - */ - private void requireS3Select() { - Assume.assumeTrue("S3 Select is not enabled", - getFileSystem().hasCapability(S3_SELECT_CAPABILITY)); - } - - /** - * Spy on the filesystem at the S3 client level. - * @return a mocked S3 client to which the test FS is bonded. - */ - private AmazonS3 spyOnFilesystem() { - AmazonS3 s3ClientSpy = Mockito.spy( - fs.getAmazonS3ClientForTesting("mocking")); - fs.setAmazonS3Client(s3ClientSpy); - return s3ClientSpy; - } - - /** - * Expect reading this stream to fail. - * @param instream input stream. - * @return the caught exception. - * @throws Exception an other exception - */ - - private RemoteFileChangedException expectReadFailure( - final FSDataInputStream instream) - throws Exception { - return intercept(RemoteFileChangedException.class, "", - "read() returned", - () -> readToText(instream.read())); - } - - /** - * Convert the result of a read to a text string for errors. - * @param r result of the read() call. - * @return a string for exception text. - */ - private String readToText(int r) { - return r < 32 - ? (String.format("%02d", r)) - : (String.format("%c", (char) r)); - } - - /** - * Is the version checking on the server? - * @return true if the server returns 412 errors. - */ - private boolean versionCheckingIsOnServer() { - return fs.getChangeDetectionPolicy().getMode() == Mode.Server; - } - - /** - * Stubs {@link AmazonS3#getObject(GetObjectRequest)} - * within s3ClientSpy to return throw a FileNotFoundException - * until inconsistentCallCount calls have been made. - * This simulates the condition where the S3 endpoint is caching - * a 404 request, or there is a tombstone in the way which has yet - * to clear. - * @param s3ClientSpy the spy to stub - * @param inconsistentCallCount the number of calls that should return the - * null response - * @param testpath the path of the object the stub should apply to - */ - private void stubTemporaryNotFound(AmazonS3 s3ClientSpy, - int inconsistentCallCount, Path testpath) { - Answer notFound = new Answer() { - private int callCount = 0; - - @Override - public ObjectMetadata answer(InvocationOnMock invocation - ) throws Throwable { - // simulates delayed visibility. - callCount++; - if (callCount <= inconsistentCallCount) { - LOG.info("Temporarily unavailable {} count {} of {}", - testpath, callCount, inconsistentCallCount); - logLocationAtDebug(); - throw new FileNotFoundException(testpath.toString()); - } - return (ObjectMetadata) invocation.callRealMethod(); - } - }; - - // HEAD requests will fail - doAnswer(notFound).when(s3ClientSpy).getObjectMetadata( - matchingMetadataRequest(testpath)); - } - -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3ATemporaryCredentials.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3ATemporaryCredentials.java index 041b6f4c179..112d0fcb502 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3ATemporaryCredentials.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3ATemporaryCredentials.java @@ -275,8 +275,7 @@ public class ITestS3ATemporaryCredentials extends AbstractS3ATestBase { // this is a failure path, so fail with a meaningful error fail("request to create a file should have failed"); } catch (AWSBadRequestException expected){ - // likely at two points in the operation, depending on - // S3Guard state + // could fail in fs creation or file IO } finally { IOUtils.closeStream(fs); } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3GuardCreate.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3GuardCreate.java deleted file mode 100644 index dcc2538ec61..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3GuardCreate.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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.hadoop.fs.s3a; - -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.s3guard.DirListingMetadata; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; -import org.junit.Assume; -import org.junit.Test; - -import static org.apache.hadoop.fs.contract.ContractTestUtils.touch; - -/** - * Home for testing the creation of new files and directories with S3Guard - * enabled. - */ -public class ITestS3GuardCreate extends AbstractS3ATestBase { - - /** - * Test that ancestor creation during S3AFileSystem#create() is properly - * accounted for in the MetadataStore. This should be handled by the - * FileSystem, and be a FS contract test, but S3A does not handle ancestors on - * create(), so we need to take care in the S3Guard code to do the right - * thing. This may change: See HADOOP-13221 for more detail. - */ - @Test - public void testCreatePopulatesFileAncestors() throws Exception { - final S3AFileSystem fs = getFileSystem(); - Assume.assumeTrue(fs.hasMetadataStore()); - final MetadataStore ms = fs.getMetadataStore(); - final Path parent = path("testCreatePopulatesFileAncestors"); - - try { - fs.mkdirs(parent); - final Path nestedFile = new Path(parent, "dir1/dir2/file4"); - touch(fs, nestedFile); - - DirListingMetadata list = ms.listChildren(parent); - assertFalse("MetadataStore falsely reports authoritative empty list", - list.isEmpty() == Tristate.TRUE); - } finally { - fs.delete(parent, true); - } - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3GuardEmptyDirs.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3GuardEmptyDirs.java deleted file mode 100644 index d603d384322..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3GuardEmptyDirs.java +++ /dev/null @@ -1,314 +0,0 @@ -/* - * 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.hadoop.fs.s3a; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.stream.Stream; - -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.model.ListObjectsV2Request; -import com.amazonaws.services.s3.model.ListObjectsV2Result; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.S3ObjectSummary; -import org.assertj.core.api.Assertions; -import org.junit.Test; - -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.impl.StatusProbeEnum; -import org.apache.hadoop.fs.s3a.impl.StoreContext; -import org.apache.hadoop.fs.s3a.s3guard.DDBPathMetadata; -import org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.NullMetadataStore; -import org.apache.hadoop.fs.store.audit.AuditSpan; - -import static org.apache.hadoop.fs.contract.ContractTestUtils.assertRenameOutcome; -import static org.apache.hadoop.fs.contract.ContractTestUtils.touch; -import static org.apache.hadoop.fs.s3a.Statistic.INVOCATION_MKDIRS; -import static org.apache.hadoop.test.LambdaTestUtils.intercept; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.assume; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.assumeFilesystemHasMetadatastore; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.getStatusWithEmptyDirFlag; - -/** - * Test logic around whether or not a directory is empty, with S3Guard enabled. - * The fact that S3AFileStatus has an isEmptyDirectory flag in it makes caching - * S3AFileStatus's really tricky, as the flag can change as a side effect of - * changes to other paths. - * After S3Guard is merged to trunk, we should try to remove the - * isEmptyDirectory flag from S3AFileStatus, or maintain it outside - * of the MetadataStore. - */ -public class ITestS3GuardEmptyDirs extends AbstractS3ATestBase { - - /** - * Rename an empty directory, verify that the empty dir - * marker moves in both S3Guard and in the S3A FS. - */ - @Test - public void testRenameEmptyDir() throws Throwable { - S3AFileSystem fs = getFileSystem(); - Path basePath = path(getMethodName()); - Path sourceDir = new Path(basePath, "AAA-source"); - String sourceDirMarker = fs.pathToKey(sourceDir) + "/"; - Path destDir = new Path(basePath, "BBB-dest"); - String destDirMarker = fs.pathToKey(destDir) + "/"; - // set things up. - mkdirs(sourceDir); - // create a span for all the low level operations - span(); - // there's source directory marker+ - fs.getObjectMetadata(sourceDirMarker); - S3AFileStatus srcStatus = getEmptyDirStatus(sourceDir); - assertEquals("Must be an empty dir: " + srcStatus, Tristate.TRUE, - srcStatus.isEmptyDirectory()); - // do the rename - assertRenameOutcome(fs, sourceDir, destDir, true); - S3AFileStatus destStatus = getEmptyDirStatus(destDir); - assertEquals("Must be an empty dir: " + destStatus, Tristate.TRUE, - destStatus.isEmptyDirectory()); - // source does not exist. - intercept(FileNotFoundException.class, - () -> getEmptyDirStatus(sourceDir)); - // and verify that there's no dir marker hidden under a tombstone - intercept(FileNotFoundException.class, - () -> Invoker.once("HEAD", sourceDirMarker, () -> { - span(); - ObjectMetadata md = fs.getObjectMetadata(sourceDirMarker); - return String.format("Object %s of length %d", - sourceDirMarker, md.getInstanceLength()); - })); - - // the parent dir mustn't be confused - S3AFileStatus baseStatus = getEmptyDirStatus(basePath); - assertEquals("Must not be an empty dir: " + baseStatus, Tristate.FALSE, - baseStatus.isEmptyDirectory()); - // and verify the dest dir has a marker - span(); - fs.getObjectMetadata(destDirMarker); - } - - private S3AFileStatus getEmptyDirStatus(Path dir) throws IOException { - try (AuditSpan span = span()) { - return getFileSystem().innerGetFileStatus(dir, true, StatusProbeEnum.ALL); - } - } - - @Test - public void testEmptyDirs() throws Exception { - S3AFileSystem fs = getFileSystem(); - assumeFilesystemHasMetadatastore(getFileSystem()); - MetadataStore configuredMs = fs.getMetadataStore(); - Path existingDir = path("existing-dir"); - Path existingFile = path("existing-dir/existing-file"); - try { - // 1. Simulate files already existing in the bucket before we started our - // cluster. Temporarily disable the MetadataStore so it doesn't witness - // us creating these files. - - fs.setMetadataStore(new NullMetadataStore()); - assertTrue(fs.mkdirs(existingDir)); - touch(fs, existingFile); - - - // 2. Simulate (from MetadataStore's perspective) starting our cluster and - // creating a file in an existing directory. - fs.setMetadataStore(configuredMs); // "start cluster" - Path newFile = path("existing-dir/new-file"); - touch(fs, newFile); - span(); - S3AFileStatus status = fs.innerGetFileStatus(existingDir, true, - StatusProbeEnum.ALL); - assertEquals("Should not be empty dir", Tristate.FALSE, - status.isEmptyDirectory()); - - // 3. Assert that removing the only file the MetadataStore witnessed - // being created doesn't cause it to think the directory is now empty. - fs.delete(newFile, false); - span(); - status = fs.innerGetFileStatus(existingDir, true, StatusProbeEnum.ALL); - assertEquals("Should not be empty dir", Tristate.FALSE, - status.isEmptyDirectory()); - - // 4. Assert that removing the final file, that existed "before" - // MetadataStore started, *does* cause the directory to be marked empty. - fs.delete(existingFile, false); - span(); - status = fs.innerGetFileStatus(existingDir, true, StatusProbeEnum.ALL); - assertEquals("Should be empty dir now", Tristate.TRUE, - status.isEmptyDirectory()); - } finally { - configuredMs.forgetMetadata(existingFile); - configuredMs.forgetMetadata(existingDir); - } - } - - /** - * Test tombstones don't get in the way of a listing of the - * root dir. - * This test needs to create a path which appears first in the listing, - * and an entry which can come later. To allow the test to proceed - * while other tests are running, the filename "0000" is used for that - * deleted entry. - */ - @Test - public void testTombstonesAndEmptyDirectories() throws Throwable { - S3AFileSystem fs = getFileSystem(); - assumeFilesystemHasMetadatastore(getFileSystem()); - - // Create the first and last files. - Path base = path(getMethodName()); - // use something ahead of all the ASCII alphabet characters so - // even during parallel test runs, this test is expected to work. - String first = "0000"; - Path firstPath = new Path(base, first); - - // this path is near the bottom of the ASCII string space. - // This isn't so critical. - String last = "zzzz"; - Path lastPath = new Path(base, last); - touch(fs, firstPath); - touch(fs, lastPath); - // Delete first entry (+assert tombstone) - assertDeleted(firstPath, false); - DynamoDBMetadataStore ddbMs = getRequiredDDBMetastore(fs); - DDBPathMetadata firstMD = ddbMs.get(firstPath); - assertNotNull("No MD for " + firstPath, firstMD); - assertTrue("Not a tombstone " + firstMD, - firstMD.isDeleted()); - // PUT child to store going past the FS entirely. - // This is not going to show up on S3Guard. - Path child = new Path(firstPath, "child"); - StoreContext ctx = fs.createStoreContext(); - String childKey = ctx.pathToKey(child); - String baseKey = ctx.pathToKey(base) + "/"; - AmazonS3 s3 = fs.getAmazonS3ClientForTesting("LIST"); - String bucket = ctx.getBucket(); - try { - createEmptyObject(fs, childKey); - - // Do a list - span(); - ListObjectsV2Request listReq = ctx.getRequestFactory() - .newListObjectsV2Request(baseKey, "/", 10); - ListObjectsV2Result listing = s3.listObjectsV2(listReq); - - // the listing has the first path as a prefix, because of the child - Assertions.assertThat(listing.getCommonPrefixes()) - .describedAs("The prefixes of a LIST of %s", base) - .contains(baseKey + first + "/"); - - // and the last file is one of the files - Stream files = listing.getObjectSummaries() - .stream() - .map(S3ObjectSummary::getKey); - Assertions.assertThat(files) - .describedAs("The files of a LIST of %s", base) - .contains(baseKey + last); - - // verify absolutely that the last file exists - assertPathExists("last file", lastPath); - - boolean isDDB = fs.getMetadataStore() instanceof DynamoDBMetadataStore; - // if DDB is the metastore, then we expect no FS requests to be made - // at all. - S3ATestUtils.MetricDiff listMetric = new S3ATestUtils.MetricDiff(fs, - Statistic.OBJECT_LIST_REQUEST); - S3ATestUtils.MetricDiff getMetric = new S3ATestUtils.MetricDiff(fs, - Statistic.OBJECT_METADATA_REQUESTS); - // do a getFile status with empty dir flag - S3AFileStatus status = getStatusWithEmptyDirFlag(fs, base); - assertNonEmptyDir(status); - if (isDDB) { - listMetric.assertDiffEquals( - "FileSystem called S3 LIST rather than use DynamoDB", - 0); - getMetric.assertDiffEquals( - "FileSystem called S3 GET rather than use DynamoDB", - 0); - LOG.info("Verified that DDB directory status was accepted"); - } - - } finally { - // try to recover from the defective state. - span(); - s3.deleteObject(bucket, childKey); - fs.delete(lastPath, true); - ddbMs.forgetMetadata(firstPath); - } - } - - protected void assertNonEmptyDir(final S3AFileStatus status) { - assertEquals("Should not be empty dir: " + status, Tristate.FALSE, - status.isEmptyDirectory()); - } - - /** - * Get the DynamoDB metastore; assume false if it is of a different - * type. - * @return extracted and cast metadata store. - */ - @SuppressWarnings("ConstantConditions") - private DynamoDBMetadataStore getRequiredDDBMetastore(S3AFileSystem fs) { - MetadataStore ms = fs.getMetadataStore(); - assume("Not a DynamoDBMetadataStore: " + ms, - ms instanceof DynamoDBMetadataStore); - return (DynamoDBMetadataStore) ms; - } - - /** - * From {@code S3AFileSystem.createEmptyObject()}. - * @param fs filesystem - * @param key key - */ - private void createEmptyObject(S3AFileSystem fs, String key) - throws IOException { - - try (AuditSpan span = fs.getAuditSpanSource() - .createSpan(INVOCATION_MKDIRS.getSymbol(), key, null)) { - fs.createMkdirOperationCallbacks().createFakeDirectory(key); - } - } - - @Test - public void testDirMarkerDelete() throws Throwable { - S3AFileSystem fs = getFileSystem(); - assumeFilesystemHasMetadatastore(getFileSystem()); - Path baseDir = methodPath(); - Path subFile = new Path(baseDir, "subdir/file.txt"); - // adds the s3guard entry - fs.mkdirs(baseDir); - touch(fs, subFile); - // PUT a marker - createEmptyObject(fs, fs.pathToKey(baseDir) + "/"); - fs.delete(baseDir, true); - assertPathDoesNotExist("Should have been deleted", baseDir); - - // now create the dir again - fs.mkdirs(baseDir); - FileStatus fileStatus = fs.getFileStatus(baseDir); - Assertions.assertThat(fileStatus) - .matches(FileStatus::isDirectory, "Not a directory"); - Assertions.assertThat(fs.listStatus(baseDir)) - .describedAs("listing of %s", baseDir) - .isEmpty(); - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3GuardListConsistency.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3GuardListConsistency.java deleted file mode 100644 index 0d5471939cd..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3GuardListConsistency.java +++ /dev/null @@ -1,769 +0,0 @@ -/* - * 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.hadoop.fs.s3a; - -import com.amazonaws.services.s3.model.ListObjectsV2Request; -import com.amazonaws.services.s3.model.ListObjectsV2Result; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FSDataOutputStream; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.LocatedFileStatus; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.RemoteIterator; -import org.apache.hadoop.fs.contract.AbstractFSContract; -import org.apache.hadoop.fs.contract.ContractTestUtils; -import org.apache.hadoop.fs.contract.s3a.S3AContract; - -import com.amazonaws.services.s3.model.S3ObjectSummary; -import org.apache.hadoop.thirdparty.com.google.common.collect.Lists; -import org.assertj.core.api.Assertions; -import org.junit.Assume; -import org.junit.Test; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.stream.Collectors; - -import static org.apache.hadoop.fs.contract.ContractTestUtils.touch; -import static org.apache.hadoop.fs.contract.ContractTestUtils.writeTextFile; -import static org.apache.hadoop.fs.s3a.Constants.*; -import static org.apache.hadoop.fs.s3a.FailureInjectionPolicy.*; -import static org.apache.hadoop.fs.s3a.InconsistentAmazonS3Client.*; -import static org.apache.hadoop.test.LambdaTestUtils.intercept; - -/** - * Test S3Guard list consistency feature by injecting delayed listObjects() - * visibility via {@link InconsistentAmazonS3Client}. - * - * Tests here generally: - * 1. Use the inconsistency injection mentioned above. - * 2. Only run when S3Guard is enabled. - */ -public class ITestS3GuardListConsistency extends AbstractS3ATestBase { - - private Invoker invoker; - - @Override - public void setup() throws Exception { - super.setup(); - invoker = new Invoker(new S3ARetryPolicy(getConfiguration()), - Invoker.NO_OP - ); - skipIfClientSideEncryption(); - Assume.assumeTrue("No metadata store in test filesystem", - getFileSystem().hasMetadataStore()); - } - - @Override - public void teardown() throws Exception { - if (getFileSystem() != null && getFileSystem() - .getAmazonS3Client() instanceof InconsistentAmazonS3Client) { - clearInconsistency(getFileSystem()); - } - super.teardown(); - } - - @Override - protected AbstractFSContract createContract(Configuration conf) { - conf.setClass(S3_CLIENT_FACTORY_IMPL, InconsistentS3ClientFactory.class, - S3ClientFactory.class); - // Other configs would break test assumptions - conf.set(FAIL_INJECT_INCONSISTENCY_KEY, DEFAULT_DELAY_KEY_SUBSTRING); - conf.setFloat(FAIL_INJECT_INCONSISTENCY_PROBABILITY, 1.0f); - // this is a long value to guarantee that the inconsistency holds - // even over long-haul connections, and in the debugger too/ - conf.setLong(FAIL_INJECT_INCONSISTENCY_MSEC, 600_1000L); - return new S3AContract(conf); - } - - /** - * Helper function for other test cases: does a single rename operation and - * validates the aftermath. - * @param mkdirs Directories to create - * @param srcdirs Source paths for rename operation - * @param dstdirs Destination paths for rename operation - * @param yesdirs Files that must exist post-rename (e.g. srcdirs children) - * @param nodirs Files that must not exist post-rename (e.g. dstdirs children) - * @throws Exception - */ - private void doTestRenameSequence(Path[] mkdirs, Path[] srcdirs, - Path[] dstdirs, Path[] yesdirs, Path[] nodirs) throws Exception { - S3AFileSystem fs = getFileSystem(); - - - if (mkdirs != null) { - for (Path mkdir : mkdirs) { - assertTrue(fs.mkdirs(mkdir)); - } - clearInconsistency(fs); - } - - assertEquals("srcdirs and dstdirs must have equal length", - srcdirs.length, dstdirs.length); - for (int i = 0; i < srcdirs.length; i++) { - assertTrue("Rename returned false: " + srcdirs[i] + " -> " + dstdirs[i], - fs.rename(srcdirs[i], dstdirs[i])); - } - - for (Path yesdir : yesdirs) { - assertTrue("Path was supposed to exist: " + yesdir, fs.exists(yesdir)); - } - for (Path nodir : nodirs) { - assertFalse("Path is not supposed to exist: " + nodir, fs.exists(nodir)); - } - } - - /** - * Delete an array of paths; log exceptions. - * @param paths paths to delete - */ - private void deletePathsQuietly(Path...paths) { - for (Path dir : paths) { - try { - getFileSystem().delete(dir, true); - } catch (IOException e) { - LOG.info("Failed to delete {}: {}", dir, e.toString()); - LOG.debug("Delete failure:, e"); - } - } - } - - /** - * Tests that after renaming a directory, the original directory and its - * contents are indeed missing and the corresponding new paths are visible. - * @throws Exception - */ - @Test - public void testConsistentListAfterRename() throws Exception { - Path d1f = path("d1/f"); - Path d1f2 = path("d1/f-" + DEFAULT_DELAY_KEY_SUBSTRING); - Path[] mkdirs = {d1f, d1f2}; - Path d1 = path("d1"); - Path[] srcdirs = {d1}; - Path d2 = path("d2"); - Path[] dstdirs = {d2}; - Path d2f2 = path("d2/f-" + DEFAULT_DELAY_KEY_SUBSTRING); - Path[] yesdirs = {d2, path("d2/f"), d2f2}; - Path[] nodirs = { - d1, d1f, d1f2}; - try { - doTestRenameSequence(mkdirs, srcdirs, dstdirs, yesdirs, nodirs); - } finally { - clearInconsistency(getFileSystem()); - deletePathsQuietly(d1, d2, d1f, d1f2, d2f2); - } - } - - /** - * Tests a circular sequence of renames to verify that overwriting recently - * deleted files and reading recently created files from rename operations - * works as expected. - * @throws Exception - */ - @Test - public void testRollingRenames() throws Exception { - Path[] dir0 = {path("rolling/1")}; - Path[] dir1 = {path("rolling/2")}; - Path[] dir2 = {path("rolling/3")}; - // These sets have to be in reverse order compared to the movement - Path[] setA = {dir1[0], dir0[0]}; - Path[] setB = {dir2[0], dir1[0]}; - Path[] setC = {dir0[0], dir2[0]}; - - try { - for(int i = 0; i < 2; i++) { - Path[] firstSet = i == 0 ? setA : null; - doTestRenameSequence(firstSet, setA, setB, setB, dir0); - doTestRenameSequence(null, setB, setC, setC, dir1); - doTestRenameSequence(null, setC, setA, setA, dir2); - } - - S3AFileSystem fs = getFileSystem(); - intercept(FileNotFoundException.class, () -> - fs.rename(dir2[0], dir1[0])); - assertTrue("Renaming over existing file should have succeeded", - fs.rename(dir1[0], dir0[0])); - } finally { - clearInconsistency(getFileSystem()); - deletePathsQuietly(dir0[0], dir1[0], dir2[0]); - } - } - - /** - * Tests that deleted files immediately stop manifesting in list operations - * even when the effect in S3 is delayed. - * @throws Exception - */ - @Test - public void testConsistentListAfterDelete() throws Exception { - S3AFileSystem fs = getFileSystem(); - - // Any S3 keys that contain DELAY_KEY_SUBSTRING will be delayed - // in listObjects() results via InconsistentS3Client - Path inconsistentPath = - path("a/b/dir3-" + DEFAULT_DELAY_KEY_SUBSTRING); - - Path dir1 = path("a/b/dir1"); - Path dir2 = path("a/b/dir2"); - Path[] testDirs = { - dir1, - dir2, - inconsistentPath}; - - for (Path path : testDirs) { - assertTrue("Can't create directory: " + path, fs.mkdirs(path)); - } - clearInconsistency(fs); - for (Path path : testDirs) { - assertTrue("Can't delete path: " + path, fs.delete(path, false)); - } - - FileStatus[] paths = fs.listStatus(path("a/b/")); - List list = new ArrayList<>(); - for (FileStatus fileState : paths) { - list.add(fileState.getPath()); - } - Assertions.assertThat(list) - .describedAs("Expected deleted files to be excluded") - .doesNotContain(dir1) - .doesNotContain(dir2) - .doesNotContain(inconsistentPath); - } - - /** - * Tests that rename immediately after files in the source directory are - * deleted results in exactly the correct set of destination files and none - * of the source files. - * @throws Exception - */ - @Test - public void testConsistentRenameAfterDelete() throws Exception { - S3AFileSystem fs = getFileSystem(); - - // Any S3 keys that contain DELAY_KEY_SUBSTRING will be delayed - // in listObjects() results via InconsistentS3Client - Path inconsistentPath = - path("a/b/dir3-" + DEFAULT_DELAY_KEY_SUBSTRING); - - Path[] testDirs = {path("a/b/dir1"), - path("a/b/dir2"), - inconsistentPath}; - - for (Path path : testDirs) { - assertTrue(fs.mkdirs(path)); - } - clearInconsistency(fs); - assertTrue(fs.delete(testDirs[1], false)); - assertTrue(fs.delete(testDirs[2], false)); - - ContractTestUtils.rename(fs, path("a"), path("a3")); - ContractTestUtils.assertPathsDoNotExist(fs, - "Source paths shouldn't exist post rename operation", - testDirs[0], testDirs[1], testDirs[2]); - FileStatus[] paths = fs.listStatus(path("a3/b")); - List list = new ArrayList<>(); - for (FileStatus fileState : paths) { - list.add(fileState.getPath()); - } - Assertions.assertThat(list) - .contains(path("a3/b/dir1")) - .doesNotContain(path("a3/b/dir2")) - .doesNotContain(path("a3/b/dir3-" + - DEFAULT_DELAY_KEY_SUBSTRING)); - - intercept(FileNotFoundException.class, "", - "Recently renamed dir should not be visible", - () -> S3AUtils.mapLocatedFiles( - fs.listFilesAndEmptyDirectories(path("a"), true), - FileStatus::getPath)); - } - - @Test - public void testConsistentListStatusAfterPut() throws Exception { - - S3AFileSystem fs = getFileSystem(); - - // Any S3 keys that contain DELAY_KEY_SUBSTRING will be delayed - // in listObjects() results via InconsistentS3Client - Path inconsistentPath = - path("a/b/dir3-" + DEFAULT_DELAY_KEY_SUBSTRING); - - Path[] testDirs = {path("a/b/dir1"), - path("a/b/dir2"), - inconsistentPath}; - - for (Path path : testDirs) { - assertTrue(fs.mkdirs(path)); - } - - FileStatus[] paths = fs.listStatus(path("a/b/")); - List list = new ArrayList<>(); - for (FileStatus fileState : paths) { - list.add(fileState.getPath()); - } - Assertions.assertThat(list) - .contains(path("a/b/dir1")) - .contains(path("a/b/dir2")) - .contains(inconsistentPath); - } - - /** - * Similar to {@link #testConsistentListStatusAfterPut()}, this tests that the - * FS listLocatedStatus() call will return consistent list. - */ - @Test - public void testConsistentListLocatedStatusAfterPut() throws Exception { - final S3AFileSystem fs = getFileSystem(); - String rootDir = "doTestConsistentListLocatedStatusAfterPut"; - fs.mkdirs(path(rootDir)); - - final int[] numOfPaths = {0, 1, 5}; - for (int normalPathNum : numOfPaths) { - for (int delayedPathNum : new int[] {0, 2}) { - LOG.info("Testing with normalPathNum={}, delayedPathNum={}", - normalPathNum, delayedPathNum); - doTestConsistentListLocatedStatusAfterPut(fs, rootDir, normalPathNum, - delayedPathNum); - } - } - } - - /** - * Helper method to implement the tests of consistent listLocatedStatus(). - * @param fs The S3 file system from contract - * @param normalPathNum number paths listed directly from S3 without delaying - * @param delayedPathNum number paths listed with delaying - * @throws Exception - */ - private void doTestConsistentListLocatedStatusAfterPut(S3AFileSystem fs, - String rootDir, int normalPathNum, int delayedPathNum) throws Exception { - final List testDirs = new ArrayList<>(normalPathNum + delayedPathNum); - int index = 0; - for (; index < normalPathNum; index++) { - testDirs.add(path(rootDir + "/dir-" + - index)); - } - for (; index < normalPathNum + delayedPathNum; index++) { - // Any S3 keys that contain DELAY_KEY_SUBSTRING will be delayed - // in listObjects() results via InconsistentS3Client - testDirs.add(path(rootDir + "/dir-" + index + - DEFAULT_DELAY_KEY_SUBSTRING)); - } - - for (Path path : testDirs) { - // delete the old test path (if any) so that when we call mkdirs() later, - // the to delay directories will be tracked via putObject() request. - fs.delete(path, true); - assertTrue(fs.mkdirs(path)); - } - - // this should return the union data from S3 and MetadataStore - final RemoteIterator statusIterator = - fs.listLocatedStatus(path(rootDir + "/")); - List list = new ArrayList<>(); - for (; statusIterator.hasNext();) { - list.add(statusIterator.next().getPath()); - } - - // This should fail without S3Guard, and succeed with it because part of the - // children under test path are delaying visibility - for (Path path : testDirs) { - assertTrue("listLocatedStatus should list " + path, list.contains(path)); - } - } - - /** - * Tests that the S3AFS listFiles() call will return consistent file list. - */ - @Test - public void testConsistentListFiles() throws Exception { - final S3AFileSystem fs = getFileSystem(); - - final int[] numOfPaths = {0, 2}; - for (int dirNum : numOfPaths) { - for (int normalFile : numOfPaths) { - for (int delayedFile : new int[] {0, 1}) { - for (boolean recursive : new boolean[] {true, false}) { - doTestListFiles(fs, dirNum, normalFile, delayedFile, recursive); - } - } - } - } - } - - /** - * Helper method to implement the tests of consistent listFiles(). - * - * The file structure has dirNum subdirectories, and each directory (including - * the test base directory itself) has normalFileNum normal files and - * delayedFileNum delayed files. - * - * @param fs The S3 file system from contract - * @param dirNum number of subdirectories - * @param normalFileNum number files in each directory without delay to list - * @param delayedFileNum number files in each directory with delay to list - * @param recursive listFiles recursively if true - * @throws Exception if any unexpected error - */ - private void doTestListFiles(S3AFileSystem fs, int dirNum, int normalFileNum, - int delayedFileNum, boolean recursive) throws Exception { - describe("Testing dirNum=%d, normalFile=%d, delayedFile=%d, " - + "recursive=%s", dirNum, normalFileNum, delayedFileNum, recursive); - final Path baseTestDir = path("doTestListFiles-" + dirNum + "-" - + normalFileNum + "-" + delayedFileNum + "-" + recursive); - // delete the old test path (if any) so that when we call mkdirs() later, - // the to delay sub directories will be tracked via putObject() request. - fs.delete(baseTestDir, true); - - // make subdirectories (if any) - final List testDirs = new ArrayList<>(dirNum + 1); - assertTrue(fs.mkdirs(baseTestDir)); - testDirs.add(baseTestDir); - for (int i = 0; i < dirNum; i++) { - final Path subdir = path(baseTestDir + "/dir-" + i); - assertTrue(fs.mkdirs(subdir)); - testDirs.add(subdir); - } - - final Collection fileNames - = new ArrayList<>(normalFileNum + delayedFileNum); - int index = 0; - for (; index < normalFileNum; index++) { - fileNames.add("file-" + index); - } - for (; index < normalFileNum + delayedFileNum; index++) { - // Any S3 keys that contain DELAY_KEY_SUBSTRING will be delayed - // in listObjects() results via InconsistentS3Client - fileNames.add("file-" + index + "-" + DEFAULT_DELAY_KEY_SUBSTRING); - } - - int filesAndEmptyDirectories = 0; - - // create files under each test directory - for (Path dir : testDirs) { - for (String fileName : fileNames) { - writeTextFile(fs, new Path(dir, fileName), "I, " + fileName, false); - filesAndEmptyDirectories++; - } - } - - // this should return the union data from S3 and MetadataStore - final RemoteIterator statusIterator - = fs.listFiles(baseTestDir, recursive); - final Collection listedFiles = new HashSet<>(); - for (; statusIterator.hasNext();) { - final FileStatus status = statusIterator.next(); - assertTrue("FileStatus " + status + " is not a file!", status.isFile()); - listedFiles.add(status.getPath()); - } - LOG.info("S3AFileSystem::listFiles('{}', {}) -> {}", - baseTestDir, recursive, listedFiles); - - // This should fail without S3Guard, and succeed with it because part of the - // files to list are delaying visibility - if (!recursive) { - // in this case only the top level files are listed - verifyFileIsListed(listedFiles, baseTestDir, fileNames); - assertEquals("Unexpected number of files returned by listFiles() call", - normalFileNum + delayedFileNum, listedFiles.size()); - } else { - for (Path dir : testDirs) { - verifyFileIsListed(listedFiles, dir, fileNames); - } - assertEquals("Unexpected number of files returned by listFiles() call", - filesAndEmptyDirectories, - listedFiles.size()); - } - } - - private static void verifyFileIsListed(Collection listedFiles, - Path currentDir, Collection fileNames) { - for (String fileName : fileNames) { - final Path file = new Path(currentDir, fileName); - assertTrue(file + " should have been listed", listedFiles.contains(file)); - } - } - - @Test - public void testCommitByRenameOperations() throws Throwable { - S3AFileSystem fs = getFileSystem(); - Path work = path("test-commit-by-rename-" + DEFAULT_DELAY_KEY_SUBSTRING); - Path task00 = new Path(work, "task00"); - fs.mkdirs(task00); - String name = "part-00"; - try (FSDataOutputStream out = - fs.create(new Path(task00, name), false)) { - out.writeChars("hello"); - } - for (FileStatus stat : fs.listStatus(task00)) { - fs.rename(stat.getPath(), work); - } - List files = new ArrayList<>(2); - for (FileStatus stat : fs.listStatus(work)) { - if (stat.isFile()) { - files.add(stat); - } - } - assertFalse("renamed file " + name + " not found in " + work, - files.isEmpty()); - assertEquals("more files found than expected in " + work - + " " + ls(work), 1, files.size()); - FileStatus status = files.get(0); - assertEquals("Wrong filename in " + status, - name, status.getPath().getName()); - } - - @Test - public void testInconsistentS3ClientDeletes() throws Throwable { - describe("Verify that delete adds tombstones which block entries" - + " returned in (inconsistent) listings"); - // Test only implemented for v2 S3 list API - assumeV2ListAPI(); - - S3AFileSystem fs = getFileSystem(); - Path root = path("testInconsistentS3ClientDeletes-" - + DEFAULT_DELAY_KEY_SUBSTRING); - for (int i = 0; i < 3; i++) { - fs.mkdirs(new Path(root, "dir-" + i)); - touch(fs, new Path(root, "file-" + i)); - for (int j = 0; j < 3; j++) { - touch(fs, new Path(new Path(root, "dir-" + i), "file-" + i + "-" + j)); - } - } - clearInconsistency(fs); - - String key = fs.pathToKey(root) + "/"; - - LOG.info("Listing objects before executing delete()"); - ListObjectsV2Result preDeleteDelimited = listObjectsV2(fs, key, "/"); - ListObjectsV2Result preDeleteUndelimited = listObjectsV2(fs, key, null); - - LOG.info("Deleting the directory {}", root); - fs.delete(root, true); - LOG.info("Delete completed; listing results which must exclude deleted" - + " paths"); - - ListObjectsV2Result postDeleteDelimited = listObjectsV2(fs, key, "/"); - boolean stripTombstones = false; - assertObjectSummariesEqual( - "InconsistentAmazonS3Client added back objects incorrectly " + - "in a non-recursive listing", - preDeleteDelimited, postDeleteDelimited, - stripTombstones); - - assertListSizeEqual("InconsistentAmazonS3Client added back prefixes incorrectly " + - "in a non-recursive listing", - preDeleteDelimited.getCommonPrefixes(), - postDeleteDelimited.getCommonPrefixes()); - LOG.info("Executing Deep listing"); - ListObjectsV2Result postDeleteUndelimited = listObjectsV2(fs, key, null); - assertObjectSummariesEqual("InconsistentAmazonS3Client added back objects" - + " incorrectly in a recursive listing", - preDeleteUndelimited, postDeleteUndelimited, - stripTombstones); - - assertListSizeEqual("InconsistentAmazonS3Client added back prefixes incorrectly " + - "in a recursive listing", - preDeleteUndelimited.getCommonPrefixes(), - postDeleteUndelimited.getCommonPrefixes() - ); - } - - private void assertObjectSummariesEqual(final String message, - final ListObjectsV2Result expected, - final ListObjectsV2Result actual, - final boolean stripTombstones) { - assertCollectionsEqual( - message, - stringify(expected.getObjectSummaries(), stripTombstones), - stringify(actual.getObjectSummaries(), stripTombstones)); - } - - List stringify(List objects, - boolean stripTombstones) { - return objects.stream() - .filter(s -> !stripTombstones || !(s.getKey().endsWith("/"))) - .map(s -> s.getKey()) - .collect(Collectors.toList()); - } - - /** - * Require the v2 S3 list API. - */ - protected void assumeV2ListAPI() { - Assume.assumeTrue(getConfiguration() - .getInt(LIST_VERSION, DEFAULT_LIST_VERSION) == 2); - } - - /** - * Verify that delayed S3 listings doesn't stop the FS from deleting - * a directory tree. This has not always been the case; this test - * verifies the fix and prevents regression. - */ - @Test - public void testDeleteUsesS3Guard() throws Throwable { - describe("Verify that delete() uses S3Guard to get a consistent" - + " listing of its directory structure"); - assumeV2ListAPI(); - S3AFileSystem fs = getFileSystem(); - Path root = path( - "testDeleteUsesS3Guard-" + DEFAULT_DELAY_KEY_SUBSTRING); - for (int i = 0; i < 3; i++) { - Path path = new Path(root, "file-" + i); - touch(fs, path); - } - // we now expect the listing to miss these - String key = fs.pathToKey(root) + "/"; - - // verify that the inconsistent listing does not show these - LOG.info("Listing objects before executing delete()"); - List preDeletePaths = objectsToPaths(listObjectsV2(fs, key, null)); - Assertions.assertThat(preDeletePaths) - .isEmpty(); - // do the delete - fs.delete(root, true); - - // now go through every file and verify that it is not there. - // if you comment out the delete above and run this test case, - // the assertion will fail; this is how the validity of the assertions - // were verified. - clearInconsistency(fs); - List postDeletePaths = - objectsToPaths(listObjectsV2(fs, key, null)); - Assertions.assertThat(postDeletePaths) - .isEmpty(); - } - - private List objectsToPaths(ListObjectsV2Result r) { - S3AFileSystem fs = getFileSystem(); - return r.getObjectSummaries().stream() - .map(s -> fs.keyToQualifiedPath(s.getKey())) - .collect(Collectors.toList()); - } - - /** - * Tests that the file's eTag and versionId are preserved in recursive - * listings. - */ - @Test - public void testListingReturnsVersionMetadata() throws Throwable { - S3AFileSystem fs = getFileSystem(); - - // write simple file - Path parent = path(getMethodName()); - Path file = new Path(parent, "file1"); - try (FSDataOutputStream outputStream = fs.create(file)) { - outputStream.writeChars("hello"); - } - - // get individual file status - FileStatus[] fileStatuses = fs.listStatus(file); - assertEquals(1, fileStatuses.length); - S3AFileStatus status = (S3AFileStatus) fileStatuses[0]; - String eTag = status.getETag(); - assertNotNull("Etag in " + eTag, eTag); - String versionId = status.getVersionId(); - - // get status through recursive directory listing - RemoteIterator filesIterator = fs.listFiles( - parent, true); - List files = Lists.newArrayList(); - while (filesIterator.hasNext()) { - files.add(filesIterator.next()); - } - Assertions.assertThat(files) - .hasSize(1); - - // ensure eTag and versionId are preserved in directory listing - S3ALocatedFileStatus locatedFileStatus = - (S3ALocatedFileStatus) files.get(0); - assertEquals("etag of " + locatedFileStatus, - eTag, locatedFileStatus.getETag()); - assertEquals("versionID of " + locatedFileStatus, - versionId, locatedFileStatus.getVersionId()); - } - - /** - * Assert that the two collections match using - * object equality of the elements within. - * @param message text for the assertion - * @param expected expected list - * @param actual actual list - * @param type of list - */ - private void assertCollectionsEqual(String message, - Collection expected, - Collection actual) { - Assertions.assertThat(actual) - .describedAs(message) - .containsExactlyInAnyOrderElementsOf(expected); - } - - /** - * Assert that the two list sizes match; failure message includes the lists. - * @param message text for the assertion - * @param expected expected list - * @param actual actual list - * @param type of list - */ - private void assertListSizeEqual(String message, - List expected, - List actual) { - String leftContents = expected.stream() - .map(n -> n.toString()) - .collect(Collectors.joining("\n")); - String rightContents = actual.stream() - .map(n -> n.toString()) - .collect(Collectors.joining("\n")); - String summary = "\nExpected:" + leftContents - + "\n-----------\n" - + "Actual:" + rightContents - + "\n-----------\n"; - - if (expected.size() != actual.size()) { - LOG.error(message + summary); - } - assertEquals(message + summary, expected.size(), actual.size()); - } - - /** - * Retrying v2 list directly through the s3 client. - * @param fs filesystem - * @param key key to list under - * @param delimiter any delimiter - * @return the listing - * @throws IOException on error - */ - @Retries.RetryRaw - private ListObjectsV2Result listObjectsV2(S3AFileSystem fs, - String key, String delimiter) throws IOException { - ListObjectsV2Request k = fs.createListObjectsRequest(key, delimiter) - .getV2(); - return invoker.retryUntranslated("list", true, - () -> { - return fs.getAmazonS3ClientForTesting("list").listObjectsV2(k); - }); - } - -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3GuardOutOfBandOperations.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3GuardOutOfBandOperations.java deleted file mode 100644 index 2d4173d1c2a..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3GuardOutOfBandOperations.java +++ /dev/null @@ -1,1191 +0,0 @@ -/* - * 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.hadoop.fs.s3a; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.URI; -import java.util.Arrays; -import java.util.Collection; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; - -import org.assertj.core.api.Assertions; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import org.apache.hadoop.fs.FSDataInputStream; -import org.apache.hadoop.fs.FutureDataInputStreamBuilder; -import org.apache.hadoop.fs.s3a.s3guard.NullMetadataStore; -import org.apache.hadoop.io.IOUtils; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.FSDataOutputStream; -import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.impl.ChangeDetectionPolicy.Source; -import org.apache.hadoop.fs.s3a.s3guard.DirListingMetadata; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.PathMetadata; -import org.apache.hadoop.fs.s3a.s3guard.ITtlTimeProvider; -import org.apache.hadoop.fs.contract.ContractTestUtils; -import org.apache.hadoop.test.LambdaTestUtils; - -import static org.apache.hadoop.fs.contract.ContractTestUtils.readBytesToString; -import static org.apache.hadoop.fs.contract.ContractTestUtils.readDataset; -import static org.apache.hadoop.fs.contract.ContractTestUtils.toChar; -import static org.apache.hadoop.fs.contract.ContractTestUtils.touch; -import static org.apache.hadoop.fs.contract.ContractTestUtils.writeTextFile; -import static org.apache.hadoop.fs.s3a.Constants.AUTHORITATIVE_PATH; -import static org.apache.hadoop.fs.s3a.Constants.CHANGE_DETECT_MODE; -import static org.apache.hadoop.fs.s3a.Constants.CHANGE_DETECT_MODE_NONE; -import static org.apache.hadoop.fs.s3a.Constants.METADATASTORE_AUTHORITATIVE; -import static org.apache.hadoop.fs.s3a.Constants.METADATASTORE_METADATA_TTL; -import static org.apache.hadoop.fs.s3a.Constants.RETRY_INTERVAL; -import static org.apache.hadoop.fs.s3a.Constants.RETRY_LIMIT; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_CONSISTENCY_RETRY_INTERVAL; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_CONSISTENCY_RETRY_LIMIT; -import static org.apache.hadoop.fs.s3a.Constants.S3_METADATA_STORE_IMPL; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.PROBE_INTERVAL_MILLIS; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.STABILIZATION_TIME; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.TIMESTAMP_SLEEP; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.checkListingContainsPath; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.checkListingDoesNotContainPath; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.metadataStorePersistsAuthoritativeBit; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.read; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.readWithStatus; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.removeBaseAndBucketOverrides; -import static org.apache.hadoop.test.LambdaTestUtils.eventually; -import static org.apache.hadoop.test.LambdaTestUtils.intercept; - -import static org.apache.hadoop.test.LambdaTestUtils.interceptFuture; -import static org.junit.Assume.assumeTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * - * This integration test is for documenting and defining how S3Guard should - * behave in case of out-of-band (OOB) operations. - *
    - * The behavior is the following in case of S3AFileSystem.getFileStatus:
    - * A client with S3Guard
    - * B client without S3Guard (Directly to S3)
    - *
    - * * OOB OVERWRITE, authoritative mode:
    - * ** A client creates F1 file
    - * ** B client overwrites F1 file with F2 (Same, or different file size)
    - * ** A client's getFileStatus returns F1 metadata
    - *
    - * * OOB OVERWRITE, NOT authoritative mode:
    - * ** A client creates F1 file
    - * ** B client overwrites F1 file with F2 (Same, or different file size)
    - * ** A client's getFileStatus returns F2 metadata. In not authoritative
    - * mode we check S3 for the file. If the modification time of the file in S3
    - * is greater than in S3Guard, we can safely return the S3 file metadata and
    - * update the cache.
    - *
    - * * OOB DELETE, authoritative mode:
    - * ** A client creates F file
    - * ** B client deletes F file
    - * ** A client's getFileStatus returns that the file is still there
    - *
    - * * OOB DELETE, NOT authoritative mode:
    - * ** A client creates F file
    - * ** B client deletes F file
    - * ** A client's getFileStatus returns that the file is still there
    - *
    - * As you can see, authoritative and NOT authoritative mode behaves the same
    - * at OOB DELETE case.
    - *
    - * The behavior is the following in case of S3AFileSystem.listStatus:
    - * * File status in metadata store gets updated during the listing (in
    - * S3Guard.dirListingUnion) the same way as in getFileStatus.
    - * 
    - */ -@RunWith(Parameterized.class) -public class ITestS3GuardOutOfBandOperations extends AbstractS3ATestBase { - - private S3AFileSystem guardedFs; - private S3AFileSystem rawFS; - - private MetadataStore realMs; - - /** - * Is the "real" FS Authoritative. - */ - private final boolean authoritative; - - /** - * Test array for parameterized test runs. - * @return a list of parameter tuples. - */ - @Parameterized.Parameters(name="auth={0}") - public static Collection params() { - return Arrays.asList(new Object[][]{ - {true}, {false} - }); - } - - public ITestS3GuardOutOfBandOperations(final boolean authoritative) { - this.authoritative = authoritative; - } - - /** - * By changing the method name, the thread name is changed and - * so you can see in the logs which mode is being tested. - * @return a string to use for the thread namer. - */ - @Override - protected String getMethodName() { - return super.getMethodName() + - (authoritative ? "-auth" : "-nonauth"); - } - - @Override - protected Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - // reduce retry limit so FileNotFoundException cases timeout faster, - // speeding up the tests - removeBaseAndBucketOverrides(conf, - RETRY_LIMIT, - RETRY_INTERVAL, - S3GUARD_CONSISTENCY_RETRY_INTERVAL, - S3GUARD_CONSISTENCY_RETRY_LIMIT, - CHANGE_DETECT_MODE, - METADATASTORE_METADATA_TTL); - conf.setInt(RETRY_LIMIT, 3); - conf.setInt(S3GUARD_CONSISTENCY_RETRY_LIMIT, 3); - conf.set(CHANGE_DETECT_MODE, CHANGE_DETECT_MODE_NONE); - final String delay = "10ms"; - conf.set(RETRY_INTERVAL, delay); - conf.set(S3GUARD_CONSISTENCY_RETRY_INTERVAL, delay); - conf.set(METADATASTORE_METADATA_TTL, delay); - return conf; - } - - @Before - public void setup() throws Exception { - super.setup(); - S3AFileSystem fs = getFileSystem(); - // These test will fail if no ms - assumeTrue("FS needs to have a metadatastore.", - fs.hasMetadataStore()); - assumeTrue("Metadatastore should persist authoritative bit", - metadataStorePersistsAuthoritativeBit(fs.getMetadataStore())); - - // This test setup shares a single metadata store across instances, - // so that test runs with a local FS work. - // but this needs to be addressed in teardown, where the guarded fs - // needs to be detached from the metadata store before it is closed, - realMs = fs.getMetadataStore(); - // now we create a new FS with the auth parameter - guardedFs = createGuardedFS(authoritative); - assertTrue("No S3Guard store for " + guardedFs, - guardedFs.hasMetadataStore()); - assertEquals("Authoritative status in " + guardedFs, - authoritative, guardedFs.hasAuthoritativeMetadataStore()); - - // create raw fs without s3guard - rawFS = createUnguardedFS(); - assertFalse("Raw FS still has S3Guard " + rawFS, - rawFS.hasMetadataStore()); - nameThread(); - } - - @Override - public void teardown() throws Exception { - if (guardedFs != null) { - // detach from the (shared) metadata store. - guardedFs.setMetadataStore(new NullMetadataStore()); - // and only then close it. - IOUtils.cleanupWithLogger(LOG, guardedFs); - } - IOUtils.cleanupWithLogger(LOG, rawFS); - super.teardown(); - } - - /** - * Create a new FS which is the same config as the test FS, except - * that it is guarded with the specific authoritative mode. - * @param authoritativeMode mode of the new FS's metastore - * @return the new FS - */ - private S3AFileSystem createGuardedFS(boolean authoritativeMode) - throws Exception { - S3AFileSystem testFS = getFileSystem(); - Configuration config = new Configuration(testFS.getConf()); - URI uri = testFS.getUri(); - - removeBaseAndBucketOverrides(uri.getHost(), config, - CHANGE_DETECT_MODE, - METADATASTORE_AUTHORITATIVE, - METADATASTORE_METADATA_TTL, - AUTHORITATIVE_PATH); - config.setBoolean(METADATASTORE_AUTHORITATIVE, authoritativeMode); - config.setLong(METADATASTORE_METADATA_TTL, - 5_000); - final S3AFileSystem gFs = createFS(uri, config); - // set back the same metadata store instance - gFs.setMetadataStore(realMs); - return gFs; - } - - /** - * Create a test filesystem which is always unguarded. - * This filesystem MUST be closed in test teardown. - * @return the new FS - */ - private S3AFileSystem createUnguardedFS() throws Exception { - S3AFileSystem testFS = getFileSystem(); - Configuration config = new Configuration(testFS.getConf()); - URI uri = testFS.getUri(); - - removeBaseAndBucketOverrides(uri.getHost(), config, - S3_METADATA_STORE_IMPL); - removeBaseAndBucketOverrides(uri.getHost(), config, - METADATASTORE_AUTHORITATIVE, - AUTHORITATIVE_PATH); - return createFS(uri, config); - } - - /** - * Create and initialize a new filesystem. - * This filesystem MUST be closed in test teardown. - * @param uri FS URI - * @param config config. - * @return new instance - * @throws IOException failure - */ - private S3AFileSystem createFS(final URI uri, final Configuration config) - throws IOException { - S3AFileSystem fs2 = new S3AFileSystem(); - fs2.initialize(uri, config); - return fs2; - } - - @Test - public void testSameLengthOverwrite() throws Exception { - String firstText = "hello, world!"; - String secondText = "HELLO, WORLD!"; - overwriteFile(firstText, secondText); - } - - @Test - public void testLongerLengthOverwrite() throws Exception { - String firstText = "Hello, World!"; - String secondText = firstText + " " + firstText; - overwriteFile(firstText, secondText); - } - - @Test - public void testOutOfBandDeletes() throws Exception { - - Path testFileName = path("OutOfBandDelete-" + UUID.randomUUID()); - outOfBandDeletes(testFileName, authoritative); - } - - @Test - public void testListingSameLengthOverwrite() throws Exception { - overwriteFileInListing("THE TEXT", "the text"); - } - - @Test - public void testListingLongerLengthOverwrite() throws Exception { - overwriteFileInListing("THE TEXT", "THE LONGER TEXT"); - } - - /** - * Tests that tombstone expiry is implemented. If a file is created raw - * while the tombstone exist in ms for with the same name then S3Guard will - * check S3 for the file. - * - * Seq: create guarded; delete guarded; create raw (same path); read guarded; - * This will fail if no tombstone expiry is set - * - * @throws Exception - */ - @Test - public void testTombstoneExpiryGuardedDeleteRawCreate() throws Exception { - boolean allowAuthoritative = authoritative; - Path testFilePath = path("TEGDRC-" + UUID.randomUUID() + "/file"); - LOG.info("Allow authoritative param: {}", allowAuthoritative); - String originalText = "some test"; - String newText = "the new originalText for test"; - - final ITtlTimeProvider originalTimeProvider = - guardedFs.getTtlTimeProvider(); - try { - final AtomicLong now = new AtomicLong(1); - final AtomicLong metadataTtl = new AtomicLong(1); - - // SET TTL TIME PROVIDER FOR TESTING - ITtlTimeProvider testTimeProvider = - new ITtlTimeProvider() { - @Override public long getNow() { - return now.get(); - } - - @Override public long getMetadataTtl() { - return metadataTtl.get(); - } - }; - guardedFs.setTtlTimeProvider(testTimeProvider); - - // CREATE GUARDED - createAndAwaitFs(guardedFs, testFilePath, originalText); - - // DELETE GUARDED - deleteGuardedTombstoned(guardedFs, testFilePath, now); - - // CREATE RAW - createAndAwaitFs(rawFS, testFilePath, newText); - - // CHECK LISTING - THE FILE SHOULD NOT BE THERE, EVEN IF IT'S CREATED RAW - checkListingDoesNotContainPath(guardedFs, testFilePath); - - // CHANGE TTL SO ENTRY (& TOMBSTONE METADATA) WILL EXPIRE - long willExpire = now.get() + metadataTtl.get() + 1L; - now.set(willExpire); - LOG.info("willExpire: {}, ttlNow: {}; ttlTTL: {}", willExpire, - testTimeProvider.getNow(), testTimeProvider.getMetadataTtl()); - - // READ GUARDED - // This should fail in authoritative mode since we trust the metadatastore - // despite of the expiry. The metadata will not expire. - if (authoritative) { - intercept(FileNotFoundException.class, testFilePath.toString(), - "File should not be present in the metedatastore in authoritative mode.", - () -> readBytesToString(guardedFs, testFilePath, newText.length())); - } else { - String newRead = readBytesToString(guardedFs, testFilePath, - newText.length()); - - // CHECK LISTING - THE FILE SHOULD BE THERE, TOMBSTONE EXPIRED - checkListingContainsPath(guardedFs, testFilePath); - - // we can assert that the originalText is the new one, which created raw - LOG.info("Old: {}, New: {}, Read: {}", originalText, newText, newRead); - assertEquals("The text should be modified with a new.", newText, - newRead); - } - - } finally { - guardedFs.delete(testFilePath, true); - guardedFs.setTtlTimeProvider(originalTimeProvider); - } - } - - private void createAndAwaitFs(S3AFileSystem fs, Path testFilePath, - String text) throws Exception { - writeTextFile(fs, testFilePath, text, true); - final FileStatus newStatus = awaitFileStatus(fs, testFilePath); - assertNotNull("Newly created file status should not be null.", newStatus); - } - - private void deleteGuardedTombstoned(S3AFileSystem guarded, - Path testFilePath, AtomicLong now) throws Exception { - guarded.delete(testFilePath, true); - - final PathMetadata metadata = - guarded.getMetadataStore().get(testFilePath); - assertNotNull("Created file metadata should not be null in ms", - metadata); - assertEquals("Created file metadata last_updated should equal with " - + "mocked now", now.get(), metadata.getLastUpdated()); - - intercept(FileNotFoundException.class, testFilePath.toString(), - "This file should throw FNFE when reading through " - + "the guarded fs, and the metadatastore tombstoned the file.", - () -> guarded.getFileStatus(testFilePath)); - } - - /** - * createNonRecursive must fail if the parent directory has been deleted, - * and succeed if the tombstone has expired and the directory has been - * created out of band. - */ - @Test - public void testCreateNonRecursiveFailsIfParentDeleted() throws Exception { - LOG.info("Authoritative mode: {}", authoritative); - - String dirToDelete = methodName + UUID.randomUUID().toString(); - String fileToTry = dirToDelete + "/theFileToTry"; - - final Path dirPath = path(dirToDelete); - final Path filePath = path(fileToTry); - - // Create a directory with - ITtlTimeProvider mockTimeProvider = mock(ITtlTimeProvider.class); - ITtlTimeProvider originalTimeProvider = guardedFs.getTtlTimeProvider(); - - try { - guardedFs.setTtlTimeProvider(mockTimeProvider); - when(mockTimeProvider.getNow()).thenReturn(100L); - when(mockTimeProvider.getMetadataTtl()).thenReturn(5L); - - // CREATE DIRECTORY - guardedFs.mkdirs(dirPath); - - // DELETE DIRECTORY - guardedFs.delete(dirPath, true); - - // WRITE TO DELETED DIRECTORY - FAIL - intercept(FileNotFoundException.class, - dirToDelete, - "createNonRecursive must fail if the parent directory has been deleted.", - () -> createNonRecursive(guardedFs, filePath)); - - // CREATE THE DIRECTORY RAW - rawFS.mkdirs(dirPath); - awaitFileStatus(rawFS, dirPath); - - // SET TIME SO METADATA EXPIRES - when(mockTimeProvider.getNow()).thenReturn(110L); - - // WRITE TO DELETED DIRECTORY - // - FAIL ON AUTH = TRUE - // - SUCCESS ON AUTH = FALSE - if (authoritative) { - intercept(FileNotFoundException.class, filePath.getParent().toString(), - "Parent does not exist, so in authoritative mode this should fail.", - () -> createNonRecursive(guardedFs, filePath)); - } else { - createNonRecursive(guardedFs, filePath); - } - - } finally { - guardedFs.delete(filePath, true); - guardedFs.delete(dirPath, true); - guardedFs.setTtlTimeProvider(originalTimeProvider); - } - } - - /** - * When lastUpdated = 0 the entry should not expire. This is a special case - * eg. for old metadata entries - */ - @Test - public void testLastUpdatedZeroWontExpire() throws Exception { - LOG.info("Authoritative mode: {}", authoritative); - - String testFile = methodName + UUID.randomUUID().toString() + - "/theFileToTry"; - - long ttl = 10L; - final Path filePath = path(testFile); - - ITtlTimeProvider mockTimeProvider = mock(ITtlTimeProvider.class); - ITtlTimeProvider originalTimeProvider = guardedFs.getTtlTimeProvider(); - - try { - guardedFs.setTtlTimeProvider(mockTimeProvider); - when(mockTimeProvider.getMetadataTtl()).thenReturn(ttl); - - // create a file while the NOW is 0, so it will set 0 as the last_updated - when(mockTimeProvider.getNow()).thenReturn(0L); - touch(guardedFs, filePath); - deleteFile(guardedFs, filePath); - - final PathMetadata pathMetadata = - guardedFs.getMetadataStore().get(filePath); - assertNotNull("pathMetadata should not be null after deleting with " - + "tombstones", pathMetadata); - assertEquals("pathMetadata lastUpdated field should be 0", 0, - pathMetadata.getLastUpdated()); - - // set the time, so the metadata would expire - when(mockTimeProvider.getNow()).thenReturn(2*ttl); - intercept(FileNotFoundException.class, filePath.toString(), - "This file should throw FNFE when reading through " - + "the guarded fs, and the metadatastore tombstoned the file. " - + "The tombstone won't expire if lastUpdated is set to 0.", - () -> guardedFs.getFileStatus(filePath)); - - } finally { - guardedFs.delete(filePath, true); - guardedFs.setTtlTimeProvider(originalTimeProvider); - } - } - - /** - * 1. File is deleted in the guarded fs. - * 2. File is replaced in the raw fs. - * 3. File is deleted in the guarded FS after the expiry time. - * 4. File MUST NOT exist in raw FS. - */ - @Test - public void deleteAfterTombstoneExpiryOobCreate() throws Exception { - LOG.info("Authoritative mode: {}", authoritative); - - String testFile = methodName + UUID.randomUUID().toString() + - "/theFileToTry"; - - long ttl = 10L; - final Path filePath = path(testFile); - - ITtlTimeProvider mockTimeProvider = mock(ITtlTimeProvider.class); - ITtlTimeProvider originalTimeProvider = guardedFs.getTtlTimeProvider(); - - try { - guardedFs.setTtlTimeProvider(mockTimeProvider); - when(mockTimeProvider.getMetadataTtl()).thenReturn(ttl); - - // CREATE AND DELETE WITH GUARDED FS - when(mockTimeProvider.getNow()).thenReturn(100L); - touch(guardedFs, filePath); - deleteFile(guardedFs, filePath); - - final PathMetadata pathMetadata = - guardedFs.getMetadataStore().get(filePath); - assertNotNull("pathMetadata should not be null after deleting with " - + "tombstones", pathMetadata); - - // REPLACE WITH RAW FS - touch(rawFS, filePath); - awaitFileStatus(rawFS, filePath); - - // SET EXPIRY TIME, SO THE TOMBSTONE IS EXPIRED - when(mockTimeProvider.getNow()).thenReturn(100L + 2 * ttl); - - // DELETE IN GUARDED FS - // NOTE: in auth this will be ineffective: - // we already have the tombstone marker on the item, it won't expire, - // so we don't delete the raw S3 file. - guardedFs.delete(filePath, true); - - // FILE MUST NOT EXIST IN RAW - // If authoritative, the file status can be retrieved raw: - // deleting with guarded FS won't do anything because the tombstone - // marker won't expire in auth mode. - // If not authoritative, we go to the S3 bucket and get an FNFE - if (authoritative) { - rawFS.getFileStatus(filePath); - } else { - intercept(FileNotFoundException.class, filePath.toString(), - "This file should throw FNFE when reading through " - + "the raw fs, and the guarded fs deleted the file.", - () -> rawFS.getFileStatus(filePath)); - } - - } finally { - guardedFs.delete(filePath, true); - guardedFs.setTtlTimeProvider(originalTimeProvider); - } - } - - /** - * Test that a tombstone won't hide an entry after it's expired in the - * listing. - */ - @Test - public void testRootTombstones() throws Exception { - long ttl = 10L; - ITtlTimeProvider mockTimeProvider = mock(ITtlTimeProvider.class); - when(mockTimeProvider.getMetadataTtl()).thenReturn(ttl); - when(mockTimeProvider.getNow()).thenReturn(100L); - ITtlTimeProvider originalTimeProvider = guardedFs.getTtlTimeProvider(); - guardedFs.setTtlTimeProvider(mockTimeProvider); - - Path base = path(getMethodName() + UUID.randomUUID()); - Path testFile = new Path(base, "test.file"); - - try { - touch(guardedFs, testFile); - ContractTestUtils.assertDeleted(guardedFs, testFile, false); - - touch(rawFS, testFile); - awaitFileStatus(rawFS, testFile); - - // the rawFS will include the file= - LambdaTestUtils.eventually(5000, 1000, () -> { - checkListingContainsPath(rawFS, testFile); - }); - - // it will be hidden because of the tombstone - checkListingDoesNotContainPath(guardedFs, testFile); - - // the tombstone is expired, so we should detect the file - // in non-authoritative mode - when(mockTimeProvider.getNow()).thenReturn(100 + ttl); - if (authoritative) { - checkListingDoesNotContainPath(guardedFs, testFile); - } else { - checkListingContainsPath(guardedFs, testFile); - } - } finally { - // cleanup - guardedFs.delete(base, true); - guardedFs.setTtlTimeProvider(originalTimeProvider); - } - } - - /** - * Perform an out-of-band delete. - * @param testFilePath filename - * @param allowAuthoritative is the store authoritative - * @throws Exception failure - */ - private void outOfBandDeletes( - final Path testFilePath, - final boolean allowAuthoritative) - throws Exception { - try { - // Create initial file - String text = "Hello, World!"; - writeTextFile(guardedFs, testFilePath, text, true); - awaitFileStatus(rawFS, testFilePath); - - // Delete the file without S3Guard (raw) - deleteFile(rawFS, testFilePath); - - // The check is the same if s3guard is authoritative and if it's not - // it should be in the ms - FileStatus status = guardedFs.getFileStatus(testFilePath); - LOG.info("Authoritative: {} status path: {}", - allowAuthoritative, status.getPath()); - final boolean versionedChangeDetection = - isVersionedChangeDetection(); - if (!versionedChangeDetection) { - expectExceptionWhenReading(testFilePath, text); - expectExceptionWhenReadingOpenFileAPI(testFilePath, text, null); - expectExceptionWhenReadingOpenFileAPI(testFilePath, text, status); - } else { - // FNFE not expected when using a bucket with object versioning - final String read1 = read(guardedFs, testFilePath); - assertEquals("File read from the auth FS", text, read1); - // and when the status is passed in, even the raw FS will ask for it - // via the versionId in the status - final String read2 = readWithStatus(rawFS, status); - assertEquals("File read from the raw FS", text, read2); - } - } finally { - guardedFs.delete(testFilePath, true); - } - } - - /** - * Overwrite a file out of band. - * @param firstText first text - * @param secondText second text - * @throws Exception failure - */ - private void overwriteFile(String firstText, String secondText) - throws Exception { - boolean allowAuthoritative = authoritative; - Path testFilePath = path("OverwriteFileTest-" + UUID.randomUUID()); - LOG.info("Allow authoritative param: {}", allowAuthoritative); - try { - // Create initial file - writeTextFile( - guardedFs, testFilePath, firstText, true); - // and cache the value for later - final FileStatus origStatus = awaitFileStatus(rawFS, testFilePath); - waitForDifferentTimestamps(); - // Overwrite the file without S3Guard - writeTextFile( - rawFS, testFilePath, secondText, true); - - // Read the file and verify the data - eventually(STABILIZATION_TIME, PROBE_INTERVAL_MILLIS, - () -> { - FileStatus rawFileStatus = rawFS.getFileStatus(testFilePath); - final FileStatus guardedFileStatus = - guardedFs.getFileStatus(testFilePath); - verifyFileStatusAsExpected(firstText, secondText, - allowAuthoritative, - origStatus, - rawFileStatus, - guardedFileStatus); - }); - } finally { - guardedFs.delete(testFilePath, true); - } - } - - /** - * Assert that an array has a given size; in failure the full string values - * of the array will be included, one per line. - * @param message message for errors. - * @param expected expected length. - * @param array the array to probe - */ - private void assertArraySize( - final String message, - final int expected, - final T[] array) { - if (expected != array.length) { - // condition is not met, build an error which includes all the entries - String listing = Arrays.stream(array) - .map(Object::toString) - .collect(Collectors.joining("\n")); - fail(message + ": expected " + expected + " elements but found " - + array.length - + "\n" + listing); - } - } - - /** - * Overwrite a file, verify that the text is different as is the timestamp. - * There are some pauses in the test to ensure that timestamps are different. - * @param firstText first text to write - * @param secondText second text to write - */ - private void overwriteFileInListing(String firstText, String secondText) - throws Exception { - boolean allowAuthoritative = authoritative; - - LOG.info("Authoritative mode enabled: {}", allowAuthoritative); - String rUUID = UUID.randomUUID().toString(); - String testDir = "dir-" + rUUID + "/"; - String testFile = testDir + "file-1-" + rUUID; - Path testDirPath = path(testDir); - Path testFilePath = guardedFs.qualify(path(testFile)); - - try { - // Create initial statusIterator with guarded ms - writeTextFile(guardedFs, testFilePath, firstText, true); - // and cache the value for later - final S3AFileStatus origStatus = awaitFileStatus(rawFS, testFilePath); - assertNotNull("No etag in raw status " + origStatus, - origStatus.getETag()); - - // Do a listing to cache the lists. Should be authoritative if it's set. - final S3AFileStatus[] origList = (S3AFileStatus[]) guardedFs.listStatus( - testDirPath); - assertArraySize("Added one file to the new dir, so the number of " - + "files in the dir should be one.", 1, origList); - S3AFileStatus origGuardedFileStatus = origList[0]; - assertNotNull("No etag in origGuardedFileStatus " + origGuardedFileStatus, - origGuardedFileStatus.getETag()); - final DirListingMetadata dirListingMetadata = - realMs.listChildren(guardedFs.qualify(testDirPath)); - assertListingAuthority(allowAuthoritative, dirListingMetadata); - - // a brief pause to guarantee timestamps are different. - waitForDifferentTimestamps(); - - // Update file with second text without S3Guard (raw) - deleteFile(rawFS, testFilePath); - - // write to the test path with the second text - writeTextFile(rawFS, testFilePath, secondText, true); - // and await it becoming visible again. - final FileStatus rawFileStatus = awaitFileStatus(rawFS, testFilePath); - - // check listing in guarded store. - final S3AFileStatus[] modList = (S3AFileStatus[]) guardedFs.listStatus( - testDirPath); - assertArraySize("Added one file to the new dir then modified it, " - + "so the number of files in the dir should be one.", 1, - modList); - assertEquals("The only file path in the directory listing should be " - + "equal to the testFilePath.", testFilePath, - modList[0].getPath()); - - // Read the file and verify the data - eventually(STABILIZATION_TIME, PROBE_INTERVAL_MILLIS, - () -> { - final FileStatus guardedFileStatus = - guardedFs.getFileStatus(testFilePath); - verifyFileStatusAsExpected(firstText, secondText, - allowAuthoritative, - origStatus, - rawFileStatus, - guardedFileStatus); - }); - } finally { - guardedFs.delete(testDirPath, true); - } - } - - private void deleteFile(final S3AFileSystem fs, final Path testFilePath) - throws Exception { - fs.delete(testFilePath, true); - awaitDeletedFileDisappearance(fs, testFilePath); - } - - - /** - * Verify that the file status of a file which has been overwritten - * is as expected, throwing informative exceptions if not. - * @param firstText text of the first write - * @param secondText text of the second - * @param allowAuthoritative is S3Guard being authoritative - * @param origStatus filestatus of the first written file - * @param rawFileStatus status of the updated file from the raw FS - * @param guardedFileStatus status of the updated file from the guarded FS - */ - private void verifyFileStatusAsExpected(final String firstText, - final String secondText, - final boolean allowAuthoritative, - final FileStatus origStatus, - final FileStatus rawFileStatus, - final FileStatus guardedFileStatus) { - String stats = "\nRaw: " + rawFileStatus.toString() + - "\nGuarded: " + guardedFileStatus.toString(); - if (firstText.length() != secondText.length()) { - // the file lengths are different, so compare that first. - // it's not going to be brittle to timestamps, and easy to understand - // when there is an error. - - // check the file length in the raw FS To verify that status is actually - // stabilized w.r.t the last write. - long expectedLength = secondText.length(); - assertEquals("Length of raw file status did not match the updated text " - + rawFileStatus, - expectedLength, rawFileStatus.getLen()); - // now compare the lengths of the the raw and guarded files - long guardedLength = guardedFileStatus.getLen(); - if (allowAuthoritative) { - // expect the length to be out of sync - assertNotEquals( - "File length in authoritative table with " + stats, - expectedLength, guardedLength); - } else { - assertEquals( - "File length in non-authoritative table with " + stats, - expectedLength, guardedLength); - } - } - // check etag. This relies on first and second text being different. - final S3AFileStatus rawS3AFileStatus = (S3AFileStatus) rawFileStatus; - final S3AFileStatus guardedS3AFileStatus = (S3AFileStatus) - guardedFileStatus; - final S3AFileStatus origS3AFileStatus = (S3AFileStatus) origStatus; - assertNotEquals( - "raw status still no to date with changes" + stats, - origS3AFileStatus.getETag(), rawS3AFileStatus.getETag()); - if (allowAuthoritative) { - // expect the etag to be out of sync - assertNotEquals( - "etag in authoritative table with " + stats, - rawS3AFileStatus.getETag(), guardedS3AFileStatus.getETag()); - } else { - assertEquals( - "etag in non-authoritative table with " + stats, - rawS3AFileStatus.getETag(), guardedS3AFileStatus.getETag()); - } - // Next: modification time. - long rawModTime = rawFileStatus.getModificationTime(); - long guardedModTime = guardedFileStatus.getModificationTime(); - assertNotEquals( - "Updated file still has original timestamp\n" - + " original " + origStatus + stats, - origStatus.getModificationTime(), rawModTime); - if (allowAuthoritative) { - // If authoritative is allowed metadata is not updated, so mod_time - // won't match - assertNotEquals("Authoritative is enabled, so metadata is not " - + "updated in ms, so mod_time won't match. Expecting " - + "different values for raw and guarded filestatus." - + stats, - rawModTime, - guardedModTime); - } else { - // If authoritative is not enabled metadata is updated, mod_time - // will match - assertEquals("Authoritative is disabled, so metadata is" - + " updated in ms, so mod_time must match. Expecting " - + " same values for raw and guarded filestatus." - + stats, - rawModTime, - guardedModTime); - } - } - - /** - * A brief pause to guarantee timestamps are different. - * This doesn't have to be as long as a stabilization delay. - */ - private void waitForDifferentTimestamps() throws InterruptedException { - Thread.sleep(TIMESTAMP_SLEEP); - } - - /** - * Assert that a listing has the specific authority. - * @param expectAuthoritative expect authority bit of listing - * @param dirListingMetadata listing to check - */ - private void assertListingAuthority(final boolean expectAuthoritative, - final DirListingMetadata dirListingMetadata) { - if (expectAuthoritative) { - assertTrue("DirListingMeta should be authoritative if authoritative " - + "mode is enabled.", - dirListingMetadata.isAuthoritative()); - } else { - assertFalse("DirListingMeta should not be authoritative if " - + "authoritative mode is disabled.", - dirListingMetadata.isAuthoritative()); - } - } - - /** - * Delete a file and use listStatus to build up the S3Guard cache. - */ - @Test - public void testListingDelete() throws Exception { - - boolean allowAuthoritative = authoritative; - LOG.info("Authoritative mode enabled: {}", allowAuthoritative); - String rUUID = UUID.randomUUID().toString(); - String testDir = "dir-" + rUUID + "/"; - String testFile = testDir + "file-1-" + rUUID; - Path testDirPath = path(testDir); - Path testFilePath = guardedFs.qualify(path(testFile)); - String text = "Some random text"; - - try { - // Create initial statusIterator with real ms - writeTextFile( - guardedFs, testFilePath, text, true); - awaitFileStatus(rawFS, testFilePath); - - // Do a listing to cache the lists. Should be authoritative if it's set. - final FileStatus[] origList = guardedFs.listStatus(testDirPath); - assertEquals("Added one file to the new dir, so the number of " - + "files in the dir should be one.", 1, origList.length); - final DirListingMetadata dirListingMetadata = - realMs.listChildren(guardedFs.qualify(testDirPath)); - assertListingAuthority(allowAuthoritative, dirListingMetadata); - - // Delete the file without S3Guard (raw) - deleteFile(rawFS, testFilePath); - - // now, versioned FS or not, it will not be readable from the - // raw FS, and this will fail in both open APIs during the open - // phase, rather than when a read is attempted. - interceptFuture(FileNotFoundException.class, "", - rawFS.openFile(testFilePath).build()); - intercept(FileNotFoundException.class, () -> - rawFS.open(testFilePath).close()); - - // File status will be still readable from s3guard - S3AFileStatus status = (S3AFileStatus) - guardedFs.getFileStatus(testFilePath); - LOG.info("authoritative: {} status: {}", allowAuthoritative, status); - if (isVersionedChangeDetection() && status.getVersionId() != null) { - // when the status entry has a version ID, then that may be used - // when opening the file on what is clearly a versioned store. - int length = text.length(); - byte[] bytes = readOpenFileAPI(guardedFs, testFilePath, length, null); - Assertions.assertThat(toChar(bytes)) - .describedAs("openFile(%s)", testFilePath) - .isEqualTo(text); - // reading the rawFS with status will also work. - bytes = readOpenFileAPI(rawFS, testFilePath, length, status); - Assertions.assertThat(toChar(bytes)) - .describedAs("openFile(%s)", testFilePath) - .isEqualTo(text); - bytes = readDataset(guardedFs, testFilePath, length); - Assertions.assertThat(toChar(bytes)) - .describedAs("open(%s)", testFilePath) - .isEqualTo(text); - } else { - // unversioned sequence - expectExceptionWhenReading(testFilePath, text); - expectExceptionWhenReadingOpenFileAPI(testFilePath, text, null); - expectExceptionWhenReadingOpenFileAPI(testFilePath, text, status); - } - } finally { - guardedFs.delete(testDirPath, true); - } - } - - private boolean isVersionedChangeDetection() { - return getFileSystem().getChangeDetectionPolicy().getSource() - == Source.VersionId; - } - - /** - * We expect the read to fail with an FNFE: open will be happy. - * @param testFilePath path of the test file - * @param text the context in the file. - * @throws Exception failure other than the FNFE - */ - private void expectExceptionWhenReading(Path testFilePath, String text) - throws Exception { - try (FSDataInputStream in = guardedFs.open(testFilePath)) { - intercept(FileNotFoundException.class, () -> { - byte[] bytes = new byte[text.length()]; - return in.read(bytes, 0, bytes.length); - }); - } - } - - /** - * We expect the read to fail with an FNFE: open will be happy. - * @param testFilePath path of the test file - * @param text the context in the file. - * @param status optional status for the withFileStatus operation. - * @throws Exception failure other than the FNFE - */ - private void expectExceptionWhenReadingOpenFileAPI( - Path testFilePath, String text, FileStatus status) - throws Exception { - expectExceptionWhenReadingOpenFileAPI(guardedFs, - testFilePath, text, status); - } - - /** - * We expect the read to fail with an FNFE: open will be happy. - * @param fs filesystem - * @param testFilePath path of the test file - * @param text the context in the file. - * @param status optional status for the withFileStatus operation. - * @throws Exception failure other than the FNFE - */ - private void expectExceptionWhenReadingOpenFileAPI( - final S3AFileSystem fs, - final Path testFilePath - , final String text, - final FileStatus status) - throws Exception { - final FutureDataInputStreamBuilder builder - = fs.openFile(testFilePath); - if (status != null) { - builder.withFileStatus(status); - } - try (FSDataInputStream in = builder.build().get()) { - intercept(FileNotFoundException.class, () -> { - byte[] bytes = new byte[text.length()]; - return in.read(bytes, 0, bytes.length); - }); - } - } - - /** - * Open and read a file with the openFile API. - * @param fs FS to read from - * @param testFilePath path of the test file - * @param len data length to read - * @param status optional status for the withFileStatus operation. - * @throws Exception failure - * @return the data - */ - private byte[] readOpenFileAPI( - S3AFileSystem fs, - Path testFilePath, - int len, - FileStatus status) throws Exception { - FutureDataInputStreamBuilder builder = fs.openFile(testFilePath); - if (status != null) { - builder.withFileStatus(status); - } - try (FSDataInputStream in = builder.build().get()) { - byte[] bytes = new byte[len]; - in.readFully(0, bytes); - return bytes; - } - } - - /** - * Wait for a deleted file to no longer be visible. - * @param fs filesystem - * @param testFilePath path to query - * @throws Exception failure - */ - private void awaitDeletedFileDisappearance(final S3AFileSystem fs, - final Path testFilePath) throws Exception { - eventually( - STABILIZATION_TIME, PROBE_INTERVAL_MILLIS, - () -> intercept(FileNotFoundException.class, - () -> fs.getFileStatus(testFilePath))); - } - - /** - * Wait for a file to be visible. - * @param fs filesystem - * @param testFilePath path to query - * @return the file status. - * @throws Exception failure - */ - private S3AFileStatus awaitFileStatus(S3AFileSystem fs, - final Path testFilePath) - throws Exception { - return (S3AFileStatus) eventually( - STABILIZATION_TIME, PROBE_INTERVAL_MILLIS, - () -> fs.getFileStatus(testFilePath)); - } - - @Test - public void testDeleteIgnoresTombstones() throws Throwable { - describe("Verify that directory delete goes behind tombstones"); - Path dir = path("oobdir"); - Path testFilePath = new Path(dir, "file"); - // create a file under the store - createAndAwaitFs(guardedFs, testFilePath, "Original File is long"); - // Delete the file leaving a tombstone in the metastore - LOG.info("Initial delete of guarded FS dir {}", dir); - guardedFs.delete(dir, true); -// deleteFile(guardedFs, testFilePath); - awaitDeletedFileDisappearance(guardedFs, testFilePath); - // add a new file in raw - createAndAwaitFs(rawFS, testFilePath, "hi!"); - // now we need to be sure that the file appears in a listing - awaitListingContainsChild(rawFS, dir, testFilePath); - - // now a hack to remove the empty dir up the tree - Path sibling = new Path(dir, "sibling"); - guardedFs.mkdirs(sibling); - // now do a delete of the parent dir. This is required to also - // check the underlying fs. - LOG.info("Deleting guarded FS dir {} with OOB child", dir); - guardedFs.delete(dir, true); - LOG.info("Now waiting for child to be deleted in raw fs: {}", testFilePath); - - // so eventually the file will go away. - // this is required to be true in auth as well as non-auth. - - awaitDeletedFileDisappearance(rawFS, testFilePath); - } - - /** - * Wait for a file to be visible. - * @param fs filesystem - * @param testFilePath path to query - * @throws Exception failure - */ - private void awaitListingContainsChild(S3AFileSystem fs, - final Path dir, - final Path testFilePath) - throws Exception { - LOG.info("Awaiting list of {} to include {}", dir, testFilePath); - eventually( - STABILIZATION_TIME, PROBE_INTERVAL_MILLIS, - () -> { - FileStatus[] stats = fs.listStatus(dir); - Assertions.assertThat(stats) - .describedAs("listing of %s", dir) - .filteredOn(s -> s.getPath().equals(testFilePath)) - .isNotEmpty(); - return null; - }); - } - - private FSDataOutputStream createNonRecursive(FileSystem fs, Path path) - throws Exception { - return fs - .createNonRecursive(path, false, 4096, (short) 3, (short) 4096, null); - } - -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3GuardTtl.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3GuardTtl.java deleted file mode 100644 index 4143283d71f..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3GuardTtl.java +++ /dev/null @@ -1,402 +0,0 @@ -/* - * 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.hadoop.fs.s3a; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.s3guard.DirListingMetadata; -import org.apache.hadoop.fs.s3a.s3guard.ITtlTimeProvider; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.S3Guard; - -import org.assertj.core.api.Assertions; -import org.junit.Assume; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import static org.apache.hadoop.fs.contract.ContractTestUtils.touch; -import static org.apache.hadoop.fs.s3a.Constants.METADATASTORE_AUTHORITATIVE; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.isMetadataStoreAuthoritative; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.metadataStorePersistsAuthoritativeBit; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * These tests are testing the S3Guard TTL (time to live) features. - */ -@RunWith(Parameterized.class) -public class ITestS3GuardTtl extends AbstractS3ATestBase { - - private final boolean authoritative; - - /** - * Test array for parameterized test runs. - * @return a list of parameter tuples. - */ - @Parameterized.Parameters(name = "auth={0}") - public static Collection params() { - return Arrays.asList(new Object[][]{ - {true}, {false} - }); - } - - /** - * By changing the method name, the thread name is changed and - * so you can see in the logs which mode is being tested. - * @return a string to use for the thread namer. - */ - @Override - protected String getMethodName() { - return super.getMethodName() + - (authoritative ? "-auth" : "-nonauth"); - } - - public ITestS3GuardTtl(boolean authoritative) { - this.authoritative = authoritative; - } - - /** - * Patch the configuration - this test needs disabled filesystem caching. - * These tests modify the fs instance that would cause flaky tests. - * @return a configuration - */ - @Override - protected Configuration createConfiguration() { - Configuration configuration = super.createConfiguration(); - S3ATestUtils.disableFilesystemCaching(configuration); - configuration = - S3ATestUtils.prepareTestConfiguration(configuration); - configuration.setBoolean(METADATASTORE_AUTHORITATIVE, authoritative); - return configuration; - } - - @Override - public void setup() throws Exception { - super.setup(); - Assume.assumeTrue(getFileSystem().hasMetadataStore()); - } - - @Test - public void testDirectoryListingAuthoritativeTtl() throws Exception { - LOG.info("Authoritative mode: {}", authoritative); - - final S3AFileSystem fs = getFileSystem(); - Assume.assumeTrue(fs.hasMetadataStore()); - final MetadataStore ms = fs.getMetadataStore(); - - Assume.assumeTrue("MetadataStore should be capable for authoritative " - + "storage of directories to run this test.", - metadataStorePersistsAuthoritativeBit(ms)); - - Assume.assumeTrue("MetadataStore should be authoritative for this test", - isMetadataStoreAuthoritative(getFileSystem().getConf())); - - ITtlTimeProvider mockTimeProvider = - mock(ITtlTimeProvider.class); - ITtlTimeProvider restoreTimeProvider = fs.getTtlTimeProvider(); - fs.setTtlTimeProvider(mockTimeProvider); - when(mockTimeProvider.getNow()).thenReturn(100L); - when(mockTimeProvider.getMetadataTtl()).thenReturn(1L); - - Path dir = path("ttl/"); - Path file = path("ttl/afile"); - - try { - fs.mkdirs(dir); - touch(fs, file); - - // get an authoritative listing in ms - fs.listStatus(dir); - - // check if authoritative - DirListingMetadata dirListing = - S3Guard.listChildrenWithTtl(ms, dir, mockTimeProvider, authoritative); - assertTrue("Listing should be authoritative.", - dirListing.isAuthoritative()); - // change the time, and assume it's not authoritative anymore - // if the metadatastore is not authoritative. - when(mockTimeProvider.getNow()).thenReturn(102L); - dirListing = S3Guard.listChildrenWithTtl(ms, dir, mockTimeProvider, - authoritative); - if (authoritative) { - assertTrue("Listing should be authoritative.", - dirListing.isAuthoritative()); - } else { - assertFalse("Listing should not be authoritative.", - dirListing.isAuthoritative()); - } - - // get an authoritative listing in ms again - retain test - fs.listStatus(dir); - // check if authoritative - dirListing = S3Guard.listChildrenWithTtl(ms, dir, mockTimeProvider, - authoritative); - assertTrue("Listing shoud be authoritative after listStatus.", - dirListing.isAuthoritative()); - } finally { - fs.delete(dir, true); - fs.setTtlTimeProvider(restoreTimeProvider); - } - } - - @Test - public void testFileMetadataExpiresTtl() throws Exception { - LOG.info("Authoritative mode: {}", authoritative); - - Path fileExpire1 = path("expirettl-" + UUID.randomUUID()); - Path fileExpire2 = path("expirettl-" + UUID.randomUUID()); - Path fileRetain = path("expirettl-" + UUID.randomUUID()); - - final S3AFileSystem fs = getFileSystem(); - Assume.assumeTrue(fs.hasMetadataStore()); - final MetadataStore ms = fs.getMetadataStore(); - - ITtlTimeProvider mockTimeProvider = mock(ITtlTimeProvider.class); - ITtlTimeProvider originalTimeProvider = fs.getTtlTimeProvider(); - - try { - fs.setTtlTimeProvider(mockTimeProvider); - when(mockTimeProvider.getMetadataTtl()).thenReturn(5L); - - // set the time, so the fileExpire1 will expire - when(mockTimeProvider.getNow()).thenReturn(100L); - touch(fs, fileExpire1); - // set the time, so fileExpire2 will expire - when(mockTimeProvider.getNow()).thenReturn(101L); - touch(fs, fileExpire2); - // set the time, so fileRetain won't expire - when(mockTimeProvider.getNow()).thenReturn(109L); - touch(fs, fileRetain); - final FileStatus origFileRetainStatus = fs.getFileStatus(fileRetain); - // change time, so the first two file metadata is expired - when(mockTimeProvider.getNow()).thenReturn(110L); - - // metadata is expired so this should refresh the metadata with - // last_updated to the getNow() if the store is not authoritative - final FileStatus fileExpire1Status = fs.getFileStatus(fileExpire1); - assertNotNull(fileExpire1Status); - if (authoritative) { - assertEquals(100L, ms.get(fileExpire1).getLastUpdated()); - } else { - assertEquals(110L, ms.get(fileExpire1).getLastUpdated()); - } - - // metadata is expired so this should refresh the metadata with - // last_updated to the getNow() if the store is not authoritative - final FileStatus fileExpire2Status = fs.getFileStatus(fileExpire2); - assertNotNull(fileExpire2Status); - if (authoritative) { - assertEquals(101L, ms.get(fileExpire2).getLastUpdated()); - } else { - assertEquals(110L, ms.get(fileExpire2).getLastUpdated()); - } - - final FileStatus fileRetainStatus = fs.getFileStatus(fileRetain); - assertEquals("Modification time of these files should be equal.", - origFileRetainStatus.getModificationTime(), - fileRetainStatus.getModificationTime()); - assertNotNull(fileRetainStatus); - assertEquals(109L, ms.get(fileRetain).getLastUpdated()); - } finally { - fs.delete(fileExpire1, true); - fs.delete(fileExpire2, true); - fs.delete(fileRetain, true); - fs.setTtlTimeProvider(originalTimeProvider); - } - } - - /** - * create(tombstone file) must succeed irrespective of overwrite flag. - */ - @Test - public void testCreateOnTombstonedFileSucceeds() throws Exception { - LOG.info("Authoritative mode: {}", authoritative); - final S3AFileSystem fs = getFileSystem(); - - String fileToTry = methodName + UUID.randomUUID().toString(); - - final Path filePath = path(fileToTry); - - // Create a directory with - ITtlTimeProvider mockTimeProvider = mock(ITtlTimeProvider.class); - ITtlTimeProvider originalTimeProvider = fs.getTtlTimeProvider(); - - try { - fs.setTtlTimeProvider(mockTimeProvider); - when(mockTimeProvider.getNow()).thenReturn(100L); - when(mockTimeProvider.getMetadataTtl()).thenReturn(5L); - - // CREATE A FILE - touch(fs, filePath); - - // DELETE THE FILE - TOMBSTONE - fs.delete(filePath, true); - - // CREATE THE SAME FILE WITHOUT ERROR DESPITE THE TOMBSTONE - touch(fs, filePath); - - } finally { - fs.delete(filePath, true); - fs.setTtlTimeProvider(originalTimeProvider); - } - } - - /** - * create("parent has tombstone") must always succeed (We dont check the - * parent), but after the file has been written, all entries up the tree - * must be valid. That is: the putAncestor code will correct everything - */ - @Test - public void testCreateParentHasTombstone() throws Exception { - LOG.info("Authoritative mode: {}", authoritative); - final S3AFileSystem fs = getFileSystem(); - - String dirToDelete = methodName + UUID.randomUUID().toString(); - String fileToTry = dirToDelete + "/theFileToTry"; - - final Path dirPath = path(dirToDelete); - final Path filePath = path(fileToTry); - - // Create a directory with - ITtlTimeProvider mockTimeProvider = mock(ITtlTimeProvider.class); - ITtlTimeProvider originalTimeProvider = fs.getTtlTimeProvider(); - - try { - fs.setTtlTimeProvider(mockTimeProvider); - when(mockTimeProvider.getNow()).thenReturn(100L); - when(mockTimeProvider.getMetadataTtl()).thenReturn(5L); - - // CREATE DIRECTORY - fs.mkdirs(dirPath); - - // DELETE DIRECTORY - fs.delete(dirPath, true); - - // WRITE TO DELETED DIRECTORY - SUCCESS - touch(fs, filePath); - - // SET TIME SO METADATA EXPIRES - when(mockTimeProvider.getNow()).thenReturn(110L); - - // WRITE TO DELETED DIRECTORY - SUCCESS - touch(fs, filePath); - - } finally { - fs.delete(filePath, true); - fs.delete(dirPath, true); - fs.setTtlTimeProvider(originalTimeProvider); - } - } - - /** - * Test that listing of metadatas is filtered from expired items. - */ - @Test - public void testListingFilteredExpiredItems() throws Exception { - LOG.info("Authoritative mode: {}", authoritative); - final S3AFileSystem fs = getFileSystem(); - - long oldTime = 100L; - long newTime = 110L; - long ttl = 9L; - final String basedir = "testListingFilteredExpiredItems"; - final Path tombstonedPath = path(basedir + "/tombstonedPath"); - final Path baseDirPath = path(basedir); - final List filesToCreate = new ArrayList<>(); - final MetadataStore ms = fs.getMetadataStore(); - - for (int i = 0; i < 10; i++) { - filesToCreate.add(path(basedir + "/file" + i)); - } - - ITtlTimeProvider mockTimeProvider = mock(ITtlTimeProvider.class); - ITtlTimeProvider originalTimeProvider = fs.getTtlTimeProvider(); - - try { - fs.setTtlTimeProvider(mockTimeProvider); - when(mockTimeProvider.getMetadataTtl()).thenReturn(ttl); - - // add and delete entry with the oldtime - when(mockTimeProvider.getNow()).thenReturn(oldTime); - touch(fs, tombstonedPath); - fs.delete(tombstonedPath, false); - - // create items with newTime - when(mockTimeProvider.getNow()).thenReturn(newTime); - for (Path path : filesToCreate) { - touch(fs, path); - } - - // listing will contain the tombstone with oldtime - when(mockTimeProvider.getNow()).thenReturn(oldTime); - final DirListingMetadata fullDLM = getDirListingMetadata(ms, baseDirPath); - List containedPaths = fullDLM.getListing().stream() - .map(pm -> pm.getFileStatus().getPath()) - .collect(Collectors.toList()); - Assertions.assertThat(containedPaths) - .describedAs("Full listing of path %s", baseDirPath) - .hasSize(11) - .contains(tombstonedPath); - - // listing will be filtered if the store is not authoritative, - // and won't contain the tombstone with oldtime - when(mockTimeProvider.getNow()).thenReturn(newTime); - final DirListingMetadata filteredDLM = S3Guard.listChildrenWithTtl( - ms, baseDirPath, mockTimeProvider, authoritative); - containedPaths = filteredDLM.getListing().stream() - .map(pm -> pm.getFileStatus().getPath()) - .collect(Collectors.toList()); - if (authoritative) { - Assertions.assertThat(containedPaths) - .describedAs("Full listing of path %s", baseDirPath) - .hasSize(11) - .contains(tombstonedPath); - } else { - Assertions.assertThat(containedPaths) - .describedAs("Full listing of path %s", baseDirPath) - .hasSize(10) - .doesNotContain(tombstonedPath); - } - } finally { - fs.delete(baseDirPath, true); - fs.setTtlTimeProvider(originalTimeProvider); - } - } - - protected DirListingMetadata getDirListingMetadata(final MetadataStore ms, - final Path baseDirPath) throws IOException { - final DirListingMetadata fullDLM = ms.listChildren(baseDirPath); - Assertions.assertThat(fullDLM) - .describedAs("Metastrore directory listing of %s", - baseDirPath) - .isNotNull(); - return fullDLM; - } - -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3GuardWriteBack.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3GuardWriteBack.java deleted file mode 100644 index c5e4cbcd459..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/ITestS3GuardWriteBack.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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.hadoop.fs.s3a; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.contract.ContractTestUtils; -import org.apache.hadoop.fs.s3a.s3guard.DirListingMetadata; -import org.apache.hadoop.fs.s3a.s3guard.S3Guard; - -import org.junit.Test; - -import java.io.IOException; -import java.net.URI; -import java.util.Arrays; - -import static org.apache.hadoop.fs.s3a.Constants.*; -import static org.junit.Assume.assumeTrue; - -/** - * Test cases that validate S3Guard's behavior for writing things like - * directory listings back to the MetadataStore. - */ -public class ITestS3GuardWriteBack extends AbstractS3ATestBase { - - @Override - public void setup() throws Exception { - assumeTrue("dirListingUnion always writes back records", - !S3Guard.DIR_MERGE_UPDATES_ALL_RECORDS_NONAUTH); - super.setup(); - } - - /** - * In listStatus(), when S3Guard is enabled, the full listing for a - * directory is "written back" to the MetadataStore before the listing is - * returned. Currently this "write back" behavior occurs when - * fs.s3a.metadatastore.authoritative is true. This test validates this - * behavior. - * @throws Exception on failure - */ - @Test - public void testListStatusWriteBack() throws Exception { - assumeTrue(getFileSystem().hasMetadataStore()); - - Path directory = path("ListStatusWriteBack"); - - // "raw" S3AFileSystem without S3Guard - S3AFileSystem noS3Guard = createTestFS(directory.toUri(), true, false); - - // Another with S3Guard and write-back disabled - S3AFileSystem noWriteBack = createTestFS(directory.toUri(), false, false); - - // Another S3Guard and write-back enabled - S3AFileSystem yesWriteBack = createTestFS(directory.toUri(), false, true); - - // delete the existing directory (in case of last test failure) - noS3Guard.delete(directory, true); - // Create a directory on S3 only - Path onS3 = new Path(directory, "OnS3"); - noS3Guard.mkdirs(onS3); - // Create a directory on both S3 and metadata store - Path onS3AndMS = new Path(directory, "OnS3AndMS"); - ContractTestUtils.assertPathDoesNotExist(noWriteBack, "path", onS3AndMS); - noWriteBack.mkdirs(onS3AndMS); - - FileStatus[] fsResults; - DirListingMetadata mdResults; - - // FS should return both even though S3Guard is not writing back to MS - fsResults = noWriteBack.listStatus(directory); - assertEquals("Filesystem enabled S3Guard without write back should have " - + "both /OnS3 and /OnS3AndMS: " + Arrays.toString(fsResults), - 2, fsResults.length); - - // Metadata store without write-back should still only contain /OnS3AndMS, - // because newly discovered /OnS3 is not written back to metadata store - mdResults = noWriteBack.getMetadataStore().listChildren(directory); - assertNotNull("No results from noWriteBack listChildren " + directory, - mdResults); - assertEquals("Metadata store without write back should still only know " - + "about /OnS3AndMS, but it has: " + mdResults, - 1, mdResults.numEntries()); - - // FS should return both (and will write it back) - fsResults = yesWriteBack.listStatus(directory); - assertEquals("Filesystem enabled S3Guard with write back should have" - + " both /OnS3 and /OnS3AndMS: " + Arrays.toString(fsResults), - 2, fsResults.length); - - // Metadata store with write-back should contain both because the newly - // discovered /OnS3 should have been written back to metadata store - mdResults = yesWriteBack.getMetadataStore().listChildren(directory); - assertEquals("Unexpected number of results from metadata store. " - + "Should have /OnS3 and /OnS3AndMS: " + mdResults, - 2, mdResults.numEntries()); - - // If we don't clean this up, the next test run will fail because it will - // have recorded /OnS3 being deleted even after it's written to noS3Guard. - getFileSystem().getMetadataStore().forgetMetadata(onS3); - } - - /** - * Create a separate S3AFileSystem instance for testing. - * There's a bit of complexity as it forces pushes up s3guard options from - * the base values to the per-bucket options. This stops explicit bucket - * settings in test XML configs from unintentionally breaking tests. - */ - private S3AFileSystem createTestFS(URI fsURI, boolean disableS3Guard, - boolean authoritativeMeta) throws IOException { - Configuration conf; - - // Create a FileSystem that is S3-backed only - conf = createConfiguration(); - String host = fsURI.getHost(); - String metastore; - - metastore = S3GUARD_METASTORE_NULL; - if (!disableS3Guard) { - // pick up the metadata store used by the main test - metastore = getFileSystem().getConf().get(S3_METADATA_STORE_IMPL); - assertNotEquals(S3GUARD_METASTORE_NULL, metastore); - } - - conf.set(Constants.S3_METADATA_STORE_IMPL, metastore); - conf.setBoolean(METADATASTORE_AUTHORITATIVE, authoritativeMeta); - conf.unset(AUTHORITATIVE_PATH); - S3AUtils.setBucketOption(conf, host, - METADATASTORE_AUTHORITATIVE, - Boolean.toString(authoritativeMeta)); - S3AUtils.setBucketOption(conf, host, - S3_METADATA_STORE_IMPL, metastore); - - S3AFileSystem fs = asS3AFS(FileSystem.newInstance(fsURI, conf)); - // do a check to verify that everything got through - assertEquals("Metadata store should have been disabled: " + fs, - disableS3Guard, !fs.hasMetadataStore()); - assertEquals("metastore option did not propagate", - metastore, fs.getConf().get(S3_METADATA_STORE_IMPL)); - - return fs; - - } - - private static S3AFileSystem asS3AFS(FileSystem fs) { - assertTrue("Not a S3AFileSystem: " + fs, fs instanceof S3AFileSystem); - return (S3AFileSystem)fs; - } - -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/MockS3AFileSystem.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/MockS3AFileSystem.java index 624bfd6c945..5c8383a68c4 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/MockS3AFileSystem.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/MockS3AFileSystem.java @@ -43,7 +43,6 @@ import org.apache.hadoop.fs.s3a.commit.staging.StagingTestBase; import org.apache.hadoop.fs.s3a.impl.RequestFactoryImpl; import org.apache.hadoop.fs.s3a.statistics.CommitterStatistics; import org.apache.hadoop.fs.s3a.statistics.impl.EmptyS3AStatisticsContext; -import org.apache.hadoop.fs.s3a.s3guard.BulkOperationState; import org.apache.hadoop.fs.statistics.DurationTrackerFactory; import org.apache.hadoop.util.Progressable; @@ -212,8 +211,7 @@ public class MockS3AFileSystem extends S3AFileSystem { } @Override - void finishedWrite(String key, long length, String eTag, String versionId, - BulkOperationState operationState) { + void finishedWrite(String key, long length, String eTag, String versionId) { } @@ -339,8 +337,7 @@ public class MockS3AFileSystem extends S3AFileSystem { @Override void deleteObjectAtPath(Path f, String key, - boolean isFile, - final BulkOperationState operationState) + boolean isFile) throws AmazonClientException, IOException { deleteObject(key); } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/S3ATestConstants.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/S3ATestConstants.java index 945d3f01c05..aca622a9e20 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/S3ATestConstants.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/S3ATestConstants.java @@ -163,21 +163,6 @@ public interface S3ATestConstants { @Deprecated String TEST_STS_ENDPOINT = "test.fs.s3a.sts.endpoint"; - /** - * Various S3Guard tests. - */ - String TEST_S3GUARD_PREFIX = "fs.s3a.s3guard.test"; - String TEST_S3GUARD_ENABLED = TEST_S3GUARD_PREFIX + ".enabled"; - String TEST_S3GUARD_AUTHORITATIVE = TEST_S3GUARD_PREFIX + ".authoritative"; - String TEST_S3GUARD_IMPLEMENTATION = TEST_S3GUARD_PREFIX + ".implementation"; - String TEST_S3GUARD_IMPLEMENTATION_LOCAL = "local"; - String TEST_S3GUARD_IMPLEMENTATION_DYNAMO = "dynamo"; - String TEST_S3GUARD_IMPLEMENTATION_NONE = "none"; - - String TEST_S3GUARD_DYNAMO_TABLE_PREFIX = - "fs.s3a.s3guard.test.dynamo.table.prefix"; - String TEST_S3GUARD_DYNAMO_TABLE_PREFIX_DEFAULT = "s3guard.test."; - /** * ACL for S3 Logging; used in some tests: {@value}. */ @@ -218,17 +203,6 @@ public interface S3ATestConstants { Duration TEST_SESSION_TOKEN_DURATION = Duration.ofSeconds( TEST_SESSION_TOKEN_DURATION_SECONDS); - /** - * Test table name to use during DynamoDB integration tests in - * {@code ITestDynamoDBMetadataStore}. - * - * The table will be modified, and deleted in the end of the tests. - * If this value is not set, the integration tests that would be destructive - * won't run. - */ - String S3GUARD_DDB_TEST_TABLE_NAME_KEY = - "fs.s3a.s3guard.ddb.test.table"; - /** * Test option to enable audits of the method path after * every test case. diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/S3ATestUtils.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/S3ATestUtils.java index 113b68ae6c6..b39e0f02fe3 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/S3ATestUtils.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/S3ATestUtils.java @@ -39,16 +39,11 @@ import org.apache.hadoop.fs.s3a.commit.CommitConstants; import org.apache.hadoop.fs.s3a.impl.ChangeDetectionPolicy; import org.apache.hadoop.fs.s3a.impl.ContextAccessors; -import org.apache.hadoop.fs.s3a.impl.InternalConstants; import org.apache.hadoop.fs.s3a.impl.StatusProbeEnum; import org.apache.hadoop.fs.s3a.impl.StoreContext; import org.apache.hadoop.fs.s3a.impl.StoreContextBuilder; import org.apache.hadoop.fs.s3a.statistics.BlockOutputStreamStatistics; import org.apache.hadoop.fs.s3a.statistics.impl.EmptyS3AStatisticsContext; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStoreCapabilities; -import org.apache.hadoop.fs.s3a.s3guard.S3Guard; -import org.apache.hadoop.fs.s3a.test.OperationTrackingStore; import org.apache.hadoop.fs.s3native.S3xLoginHelper; import org.apache.hadoop.io.DataInputBuffer; import org.apache.hadoop.io.DataOutputBuffer; @@ -63,7 +58,6 @@ import org.apache.hadoop.util.functional.CallableRaisingIOE; import com.amazonaws.auth.AWSCredentialsProvider; import org.assertj.core.api.Assertions; -import org.hamcrest.core.Is; import org.junit.Assert; import org.junit.Assume; import org.slf4j.Logger; @@ -77,7 +71,6 @@ import java.net.URISyntaxException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.Callable; @@ -85,17 +78,14 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkNotNull; +import static org.apache.hadoop.util.Preconditions.checkNotNull; import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.HADOOP_SECURITY_CREDENTIAL_PROVIDER_PATH; import static org.apache.commons.lang3.StringUtils.isNotEmpty; import static org.apache.hadoop.fs.contract.ContractTestUtils.skip; import static org.apache.hadoop.fs.impl.FutureIOSupport.awaitFuture; -import static org.apache.hadoop.fs.s3a.FailureInjectionPolicy.*; import static org.apache.hadoop.fs.s3a.S3ATestConstants.*; import static org.apache.hadoop.fs.s3a.Constants.*; import static org.apache.hadoop.fs.s3a.S3AUtils.buildEncryptionSecrets; -import static org.apache.hadoop.fs.s3a.S3AUtils.getEncryptionAlgorithm; -import static org.apache.hadoop.fs.s3a.S3AUtils.propagateBucketOptions; import static org.apache.hadoop.test.LambdaTestUtils.intercept; import static org.junit.Assert.*; @@ -115,10 +105,6 @@ public final class S3ATestUtils { public static final String UNSET_PROPERTY = "unset"; public static final int PURGE_DELAY_SECONDS = 60 * 60; - public static final int TIMESTAMP_SLEEP = 2000; - public static final int STABILIZATION_TIME = 20_000; - public static final int PROBE_INTERVAL_MILLIS = 500; - /** Add any deprecated keys. */ @SuppressWarnings("deprecation") private static void addDeprecatedKeys() { @@ -192,10 +178,7 @@ public final class S3ATestUtils { // make this whole class not run by default Assume.assumeTrue("No test filesystem in " + TEST_FS_S3A_NAME, liveTest); - // Skip if S3Guard and S3-CSE are enabled. - skipIfS3GuardAndS3CSEEnabled(conf); - // patch in S3Guard options - maybeEnableS3Guard(conf); + S3AFileSystem fs1 = new S3AFileSystem(); //enable purging in tests if (purge) { @@ -237,42 +220,24 @@ public final class S3ATestUtils { // make this whole class not run by default Assume.assumeTrue("No test filesystem in " + TEST_FS_S3A_NAME, liveTest); - // Skip if S3Guard and S3-CSE are enabled. - skipIfS3GuardAndS3CSEEnabled(conf); - // patch in S3Guard options - maybeEnableS3Guard(conf); FileContext fc = FileContext.getFileContext(testURI, conf); return fc; } /** - * Skip if S3Guard and S3CSE are enabled together. - * - * @param conf Test Configuration. - */ - private static void skipIfS3GuardAndS3CSEEnabled(Configuration conf) - throws IOException { - String encryptionMethod = getEncryptionAlgorithm(getTestBucketName(conf), - conf).getMethod(); - String metaStore = conf.getTrimmed(S3_METADATA_STORE_IMPL, ""); - if (encryptionMethod.equals(S3AEncryptionMethods.CSE_KMS.getMethod()) && - !metaStore.equals(S3GUARD_METASTORE_NULL)) { - skip("Skipped if CSE is enabled with S3Guard."); - } - } - - /** - * Either skip if PathIOE occurred due to S3CSE and S3Guard - * incompatibility or throw the PathIOE. + * Skip if PathIOE occurred due to exception which contains a message which signals + * an incompatibility or throw the PathIOE. * * @param ioe PathIOE being parsed. - * @throws PathIOException Throws PathIOE if it doesn't relate to S3CSE - * and S3Guard incompatibility. + * @param messages messages found in the PathIOE that trigger a test to skip + * @throws PathIOException Throws PathIOE if it doesn't relate to any message in {@code messages}. */ - public static void maybeSkipIfS3GuardAndS3CSEIOE(PathIOException ioe) + public static void skipIfIOEContainsMessage(PathIOException ioe, String...messages) throws PathIOException { - if (ioe.toString().contains(InternalConstants.CSE_S3GUARD_INCOMPATIBLE)) { - skip("Skipping since CSE is enabled with S3Guard."); + for (String message: messages) { + if (ioe.toString().contains(message)) { + skip("Skipping because: " + message); + } } throw ioe; } @@ -489,105 +454,6 @@ public final class S3ATestUtils { new Path("/" + testUniqueForkId, "test"); } - /** - * Test assumption that S3Guard is/is not enabled. - * @param shouldBeEnabled should S3Guard be enabled? - * @param originalConf configuration to check - * @throws URISyntaxException - */ - public static void assumeS3GuardState(boolean shouldBeEnabled, - Configuration originalConf) throws URISyntaxException { - boolean isEnabled = isS3GuardTestPropertySet(originalConf); - Assume.assumeThat("Unexpected S3Guard test state:" - + " shouldBeEnabled=" + shouldBeEnabled - + " and isEnabled=" + isEnabled, - shouldBeEnabled, Is.is(isEnabled)); - - final String fsname = originalConf.getTrimmed(TEST_FS_S3A_NAME); - Assume.assumeNotNull(fsname); - final String bucket = new URI(fsname).getHost(); - final Configuration conf = propagateBucketOptions(originalConf, bucket); - boolean usingNullImpl = S3GUARD_METASTORE_NULL.equals( - conf.getTrimmed(S3_METADATA_STORE_IMPL, S3GUARD_METASTORE_NULL)); - Assume.assumeThat("Unexpected S3Guard test state:" - + " shouldBeEnabled=" + shouldBeEnabled - + " but usingNullImpl=" + usingNullImpl, - shouldBeEnabled, Is.is(!usingNullImpl)); - } - - /** - * Is the test option for S3Guard set? - * @param conf configuration to examine. - * @return true if the config or system property turns s3guard tests on - */ - public static boolean isS3GuardTestPropertySet(final Configuration conf) { - return getTestPropertyBool(conf, TEST_S3GUARD_ENABLED, - conf.getBoolean(TEST_S3GUARD_ENABLED, false)); - } - - /** - * Conditionally set the S3Guard options from test properties. - * @param conf configuration - */ - public static void maybeEnableS3Guard(Configuration conf) { - if (isS3GuardTestPropertySet(conf)) { - // S3Guard is enabled. - boolean authoritative = getTestPropertyBool(conf, - TEST_S3GUARD_AUTHORITATIVE, - conf.getBoolean(TEST_S3GUARD_AUTHORITATIVE, true)); - String impl = getTestProperty(conf, TEST_S3GUARD_IMPLEMENTATION, - conf.get(TEST_S3GUARD_IMPLEMENTATION, - TEST_S3GUARD_IMPLEMENTATION_LOCAL)); - String implClass = ""; - switch (impl) { - case TEST_S3GUARD_IMPLEMENTATION_LOCAL: - implClass = S3GUARD_METASTORE_LOCAL; - break; - case TEST_S3GUARD_IMPLEMENTATION_DYNAMO: - implClass = S3GUARD_METASTORE_DYNAMO; - break; - case TEST_S3GUARD_IMPLEMENTATION_NONE: - implClass = S3GUARD_METASTORE_NULL; - break; - default: - fail("Unknown s3guard back end: \"" + impl + "\""); - } - LOG.debug("Enabling S3Guard, authoritative={}, implementation={}", - authoritative, implClass); - conf.setBoolean(METADATASTORE_AUTHORITATIVE, authoritative); - conf.set(AUTHORITATIVE_PATH, ""); - conf.set(S3_METADATA_STORE_IMPL, implClass); - conf.setBoolean(S3GUARD_DDB_TABLE_CREATE_KEY, true); - } - } - - /** - * Is there a MetadataStore configured for s3a with authoritative enabled? - * @param conf Configuration to test. - * @return true iff there is a MetadataStore configured, and it is - * configured allow authoritative results. This can result in reducing - * round trips to S3 service for cached results, which may affect FS/FC - * statistics. - */ - public static boolean isMetadataStoreAuthoritative(Configuration conf) { - if (conf == null) { - return Constants.DEFAULT_METADATASTORE_AUTHORITATIVE; - } - return conf.getBoolean( - Constants.METADATASTORE_AUTHORITATIVE, - Constants.DEFAULT_METADATASTORE_AUTHORITATIVE); - } - - /** - * Require a filesystem to have a metadata store; skip test - * if not. - * @param fs filesystem to check - */ - public static void assumeFilesystemHasMetadatastore(S3AFileSystem fs) { - assume("Filesystem does not have a metastore", - fs.hasMetadataStore()); - } - /** * Reset all metrics in a list. * @param metrics metrics to reset @@ -649,14 +515,12 @@ public final class S3ATestUtils { /** * Patch a configuration for testing. - * This includes possibly enabling s3guard, setting up the local + * This includes setting up the local * FS temp dir and anything else needed for test runs. * @param conf configuration to patch * @return the now-patched configuration */ public static Configuration prepareTestConfiguration(final Configuration conf) { - // patch in S3Guard options - maybeEnableS3Guard(conf); // set hadoop temp dir to a default value String testUniqueForkId = System.getProperty(TEST_UNIQUE_FORK_ID); @@ -798,16 +662,6 @@ public final class S3ATestUtils { return URI.create(bucket).getHost(); } - /** - * Get the prefix for DynamoDB table names used in tests. - * @param conf configuration to scan. - * @return the table name prefix - */ - public static String getTestDynamoTablePrefix(final Configuration conf) { - return getTestProperty(conf, TEST_S3GUARD_DYNAMO_TABLE_PREFIX, - TEST_S3GUARD_DYNAMO_TABLE_PREFIX_DEFAULT); - } - /** * Remove any values from a bucket. * @param bucket bucket whose overrides are to be removed. Can be null/empty @@ -867,16 +721,6 @@ public final class S3ATestUtils { removeBaseAndBucketOverrides(getTestBucketName(conf), conf, options); } - /** - * Disable S3Guard from the test bucket in a configuration. - * @param conf configuration. - */ - public static void disableS3GuardInTestBucket(Configuration conf) { - removeBaseAndBucketOverrides(getTestBucketName(conf), conf, - S3_METADATA_STORE_IMPL, - DIRECTORY_MARKER_POLICY); - conf.set(S3_METADATA_STORE_IMPL, S3GUARD_METASTORE_NULL); - } /** * Call a function; any exception raised is logged at info. * This is for test teardowns. @@ -941,16 +785,14 @@ public final class S3ATestUtils { /** * Create mock implementation of store context. * @param multiDelete - * @param store * @param accessors * @return * @throws URISyntaxException * @throws IOException */ public static StoreContext createMockStoreContext( - boolean multiDelete, - OperationTrackingStore store, - ContextAccessors accessors) + boolean multiDelete, + ContextAccessors accessors) throws URISyntaxException, IOException { URI name = new URI("s3a://bucket"); Configuration conf = new Configuration(); @@ -974,10 +816,8 @@ public final class S3ATestUtils { ChangeDetectionPolicy.createPolicy(ChangeDetectionPolicy.Mode.None, ChangeDetectionPolicy.Source.ETag, false)) .setMultiObjectDeleteEnabled(multiDelete) - .setMetadataStore(store) .setUseListV1(false) .setContextAccessors(accessors) - .setTimeProvider(new S3Guard.TtlTimeProvider(conf)) .build(); } @@ -1339,45 +1179,6 @@ public final class S3ATestUtils { (status) -> LOG.info("{}", status)); } - /** - * Turn on the inconsistent S3A FS client in a configuration, - * with 100% probability of inconsistency, default delays. - * For this to go live, the paths must include the element - * {@link FailureInjectionPolicy#DEFAULT_DELAY_KEY_SUBSTRING}. - * @param conf configuration to patch - * @param delay delay in millis - */ - public static void enableInconsistentS3Client(Configuration conf, - long delay) { - LOG.info("Enabling inconsistent S3 client"); - conf.setClass(S3_CLIENT_FACTORY_IMPL, InconsistentS3ClientFactory.class, - S3ClientFactory.class); - conf.set(FAIL_INJECT_INCONSISTENCY_KEY, DEFAULT_DELAY_KEY_SUBSTRING); - conf.setLong(FAIL_INJECT_INCONSISTENCY_MSEC, delay); - conf.setFloat(FAIL_INJECT_INCONSISTENCY_PROBABILITY, 0.0f); - conf.setFloat(FAIL_INJECT_THROTTLE_PROBABILITY, 0.0f); - } - - /** - * Is the filesystem using the inconsistent/throttling/unreliable client? - * @param fs filesystem - * @return true if the filesystem's client is the inconsistent one. - */ - public static boolean isFaultInjecting(S3AFileSystem fs) { - return fs.getAmazonS3Client() instanceof InconsistentAmazonS3Client; - } - - /** - * Skip a test because the client is using fault injection. - * This should only be done for those tests which are measuring the cost - * of operations or otherwise cannot handle retries. - * @param fs filesystem to check - */ - public static void skipDuringFaultInjection(S3AFileSystem fs) { - Assume.assumeFalse("Skipping as filesystem has fault injection", - isFaultInjecting(fs)); - } - /** * Date format used for mapping upload initiation time to human string. */ @@ -1411,27 +1212,6 @@ public final class S3ATestUtils { .contains(providerClassname); } - public static boolean metadataStorePersistsAuthoritativeBit(MetadataStore ms) - throws IOException { - Map diags = ms.getDiagnostics(); - String persists = - diags.get(MetadataStoreCapabilities.PERSISTS_AUTHORITATIVE_BIT); - if(persists == null){ - return false; - } - return Boolean.valueOf(persists); - } - - /** - * Set the metadata store of a filesystem instance to the given - * store, via a package-private setter method. - * @param fs filesystem. - * @param ms metastore - */ - public static void setMetadataStore(S3AFileSystem fs, MetadataStore ms) { - fs.setMetadataStore(ms); -} - public static void checkListingDoesNotContainPath(S3AFileSystem fs, Path filePath) throws IOException { final RemoteIterator listIter = diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestBucketConfiguration.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestBucketConfiguration.java index 0ac49a39d38..07e07ba1611 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestBucketConfiguration.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestBucketConfiguration.java @@ -35,6 +35,8 @@ import org.apache.hadoop.security.alias.CredentialProvider; import org.apache.hadoop.security.alias.CredentialProviderFactory; import org.apache.hadoop.test.AbstractHadoopTestBase; +import static org.apache.hadoop.fs.s3a.Constants.CHANGE_DETECT_MODE; +import static org.apache.hadoop.fs.s3a.Constants.CHANGE_DETECT_MODE_CLIENT; import static org.apache.hadoop.fs.s3a.Constants.FS_S3A_BUCKET_PREFIX; import static org.apache.hadoop.fs.s3a.Constants.S3A_SECURITY_CREDENTIAL_PROVIDER_PATH; import static org.apache.hadoop.fs.s3a.Constants.S3_ENCRYPTION_ALGORITHM; @@ -138,9 +140,9 @@ public class TestBucketConfiguration extends AbstractHadoopTestBase { String impl = "fs.s3a.impl"; config.set(impl, "orig"); setBucketOption(config, "b", impl, "b"); - String metastoreImpl = "fs.s3a.metadatastore.impl"; - String ddb = "org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore"; - setBucketOption(config, "b", metastoreImpl, ddb); + String changeDetectionMode = CHANGE_DETECT_MODE; + String client = CHANGE_DETECT_MODE_CLIENT; + setBucketOption(config, "b", changeDetectionMode, client); setBucketOption(config, "b", "impl2", "b2"); setBucketOption(config, "b", "bucket.b.loop", "b3"); assertOptionEquals(config, "fs.s3a.bucket.b.impl", "b"); @@ -148,7 +150,7 @@ public class TestBucketConfiguration extends AbstractHadoopTestBase { Configuration updated = propagateBucketOptions(config, "b"); assertOptionEquals(updated, impl, "orig"); assertOptionEquals(updated, "fs.s3a.impl2", "b2"); - assertOptionEquals(updated, metastoreImpl, ddb); + assertOptionEquals(updated, changeDetectionMode, client); assertOptionEquals(updated, "fs.s3a.bucket.b.loop", null); } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestInvoker.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestInvoker.java index 4f063904127..35199f40927 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestInvoker.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestInvoker.java @@ -23,14 +23,12 @@ import java.io.IOException; import java.io.InterruptedIOException; import java.net.SocketTimeoutException; import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.SdkBaseException; import com.amazonaws.SdkClientException; -import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughputExceededException; import com.amazonaws.services.s3.model.AmazonS3Exception; import org.junit.Assert; import org.junit.Before; @@ -261,14 +259,6 @@ public class TestInvoker extends Assert { ex, retries, false); } - @Test - public void testRetryThrottledDDB() throws Throwable { - assertRetryAction("Expected retry on connection timeout", - RETRY_POLICY, RetryPolicy.RetryAction.RETRY, - new ProvisionedThroughputExceededException("IOPs"), 1, false); - } - - protected AmazonServiceException newThrottledException() { return serviceException( AWSServiceThrottledException.STATUS_CODE, "throttled"); @@ -446,33 +436,6 @@ public class TestInvoker extends Assert { quietlyEval("", "", () -> 3 / d)); } - @Test - public void testDynamoDBThrottleConversion() throws Throwable { - ProvisionedThroughputExceededException exceededException = - new ProvisionedThroughputExceededException("iops"); - AWSServiceThrottledException ddb = verifyTranslated( - AWSServiceThrottledException.class, exceededException); - assertTrue(isThrottleException(exceededException)); - assertTrue(isThrottleException(ddb)); - assertRetryAction("Expected throttling retry", - RETRY_POLICY, - RetryPolicy.RetryAction.RETRY, - ddb, SAFE_RETRY_COUNT, false); - // and briefly attempt an operation - CatchCallback catcher = new CatchCallback(); - AtomicBoolean invoked = new AtomicBoolean(false); - invoker.retry("test", null, false, catcher, - () -> { - if (!invoked.getAndSet(true)) { - throw exceededException; - } - }); - // to verify that the ex was translated by the time it - // got to the callback - verifyExceptionClass(AWSServiceThrottledException.class, - catcher.lastException); - } - /** * Catch the exception and preserve it for later queries. */ diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestListing.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestListing.java index 77ba31c3ce8..38993b43ebf 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestListing.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/TestListing.java @@ -18,27 +18,14 @@ package org.apache.hadoop.fs.s3a; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.RemoteIterator; -import org.apache.hadoop.fs.statistics.IOStatistics; -import org.apache.hadoop.fs.statistics.IOStatisticsSource; -import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore; +import java.util.NoSuchElementException; -import org.assertj.core.api.Assertions; import org.junit.Assert; import org.junit.Test; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.Set; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.RemoteIterator; -import static org.apache.hadoop.fs.statistics.IOStatisticAssertions.extractStatistics; -import static org.apache.hadoop.fs.statistics.IOStatisticAssertions.verifyStatisticCounterValue; -import static org.apache.hadoop.fs.statistics.StoreStatisticNames.OBJECT_LIST_REQUEST; -import static org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding.iostatisticsStore; import static org.apache.hadoop.test.LambdaTestUtils.intercept; /** @@ -46,78 +33,6 @@ import static org.apache.hadoop.test.LambdaTestUtils.intercept; */ public class TestListing extends AbstractS3AMockTest { - private static class MockRemoteIterator implements - RemoteIterator, IOStatisticsSource { - - private final IOStatisticsStore ioStatistics; - - private Iterator iterator; - - MockRemoteIterator(Collection source) { - iterator = source.iterator(); - this.ioStatistics = iostatisticsStore() - .withDurationTracking(OBJECT_LIST_REQUEST) - .build(); - ioStatistics.incrementCounter(OBJECT_LIST_REQUEST); - } - - public boolean hasNext() { - return iterator.hasNext(); - } - - public S3AFileStatus next() { - return iterator.next(); - } - - @Override - public IOStatistics getIOStatistics() { - return ioStatistics; - } - } - - private S3AFileStatus blankFileStatus(Path path) { - return new S3AFileStatus(Tristate.UNKNOWN, path, null); - } - - @Test - public void testTombstoneReconcilingIterator() throws Exception { - Path parent = new Path("/parent"); - Path liveChild = new Path(parent, "/liveChild"); - Path deletedChild = new Path(parent, "/deletedChild"); - - Listing listing = fs.getListing(); - Collection statuses = new ArrayList<>(); - statuses.add(blankFileStatus(parent)); - statuses.add(blankFileStatus(liveChild)); - statuses.add(blankFileStatus(deletedChild)); - - Set tombstones = new HashSet<>(); - tombstones.add(deletedChild); - - RemoteIterator sourceIterator = new MockRemoteIterator( - statuses); - RemoteIterator locatedIterator = - listing.createLocatedFileStatusIterator(sourceIterator); - RemoteIterator reconcilingIterator = - listing.createTombstoneReconcilingIterator(locatedIterator, tombstones); - - Set expectedPaths = new HashSet<>(); - expectedPaths.add(parent); - expectedPaths.add(liveChild); - - Set actualPaths = new HashSet<>(); - while (reconcilingIterator.hasNext()) { - actualPaths.add(reconcilingIterator.next().getPath()); - } - Assertions.assertThat(actualPaths) - .describedAs("paths from iterator") - .isEqualTo(expectedPaths); - - // now verify the stats went all the way through. - IOStatistics iostats = extractStatistics(reconcilingIterator); - verifyStatisticCounterValue(iostats, OBJECT_LIST_REQUEST, 1); - } - @Test public void testProvidedFileStatusIteratorEnd() throws Exception { S3AFileStatus s3aStatus = new S3AFileStatus( diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/audit/ITestAuditAccessChecks.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/audit/ITestAuditAccessChecks.java index bd145fa9c74..c8493d9c235 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/audit/ITestAuditAccessChecks.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/audit/ITestAuditAccessChecks.java @@ -87,7 +87,7 @@ public class ITestAuditAccessChecks extends AbstractS3ACostTest { verifyMetrics( () -> access(fs, path), with(INVOCATION_ACCESS, 1), - whenRaw(FILE_STATUS_FILE_PROBE)); + always(FILE_STATUS_FILE_PROBE)); } @Test @@ -100,7 +100,7 @@ public class ITestAuditAccessChecks extends AbstractS3ACostTest { verifyMetrics( () -> access(fs, path), with(INVOCATION_ACCESS, 1), - whenRaw(FILE_STATUS_ALL_PROBES)); + always(FILE_STATUS_ALL_PROBES)); } @Test @@ -113,7 +113,7 @@ public class ITestAuditAccessChecks extends AbstractS3ACostTest { verifyMetrics( () -> access(fs, path), with(INVOCATION_ACCESS, 1), - whenRaw(ROOT_FILE_STATUS_PROBE)); + always(ROOT_FILE_STATUS_PROBE)); } /** @@ -136,7 +136,7 @@ public class ITestAuditAccessChecks extends AbstractS3ACostTest { with(AUDIT_ACCESS_CHECK_FAILURE, 1), // one S3 request: a HEAD. with(AUDIT_REQUEST_EXECUTION, 1), - whenRaw(FILE_STATUS_FILE_PROBE)); + always(FILE_STATUS_FILE_PROBE)); } /** @@ -160,7 +160,7 @@ public class ITestAuditAccessChecks extends AbstractS3ACostTest { with(AUDIT_REQUEST_EXECUTION, 2), with(STORE_IO_REQUEST, 2), with(AUDIT_ACCESS_CHECK_FAILURE, 1), - whenRaw(FILE_STATUS_ALL_PROBES)); + always(FILE_STATUS_ALL_PROBES)); } /** @@ -181,7 +181,7 @@ public class ITestAuditAccessChecks extends AbstractS3ACostTest { // two S3 requests: a HEAD and a LIST. with(AUDIT_REQUEST_EXECUTION, 2), with(AUDIT_ACCESS_CHECK_FAILURE, 0), - whenRaw(FILE_STATUS_ALL_PROBES)); + always(FILE_STATUS_ALL_PROBES)); } /** diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/ITestAssumeRole.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/ITestAssumeRole.java index 814292c45d8..86bfa2bb07d 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/ITestAssumeRole.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/ITestAssumeRole.java @@ -383,22 +383,12 @@ public class ITestAssumeRole extends AbstractS3ATestBase { public void testAssumeRoleRestrictedPolicyFS() throws Exception { describe("Restrict the policy for this session; verify that reads fail."); - // there's some special handling of S3Guard here as operations - // which only go to DDB don't fail the way S3 would reject them. Configuration conf = createAssumedRoleConfig(); bindRolePolicy(conf, RESTRICTED_POLICY); Path path = new Path(getFileSystem().getUri()); - boolean guarded = getFileSystem().hasMetadataStore(); try (FileSystem fs = path.getFileSystem(conf)) { - if (!guarded) { - // when S3Guard is enabled, the restricted policy still - // permits S3Guard record lookup, so getFileStatus calls - // will work iff the record is in the database. - // probe the store using a path other than /, so a HEAD - // request is issued. - forbidden("getFileStatus", - () -> fs.getFileStatus(methodPath())); - } + forbidden("getFileStatus", + () -> fs.getFileStatus(methodPath())); forbidden("", () -> fs.listStatus(ROOT)); forbidden("", @@ -428,7 +418,6 @@ public class ITestAssumeRole extends AbstractS3ATestBase { policy( statement(false, S3_ALL_BUCKETS, S3_GET_OBJECT_TORRENT), ALLOW_S3_GET_BUCKET_LOCATION, - STATEMENT_S3GUARD_CLIENT, STATEMENT_ALLOW_SSE_KMS_RW)); Path path = path("testAssumeRoleStillIncludesRolePerms"); roleFS = (S3AFileSystem) path.getFileSystem(conf); @@ -438,7 +427,6 @@ public class ITestAssumeRole extends AbstractS3ATestBase { /** * After blocking all write verbs used by S3A, try to write data (fail) * and read data (succeed). - * For S3Guard: full DDB RW access is retained. * SSE-KMS key access is set to decrypt only. */ @Test @@ -451,7 +439,6 @@ public class ITestAssumeRole extends AbstractS3ATestBase { policy( statement(false, S3_ALL_BUCKETS, S3_PATH_WRITE_OPERATIONS), STATEMENT_ALL_S3, - STATEMENT_S3GUARD_CLIENT, STATEMENT_ALLOW_SSE_KMS_READ)); Path path = methodPath(); roleFS = (S3AFileSystem) path.getFileSystem(conf); @@ -499,7 +486,6 @@ public class ITestAssumeRole extends AbstractS3ATestBase { Configuration conf = createAssumedRoleConfig(); bindRolePolicyStatements(conf, - STATEMENT_S3GUARD_CLIENT, STATEMENT_ALL_BUCKET_READ_ACCESS, STATEMENT_ALLOW_SSE_KMS_RW, new Statement(Effects.Allow) @@ -568,7 +554,6 @@ public class ITestAssumeRole extends AbstractS3ATestBase { fs.mkdirs(readOnlyDir); bindRolePolicyStatements(conf, - STATEMENT_S3GUARD_CLIENT, STATEMENT_ALLOW_SSE_KMS_RW, STATEMENT_ALL_BUCKET_READ_ACCESS, new Statement(Effects.Allow) @@ -720,7 +705,6 @@ public class ITestAssumeRole extends AbstractS3ATestBase { fs.delete(destDir, true); bindRolePolicyStatements(conf, - STATEMENT_S3GUARD_CLIENT, STATEMENT_ALLOW_SSE_KMS_RW, statement(true, S3_ALL_BUCKETS, S3_ALL_OPERATIONS), new Statement(Effects.Deny) @@ -752,13 +736,7 @@ public class ITestAssumeRole extends AbstractS3ATestBase { describe("Restrict role to read only"); Configuration conf = createAssumedRoleConfig(); - // S3Guard is turned off so that it isn't trying to work out - // where any table is. - removeBaseAndBucketOverrides(getTestBucketName(conf), conf, - S3_METADATA_STORE_IMPL); - bindRolePolicyStatements(conf, - STATEMENT_S3GUARD_CLIENT, STATEMENT_ALLOW_SSE_KMS_RW, statement(true, S3_ALL_BUCKETS, S3_ALL_OPERATIONS), statement(false, S3_ALL_BUCKETS, S3_GET_BUCKET_LOCATION)); @@ -773,28 +751,4 @@ public class ITestAssumeRole extends AbstractS3ATestBase { Assertions.assertThat(info) .contains(S3GuardTool.BucketInfo.LOCATION_UNKNOWN); } - /** - * Turn off access to dynamo DB Tags and see how DDB table init copes. - * There's no testing of the codepath other than checking the logs - * - this test does make sure that no regression stops the tag permission - * failures from halting the client - */ - @Test - public void testRestrictDDBTagAccess() throws Throwable { - - describe("extra policies in assumed roles need;" - + " all required policies stated"); - Configuration conf = createAssumedRoleConfig(); - - bindRolePolicyStatements(conf, - STATEMENT_S3GUARD_CLIENT, - STATEMENT_ALLOW_SSE_KMS_RW, - STATEMENT_ALL_S3, - new Statement(Effects.Deny) - .addActions(S3_PATH_RW_OPERATIONS) - .addResources(ALL_DDB_TABLES)); - Path path = path("testRestrictDDBTagAccess"); - - roleFS = (S3AFileSystem) path.getFileSystem(conf); - } } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/ITestAssumedRoleCommitOperations.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/ITestAssumedRoleCommitOperations.java index 853810602ba..dabc0abc2af 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/ITestAssumedRoleCommitOperations.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/ITestAssumedRoleCommitOperations.java @@ -37,11 +37,6 @@ import static org.apache.hadoop.io.IOUtils.cleanupWithLogger; /** * Verify that the commit operations work with a restricted set of operations. - * The superclass, {@link ITestCommitOperations} turns on an inconsistent client - * to see how things work in the presence of inconsistency. - * These tests disable it, to remove that as a factor in these tests, which are - * verifying that the policy settings to enabled MPU list/commit/abort are all - * enabled properly. */ public class ITestAssumedRoleCommitOperations extends ITestCommitOperations { @@ -58,11 +53,6 @@ public class ITestAssumedRoleCommitOperations extends ITestCommitOperations { */ private S3AFileSystem roleFS; - @Override - public boolean useInconsistentClient() { - return false; - } - @Override public void setup() throws Exception { super.setup(); @@ -72,7 +62,6 @@ public class ITestAssumedRoleCommitOperations extends ITestCommitOperations { Configuration conf = newAssumedRoleConfig(getConfiguration(), getAssumedRoleARN()); bindRolePolicyStatements(conf, - STATEMENT_S3GUARD_CLIENT, STATEMENT_ALLOW_SSE_KMS_RW, statement(true, S3_ALL_BUCKETS, S3_BUCKET_READ_OPERATIONS), new RoleModel.Statement(RoleModel.Effects.Allow) @@ -114,7 +103,7 @@ public class ITestAssumedRoleCommitOperations extends ITestCommitOperations { } /** - * switch to an inconsistent path if in inconsistent mode. + * switch to an restricted path. * {@inheritDoc} */ @Override diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/ITestRestrictedReadAccess.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/ITestRestrictedReadAccess.java index 402469eb3b7..a16e1b5e492 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/ITestRestrictedReadAccess.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/ITestRestrictedReadAccess.java @@ -22,22 +22,15 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.AccessDeniedException; -import java.util.Arrays; -import java.util.Collection; -import java.util.Optional; import java.util.concurrent.Callable; import org.assertj.core.api.Assertions; -import org.assertj.core.api.Assumptions; import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.commons.lang3.StringUtils; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.PathFilter; @@ -46,23 +39,14 @@ import org.apache.hadoop.fs.s3a.AbstractS3ATestBase; import org.apache.hadoop.fs.s3a.S3AFileSystem; import org.apache.hadoop.fs.s3a.S3ATestUtils; import org.apache.hadoop.fs.s3a.Statistic; -import org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore; import org.apache.hadoop.mapred.LocatedFileStatusFetcher; import org.apache.hadoop.mapreduce.lib.input.InvalidInputException; import static org.apache.hadoop.fs.contract.ContractTestUtils.createFile; import static org.apache.hadoop.fs.contract.ContractTestUtils.touch; import static org.apache.hadoop.fs.s3a.Constants.ASSUMED_ROLE_ARN; -import static org.apache.hadoop.fs.s3a.Constants.AUTHORITATIVE_PATH; -import static org.apache.hadoop.fs.s3a.Constants.METADATASTORE_AUTHORITATIVE; -import static org.apache.hadoop.fs.s3a.Constants.S3_METADATA_STORE_IMPL; import static org.apache.hadoop.fs.s3a.S3ATestUtils.assume; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.assumeS3GuardState; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.disableFilesystemCaching; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.getTestBucketName; import static org.apache.hadoop.fs.s3a.S3ATestUtils.lsR; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.removeBaseAndBucketOverrides; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.removeBucketOverrides; import static org.apache.hadoop.fs.s3a.auth.RoleModel.Effects; import static org.apache.hadoop.fs.s3a.auth.RoleModel.Statement; import static org.apache.hadoop.fs.s3a.auth.RoleModel.directory; @@ -79,21 +63,16 @@ import static org.apache.hadoop.test.LambdaTestUtils.intercept; /** * This test creates a client with no read access to the underlying * filesystem and then tries to perform various read operations on it. - * S3Guard in non-auth mode always goes to the FS, so we parameterize the - * test for S3Guard + Auth to see how failures move around. - *
      - *
    1. Tests only run if an assumed role is provided.
    2. - *
    3. And the S3Guard tests require DynamoDB.
    4. - *
    + * * The tests are all bundled into one big test case. * From a purist unit test perspective, this is utterly wrong as it goes * against the * "Each test case tests exactly one thing" * philosophy of JUnit. *

    - * However is significantly reduces setup costs on the parameterized test runs, + * However is significantly reduces setup costs . * as it means that the filesystems and directories only need to be - * created and destroyed once per parameterized suite, rather than + * created and destroyed once per suite, rather than * once per individual test. *

    * All the test probes have informative messages so when a test failure @@ -109,8 +88,6 @@ import static org.apache.hadoop.test.LambdaTestUtils.intercept; * created paths. * */ -@SuppressWarnings("ThrowableNotThrown") -@RunWith(Parameterized.class) public class ITestRestrictedReadAccess extends AbstractS3ATestBase { private static final Logger LOG = @@ -147,34 +124,11 @@ public class ITestRestrictedReadAccess extends AbstractS3ATestBase { */ public static final byte[] HELLO = "hello".getBytes(StandardCharsets.UTF_8); - private final boolean guardedInAuthMode; - /** * Wildcard scan to find *.txt in the no-read directory. - * When a scan/glob is done with S3Guard in auth mode, the scan will - * succeed but the file open will fail for any non-empty file. - * In non-auth mode, the read restrictions will fail the actual scan. */ private Path noReadWildcard; - /** - * Parameterization. - */ - @Parameterized.Parameters(name = "{0}") - public static Collection params() { - return Arrays.asList(new Object[][]{ - {"raw", false, false}, - {"nonauth", true, false}, - {"auth", true, true} - }); - } - - private final String name; - - private final boolean s3guard; - - private final boolean authMode; - private Path basePath; private Path noReadDir; @@ -200,50 +154,9 @@ public class ITestRestrictedReadAccess extends AbstractS3ATestBase { */ private S3AFileSystem readonlyFS; - /** - * Test suite setup. - * @param name name for logs/paths. - * @param s3guard is S3Guard enabled? - * @param authMode is S3Guard in auth mode? - */ - public ITestRestrictedReadAccess( - final String name, - final boolean s3guard, - final boolean authMode) { - this.name = name; - this.s3guard = s3guard; - this.authMode = authMode; - this.guardedInAuthMode = s3guard && authMode; - - } - - @Override - public Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - String bucketName = getTestBucketName(conf); - - removeBaseAndBucketOverrides(bucketName, conf, - METADATASTORE_AUTHORITATIVE, - AUTHORITATIVE_PATH); - removeBucketOverrides(bucketName, conf, - S3_METADATA_STORE_IMPL); - if (!s3guard) { - removeBaseAndBucketOverrides(bucketName, conf, - S3_METADATA_STORE_IMPL); - } - conf.setBoolean(METADATASTORE_AUTHORITATIVE, authMode); - disableFilesystemCaching(conf); - return conf; - } - @Override public void setup() throws Exception { super.setup(); - if (s3guard) { - // s3guard is required for those test runs where any of the - // guard options are set - assumeS3GuardState(true, getConfiguration()); - } assumeRoleTests(); } @@ -311,13 +224,8 @@ public class ITestRestrictedReadAccess extends AbstractS3ATestBase { describe("Setting up filesystem"); S3AFileSystem realFS = getFileSystem(); - verifyS3GuardSettings(realFS, "real filesystem"); - // avoiding the parameterization to steer clear of accidentally creating - // patterns; a timestamp is used to ensure tombstones from previous runs - // do not interfere - basePath = path("testNoReadAccess-" + name - + "-" + System.currentTimeMillis() / 1000); + basePath = methodPath(); // define the paths and create them. describe("Creating test directories and files"); @@ -347,51 +255,19 @@ public class ITestRestrictedReadAccess extends AbstractS3ATestBase { subdir2File2 = new Path(subDir2, "subdir2File2.docx"); createFile(realFS, subdir2File1, true, HELLO); createFile(realFS, subdir2File2, true, HELLO); - // execute a recursive list to make sure that S3Guard tables are always - // up to date - lsR(realFS, noReadDir, true); // create a role filesystem which does not have read access under a path // it still has write access, which can be explored in the final // step to delete files and directories. roleConfig = createAssumedRoleConfig(); bindRolePolicyStatements(roleConfig, - STATEMENT_S3GUARD_CLIENT, STATEMENT_ALLOW_SSE_KMS_RW, statement(true, S3_ALL_BUCKETS, S3_ALL_OPERATIONS), new Statement(Effects.Deny) .addActions(S3_ALL_GET) .addResources(directory(noReadDir))); readonlyFS = (S3AFileSystem) basePath.getFileSystem(roleConfig); - verifyS3GuardSettings(readonlyFS, "readonly"); - } - /** - * Verify that the FS (real or restricted) meets the - * requirement of the test. - * S3Guard tests are skipped if the (default) store is not - * a DDB store consistent across all FS instances. - * The raw tests fail if somehow the FS does still have a S3Guard metastore. - * @param fs filesystem - * @param storeType store role for error messages. - */ - protected void verifyS3GuardSettings(final S3AFileSystem fs, - final String storeType) { - if (s3guard) { - Assumptions.assumeThat(fs.getMetadataStore()) - .describedAs("Metadata store in " - + storeType - + " fs: %s", - fs.getMetadataStore()) - .isInstanceOf(DynamoDBMetadataStore.class); - } else { - Assertions.assertThat(fs.hasMetadataStore()) - .describedAs("Metadata store in " - + storeType - + " fs: %s", - fs.getMetadataStore()) - .isFalse(); - } } /** @@ -409,7 +285,7 @@ public class ITestRestrictedReadAccess extends AbstractS3ATestBase { lsR(readonlyFS, basePath, true); - // this is HEAD + "/" on S3; get on S3Guard auth when the path exists, + // this is HEAD + "/" on S3 readonlyFS.listStatus(emptyDir); // a recursive list of the no-read-directory works because @@ -421,30 +297,16 @@ public class ITestRestrictedReadAccess extends AbstractS3ATestBase { readonlyFS.getFileStatus(noReadDir); readonlyFS.getFileStatus(emptyDir); - // now look at a file; the outcome depends on the mode. - accessDeniedIf(!guardedInAuthMode, () -> + // now look at a file + accessDenied(() -> readonlyFS.getFileStatus(subdirFile)); - // irrespective of mode, the attempt to read the data will fail. - // the only variable is where the failure occurs + // the attempt to read the data will also fail. accessDenied(() -> ContractTestUtils.readUTF8(readonlyFS, subdirFile, HELLO.length)); - // the empty file is interesting. - // auth mode doesn't check the store. - // Furthermore, because it knows the file length is zero, - // it returns -1 without even opening the file. - // This means that permissions on the file do not get checked. - // See: HADOOP-16464. - Optional optIn = accessDeniedIf( - !guardedInAuthMode, () -> readonlyFS.open(emptyFile)); - if (optIn.isPresent()) { - try (FSDataInputStream is = optIn.get()) { - Assertions.assertThat(is.read()) - .describedAs("read of empty file") - .isEqualTo(-1); - } - } + accessDenied(() -> readonlyFS.open(emptyFile)); + } /** @@ -455,22 +317,14 @@ public class ITestRestrictedReadAccess extends AbstractS3ATestBase { describe("Glob Status operations"); // baseline: the real filesystem on a subdir globFS(getFileSystem(), subdirFile, null, false, 1); - // a file fails if not in auth mode - globFS(readonlyFS, subdirFile, null, !guardedInAuthMode, 1); + // a file fails + globFS(readonlyFS, subdirFile, null, true, 1); // empty directories don't fail. FileStatus[] st = globFS(readonlyFS, emptyDir, null, false, 1); - if (s3guard) { - assertStatusPathEquals(emptyDir, st); - } st = globFS(readonlyFS, noReadWildcard, null, false, 2); - if (s3guard) { - Assertions.assertThat(st) - .extracting(FileStatus::getPath) - .containsExactlyInAnyOrder(subdirFile, subdir2File1); - } // there is precisely one .docx file (subdir2File2.docx) globFS(readonlyFS, @@ -554,15 +408,8 @@ public class ITestRestrictedReadAccess extends AbstractS3ATestBase { true, TEXT_FILE, true); - accessDeniedIf(!guardedInAuthMode, - () -> fetcher.getFileStatuses()) - .ifPresent(stats -> { - Assertions.assertThat(stats) - .describedAs("result of located scan") - .isNotNull() - .flatExtracting(FileStatus::getPath) - .containsExactly(subdirFile); - }); + accessDenied(() -> fetcher.getFileStatuses()); + } /** @@ -614,18 +461,13 @@ public class ITestRestrictedReadAccess extends AbstractS3ATestBase { * Do some cleanup to see what happens with delete calls. * Cleanup happens in test teardown anyway; doing it here * just makes use of the delete calls to see how delete failures - * change with permissions and S3Guard settings. + * change with permissions. */ public void checkDeleteOperations() throws Throwable { describe("Testing delete operations"); readonlyFS.delete(emptyDir, true); - if (!authMode) { - // to fail on HEAD - accessDenied(() -> readonlyFS.delete(emptyFile, true)); - } else { - // checks DDB for status and then issues the DELETE - readonlyFS.delete(emptyFile, true); - } + // to fail on HEAD + accessDenied(() -> readonlyFS.delete(emptyFile, true)); // this will succeed for both readonlyFS.delete(subDir, true); @@ -664,27 +506,6 @@ public class ITestRestrictedReadAccess extends AbstractS3ATestBase { return intercept(AccessDeniedException.class, eval); } - /** - * Conditionally expect an operation to fail with an AccessDeniedException. - * @param condition the condition which must be true for access to be denied - * @param eval closure to evaluate. - * @param type of callable - * @return the return value if the call succeeded - * and did not return null. - * @throws Exception any unexpected exception - */ - protected Optional accessDeniedIf( - final boolean condition, - final Callable eval) - throws Exception { - if (condition) { - intercept(AccessDeniedException.class, eval); - return Optional.empty(); - } else { - return Optional.ofNullable(eval.call()); - } - } - /** * Assert that a status array has exactly one element and its * value is as expected. diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/RoleTestUtils.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/RoleTestUtils.java index 7163c5f4f53..186887d745b 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/RoleTestUtils.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/RoleTestUtils.java @@ -73,16 +73,12 @@ public final class RoleTestUtils { = statement(true, S3_ALL_BUCKETS, S3_GET_BUCKET_LOCATION); /** - * This is AWS policy removes read access from S3, leaves S3Guard access up. - * This will allow clients to use S3Guard list/HEAD operations, even - * the ability to write records, but not actually access the underlying - * data. + * This is AWS policy removes read access from S3. * The client does need {@link RolePolicies#S3_GET_BUCKET_LOCATION} to * get the bucket location. */ public static final Policy RESTRICTED_POLICY = policy( - DENY_S3_GET_OBJECT, STATEMENT_ALL_DDB, ALLOW_S3_GET_BUCKET_LOCATION - ); + DENY_S3_GET_OBJECT, ALLOW_S3_GET_BUCKET_LOCATION); private RoleTestUtils() { } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/delegation/ITestDelegatedMRJob.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/delegation/ITestDelegatedMRJob.java index 75ea01c0724..d5d62f2cae9 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/delegation/ITestDelegatedMRJob.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/delegation/ITestDelegatedMRJob.java @@ -49,7 +49,6 @@ import org.apache.hadoop.security.token.TokenIdentifier; import org.apache.hadoop.yarn.conf.YarnConfiguration; import static java.util.Objects.requireNonNull; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_METASTORE_NULL; import static org.apache.hadoop.fs.s3a.S3ATestUtils.assumeSessionTestsEnabled; import static org.apache.hadoop.fs.s3a.S3ATestUtils.deployService; import static org.apache.hadoop.fs.s3a.S3ATestUtils.disableFilesystemCaching; @@ -162,11 +161,7 @@ public class ITestDelegatedMRJob extends AbstractDelegationIT { conf.setInt(YarnConfiguration.RESOURCEMANAGER_CONNECT_RETRY_INTERVAL_MS, 10_000); - // turn off DDB for the job resource bucket String host = jobResource.getHost(); - conf.set( - String.format("fs.s3a.bucket.%s.metadatastore.impl", host), - S3GUARD_METASTORE_NULL); // and fix to the main endpoint if the caller has moved conf.set( String.format("fs.s3a.bucket.%s.endpoint", host), ""); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/delegation/ITestSessionDelegationInFileystem.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/delegation/ITestSessionDelegationInFileystem.java index 47fad292156..bc223bad457 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/delegation/ITestSessionDelegationInFileystem.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/auth/delegation/ITestSessionDelegationInFileystem.java @@ -329,7 +329,6 @@ public class ITestSessionDelegationInFileystem extends AbstractDelegationIT { + " if role restricted, permissions are tightened."); S3AFileSystem fs = getFileSystem(); // force a probe of the remote FS to make sure its endpoint is valid - // (this always hits S3, even when S3Guard is enabled) fs.getObjectMetadata(new Path("/")); readLandsatMetadata(fs); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractCommitITest.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractCommitITest.java index 012b6b9662c..17ce7e4ddca 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractCommitITest.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractCommitITest.java @@ -19,11 +19,9 @@ package org.apache.hadoop.fs.s3a.commit; import java.io.IOException; -import java.io.InterruptedIOException; import java.util.List; import java.util.stream.Collectors; -import com.amazonaws.services.s3.AmazonS3; import org.assertj.core.api.Assertions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,8 +33,6 @@ import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.contract.ContractTestUtils; import org.apache.hadoop.fs.s3a.AbstractS3ATestBase; -import org.apache.hadoop.fs.s3a.FailureInjectionPolicy; -import org.apache.hadoop.fs.s3a.InconsistentAmazonS3Client; import org.apache.hadoop.fs.s3a.S3AFileSystem; import org.apache.hadoop.fs.s3a.WriteOperationHelper; import org.apache.hadoop.fs.store.audit.AuditSpan; @@ -58,48 +54,12 @@ import static org.apache.hadoop.fs.statistics.IOStatisticsLogging.ioStatisticsTo /** * Base test suite for committer operations. * - * By default, these tests enable the inconsistent committer, with - * a delay of {@link #CONSISTENCY_DELAY}; they may also have throttling - * enabled/disabled. - * - * Important: all filesystem probes will have to wait for - * the FS inconsistency delays and handle things like throttle exceptions, - * or disable throttling and fault injection before the probe. - * */ public abstract class AbstractCommitITest extends AbstractS3ATestBase { private static final Logger LOG = LoggerFactory.getLogger(AbstractCommitITest.class); - protected static final int CONSISTENCY_DELAY = 500; - protected static final int CONSISTENCY_PROBE_INTERVAL = 500; - protected static final int CONSISTENCY_WAIT = CONSISTENCY_DELAY * 2; - - private InconsistentAmazonS3Client inconsistentClient; - - - /** - * Should the inconsistent S3A client be used? - * Default value: true. - * @return true for inconsistent listing - */ - public boolean useInconsistentClient() { - return true; - } - - /** - * switch to an inconsistent path if in inconsistent mode. - * {@inheritDoc} - */ - @Override - protected Path path(String filepath) throws IOException { - return useInconsistentClient() ? - super.path(FailureInjectionPolicy.DEFAULT_DELAY_KEY_SUBSTRING - + "/" + filepath) - : super.path(filepath); - } - /** * Creates a configuration for commit operations: commit is enabled in the FS * and output is multipart to on-heap arrays. @@ -121,9 +81,6 @@ public abstract class AbstractCommitITest extends AbstractS3ATestBase { conf.setLong(MIN_MULTIPART_THRESHOLD, MULTIPART_MIN_SIZE); conf.setInt(MULTIPART_SIZE, MULTIPART_MIN_SIZE); conf.set(FAST_UPLOAD_BUFFER, FAST_UPLOAD_BUFFER_ARRAY); - if (useInconsistentClient()) { - enableInconsistentS3Client(conf, CONSISTENCY_DELAY); - } return conf; } @@ -152,7 +109,6 @@ public abstract class AbstractCommitITest extends AbstractS3ATestBase { /** * Clean up a directory. - * Waits for consistency if needed * @param dir directory * @param conf configuration * @throws IOException failure @@ -161,26 +117,7 @@ public abstract class AbstractCommitITest extends AbstractS3ATestBase { if (dir != null) { describe("deleting %s", dir); FileSystem fs = dir.getFileSystem(conf); - waitForConsistency(); fs.delete(dir, true); - waitForConsistency(); - } - } - - /** - * Setup will use inconsistent client if {@link #useInconsistentClient()} - * is true. - * @throws Exception failure. - */ - @Override - public void setup() throws Exception { - super.setup(); - if (useInconsistentClient()) { - AmazonS3 client = getFileSystem() - .getAmazonS3ClientForTesting("fault injection"); - if (client instanceof InconsistentAmazonS3Client) { - inconsistentClient = (InconsistentAmazonS3Client) client; - } } } @@ -203,80 +140,6 @@ public abstract class AbstractCommitITest extends AbstractS3ATestBase { } } - /** - * Teardown waits for the consistency delay and resets failure count, so - * FS is stable, before the superclass teardown is called. This - * should clean things up better. - * @throws Exception failure. - */ - @Override - public void teardown() throws Exception { - Thread.currentThread().setName("teardown"); - LOG.info("AbstractCommitITest::teardown"); - waitForConsistency(); - // make sure there are no failures any more - resetFailures(); - super.teardown(); - } - - /** - * Wait a multiple of the inconsistency delay for things to stabilize; - * no-op if the consistent client is used. - * @throws InterruptedIOException if the sleep is interrupted - */ - protected void waitForConsistency() throws InterruptedIOException { - if (useInconsistentClient() && inconsistentClient != null) { - try { - Thread.sleep(2* inconsistentClient.getDelayKeyMsec()); - } catch (InterruptedException e) { - throw (InterruptedIOException) - (new InterruptedIOException("while waiting for consistency: " + e) - .initCause(e)); - } - } - } - - /** - * Set the throttling factor on requests. - * @param p probability of a throttling occurring: 0-1.0 - */ - protected void setThrottling(float p) { - if (inconsistentClient != null) { - inconsistentClient.setThrottleProbability(p); - } - } - - /** - * Set the throttling factor on requests and number of calls to throttle. - * @param p probability of a throttling occurring: 0-1.0 - * @param limit limit to number of calls which fail - */ - protected void setThrottling(float p, int limit) { - if (inconsistentClient != null) { - inconsistentClient.setThrottleProbability(p); - } - setFailureLimit(limit); - } - - /** - * Turn off throttling. - */ - protected void resetFailures() { - if (inconsistentClient != null) { - setThrottling(0, 0); - } - } - - /** - * Set failure limit. - * @param limit limit to number of calls which fail - */ - private void setFailureLimit(int limit) { - if (inconsistentClient != null) { - inconsistentClient.setFailureLimit(limit); - } - } - /** * Abort all multipart uploads under a path. * @param path path for uploads to abort; may be null diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractITCommitProtocol.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractITCommitProtocol.java index 1e5a9582b2c..4cb90a7373c 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractITCommitProtocol.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractITCommitProtocol.java @@ -82,7 +82,7 @@ import static org.apache.hadoop.fs.s3a.commit.InternalCommitterConstants.SPARK_W import static org.apache.hadoop.fs.s3a.Statistic.COMMITTER_TASKS_SUCCEEDED; import static org.apache.hadoop.fs.statistics.IOStatisticAssertions.assertThatStatisticCounter; import static org.apache.hadoop.fs.statistics.IOStatisticsLogging.ioStatisticsSourceToString; -import static org.apache.hadoop.test.LambdaTestUtils.*; +import static org.apache.hadoop.test.LambdaTestUtils.intercept; /** * Test the job/task commit actions of an S3A Committer, including trying to @@ -93,7 +93,7 @@ import static org.apache.hadoop.test.LambdaTestUtils.*; * This is a complex test suite as it tries to explore the full lifecycle * of committers, and is designed for subclassing. */ -@SuppressWarnings({"unchecked", "ThrowableNotThrown", "unused"}) +@SuppressWarnings({"unchecked", "unused"}) public abstract class AbstractITCommitProtocol extends AbstractCommitITest { private Path outDir; @@ -605,7 +605,7 @@ public abstract class AbstractITCommitProtocol extends AbstractCommitITest { @Test @SuppressWarnings("deprecation") public void testRecoveryAndCleanup() throws Exception { - describe("Test (unsupported) task recovery."); + describe("Test (Unsupported) task recovery."); JobData jobData = startJob(true); TaskAttemptContext tContext = jobData.tContext; AbstractS3ACommitter committer = jobData.committer; @@ -697,8 +697,7 @@ public abstract class AbstractITCommitProtocol extends AbstractCommitITest { /** * Identify any path under the directory which begins with the - * {@code "part-m-00000"} sequence. There's some compensation for - * eventual consistency here. + * {@code "part-m-00000"} sequence. * @param dir directory to scan * @return the full path * @throws FileNotFoundException the path is missing. @@ -706,22 +705,6 @@ public abstract class AbstractITCommitProtocol extends AbstractCommitITest { */ protected Path getPart0000(final Path dir) throws Exception { final FileSystem fs = dir.getFileSystem(getConfiguration()); - return eventually(CONSISTENCY_WAIT, CONSISTENCY_PROBE_INTERVAL, - () -> getPart0000Immediately(fs, dir)); - } - - /** - * Identify any path under the directory which begins with the - * {@code "part-m-00000"} sequence. There's some compensation for - * eventual consistency here. - * @param fs FS to probe - * @param dir directory to scan - * @return the full path - * @throws FileNotFoundException the path is missing. - * @throws IOException failure. - */ - private Path getPart0000Immediately(FileSystem fs, Path dir) - throws IOException { FileStatus[] statuses = fs.listStatus(dir, path -> path.getName().startsWith(PART_00000)); if (statuses.length != 1) { @@ -791,7 +774,7 @@ public abstract class AbstractITCommitProtocol extends AbstractCommitITest { commitTask(committer, tContext); // this is only task commit; there MUST be no part- files in the dest dir - waitForConsistency(); + try { applyLocatedFiles(getFileSystem().listFiles(outDir, false), (status) -> @@ -1020,7 +1003,7 @@ public abstract class AbstractITCommitProtocol extends AbstractCommitITest { // do commit commit(committer, jContext, tContext); S3AFileSystem fs = getFileSystem(); - waitForConsistency(); + lsR(fs, outDir, true); String ls = ls(outDir); describe("\nvalidating"); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractYarnClusterITest.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractYarnClusterITest.java index 1b752a8db97..374699d81d0 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractYarnClusterITest.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/AbstractYarnClusterITest.java @@ -156,7 +156,7 @@ public abstract class AbstractYarnClusterITest extends AbstractCommitITest { /** * Create the cluster binding. * The configuration will be patched by propagating down options - * from the maven build (S3Guard binding etc) and turning off unwanted + * from the maven build and turning off unwanted * YARN features. * * If an HDFS cluster is requested, diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestCommitOperations.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestCommitOperations.java index e8c5d888d10..3f0e2e7a134 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestCommitOperations.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestCommitOperations.java @@ -25,7 +25,6 @@ import java.util.ArrayList; import java.util.List; import com.amazonaws.services.s3.model.PartETag; -import org.apache.hadoop.thirdparty.com.google.common.collect.Lists; import org.assertj.core.api.Assertions; import org.junit.Test; import org.slf4j.Logger; @@ -38,7 +37,6 @@ import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.apache.hadoop.fs.s3a.Statistic; import org.apache.hadoop.fs.s3a.auth.ProgressCounter; import org.apache.hadoop.fs.s3a.commit.files.SinglePendingCommit; import org.apache.hadoop.fs.s3a.commit.magic.MagicCommitTracker; @@ -49,6 +47,7 @@ import org.apache.hadoop.mapreduce.TaskID; import org.apache.hadoop.mapreduce.lib.output.PathOutputCommitter; import org.apache.hadoop.mapreduce.lib.output.PathOutputCommitterFactory; import org.apache.hadoop.mapreduce.task.TaskAttemptContextImpl; +import org.apache.hadoop.util.Lists; import static org.apache.hadoop.fs.contract.ContractTestUtils.*; import static org.apache.hadoop.fs.s3a.S3ATestUtils.*; @@ -62,7 +61,6 @@ import static org.apache.hadoop.mapreduce.lib.output.PathOutputCommitterFactory. /** * Test the low-level binding of the S3A FS to the magic commit mechanism, * and handling of the commit operations. - * This is done with an inconsistent client. */ public class ITestCommitOperations extends AbstractCommitITest { @@ -73,21 +71,6 @@ public class ITestCommitOperations extends AbstractCommitITest { COMMITTER_FACTORY_SCHEME_PATTERN, "s3a"); private ProgressCounter progress; - /** - * A compile time flag which allows you to disable failure reset before - * assertions and teardown. - * As S3A is now required to be resilient to failure on all FS operations, - * setting it to false ensures that even the assertions are checking - * the resilience codepaths. - */ - private static final boolean RESET_FAILURES_ENABLED = false; - - private static final float HIGH_THROTTLE = 0.25f; - - private static final float FULL_THROTTLE = 1.0f; - - private static final int STANDARD_FAILURE_LIMIT = 2; - @Override protected Configuration createConfiguration() { Configuration conf = super.createConfiguration(); @@ -96,18 +79,11 @@ public class ITestCommitOperations extends AbstractCommitITest { return conf; } - @Override - public boolean useInconsistentClient() { - return true; - } - @Override public void setup() throws Exception { FileSystem.closeAll(); super.setup(); verifyIsMagicCommitFS(getFileSystem()); - // abort,; rethrow on failure - setThrottling(HIGH_THROTTLE, STANDARD_FAILURE_LIMIT); progress = new ProgressCounter(); progress.assertCount("progress", 0); } @@ -175,38 +151,27 @@ public class ITestCommitOperations extends AbstractCommitITest { Path destFile = methodPath(filename); Path pendingFilePath = makeMagic(destFile); touch(fs, pendingFilePath); - waitForConsistency(); + validateIntermediateAndFinalPaths(pendingFilePath, destFile); Path pendingDataPath = validatePendingCommitData(filename, pendingFilePath); CommitOperations actions = newCommitOperations(); // abort,; rethrow on failure - fullThrottle(); + LOG.info("Abort call"); actions.abortAllSinglePendingCommits(pendingDataPath.getParent(), true) .maybeRethrow(); - resetFailures(); + assertPathDoesNotExist("pending file not deleted", pendingDataPath); assertPathDoesNotExist("dest file was created", destFile); } - private void fullThrottle() { - setThrottling(FULL_THROTTLE, STANDARD_FAILURE_LIMIT); - } - private CommitOperations newCommitOperations() throws IOException { return new CommitOperations(getFileSystem()); } - @Override - protected void resetFailures() { - if (!RESET_FAILURES_ENABLED) { - super.resetFailures(); - } - } - /** * Create a new path which has the same filename as the dest file, but * is in a magic directory under the destination dir. @@ -289,7 +254,7 @@ public class ITestCommitOperations extends AbstractCommitITest { expectedDestPath); createFile(fs, pendingChildPath, true, DATASET); - commit("child.txt", pendingChildPath, expectedDestPath, 0, 0); + commit("child.txt", pendingChildPath, expectedDestPath); } /** @@ -365,8 +330,7 @@ public class ITestCommitOperations extends AbstractCommitITest { } stream.close(); } - FileStatus status = getFileStatusEventually(fs, magicDest, - CONSISTENCY_WAIT); + FileStatus status = fs.getFileStatus(magicDest); assertEquals("Magic marker file is not zero bytes: " + status, 0, 0); Assertions.assertThat(extractMagicFileLength(fs, @@ -374,7 +338,7 @@ public class ITestCommitOperations extends AbstractCommitITest { .describedAs("XAttribute " + XA_MAGIC_MARKER + " of " + magicDest) .isNotEmpty() .hasValue(dataSize); - commit(filename, destFile, HIGH_THROTTLE, 0); + commit(filename, destFile); verifyFileContents(fs, destFile, data); // the destination file doesn't have the attribute Assertions.assertThat(extractMagicFileLength(fs, @@ -389,38 +353,30 @@ public class ITestCommitOperations extends AbstractCommitITest { * Failures can be set; they'll be reset after the commit. * @param filename filename of file * @param destFile destination path of file - * @param throttle probability of commit throttling - * @param failures failure limit * @throws Exception any failure of the operation */ private void commit(String filename, - Path destFile, - float throttle, - int failures) throws Exception { - commit(filename, makeMagic(destFile), destFile, throttle, failures); + Path destFile) throws Exception { + commit(filename, makeMagic(destFile), destFile); } /** * Commit to a write to {@code magicFile} which is expected to * be saved to {@code destFile}. - * Failures can be set; they'll be reset after the commit. * @param magicFile path to write to * @param destFile destination to verify - * @param throttle probability of commit throttling - * @param failures failure limit */ private void commit(String filename, Path magicFile, - Path destFile, - float throttle, int failures) + Path destFile) throws IOException { - resetFailures(); + validateIntermediateAndFinalPaths(magicFile, destFile); SinglePendingCommit commit = SinglePendingCommit.load(getFileSystem(), validatePendingCommitData(filename, magicFile)); - setThrottling(throttle, failures); + commitOrFail(destFile, commit, newCommitOperations()); - resetFailures(); + verifyCommitExists(commit); } @@ -524,7 +480,6 @@ public class ITestCommitOperations extends AbstractCommitITest { Path dest = methodPath("testUploadEmptyFile"); S3AFileSystem fs = getFileSystem(); fs.delete(dest, false); - fullThrottle(); SinglePendingCommit pendingCommit = actions.uploadFileToPendingCommit(tempFile, @@ -532,11 +487,11 @@ public class ITestCommitOperations extends AbstractCommitITest { null, DEFAULT_MULTIPART_SIZE, progress); - resetFailures(); + assertPathDoesNotExist("pending commit", dest); - fullThrottle(); + commitOrFail(dest, pendingCommit, actions); - resetFailures(); + FileStatus status = verifyPathExists(fs, "uploaded file commit", dest); progress.assertCount("Progress counter should be 1.", @@ -553,7 +508,7 @@ public class ITestCommitOperations extends AbstractCommitITest { Path dest = methodPath("testUploadSmallFile"); S3AFileSystem fs = getFileSystem(); fs.delete(dest, true); - fullThrottle(); + assertPathDoesNotExist("test setup", dest); SinglePendingCommit pendingCommit = actions.uploadFileToPendingCommit(tempFile, @@ -561,12 +516,12 @@ public class ITestCommitOperations extends AbstractCommitITest { null, DEFAULT_MULTIPART_SIZE, progress); - resetFailures(); + assertPathDoesNotExist("pending commit", dest); - fullThrottle(); + LOG.debug("Postcommit validation"); commitOrFail(dest, pendingCommit, actions); - resetFailures(); + String s = readUTF8(fs, dest, -1); assertEquals(text, s); progress.assertCount("Progress counter should be 1.", @@ -579,7 +534,7 @@ public class ITestCommitOperations extends AbstractCommitITest { tempFile.delete(); CommitOperations actions = newCommitOperations(); Path dest = methodPath("testUploadMissingile"); - fullThrottle(); + actions.uploadFileToPendingCommit(tempFile, dest, null, DEFAULT_MULTIPART_SIZE, progress); progress.assertCount("Progress counter should be 1.", @@ -594,9 +549,9 @@ public class ITestCommitOperations extends AbstractCommitITest { CommitOperations actions = newCommitOperations(); SinglePendingCommit commit = new SinglePendingCommit(); commit.setDestinationKey(fs.pathToKey(destFile)); - fullThrottle(); - actions.revertCommit(commit, null); - resetFailures(); + + actions.revertCommit(commit); + assertPathExists("parent of reverted commit", destFile.getParent()); } @@ -608,26 +563,13 @@ public class ITestCommitOperations extends AbstractCommitITest { CommitOperations actions = newCommitOperations(); SinglePendingCommit commit = new SinglePendingCommit(); commit.setDestinationKey(fs.pathToKey(destFile)); - fullThrottle(); - actions.revertCommit(commit, null); - resetFailures(); + + actions.revertCommit(commit); + assertPathExists("parent of reverted (nonexistent) commit", destFile.getParent()); } - @Test - public void testFailuresInAbortListing() throws Throwable { - CommitOperations actions = newCommitOperations(); - Path path = path("testFailuresInAbort"); - getFileSystem().mkdirs(path); - setThrottling(HIGH_THROTTLE); - LOG.info("Aborting"); - actions.abortPendingUploadsUnderPath(path); - LOG.info("Abort completed"); - resetFailures(); - } - - /** * Test a normal stream still works as expected in a magic filesystem, * with a call of {@code hasCapability()} to check that it is normal. @@ -644,30 +586,21 @@ public class ITestCommitOperations extends AbstractCommitITest { out.hasCapability(STREAM_CAPABILITY_MAGIC_OUTPUT)); out.close(); } - FileStatus status = getFileStatusEventually(fs, destFile, - CONSISTENCY_WAIT); + FileStatus status = fs.getFileStatus(destFile); assertTrue("Empty marker file: " + status, status.getLen() > 0); } /** * Creates a bulk commit and commits multiple files. - * If the DDB metastore is in use, use the instrumentation to - * verify that the write count is as expected. - * This is done without actually looking into the store -just monitoring - * changes in the filesystem's instrumentation counters. - * As changes to the store may be made during get/list calls, - * when the counters must be reset before each commit, this must be - * *after* all probes for the outcome of the previous operation. */ @Test public void testBulkCommitFiles() throws Throwable { - describe("verify bulk commit including metastore update count"); + describe("verify bulk commit"); File localFile = File.createTempFile("commit", ".txt"); CommitOperations actions = newCommitOperations(); Path destDir = methodPath("out"); S3AFileSystem fs = getFileSystem(); fs.delete(destDir, false); - fullThrottle(); Path destFile1 = new Path(destDir, "file1"); // this subdir will only be created in the commit of file 2 @@ -687,60 +620,31 @@ public class ITestCommitOperations extends AbstractCommitITest { progress); commits.add(commit1); } - resetFailures(); + assertPathDoesNotExist("destination dir", destDir); assertPathDoesNotExist("subdirectory", subdir); LOG.info("Initiating commit operations"); try (CommitOperations.CommitContext commitContext = actions.initiateCommitOperation(destDir)) { - // how many records have been written - MetricDiff writes = new MetricDiff(fs, - Statistic.S3GUARD_METADATASTORE_RECORD_WRITES); LOG.info("Commit #1"); commitContext.commitOrFail(commits.get(0)); final String firstCommitContextString = commitContext.toString(); LOG.info("First Commit state {}", firstCommitContextString); - long writesOnFirstCommit = writes.diff(); assertPathExists("destFile1", destFile1); assertPathExists("destination dir", destDir); LOG.info("Commit #2"); - writes.reset(); commitContext.commitOrFail(commits.get(1)); assertPathExists("subdirectory", subdir); assertPathExists("destFile2", destFile2); final String secondCommitContextString = commitContext.toString(); LOG.info("Second Commit state {}", secondCommitContextString); - if (writesOnFirstCommit != 0) { - LOG.info("DynamoDB Metastore is in use: checking write count"); - // S3Guard is in use against DDB, so the metrics can be checked - // to see how many records were updated. - // there should only be two new entries: one for the file and - // one for the parent. - // we include the string values of the contexts because that includes - // the internals of the bulk operation state. - writes.assertDiffEquals("Number of records written after commit #2" - + "; first commit had " + writesOnFirstCommit - + "; first commit ancestors " + firstCommitContextString - + "; second commit ancestors: " + secondCommitContextString, - 2); - } - LOG.info("Commit #3"); - writes.reset(); commitContext.commitOrFail(commits.get(2)); assertPathExists("destFile3", destFile3); - if (writesOnFirstCommit != 0) { - // this file is in the same dir as destFile2, so only its entry - // is added - writes.assertDiffEquals( - "Number of records written after third commit; " - + "first commit had " + writesOnFirstCommit, - 1); - } } - resetFailures(); + } } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestS3ACommitterFactory.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestS3ACommitterFactory.java index 0d17016927d..a8547d67289 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestS3ACommitterFactory.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/ITestS3ACommitterFactory.java @@ -107,11 +107,6 @@ public class ITestS3ACommitterFactory extends AbstractCommitITest { taskConfRef = tContext.getConfiguration(); } - @Override - public boolean useInconsistentClient() { - return false; - } - @Test public void testEverything() throws Throwable { testImplicitFileBinding(); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/integration/ITestS3ACommitterMRJob.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/integration/ITestS3ACommitterMRJob.java index 79838d6f00b..75c488ba733 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/integration/ITestS3ACommitterMRJob.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/integration/ITestS3ACommitterMRJob.java @@ -190,11 +190,6 @@ public class ITestS3ACommitterMRJob extends AbstractYarnClusterITest { return committerTestBinding.getCommitterName(); } - @Override - public boolean useInconsistentClient() { - return committerTestBinding.useInconsistentClient(); - } - /** * Verify that the committer binding is happy. */ @@ -218,9 +213,6 @@ public class ITestS3ACommitterMRJob extends AbstractYarnClusterITest { // that and URI creation fails. Path outputPath = path("ITestS3ACommitterMRJob-execute-"+ committerName()); - // create and delete to force in a tombstone marker -see HADOOP-16207 - fs.mkdirs(outputPath); - fs.delete(outputPath, true); String commitUUID = UUID.randomUUID().toString(); String suffix = isUniqueFilenames() ? ("-" + commitUUID) : ""; @@ -303,7 +295,6 @@ public class ITestS3ACommitterMRJob extends AbstractYarnClusterITest { fail(message); } - waitForConsistency(); Path successPath = new Path(outputPath, _SUCCESS); SuccessData successData = validateSuccessFile(outputPath, committerName(), @@ -483,12 +474,6 @@ public class ITestS3ACommitterMRJob extends AbstractYarnClusterITest { throws IOException { } - /** - * Should the inconsistent S3A client be used? - * @return true for inconsistent listing - */ - public abstract boolean useInconsistentClient(); - /** * Override point for any committer specific validation operations; * called after the base assertions have all passed. @@ -543,13 +528,6 @@ public class ITestS3ACommitterMRJob extends AbstractYarnClusterITest { super(DirectoryStagingCommitter.NAME); } - /** - * @return true for inconsistent listing - */ - public boolean useInconsistentClient() { - return true; - } - /** * Verify that staging commit dirs are made absolute under the user's * home directory, so, in a secure cluster, private. @@ -584,12 +562,6 @@ public class ITestS3ACommitterMRJob extends AbstractYarnClusterITest { super(PartitionedStagingCommitter.NAME); } - /** - * @return true for inconsistent listing - */ - public boolean useInconsistentClient() { - return true; - } } /** @@ -603,13 +575,6 @@ public class ITestS3ACommitterMRJob extends AbstractYarnClusterITest { super(MagicS3GuardCommitter.NAME); } - /** - * @return we need a consistent store. - */ - public boolean useInconsistentClient() { - return false; - } - /** * The result validation here is that there isn't a __magic directory * any more. diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/magic/ITestMagicCommitProtocol.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/magic/ITestMagicCommitProtocol.java index 2b2fc2bb446..5265163d83f 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/magic/ITestMagicCommitProtocol.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/magic/ITestMagicCommitProtocol.java @@ -57,15 +57,6 @@ public class ITestMagicCommitProtocol extends AbstractITCommitProtocol { return "ITestMagicCommitProtocol"; } - /** - * Need consistency here. - * @return false - */ - @Override - public boolean useInconsistentClient() { - return false; - } - @Override protected String getCommitterFactoryName() { return CommitConstants.S3A_COMMITTER_FACTORY; diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/integration/ITestStagingCommitProtocol.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/integration/ITestStagingCommitProtocol.java index 826c3cd2743..3a820bcc11e 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/integration/ITestStagingCommitProtocol.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/staging/integration/ITestStagingCommitProtocol.java @@ -27,8 +27,6 @@ import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.contract.ContractTestUtils; -import org.apache.hadoop.fs.s3a.InconsistentS3ClientFactory; -import org.apache.hadoop.fs.s3a.S3ClientFactory; import org.apache.hadoop.fs.s3a.commit.AbstractITCommitProtocol; import org.apache.hadoop.fs.s3a.commit.AbstractS3ACommitter; import org.apache.hadoop.fs.s3a.commit.CommitterFaultInjection; @@ -41,7 +39,6 @@ import org.apache.hadoop.mapreduce.JobID; import org.apache.hadoop.mapreduce.JobStatus; import org.apache.hadoop.mapreduce.TaskAttemptContext; -import static org.apache.hadoop.fs.s3a.Constants.S3_CLIENT_FACTORY_IMPL; import static org.apache.hadoop.fs.s3a.commit.CommitConstants.*; /** Test the staging committer's handling of the base protocol operations. */ @@ -56,9 +53,6 @@ public class ITestStagingCommitProtocol extends AbstractITCommitProtocol { protected Configuration createConfiguration() { Configuration conf = super.createConfiguration(); conf.setInt(FS_S3A_COMMITTER_THREADS, 1); - // switch to the inconsistent filesystem - conf.setClass(S3_CLIENT_FACTORY_IMPL, InconsistentS3ClientFactory.class, - S3ClientFactory.class); // disable unique filenames so that the protocol tests of FileOutputFormat // and this test generate consistent names. diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/terasort/ITestTerasortOnS3A.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/terasort/ITestTerasortOnS3A.java index 32f909231c2..991969b0f05 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/terasort/ITestTerasortOnS3A.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/commit/terasort/ITestTerasortOnS3A.java @@ -125,15 +125,6 @@ public class ITestTerasortOnS3A extends AbstractYarnClusterITest { this.committerName = committerName; } - /** - * Not using special paths here. - * @return false - */ - @Override - public boolean useInconsistentClient() { - return false; - } - @Override protected String committerName() { return committerName; diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/fileContext/ITestS3AFileContextURI.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/fileContext/ITestS3AFileContextURI.java index 725646ce1bb..bef359cca73 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/fileContext/ITestS3AFileContextURI.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/fileContext/ITestS3AFileContextURI.java @@ -16,29 +16,22 @@ package org.apache.hadoop.fs.s3a.fileContext; import java.io.IOException; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileContextURIBase; -import org.apache.hadoop.fs.s3a.S3AFileSystem; import org.apache.hadoop.fs.s3a.S3ATestUtils; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.assume; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.createTestFileSystem; - /** * S3a implementation of FileContextURIBase. */ public class ITestS3AFileContextURI extends FileContextURIBase { private Configuration conf; - private boolean hasMetadataStore; @Before public void setUp() throws IOException, Exception { conf = new Configuration(); - try(S3AFileSystem s3aFS = createTestFileSystem(conf)) { - hasMetadataStore = s3aFS.hasMetadataStore(); - } + fc1 = S3ATestUtils.createTestFileContext(conf); fc2 = S3ATestUtils.createTestFileContext(conf); //different object, same FS super.setUp(); @@ -51,11 +44,4 @@ public class ITestS3AFileContextURI extends FileContextURIBase { // (the statistics tested with this method are not relevant for an S3FS) } - @Test - @Override - public void testModificationTime() throws IOException { - // skip modtime tests as there may be some inconsistency during creation - assume("modification time tests are skipped", !hasMetadataStore); - super.testModificationTime(); - } } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/ITestPartialRenamesDeletes.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/ITestPartialRenamesDeletes.java index b03d52fd567..068b7b2dda5 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/ITestPartialRenamesDeletes.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/ITestPartialRenamesDeletes.java @@ -29,7 +29,6 @@ import java.util.TreeSet; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import java.util.stream.Stream; import com.amazonaws.services.s3.model.MultiObjectDeleteException; import org.apache.hadoop.thirdparty.com.google.common.base.Charsets; @@ -72,9 +71,6 @@ import static org.apache.hadoop.fs.s3a.auth.RoleTestUtils.newAssumedRoleConfig; import static org.apache.hadoop.fs.s3a.auth.delegation.DelegationConstants.DELEGATION_TOKEN_BINDING; import static org.apache.hadoop.fs.s3a.impl.CallableSupplier.submit; import static org.apache.hadoop.fs.s3a.impl.CallableSupplier.waitForCompletion; -import static org.apache.hadoop.fs.s3a.impl.MultiObjectDeleteSupport.extractUndeletedPaths; -import static org.apache.hadoop.fs.s3a.impl.MultiObjectDeleteSupport.removeUndeletedPaths; -import static org.apache.hadoop.fs.s3a.impl.MultiObjectDeleteSupport.toPathList; import static org.apache.hadoop.fs.s3a.test.ExtraAssertions.assertFileCount; import static org.apache.hadoop.fs.s3a.test.ExtraAssertions.extractCause; import static org.apache.hadoop.fs.statistics.IOStatisticsLogging.ioStatisticsSourceToString; @@ -83,9 +79,7 @@ import static org.apache.hadoop.test.GenericTestUtils.buildPaths; import static org.apache.hadoop.test.LambdaTestUtils.eval; /** - * Test partial failures of delete and rename operations, especially - * that the S3Guard tables are consistent with the state of - * the filesystem. + * Test partial failures of delete and rename operations,. * * All these test have a unique path for each run, with a roleFS having * full RW access to part of it, and R/O access to a restricted subdirectory @@ -105,11 +99,6 @@ import static org.apache.hadoop.test.LambdaTestUtils.eval; * * * - * This test manages to create lots of load on the s3guard prune command - * when that is tested in a separate test suite; - * too many tombstone files for the test to complete. - * An attempt is made in {@link #deleteTestDirInTeardown()} to prune these test - * files. */ @SuppressWarnings("ThrowableNotThrown") @RunWith(Parameterized.class) @@ -261,7 +250,6 @@ public class ITestPartialRenamesDeletes extends AbstractS3ATestBase { // create the baseline assumed role assumedRoleConfig = createAssumedRoleConfig(); bindRolePolicyStatements(assumedRoleConfig, - STATEMENT_S3GUARD_CLIENT, STATEMENT_ALLOW_SSE_KMS_RW, STATEMENT_ALL_BUCKET_READ_ACCESS, // root: r-x new Statement(Effects.Allow) // dest: rwx @@ -335,14 +323,10 @@ public class ITestPartialRenamesDeletes extends AbstractS3ATestBase { removeBucketOverrides(bucketName, conf, MAX_THREADS, MAXIMUM_CONNECTIONS, - S3GUARD_DDB_BACKGROUND_SLEEP_MSEC_KEY, DIRECTORY_MARKER_POLICY, BULK_DELETE_PAGE_SIZE); conf.setInt(MAX_THREADS, EXECUTOR_THREAD_COUNT); conf.setInt(MAXIMUM_CONNECTIONS, EXECUTOR_THREAD_COUNT * 2); - // turn off prune delays, so as to stop scale tests creating - // so much cruft that future CLI prune commands take forever - conf.setInt(S3GUARD_DDB_BACKGROUND_SLEEP_MSEC_KEY, 0); // use the keep policy to ensure that surplus markers exist // to complicate failures conf.set(DIRECTORY_MARKER_POLICY, DIRECTORY_MARKER_POLICY_KEEP); @@ -406,7 +390,6 @@ public class ITestPartialRenamesDeletes extends AbstractS3ATestBase { describe("rename with parent paths not writeable; multi=%s", multiDelete); final Configuration conf = createAssumedRoleConfig(); bindRolePolicyStatements(conf, - STATEMENT_S3GUARD_CLIENT, STATEMENT_ALLOW_SSE_KMS_RW, STATEMENT_ALL_BUCKET_READ_ACCESS, new Statement(Effects.Allow) @@ -510,34 +493,19 @@ public class ITestPartialRenamesDeletes extends AbstractS3ATestBase { // look in that exception for a multidelete MultiObjectDeleteException mde = extractCause( MultiObjectDeleteException.class, deniedException); - final List undeleted - = extractUndeletedPaths(mde, fs::keyToQualifiedPath); - - List expectedUndeletedFiles = new ArrayList<>(createdFiles); - if (getFileSystem().getDirectoryMarkerPolicy() - .keepDirectoryMarkers(readOnlyDir)) { - // directory markers are being retained, - // so will also be in the list of undeleted files - expectedUndeletedFiles.addAll(dirs); - } - Assertions.assertThat(undeleted) - .as("files which could not be deleted") - .containsExactlyInAnyOrderElementsOf(expectedUndeletedFiles); } LOG.info("Result of renaming read-only files is as expected", deniedException); assertFileCount("files in the source directory", roleFS, readOnlyDir, expectedFileCount); // now lets look at the destination. - // even with S3Guard on, we expect the destination to match that of + // we expect the destination to match that of // the remote state. - // the test will exist describe("Verify destination directory exists"); assertIsDirectory(writableDir); assertFileCount("files in the dest directory", roleFS, writableDir, expectedFileCount); // all directories in the source tree must still exist, - // which for S3Guard means no tombstone markers were added LOG.info("Verifying all directories still exist"); for (Path dir : dirs) { assertIsDirectory(dir); @@ -653,17 +621,6 @@ public class ITestPartialRenamesDeletes extends AbstractS3ATestBase { Path head = deletableFiles.remove(0); assertTrue("delete " + head + " failed", roleFS.delete(head, false)); - List allFiles = Stream.concat( - readOnlyFiles.stream(), - deletableFiles.stream()) - .collect(Collectors.toList()); - List keyPaths = allFiles.stream() - .map(path -> - new MultiObjectDeleteSupport.KeyPath( - storeContext.pathToKey(path), - path, - false)) - .collect(Collectors.toList()); // this set can be deleted by the role FS MetricDiff rejectionCount = new MetricDiff(roleFS, FILES_DELETE_REJECTED); @@ -697,34 +654,10 @@ public class ITestPartialRenamesDeletes extends AbstractS3ATestBase { ex = expectDeleteForbidden(basePath); String iostats = ioStatisticsSourceToString(roleFS); - if (multiDelete) { - // multi-delete status checks - deleteVerbCount.assertDiffEquals("Wrong delete request count", 0); - bulkDeleteVerbCount.assertDiffEquals( - "Wrong count of delete operations in " + iostats, 1); - MultiObjectDeleteException mde = extractCause( - MultiObjectDeleteException.class, ex); - List undeletedKeyPaths = - removeUndeletedPaths(mde, keyPaths, storeContext::keyToPath); - final List undeleted = toPathList( - undeletedKeyPaths); - deleteObjectCount.assertDiffEquals( - "Wrong count of objects in delete request", - allFiles.size()); - Assertions.assertThat(undeleted) - .as("files which could not be deleted") - .containsExactlyInAnyOrderElementsOf(readOnlyFiles); - Assertions.assertThat(toPathList(keyPaths)) - .as("files which were deleted") - .containsExactlyInAnyOrderElementsOf(deletableFiles); - rejectionCount.assertDiffEquals("Wrong rejection count", - readOnlyFiles.size()); - } reset(rejectionCount, deleteVerbCount); // build the set of all paths under the directory tree through // a directory listing (i.e. not getFileStatus()). - // small risk of observed inconsistency here on unguarded stores. final Set readOnlyListing = listFilesUnderPath(readOnlyDir, true); String directoryList = readOnlyListing.stream() @@ -736,25 +669,6 @@ public class ITestPartialRenamesDeletes extends AbstractS3ATestBase { .containsExactlyInAnyOrderElementsOf(readOnlyFiles); } - /** - * Verifies the logic of handling directory markers in - * delete operations, specifically: - *

      - *
    1. all markers above empty directories MUST be deleted
    2. - *
    3. all markers above non-empty directories MUST NOT be deleted
    4. - *
    - * As the delete list may include subdirectories, we need to work up from - * the bottom of the list of deleted files before probing the parents, - * that being done by a s3guard get(path, need-empty-directory) call. - *

    - * This is pretty sensitive code. - */ - @Test - public void testSubdirDeleteFailures() throws Throwable { - describe("Multiobject delete handling of directorYesFory markers"); - assume("Multiobject delete only", multiDelete); - } - /** * Expect the delete() call to fail. * @param path path to delete. @@ -913,9 +827,6 @@ public class ITestPartialRenamesDeletes extends AbstractS3ATestBase { * Verifies that s3:DeleteObjectVersion is not required for rename. *

    * See HADOOP-17621. - *

    - * This test will only show a regression if the bucket has versioning - * enabled *and* S3Guard is enabled. */ @Test public void testRenamePermissionRequirements() throws Throwable { @@ -928,7 +839,6 @@ public class ITestPartialRenamesDeletes extends AbstractS3ATestBase { // and then delete. Configuration roleConfig = createAssumedRoleConfig(); bindRolePolicyStatements(roleConfig, - STATEMENT_S3GUARD_CLIENT, STATEMENT_ALLOW_SSE_KMS_RW, STATEMENT_ALL_BUCKET_READ_ACCESS, // root: r-x new Statement(Effects.Allow) // dest: rwx diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/ITestRenameDeleteRace.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/ITestRenameDeleteRace.java index 9885eb56984..2610f54b44e 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/ITestRenameDeleteRace.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/ITestRenameDeleteRace.java @@ -39,7 +39,6 @@ import org.apache.hadoop.util.BlockingThreadPoolExecutorService; import static org.apache.hadoop.fs.s3a.Constants.DIRECTORY_MARKER_POLICY; import static org.apache.hadoop.fs.s3a.Constants.DIRECTORY_MARKER_POLICY_DELETE; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.disableS3GuardInTestBucket; import static org.apache.hadoop.fs.s3a.S3ATestUtils.getTestBucketName; import static org.apache.hadoop.fs.s3a.S3ATestUtils.removeBaseAndBucketOverrides; import static org.apache.hadoop.fs.s3a.impl.CallableSupplier.submit; @@ -82,7 +81,7 @@ public class ITestRenameDeleteRace extends AbstractS3ATestBase { removeBaseAndBucketOverrides(getTestBucketName(conf), conf, DIRECTORY_MARKER_POLICY); - disableS3GuardInTestBucket(conf); + return conf; } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/ITestXAttrCost.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/ITestXAttrCost.java index 415dcba0f57..3a390e34eca 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/ITestXAttrCost.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/ITestXAttrCost.java @@ -58,8 +58,7 @@ public class ITestXAttrCost extends AbstractS3ACostTest { private static final int GET_METADATA_ON_DIR = GET_METADATA_ON_OBJECT * 2; public ITestXAttrCost() { - // no parameterization here - super(false, true, false); + super(true); } @Test diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestHeaderProcessing.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestHeaderProcessing.java index 15c7ae917df..82592b1d019 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestHeaderProcessing.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestHeaderProcessing.java @@ -37,7 +37,6 @@ import org.apache.hadoop.fs.s3a.MockS3AFileSystem; import org.apache.hadoop.fs.s3a.S3ATestUtils; import org.apache.hadoop.fs.s3a.api.RequestFactory; import org.apache.hadoop.fs.s3a.audit.AuditTestSupport; -import org.apache.hadoop.fs.s3a.test.OperationTrackingStore; import org.apache.hadoop.fs.store.audit.AuditSpan; import org.apache.hadoop.test.HadoopTestBase; @@ -98,7 +97,7 @@ public class TestHeaderProcessing extends HadoopTestBase { X_HEADER_MAGIC_MARKER, Long.toString(MAGIC_LEN)); context = S3ATestUtils.createMockStoreContext(true, - new OperationTrackingStore(), CONTEXT_ACCESSORS); + CONTEXT_ACCESSORS); headerProcessing = new HeaderProcessing(context, CONTEXT_ACCESSORS); } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestPartialDeleteFailures.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestPartialDeleteFailures.java deleted file mode 100644 index 0d9ba1d3044..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestPartialDeleteFailures.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * 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.hadoop.fs.s3a.impl; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -import com.amazonaws.services.s3.model.DeleteObjectsRequest; -import com.amazonaws.services.s3.model.MultiObjectDeleteException; -import org.apache.hadoop.thirdparty.com.google.common.collect.Lists; -import org.assertj.core.api.Assertions; -import org.junit.Before; -import org.junit.Test; - -import org.apache.commons.lang3.tuple.Pair; -import org.apache.commons.lang3.tuple.Triple; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.MockS3AFileSystem; -import org.apache.hadoop.fs.s3a.S3ATestUtils; -import org.apache.hadoop.fs.s3a.api.RequestFactory; -import org.apache.hadoop.fs.s3a.audit.AuditTestSupport; -import org.apache.hadoop.fs.s3a.test.OperationTrackingStore; -import org.apache.hadoop.fs.store.audit.AuditSpan; - -import static org.apache.hadoop.fs.s3a.impl.MultiObjectDeleteSupport.ACCESS_DENIED; -import static org.apache.hadoop.fs.s3a.impl.MultiObjectDeleteSupport.removeUndeletedPaths; -import static org.apache.hadoop.fs.s3a.impl.MultiObjectDeleteSupport.toPathList; -import static org.junit.Assert.assertEquals; - -/** - * Unit test suite covering translation of AWS SDK exceptions to S3A exceptions, - * and retry/recovery policies. - */ -public class TestPartialDeleteFailures { - - private static final ContextAccessors CONTEXT_ACCESSORS - = new MinimalContextAccessor(); - - private StoreContext context; - - private static Path qualifyKey(String k) { - return new Path("s3a://bucket/" + k); - } - - private static String toKey(Path path) { - return path.toUri().getPath(); - } - - @Before - public void setUp() throws Exception { - context = S3ATestUtils.createMockStoreContext(true, - new OperationTrackingStore(), CONTEXT_ACCESSORS); - } - - @Test - public void testDeleteExtraction() { - List src = pathList("a", "a/b", "a/c"); - List rejected = pathList("a/b"); - MultiObjectDeleteException ex = createDeleteException(ACCESS_DENIED, - rejected); - List undeleted = - removeUndeletedPaths(ex, src, - TestPartialDeleteFailures::qualifyKey); - assertEquals("mismatch of rejected and undeleted entries", - rejected, undeleted); - } - - @Test - public void testSplitKeysFromResults() throws Throwable { - List src = pathList("a", "a/b", "a/c"); - List rejected = pathList("a/b"); - List keys = keysToDelete(toPathList(src)); - MultiObjectDeleteException ex = createDeleteException(ACCESS_DENIED, - rejected); - Pair, - List> pair = - new MultiObjectDeleteSupport(context, null) - .splitUndeletedKeys(ex, keys); - List undeleted = pair.getLeft(); - List deleted = pair.getRight(); - assertEquals(rejected, undeleted); - // now check the deleted list to verify that it is valid - src.remove(rejected.get(0)); - assertEquals(src, deleted); - } - - /** - * Build a list of qualified paths from vararg parameters. - * @param paths paths to qualify and then convert to a lst. - * @return same paths as a list. - */ - private List pathList(String... paths) { - return Arrays.stream(paths) - .map(k-> - new MultiObjectDeleteSupport.KeyPath(k, - qualifyKey(k), - k.endsWith("/"))) - .collect(Collectors.toList()); - } - - /** - * Build a delete exception containing all the rejected paths. - * The list of successful entries is empty. - * @param rejected the rejected paths. - * @return a new exception - */ - private MultiObjectDeleteException createDeleteException( - final String code, - final List rejected) { - List errors = rejected.stream() - .map((kp) -> { - Path p = kp.getPath(); - MultiObjectDeleteException.DeleteError e - = new MultiObjectDeleteException.DeleteError(); - e.setKey(kp.getKey()); - e.setCode(code); - e.setMessage("forbidden"); - return e; - }).collect(Collectors.toList()); - return new MultiObjectDeleteException(errors, Collections.emptyList()); - } - - /** - * From a list of paths, build up the list of KeyVersion records - * for a delete request. - * All the entries will be files (i.e. no trailing /) - * @param paths path list - * @return a key list suitable for a delete request. - */ - public static List keysToDelete( - List paths) { - return paths.stream() - .map(p -> { - String uripath = p.toUri().getPath(); - return uripath.substring(1); - }) - .map(DeleteObjectsRequest.KeyVersion::new) - .collect(Collectors.toList()); - } - - /** - * From a list of keys, build up the list of keys for a delete request. - * If a key has a trailing /, that will be retained, so it will be - * considered a directory during multi-object delete failure handling - * @param keys key list - * @return a key list suitable for a delete request. - */ - public static List toDeleteRequests( - List keys) { - return keys.stream() - .map(DeleteObjectsRequest.KeyVersion::new) - .collect(Collectors.toList()); - } - - /** - * Verify that on a partial delete, the S3Guard tables are updated - * with deleted items. And only them. - */ - @Test - public void testProcessDeleteFailure() throws Throwable { - String keyA = "/a/"; - String keyAB = "/a/b"; - String keyAC = "/a/c"; - Path pathA = qualifyKey(keyA); - Path pathAB = qualifyKey(keyAB); - Path pathAC = qualifyKey(keyAC); - List srcKeys = Lists.newArrayList(keyA, keyAB, keyAC); - List src = Lists.newArrayList(pathA, pathAB, pathAC); - List keyList = toDeleteRequests(srcKeys); - List deleteForbidden = Lists.newArrayList(pathAB); - final List deleteAllowed = Lists.newArrayList(pathA, pathAC); - List forbiddenKP = - Lists.newArrayList( - new MultiObjectDeleteSupport.KeyPath(keyAB, pathAB, true)); - MultiObjectDeleteException ex = createDeleteException(ACCESS_DENIED, - forbiddenKP); - OperationTrackingStore store - = new OperationTrackingStore(); - StoreContext storeContext = S3ATestUtils - .createMockStoreContext(true, store, CONTEXT_ACCESSORS); - MultiObjectDeleteSupport deleteSupport - = new MultiObjectDeleteSupport(storeContext, null); - List retainedMarkers = new ArrayList<>(); - Triple, List, List>> - triple = deleteSupport.processDeleteFailure(ex, - keyList, - retainedMarkers); - Assertions.assertThat(triple.getRight()) - .as("failure list") - .isEmpty(); - List undeleted = triple.getLeft(); - List deleted = triple.getMiddle(); - Assertions.assertThat(deleted). - as("deleted files") - .containsAll(deleteAllowed) - .doesNotContainAnyElementsOf(deleteForbidden); - Assertions.assertThat(undeleted). - as("undeleted store entries") - .containsAll(deleteForbidden) - .doesNotContainAnyElementsOf(deleteAllowed); - // because dir marker retention is on, we expect at least one retained - // marker - Assertions.assertThat(retainedMarkers). - as("Retained Markers") - .containsExactly(pathA); - Assertions.assertThat(store.getDeleted()). - as("List of tombstoned records") - .doesNotContain(pathA); - } - - - private static final class MinimalContextAccessor - implements ContextAccessors { - - @Override - public Path keyToPath(final String key) { - return qualifyKey(key); - } - - @Override - public String pathToKey(final Path path) { - return null; - } - - @Override - public File createTempFile(final String prefix, final long size) - throws IOException { - throw new UnsupportedOperationException("unsppported"); - } - - @Override - public String getBucketLocation() throws IOException { - return null; - } - - @Override - public Path makeQualified(final Path path) { - return path; - } - - @Override - public AuditSpan getActiveAuditSpan() { - return AuditTestSupport.NOOP_SPAN; - } - - @Override - public RequestFactory getRequestFactory() { - return MockS3AFileSystem.REQUEST_FACTORY; - } - - } - -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/AbstractS3ACostTest.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/AbstractS3ACostTest.java index 99a64e6a942..a0cc1190a20 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/AbstractS3ACostTest.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/AbstractS3ACostTest.java @@ -58,30 +58,11 @@ import static org.apache.hadoop.test.AssertExtensions.dynamicDescription; */ public class AbstractS3ACostTest extends AbstractS3ATestBase { - /** - * Parameter: should the stores be guarded? - */ - private final boolean s3guard; - /** * Parameter: should directory markers be retained? */ private final boolean keepMarkers; - /** - * Is this an auth mode test run? - */ - private final boolean authoritative; - - /** probe states calculated from the configuration options. */ - private boolean isGuarded; - - private boolean isRaw; - - private boolean isAuthoritative; - - private boolean isNonAuth; - private boolean isKeeping; private boolean isDeleting; @@ -100,51 +81,28 @@ public class AbstractS3ACostTest extends AbstractS3ATestBase { */ private Statistic deleteMarkerStatistic; - public AbstractS3ACostTest( - final boolean s3guard, - final boolean keepMarkers, - final boolean authoritative) { - this.s3guard = s3guard; - this.keepMarkers = keepMarkers; - this.authoritative = authoritative; - } /** - * Constructor for tests which don't include - * any for S3Guard. + * Constructor for parameterized tests. * @param keepMarkers should markers be tested. */ - public AbstractS3ACostTest( + protected AbstractS3ACostTest( final boolean keepMarkers) { - this.s3guard = false; this.keepMarkers = keepMarkers; - this.authoritative = false; } @Override public Configuration createConfiguration() { Configuration conf = super.createConfiguration(); String bucketName = getTestBucketName(conf); - removeBucketOverrides(bucketName, conf, - S3_METADATA_STORE_IMPL); - if (!isGuarded()) { - // in a raw run remove all s3guard settings - removeBaseAndBucketOverrides(bucketName, conf, - S3_METADATA_STORE_IMPL); - } removeBaseAndBucketOverrides(bucketName, conf, DIRECTORY_MARKER_POLICY, - METADATASTORE_AUTHORITATIVE, AUTHORITATIVE_PATH); // directory marker options conf.set(DIRECTORY_MARKER_POLICY, keepMarkers ? DIRECTORY_MARKER_POLICY_KEEP : DIRECTORY_MARKER_POLICY_DELETE); - if (isGuarded()) { - conf.set(S3_METADATA_STORE_IMPL, S3GUARD_METASTORE_DYNAMO); - conf.setBoolean(METADATASTORE_AUTHORITATIVE, authoritative); - } disableFilesystemCaching(conf); return conf; } @@ -152,21 +110,7 @@ public class AbstractS3ACostTest extends AbstractS3ATestBase { @Override public void setup() throws Exception { super.setup(); - if (isGuarded()) { - // s3guard is required for those test runs where any of the - // guard options are set - assumeS3GuardState(true, getConfiguration()); - } S3AFileSystem fs = getFileSystem(); - skipDuringFaultInjection(fs); - - // build up the states - isGuarded = isGuarded(); - - isRaw = !isGuarded; - isAuthoritative = isGuarded && authoritative; - isNonAuth = isGuarded && !authoritative; - isKeeping = isKeepingMarkers(); isDeleting = !isKeeping; @@ -200,42 +144,6 @@ public class AbstractS3ACostTest extends AbstractS3ATestBase { setSpanSource(fs); } - public void assumeUnguarded() { - assume("Unguarded FS only", !isGuarded()); - } - - /** - * Is the store guarded authoritatively on the test path? - * @return true if the condition is met on this test run. - */ - public boolean isAuthoritative() { - return authoritative; - } - - /** - * Is the store guarded? - * @return true if the condition is met on this test run. - */ - public boolean isGuarded() { - return s3guard; - } - - /** - * Is the store raw? - * @return true if the condition is met on this test run. - */ - public boolean isRaw() { - return isRaw; - } - - /** - * Is the store guarded non-authoritatively on the test path? - * @return true if the condition is met on this test run. - */ - public boolean isNonAuth() { - return isNonAuth; - } - public boolean isDeleting() { return isDeleting; } @@ -265,7 +173,7 @@ public class AbstractS3ACostTest extends AbstractS3ATestBase { boolean recursive, OperationCost cost) throws Exception { resetStatistics(); - verifyRaw(cost, () -> { + verify(cost, () -> { FSDataOutputStreamBuilder builder = getFileSystem().createFile(path) .overwrite(overwrite); if (recursive) { @@ -328,7 +236,7 @@ public class AbstractS3ACostTest extends AbstractS3ATestBase { */ protected Path create(Path path, boolean overwrite, OperationCost cost) throws Exception { - return verifyRaw(cost, () -> + return verify(cost, () -> file(path, overwrite)); } @@ -410,12 +318,12 @@ public class AbstractS3ACostTest extends AbstractS3ATestBase { * @return the exception caught. * @throws Exception any other exception */ - protected E interceptRaw( + protected E interceptOperation( Class clazz, String text, OperationCost cost, Callable eval) throws Exception { - return verifyMetricsIntercepting(clazz, text, eval, whenRaw(cost)); + return verifyMetricsIntercepting(clazz, text, eval, always(cost)); } /** @@ -428,48 +336,6 @@ public class AbstractS3ACostTest extends AbstractS3ATestBase { return expect(true, cost); } - /** - * Declare the expected cost on a raw FS. - * @param cost costs to expect - * @return a probe. - */ - protected OperationCostValidator.ExpectedProbe whenRaw( - OperationCost cost) { - return expect(isRaw(), cost); - } - - /** - * Declare the expected cost on a guarded FS. - * @param cost costs to expect - * @return a probe. - */ - protected OperationCostValidator.ExpectedProbe whenGuarded( - OperationCost cost) { - return expect(isGuarded(), cost); - } - - /** - * Declare the expected cost on a guarded auth FS. - * @param cost costs to expect - * @return a probe. - */ - protected OperationCostValidator.ExpectedProbe whenAuthoritative( - OperationCost cost) { - return expect(isAuthoritative(), cost); - } - - - /** - * Declare the expected cost on a guarded nonauth FS. - * @param cost costs to expect - * @return a probe. - */ - protected OperationCostValidator.ExpectedProbe whenNonauth( - OperationCost cost) { - return expect(isNonAuth(), cost); - } - - /** * A metric diff which must hold when the fs is keeping markers. * @param cost expected cost @@ -491,8 +357,8 @@ public class AbstractS3ACostTest extends AbstractS3ATestBase { } /** - * Execute a closure expecting a specific number of HEAD/LIST calls - * on raw S3 stores only. The operation is always evaluated. + * Execute a closure expecting a specific number of HEAD/LIST calls. + * The operation is always evaluated. * A span is always created prior to the invocation; saves trouble * in tests that way. * @param cost expected cost @@ -500,29 +366,29 @@ public class AbstractS3ACostTest extends AbstractS3ATestBase { * @param return type of closure * @return the result of the evaluation */ - protected T verifyRaw( + protected T verify( OperationCost cost, Callable eval) throws Exception { return verifyMetrics(eval, - whenRaw(cost), OperationCostValidator.always()); + always(cost), OperationCostValidator.always()); } /** * Execute {@code S3AFileSystem#innerGetFileStatus(Path, boolean, Set)} * for the given probes. - * expect the specific HEAD/LIST count with a raw FS. + * expect the specific HEAD/LIST count. * @param path path * @param needEmptyDirectoryFlag look for empty directory * @param probes file status probes to perform * @param cost expected cost * @return the status */ - public S3AFileStatus verifyRawInnerGetFileStatus( + public S3AFileStatus verifyInnerGetFileStatus( Path path, boolean needEmptyDirectoryFlag, Set probes, OperationCost cost) throws Exception { - return verifyRaw(cost, () -> + return verify(cost, () -> innerGetFileStatus(getFileSystem(), path, needEmptyDirectoryFlag, @@ -532,20 +398,20 @@ public class AbstractS3ACostTest extends AbstractS3ATestBase { /** * Execute {@code S3AFileSystem#innerGetFileStatus(Path, boolean, Set)} * for the given probes -expect a FileNotFoundException, - * and the specific HEAD/LIST count with a raw FS. + * and the specific HEAD/LIST count. * @param path path * @param needEmptyDirectoryFlag look for empty directory * @param probes file status probes to perform * @param cost expected cost */ - public void interceptRawGetFileStatusFNFE( + public void interceptGetFileStatusFNFE( Path path, boolean needEmptyDirectoryFlag, Set probes, OperationCost cost) throws Exception { try (AuditSpan span = span()) { - interceptRaw(FileNotFoundException.class, "", + interceptOperation(FileNotFoundException.class, "", cost, () -> innerGetFileStatus(getFileSystem(), path, @@ -556,15 +422,14 @@ public class AbstractS3ACostTest extends AbstractS3ATestBase { /** * Probe for a path being a directory. - * Metrics are only checked on unguarded stores. * @param path path * @param expected expected outcome - * @param cost expected cost on a Raw FS. + * @param cost expected cost */ protected void isDir(Path path, boolean expected, OperationCost cost) throws Exception { - boolean b = verifyRaw(cost, () -> + boolean b = verify(cost, () -> getFileSystem().isDirectory(path)); Assertions.assertThat(b) .describedAs("isDirectory(%s)", path) @@ -573,15 +438,14 @@ public class AbstractS3ACostTest extends AbstractS3ATestBase { /** * Probe for a path being a file. - * Metrics are only checked on unguarded stores. * @param path path * @param expected expected outcome - * @param cost expected cost on a Raw FS. + * @param cost expected cost */ protected void isFile(Path path, boolean expected, OperationCost cost) throws Exception { - boolean b = verifyRaw(cost, () -> + boolean b = verify(cost, () -> getFileSystem().isFile(path)); Assertions.assertThat(b) .describedAs("isFile(%s)", path) @@ -599,53 +463,6 @@ public class AbstractS3ACostTest extends AbstractS3ATestBase { return probe(stat, expected); } - /** - * A metric diff which must hold when the fs is unguarded. - * @param stat metric source - * @param expected expected value. - * @return the diff. - */ - protected OperationCostValidator.ExpectedProbe withWhenRaw( - final Statistic stat, final int expected) { - return probe(isRaw(), stat, expected); - } - - /** - * A metric diff which must hold when the fs is guarded. - * @param stat metric source - * @param expected expected value. - * @return the diff. - */ - protected OperationCostValidator.ExpectedProbe withWhenGuarded( - final Statistic stat, - final int expected) { - return probe(isGuarded(), stat, expected); - } - - /** - * A metric diff which must hold when the fs is guarded + authoritative. - * @param stat metric source - * @param expected expected value. - * @return the diff. - */ - protected OperationCostValidator.ExpectedProbe withWhenAuthoritative( - final Statistic stat, - final int expected) { - return probe(isAuthoritative(), stat, expected); - } - - /** - * A metric diff which must hold when the fs is guarded + authoritative. - * @param stat metric source - * @param expected expected value. - * @return the diff. - */ - protected OperationCostValidator.ExpectedProbe withWhenNonauth( - final Statistic stat, - final int expected) { - return probe(isNonAuth(), stat, expected); - } - /** * A metric diff which must hold when the fs is keeping markers. * @param stat metric source diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestDirectoryMarkerListing.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestDirectoryMarkerListing.java index a08d77367b1..ccb0c0e79ec 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestDirectoryMarkerListing.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestDirectoryMarkerListing.java @@ -49,13 +49,9 @@ import org.apache.hadoop.fs.s3a.S3AUtils; import org.apache.hadoop.fs.store.audit.AuditSpan; import static org.apache.hadoop.fs.contract.ContractTestUtils.touch; -import static org.apache.hadoop.fs.s3a.Constants.AUTHORITATIVE_PATH; import static org.apache.hadoop.fs.s3a.Constants.DIRECTORY_MARKER_POLICY; import static org.apache.hadoop.fs.s3a.Constants.DIRECTORY_MARKER_POLICY_DELETE; import static org.apache.hadoop.fs.s3a.Constants.DIRECTORY_MARKER_POLICY_KEEP; -import static org.apache.hadoop.fs.s3a.Constants.METADATASTORE_AUTHORITATIVE; -import static org.apache.hadoop.fs.s3a.Constants.S3_METADATA_STORE_IMPL; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.assume; import static org.apache.hadoop.fs.s3a.S3ATestUtils.getTestBucketName; import static org.apache.hadoop.fs.s3a.S3ATestUtils.removeBaseAndBucketOverrides; import static org.apache.hadoop.test.LambdaTestUtils.intercept; @@ -199,12 +195,6 @@ public class ITestDirectoryMarkerListing extends AbstractS3ATestBase { Configuration conf = super.createConfiguration(); String bucketName = getTestBucketName(conf); - // Turn off S3Guard - removeBaseAndBucketOverrides(bucketName, conf, - S3_METADATA_STORE_IMPL, - METADATASTORE_AUTHORITATIVE, - AUTHORITATIVE_PATH); - // directory marker options removeBaseAndBucketOverrides(bucketName, conf, DIRECTORY_MARKER_POLICY); @@ -222,8 +212,6 @@ public class ITestDirectoryMarkerListing extends AbstractS3ATestBase { public void setup() throws Exception { super.setup(); S3AFileSystem fs = getFileSystem(); - assume("unguarded FS only", - !fs.hasMetadataStore()); s3client = fs.getAmazonS3ClientForTesting("markers"); bucket = fs.getBucket(); Path base = new Path(methodPath(), "base"); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3ADeleteCost.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3ADeleteCost.java index 2901767128f..01cadc7c86e 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3ADeleteCost.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3ADeleteCost.java @@ -46,7 +46,7 @@ import static org.apache.hadoop.test.LambdaTestUtils.intercept; /** * Use metrics to assert about the cost of file API calls. *

    - * Parameterized on guarded vs raw. and directory marker keep vs delete. + * Parameterized on directory marker keep vs delete. * There's extra complexity related to bulk/non-bulk delete calls. * If bulk deletes are disabled, many more requests are made to delete * parent directories. The counters of objects deleted are constant @@ -64,20 +64,14 @@ public class ITestS3ADeleteCost extends AbstractS3ACostTest { @Parameterized.Parameters(name = "{0}") public static Collection params() { return Arrays.asList(new Object[][]{ - {"raw-keep-markers", false, true, false}, - {"raw-delete-markers", false, false, false}, - {"nonauth-keep-markers", true, true, false}, - {"nonauth-delete-markers", true, false, false}, - {"auth-delete-markers", true, false, true}, - {"auth-keep-markers", true, true, true} + {"keep-markers", true}, + {"delete-markers", false}, }); } public ITestS3ADeleteCost(final String name, - final boolean s3guard, - final boolean keepMarkers, - final boolean authoritative) { - super(s3guard, keepMarkers, authoritative); + final boolean keepMarkers) { + super(keepMarkers); } @Override @@ -105,8 +99,8 @@ public class ITestS3ADeleteCost extends AbstractS3ACostTest { // still be there Path simpleFile = file(new Path(dir, "simple.txt")); - boolean rawAndKeeping = isRaw() && isDeleting(); - boolean rawAndDeleting = isRaw() && isDeleting(); + boolean rawAndKeeping = !isDeleting(); + boolean rawAndDeleting = isDeleting(); verifyMetrics(() -> { fs.delete(simpleFile, false); return "after fs.delete(simpleFile) " + getMetricSummary(); @@ -116,7 +110,7 @@ public class ITestS3ADeleteCost extends AbstractS3ACostTest { // if deleting markers, look for the parent too probe(rawAndDeleting, OBJECT_METADATA_REQUESTS, FILESTATUS_FILE_PROBE_H + FILESTATUS_DIR_PROBE_H), - withWhenRaw(OBJECT_LIST_REQUEST, + with(OBJECT_LIST_REQUEST, FILESTATUS_FILE_PROBE_L + FILESTATUS_DIR_PROBE_L), with(DIRECTORIES_DELETED, 0), with(FILES_DELETED, 1), @@ -137,7 +131,7 @@ public class ITestS3ADeleteCost extends AbstractS3ACostTest { ); // there is an empty dir for a parent - S3AFileStatus status = verifyRawInnerGetFileStatus(dir, true, + S3AFileStatus status = verifyInnerGetFileStatus(dir, true, StatusProbeEnum.ALL, GET_FILE_STATUS_ON_DIR); assertEmptyDirStatus(status, Tristate.TRUE); } @@ -157,8 +151,8 @@ public class ITestS3ADeleteCost extends AbstractS3ACostTest { Path file1 = file(new Path(dir, "file1.txt")); Path file2 = file(new Path(dir, "file2.txt")); - boolean rawAndKeeping = isRaw() && isDeleting(); - boolean rawAndDeleting = isRaw() && isDeleting(); + boolean rawAndKeeping = !isDeleting(); + boolean rawAndDeleting = isDeleting(); verifyMetrics(() -> { fs.delete(file1, false); return "after fs.delete(file1) " + getMetricSummary(); @@ -169,7 +163,7 @@ public class ITestS3ADeleteCost extends AbstractS3ACostTest { // if deleting markers, look for the parent too probe(rawAndDeleting, OBJECT_METADATA_REQUESTS, FILESTATUS_FILE_PROBE_H + FILESTATUS_DIR_PROBE_H), - withWhenRaw(OBJECT_LIST_REQUEST, + with(OBJECT_LIST_REQUEST, FILESTATUS_FILE_PROBE_L + FILESTATUS_DIR_PROBE_L), with(DIRECTORIES_DELETED, 0), with(FILES_DELETED, 1), diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3AMiscOperationCost.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3AMiscOperationCost.java index 6449d2a5e44..0ee8a72f4a0 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3AMiscOperationCost.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3AMiscOperationCost.java @@ -66,7 +66,7 @@ public class ITestS3AMiscOperationCost extends AbstractS3ACostTest { public ITestS3AMiscOperationCost(final String name, final boolean keepMarkers) { - super(false, keepMarkers, false); + super(keepMarkers); } /** @@ -105,11 +105,13 @@ public class ITestS3AMiscOperationCost extends AbstractS3ACostTest { Path childDir = new Path(baseDir, "subdir/child"); touch(fs, childDir); + // look at path to see if it is a file + // it is not: so LIST final ContentSummary summary = verifyMetrics( () -> getContentSummary(baseDir), with(INVOCATION_GET_CONTENT_SUMMARY, 1), with(AUDIT_SPAN_CREATION, 1), - whenRaw(FILE_STATUS_FILE_PROBE // look at path to see if it is a file + always(FILE_STATUS_FILE_PROBE // look at path to see if it is a file .plus(LIST_OPERATION) // it is not: so LIST .plus(LIST_OPERATION))); // and a LIST on the child dir Assertions.assertThat(summary.getDirectoryCount()) @@ -128,7 +130,7 @@ public class ITestS3AMiscOperationCost extends AbstractS3ACostTest { "", () -> getContentSummary(baseDir), with(INVOCATION_GET_CONTENT_SUMMARY, 1), with(AUDIT_SPAN_CREATION, 1), - whenRaw(FILE_STATUS_FILE_PROBE + always(FILE_STATUS_FILE_PROBE .plus(FILE_STATUS_FILE_PROBE) .plus(LIST_OPERATION) .plus(LIST_OPERATION))); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3AMkdirCost.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3AMkdirCost.java index 639e1dddbae..395eac2987b 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3AMkdirCost.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3AMkdirCost.java @@ -61,7 +61,7 @@ public class ITestS3AMkdirCost extends AbstractS3ACostTest { public ITestS3AMkdirCost(final String name, final boolean keepMarkers) { - super(false, true, false); + super(keepMarkers); } /** diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3ARenameCost.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3ARenameCost.java index 0077503e87c..ff2b3c1161f 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3ARenameCost.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/ITestS3ARenameCost.java @@ -40,7 +40,7 @@ import static org.apache.hadoop.fs.s3a.performance.OperationCostValidator.probe; /** * Use metrics to assert about the cost of file API calls. *

    - * Parameterized on guarded vs raw. and directory marker keep vs delete + * Parameterized on directory marker keep vs delete */ @RunWith(Parameterized.class) public class ITestS3ARenameCost extends AbstractS3ACostTest { @@ -54,18 +54,15 @@ public class ITestS3ARenameCost extends AbstractS3ACostTest { @Parameterized.Parameters(name = "{0}") public static Collection params() { return Arrays.asList(new Object[][]{ - {"raw-keep-markers", false, true, false}, - {"raw-delete-markers", false, false, false}, - {"nonauth-keep-markers", true, true, false}, - {"auth-delete-markers", true, false, true} + {"keep-markers", true}, + {"delete-markers", false}, }); } public ITestS3ARenameCost(final String name, - final boolean s3guard, - final boolean keepMarkers, - final boolean authoritative) { - super(s3guard, keepMarkers, authoritative); + final boolean keepMarkers) { + super(keepMarkers); + } @Test @@ -101,7 +98,7 @@ public class ITestS3ARenameCost extends AbstractS3ACostTest { final int directoriesInPath = directoriesInPath(destDir); verifyMetrics(() -> execRename(srcFilePath, destFilePath), - whenRaw(RENAME_SINGLE_FILE_DIFFERENT_DIR), + always(RENAME_SINGLE_FILE_DIFFERENT_DIR), with(DIRECTORIES_CREATED, 0), with(DIRECTORIES_DELETED, 0), // keeping: only the core delete operation is issued. @@ -152,7 +149,7 @@ public class ITestS3ARenameCost extends AbstractS3ACostTest { Path destFile = new Path(parent2, "dest"); verifyMetrics(() -> execRename(sourceFile, destFile), - whenRaw(RENAME_SINGLE_FILE_SAME_DIR), + always(RENAME_SINGLE_FILE_SAME_DIR), with(OBJECT_COPY_REQUESTS, 1), with(DIRECTORIES_CREATED, 0), with(OBJECT_DELETE_REQUEST, DELETE_OBJECT_REQUEST), @@ -174,7 +171,7 @@ public class ITestS3ARenameCost extends AbstractS3ACostTest { fs.rename(src, dest); return "after fs.rename(/src,/dest) " + getMetricSummary(); }, - whenRaw(FILE_STATUS_FILE_PROBE + always(FILE_STATUS_FILE_PROBE .plus(GET_FILE_STATUS_FNFE) .plus(COPY_OP)), // here we expect there to be no fake directories @@ -213,7 +210,7 @@ public class ITestS3ARenameCost extends AbstractS3ACostTest { with(FAKE_DIRECTORIES_DELETED, 0), with(FILES_DELETED, 1), with(OBJECT_DELETE_REQUEST, DELETE_OBJECT_REQUEST), - whenRaw(FILE_STATUS_FILE_PROBE)); /* no need to look at parent. */ + always(FILE_STATUS_FILE_PROBE)); /* no need to look at parent. */ } finally { fs.delete(src, false); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/OperationCost.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/OperationCost.java index af4cfba0aa0..7ae60a8c7d8 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/OperationCost.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/OperationCost.java @@ -120,7 +120,6 @@ public final class OperationCost { public static final OperationCost LIST_STATUS_LIST_OP = LIST_OPERATION; /** * Metadata cost of a copy operation, as used during rename. - * This happens even if the store is guarded. */ public static final OperationCost COPY_OP = new OperationCost(1, 0); @@ -161,13 +160,6 @@ public final class OperationCost { public static final OperationCost CREATE_FILE_NO_OVERWRITE = FILE_STATUS_ALL_PROBES; - /** - * S3Guard in non-auth mode always attempts a single file - * status call. - */ - public static final OperationCost S3GUARD_NONAUTH_FILE_STATUS_PROBE = - FILE_STATUS_FILE_PROBE; - /** Expected HEAD count. */ private final int head; diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/AbstractMSContract.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/AbstractMSContract.java deleted file mode 100644 index 921d4a686e0..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/AbstractMSContract.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import org.apache.hadoop.fs.FileSystem; - -import java.io.IOException; - -/** - * Test specification for MetadataStore contract tests. Supplies configuration - * and MetadataStore instance. - */ -public abstract class AbstractMSContract { - - public abstract FileSystem getFileSystem() throws IOException; - public abstract MetadataStore getMetadataStore() throws IOException; -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/AbstractS3GuardToolTestBase.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/AbstractS3GuardToolTestBase.java index dddd84b9103..95f22644c91 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/AbstractS3GuardToolTestBase.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/AbstractS3GuardToolTestBase.java @@ -18,50 +18,26 @@ package org.apache.hadoop.fs.s3a.s3guard; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import org.apache.hadoop.fs.s3a.S3AUtils; -import org.apache.hadoop.fs.s3a.UnknownStoreException; -import org.apache.hadoop.util.StopWatch; -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; -import org.apache.hadoop.fs.FileSystem; import org.junit.Test; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.contract.ContractTestUtils; import org.apache.hadoop.fs.s3a.AbstractS3ATestBase; -import org.apache.hadoop.fs.s3a.Constants; -import org.apache.hadoop.fs.s3a.S3AFileStatus; import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.apache.hadoop.fs.s3a.S3ATestUtils; +import org.apache.hadoop.fs.s3a.UnknownStoreException; import org.apache.hadoop.io.IOUtils; import org.apache.hadoop.util.ExitUtil; -import org.apache.hadoop.util.StringUtils; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_REGION_KEY; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_TABLE_CREATE_KEY; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_TABLE_NAME_KEY; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_METASTORE_NULL; -import static org.apache.hadoop.fs.s3a.Constants.S3_METADATA_STORE_IMPL; -import static org.apache.hadoop.fs.s3a.S3AUtils.clearBucketOption; import static org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.BucketInfo.IS_MARKER_AWARE; +import static org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.E_S3GUARD_UNSUPPORTED; import static org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.INVALID_ARGUMENT; import static org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.SUCCESS; +import static org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.UNSUPPORTED_COMMANDS; import static org.apache.hadoop.fs.s3a.s3guard.S3GuardToolTestHelper.exec; import static org.apache.hadoop.fs.s3a.s3guard.S3GuardToolTestHelper.runS3GuardCommand; import static org.apache.hadoop.fs.s3a.tools.MarkerTool.MARKERS; @@ -73,31 +49,14 @@ import static org.apache.hadoop.test.LambdaTestUtils.intercept; */ public abstract class AbstractS3GuardToolTestBase extends AbstractS3ATestBase { - protected static final String OWNER = "hdfs"; - protected static final String DYNAMODB_TABLE = "ireland-team"; protected static final String S3A_THIS_BUCKET_DOES_NOT_EXIST = "s3a://this-bucket-does-not-exist-00000000000"; - private static final int PRUNE_MAX_AGE_SECS = 2; - - private MetadataStore ms; - private S3AFileSystem rawFs; - /** * List of tools to close in test teardown. */ private final List toolsToClose = new ArrayList<>(); - /** - * The test timeout is increased in case previous tests have created - * many tombstone markers which now need to be purged. - * @return the test timeout. - */ - @Override - protected int getTestTimeoutMillis() { - return SCALE_TEST_TIMEOUT_SECONDS * 1000; - } - /** * Declare that the tool is to be closed in teardown. * @param tool tool to close @@ -173,272 +132,23 @@ public abstract class AbstractS3GuardToolTestBase extends AbstractS3ATestBase { } } - protected MetadataStore getMetadataStore() { - return ms; - } - @Override public void setup() throws Exception { super.setup(); - S3ATestUtils.assumeS3GuardState(true, getConfiguration()); - S3AFileSystem fs = getFileSystem(); - ms = fs.getMetadataStore(); - - // Also create a "raw" fs without any MetadataStore configured - Configuration conf = new Configuration(getConfiguration()); - clearBucketOption(conf, fs.getBucket(), S3_METADATA_STORE_IMPL); - conf.set(S3_METADATA_STORE_IMPL, S3GUARD_METASTORE_NULL); - URI fsUri = fs.getUri(); - S3AUtils.setBucketOption(conf,fsUri.getHost(), - S3_METADATA_STORE_IMPL, - S3GUARD_METASTORE_NULL); - rawFs = (S3AFileSystem) FileSystem.newInstance(fsUri, conf); } @Override public void teardown() throws Exception { super.teardown(); toolsToClose.forEach(t -> IOUtils.cleanupWithLogger(LOG, t)); - IOUtils.cleanupWithLogger(LOG, ms); - IOUtils.closeStream(rawFs); - } - - protected void mkdirs(Path path, boolean onS3, boolean onMetadataStore) - throws IOException { - Preconditions.checkArgument(onS3 || onMetadataStore); - // getFileSystem() returns an fs with MetadataStore configured - S3AFileSystem fs = onMetadataStore ? getFileSystem() : rawFs; - if (onS3) { - fs.mkdirs(path); - } else if (onMetadataStore) { - S3AFileStatus status = new S3AFileStatus(true, path, OWNER); - ms.put(new PathMetadata(status), null); - } - } - - protected static void putFile(MetadataStore ms, S3AFileStatus f) - throws IOException { - assertNotNull(f); - try (BulkOperationState bulkWrite = - ms.initiateBulkWrite( - BulkOperationState.OperationType.Put, - f.getPath())) { - ms.put(new PathMetadata(f), bulkWrite); - Path parent = f.getPath().getParent(); - while (parent != null) { - S3AFileStatus dir = new S3AFileStatus(false, parent, f.getOwner()); - ms.put(new PathMetadata(dir), bulkWrite); - parent = parent.getParent(); - } - } - } - - /** - * Create file either on S3 or in metadata store. - * @param path the file path. - * @param onS3 set to true to create the file on S3. - * @param onMetadataStore set to true to create the file on the - * metadata store. - * @throws IOException IO problem - */ - protected void createFile(Path path, boolean onS3, boolean onMetadataStore) - throws IOException { - Preconditions.checkArgument(onS3 || onMetadataStore); - // getFileSystem() returns an fs with MetadataStore configured - S3AFileSystem fs = onMetadataStore ? getFileSystem() : rawFs; - if (onS3) { - ContractTestUtils.touch(fs, path); - } else if (onMetadataStore) { - S3AFileStatus status = new S3AFileStatus(100L, System.currentTimeMillis(), - fs.makeQualified(path), 512L, "hdfs", null, null); - putFile(ms, status); - } - } - - /** - * Attempt to test prune() with sleep() without having flaky tests - * when things run slowly. Test is basically: - * 1. Set max path age to X seconds - * 2. Create some files (which writes entries to MetadataStore) - * 3. Sleep X+2 seconds (all files from above are now "stale") - * 4. Create some other files (these are "fresh"). - * 5. Run prune on MetadataStore. - * 6. Assert that only files that were created before the sleep() were pruned. - * - * Problem is: #6 can fail if X seconds elapse between steps 4 and 5, since - * the newer files also become stale and get pruned. This is easy to - * reproduce by running all integration tests in parallel with a ton of - * threads, or anything else that slows down execution a lot. - * - * Solution: Keep track of time elapsed between #4 and #5, and if it - * exceeds X, just print a warn() message instead of failing. - * - * @param cmdConf configuration for command - * @param parent path - * @param args command args - * @throws Exception - */ - private void testPruneCommand(Configuration cmdConf, Path parent, - String...args) throws Exception { - Path keepParent = path("prune-cli-keep"); - StopWatch timer = new StopWatch(); - final S3AFileSystem fs = getFileSystem(); - S3GuardTool.Prune cmd = toClose(new S3GuardTool.Prune(cmdConf)); - cmd.setMetadataStore(ms); - try { - - fs.mkdirs(parent); - fs.mkdirs(keepParent); - createFile(new Path(parent, "stale"), true, true); - createFile(new Path(keepParent, "stale-to-keep"), true, true); - - Thread.sleep(TimeUnit.SECONDS.toMillis(PRUNE_MAX_AGE_SECS + 2)); - - timer.start(); - createFile(new Path(parent, "fresh"), true, true); - - assertMetastoreListingCount(parent, "Children count before pruning", 2); - exec(cmd, args); - long msecElapsed = timer.now(TimeUnit.MILLISECONDS); - if (msecElapsed >= PRUNE_MAX_AGE_SECS * 1000) { - LOG.warn("Skipping an assertion: Test running too slowly ({} msec)", - msecElapsed); - } else { - assertMetastoreListingCount(parent, "Pruned children count remaining", - 1); - } - assertMetastoreListingCount(keepParent, - "This child should have been kept (prefix restriction).", 1); - } finally { - fs.delete(parent, true); - fs.delete(keepParent, true); - ms.prune(MetadataStore.PruneMode.ALL_BY_MODTIME, - Long.MAX_VALUE, - fs.pathToKey(parent)); - ms.prune(MetadataStore.PruneMode.ALL_BY_MODTIME, - Long.MAX_VALUE, - fs.pathToKey(keepParent)); - // reset the store before we close the tool. - cmd.setMetadataStore(new NullMetadataStore()); - } - } - - private void assertMetastoreListingCount(Path parent, - String message, - int expected) throws IOException { - Collection listing = ms.listChildren(parent).getListing(); - assertEquals(message +" [" + StringUtils.join(", ", listing) + "]", - expected, listing.size()); - } - - @Test - public void testPruneCommandCLI() throws Exception { - Path testPath = path("testPruneCommandCLI"); - testPruneCommand(getFileSystem().getConf(), testPath, - "prune", "-seconds", String.valueOf(PRUNE_MAX_AGE_SECS), - testPath.toString()); - } - - @Test - public void testPruneCommandTombstones() throws Exception { - Path testPath = path("testPruneCommandTombstones"); - getFileSystem().mkdirs(testPath); - getFileSystem().delete(testPath, true); - S3GuardTool.Prune cmd = toClose( - new S3GuardTool.Prune(getFileSystem().getConf())); - cmd.setMetadataStore(ms); - try { - exec(cmd, - "prune", "-" + S3GuardTool.Prune.TOMBSTONE, - "-seconds", "0", - testPath.toString()); - assertNotNull("Command did not create a filesystem", - cmd.getFilesystem()); - } finally { - // reset the store before we close the tool. - cmd.setMetadataStore(new NullMetadataStore()); - } - } - - /** - * HADOOP-16457. In certain cases prune doesn't create an FS. - */ - @Test - public void testMaybeInitFilesystem() throws Exception { - Path testPath = path("maybeInitFilesystem"); - try (S3GuardTool.Prune cmd = - new S3GuardTool.Prune(getFileSystem().getConf())) { - cmd.maybeInitFilesystem(Collections.singletonList(testPath.toString())); - assertNotNull("Command did not create a filesystem", - cmd.getFilesystem()); - } - } - - /** - * HADOOP-16457. In certain cases prune doesn't create an FS. - */ - @Test - public void testMaybeInitFilesystemNoPath() throws Exception { - try (S3GuardTool.Prune cmd = new S3GuardTool.Prune( - getFileSystem().getConf())) { - cmd.maybeInitFilesystem(Collections.emptyList()); - assertNull("Command should not have created a filesystem", - cmd.getFilesystem()); - } - } - - @Test - public void testPruneCommandNoPath() throws Exception { - runToFailure(INVALID_ARGUMENT, - S3GuardTool.Prune.NAME, - "-" + S3GuardTool.Prune.TOMBSTONE, - "-seconds", "0"); - } - - @Test - public void testPruneCommandConf() throws Exception { - getConfiguration().setLong(Constants.S3GUARD_CLI_PRUNE_AGE, - TimeUnit.SECONDS.toMillis(PRUNE_MAX_AGE_SECS)); - Path testPath = path("testPruneCommandConf"); - testPruneCommand(getConfiguration(), testPath, - "prune", testPath.toString()); - } - - @Test - public void testSetCapacityFailFastOnReadWriteOfZero() throws Exception{ - Configuration conf = getConfiguration(); - String bucket = getFileSystem().getBucket(); - conf.set(S3GUARD_DDB_TABLE_NAME_KEY, getFileSystem().getBucket()); - - S3GuardTool.SetCapacity cmdR = toClose(new S3GuardTool.SetCapacity(conf)); - String[] argsR = - new String[]{cmdR.getName(), "-read", "0", "s3a://" + bucket}; - intercept(IllegalArgumentException.class, - S3GuardTool.SetCapacity.READ_CAP_INVALID, () -> cmdR.run(argsR)); - - S3GuardTool.SetCapacity cmdW = toClose(new S3GuardTool.SetCapacity(conf)); - String[] argsW = - new String[]{cmdW.getName(), "-write", "0", "s3a://" + bucket}; - intercept(IllegalArgumentException.class, - S3GuardTool.SetCapacity.WRITE_CAP_INVALID, () -> cmdW.run(argsW)); } @Test public void testBucketInfoUnguarded() throws Exception { final Configuration conf = getConfiguration(); URI fsUri = getFileSystem().getUri(); - conf.set(S3GUARD_DDB_TABLE_CREATE_KEY, Boolean.FALSE.toString()); - String bucket = fsUri.getHost(); - clearBucketOption(conf, bucket, - S3GUARD_DDB_TABLE_CREATE_KEY); - clearBucketOption(conf, bucket, S3_METADATA_STORE_IMPL); - clearBucketOption(conf, bucket, S3GUARD_DDB_TABLE_NAME_KEY); - conf.set(S3_METADATA_STORE_IMPL, S3GUARD_METASTORE_NULL); - conf.set(S3GUARD_DDB_TABLE_NAME_KEY, - "testBucketInfoUnguarded-" + UUID.randomUUID()); - // run a bucket info command and look for - // confirmation that it got the output from DDB diags + // run a bucket info command S3GuardTool.BucketInfo infocmd = toClose(new S3GuardTool.BucketInfo(conf)); String info = exec(infocmd, S3GuardTool.BucketInfo.NAME, "-" + S3GuardTool.BucketInfo.UNGUARDED_FLAG, @@ -457,8 +167,7 @@ public abstract class AbstractS3GuardToolTestBase extends AbstractS3ATestBase { final Configuration conf = getConfiguration(); URI fsUri = getFileSystem().getUri(); - // run a bucket info command and look for - // confirmation that it got the output from DDB diags + // run a bucket info command S3GuardTool.BucketInfo infocmd = toClose(new S3GuardTool.BucketInfo(conf)); String info = exec(infocmd, S3GuardTool.BucketInfo.NAME, "-" + MARKERS, S3GuardTool.BucketInfo.MARKERS_AWARE, @@ -477,45 +186,14 @@ public abstract class AbstractS3GuardToolTestBase extends AbstractS3ATestBase { final Configuration conf = getConfiguration(); URI fsUri = getFileSystem().getUri(); - // run a bucket info command and look for - // confirmation that it got the output from DDB diags + // run a bucket info command and expect failure S3GuardTool.BucketInfo infocmd = toClose(new S3GuardTool.BucketInfo(conf)); - intercept(ExitUtil.ExitException.class, ""+ EXIT_NOT_ACCEPTABLE, () -> + intercept(ExitUtil.ExitException.class, "" + EXIT_NOT_ACCEPTABLE, () -> exec(infocmd, S3GuardTool.BucketInfo.NAME, "-" + MARKERS, "unknown", fsUri.toString())); } - @Test - public void testSetCapacityFailFastIfNotGuarded() throws Exception{ - Configuration conf = getConfiguration(); - bindToNonexistentTable(conf); - String bucket = rawFs.getBucket(); - clearBucketOption(conf, bucket, S3_METADATA_STORE_IMPL); - clearBucketOption(conf, bucket, S3GUARD_DDB_TABLE_NAME_KEY); - clearBucketOption(conf, bucket, S3GUARD_DDB_TABLE_CREATE_KEY); - conf.set(S3_METADATA_STORE_IMPL, S3GUARD_METASTORE_NULL); - - S3GuardTool.SetCapacity cmdR = toClose(new S3GuardTool.SetCapacity(conf)); - String[] argsR = new String[]{ - cmdR.getName(), - "s3a://" + getFileSystem().getBucket() - }; - - intercept(IllegalStateException.class, "unguarded", - () -> cmdR.run(argsR)); - } - - /** - * Binds the configuration to a nonexistent table. - * @param conf - */ - private void bindToNonexistentTable(final Configuration conf) { - conf.set(S3GUARD_DDB_TABLE_NAME_KEY, UUID.randomUUID().toString()); - conf.unset(S3GUARD_DDB_REGION_KEY); - conf.setBoolean(S3GUARD_DDB_TABLE_CREATE_KEY, false); - } - /** * Make an S3GuardTool of the specific subtype with binded configuration * to a nonexistent table. @@ -524,20 +202,15 @@ public abstract class AbstractS3GuardToolTestBase extends AbstractS3ATestBase { private S3GuardTool makeBindedTool(Class tool) throws Exception { Configuration conf = getConfiguration(); - // set a table as a safety check in case the test goes wrong - // and deletes it. - bindToNonexistentTable(conf); return tool.getDeclaredConstructor(Configuration.class).newInstance(conf); } @Test public void testToolsNoBucket() throws Throwable { List> tools = - Arrays.asList(S3GuardTool.Destroy.class, S3GuardTool.BucketInfo.class, - S3GuardTool.Diff.class, S3GuardTool.Import.class, - S3GuardTool.Prune.class, S3GuardTool.SetCapacity.class, - S3GuardTool.Uploads.class, - S3GuardTool.Authoritative.class); + Arrays.asList( + S3GuardTool.BucketInfo.class, + S3GuardTool.Uploads.class); for (Class tool : tools) { S3GuardTool cmdR = makeBindedTool(tool); @@ -551,27 +224,12 @@ public abstract class AbstractS3GuardToolTestBase extends AbstractS3ATestBase { } } - @Test - public void testToolsNoArgsForBucketAndDDBTable() throws Throwable { - List> tools = - Arrays.asList(S3GuardTool.Destroy.class, S3GuardTool.Init.class); - - for (Class tool : tools) { - S3GuardTool cmdR = makeBindedTool(tool); - describe("Calling " + cmdR.getName() + " without any arguments."); - intercept(ExitUtil.ExitException.class, - "S3 bucket url or DDB table name have to be provided explicitly", - () -> cmdR.run(new String[]{tool.getName()})); - } - } - @Test public void testToolsNoArgsForBucket() throws Throwable { List> tools = - Arrays.asList(S3GuardTool.BucketInfo.class, S3GuardTool.Diff.class, - S3GuardTool.Import.class, S3GuardTool.Prune.class, - S3GuardTool.SetCapacity.class, S3GuardTool.Uploads.class, - S3GuardTool.Authoritative.class); + Arrays.asList( + S3GuardTool.BucketInfo.class, + S3GuardTool.Uploads.class); for (Class tool : tools) { S3GuardTool cmdR = makeBindedTool(tool); @@ -582,6 +240,15 @@ public abstract class AbstractS3GuardToolTestBase extends AbstractS3ATestBase { } } + @Test + public void testUnsupported() throws Throwable { + describe("Verify the unsupported tools are rejected"); + for (String tool : UNSUPPORTED_COMMANDS) { + describe("Probing %s", tool); + runToFailure(E_S3GUARD_UNSUPPORTED, tool); + } + } + @Test public void testProbeForMagic() throws Throwable { S3AFileSystem fs = getFileSystem(); @@ -589,7 +256,7 @@ public abstract class AbstractS3GuardToolTestBase extends AbstractS3ATestBase { S3GuardTool.BucketInfo cmd = new S3GuardTool.BucketInfo( getConfiguration()); // this must always work - exec(cmd, S3GuardTool.BucketInfo.MAGIC_FLAG, name); + exec(cmd, S3GuardTool.BucketInfo.MAGIC_FLAG, name); } /** @@ -606,86 +273,4 @@ public abstract class AbstractS3GuardToolTestBase extends AbstractS3ATestBase { } } - @Test - public void testDestroyFailsIfNoBucketNameOrDDBTableSet() - throws Exception { - intercept(ExitUtil.ExitException.class, - () -> run(S3GuardTool.Destroy.NAME)); - } - - @Test - public void testInitFailsIfNoBucketNameOrDDBTableSet() throws Exception { - intercept(ExitUtil.ExitException.class, - () -> run(S3GuardTool.Init.NAME)); - } - - @Test - public void - testDiffCommand() throws Exception { - S3AFileSystem fs = getFileSystem(); - ms = getMetadataStore(); - Set filesOnS3 = new HashSet<>(); // files on S3. - Set filesOnMS = new HashSet<>(); // files on metadata store. - - Path testPath = path("test-diff"); - // clean up through the store and behind it. - fs.delete(testPath, true); - rawFs.delete(testPath, true); - mkdirs(testPath, true, true); - - Path msOnlyPath = new Path(testPath, "ms_only"); - mkdirs(msOnlyPath, false, true); - filesOnMS.add(msOnlyPath); - for (int i = 0; i < 5; i++) { - Path file = new Path(msOnlyPath, String.format("file-%d", i)); - createFile(file, false, true); - filesOnMS.add(file); - } - - Path s3OnlyPath = new Path(testPath, "s3_only"); - mkdirs(s3OnlyPath, true, false); - filesOnS3.add(s3OnlyPath); - for (int i = 0; i < 5; i++) { - Path file = new Path(s3OnlyPath, String.format("file-%d", i)); - createFile(file, true, false); - filesOnS3.add(file); - } - - ByteArrayOutputStream buf = new ByteArrayOutputStream(); - S3GuardTool.Diff cmd = toClose(new S3GuardTool.Diff(fs.getConf())); - cmd.setStore(ms); - String table = "dynamo://" + getTestTableName(DYNAMODB_TABLE); - exec(0, "", cmd, buf, "diff", "-meta", table, testPath.toString()); - - Set actualOnS3 = new HashSet<>(); - Set actualOnMS = new HashSet<>(); - boolean duplicates = false; - try (BufferedReader reader = - new BufferedReader(new InputStreamReader( - new ByteArrayInputStream(buf.toByteArray())))) { - String line; - while ((line = reader.readLine()) != null) { - String[] fields = line.split("\\s"); - assertEquals("[" + line + "] does not have enough fields", - 4, fields.length); - String where = fields[0]; - Path path = new Path(fields[3]); - if (S3GuardTool.Diff.S3_PREFIX.equals(where)) { - duplicates = duplicates || actualOnS3.contains(path); - actualOnS3.add(path); - } else if (S3GuardTool.Diff.MS_PREFIX.equals(where)) { - duplicates = duplicates || actualOnMS.contains(path); - actualOnMS.add(path); - } else { - fail("Unknown prefix: " + where); - } - } - } - String actualOut = buf.toString(); - assertEquals("Mismatched metadata store outputs: " + actualOut, - filesOnMS, actualOnMS); - assertEquals("Mismatched s3 outputs: " + actualOut, filesOnS3, actualOnS3); - assertFalse("Diff contained duplicates", duplicates); - } - } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/DDBCapacities.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/DDBCapacities.java deleted file mode 100644 index 3f1e99061b8..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/DDBCapacities.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.util.Map; -import java.util.Objects; - -import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughputDescription; -import org.junit.Assert; - -import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore.READ_CAPACITY; -import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore.WRITE_CAPACITY; - -/** - * Tuple of read and write capacity of a DDB table. - */ -class DDBCapacities { - private final long read, write; - - DDBCapacities(long read, long write) { - this.read = read; - this.write = write; - } - - public long getRead() { - return read; - } - - public long getWrite() { - return write; - } - - String getReadStr() { - return Long.toString(read); - } - - String getWriteStr() { - return Long.toString(write); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - DDBCapacities that = (DDBCapacities) o; - return read == that.read && write == that.write; - } - - @Override - public int hashCode() { - return Objects.hash(read, write); - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder("Capacities{"); - sb.append("read=").append(read); - sb.append(", write=").append(write); - sb.append('}'); - return sb.toString(); - } - - /** - * Is the the capacity that of an On-Demand table? - * @return true if the capacities are both 0. - */ - public boolean isOnDemandTable() { - return read == 0 && write == 0; - } - - /** - * Given a diagnostics map from a DDB store, extract the capacities. - * @param diagnostics diagnostics map to examine. - * @return the capacities - * @throws AssertionError if the fields are missing. - */ - public static DDBCapacities extractCapacities( - final Map diagnostics) { - String read = diagnostics.get(READ_CAPACITY); - Assert.assertNotNull("No " + READ_CAPACITY + " attribute in diagnostics", - read); - return new DDBCapacities( - Long.parseLong(read), - Long.parseLong(diagnostics.get(WRITE_CAPACITY))); - } - - /** - * Given a throughput information from table.describe(), build - * a DDBCapacities object. - * @param throughput throughput description. - * @return the capacities - */ - public static DDBCapacities extractCapacities( - ProvisionedThroughputDescription throughput) { - return new DDBCapacities(throughput.getReadCapacityUnits(), - throughput.getWriteCapacityUnits()); - } - -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestDynamoDBMetadataStore.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestDynamoDBMetadataStore.java deleted file mode 100644 index 580386a09f6..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestDynamoDBMetadataStore.java +++ /dev/null @@ -1,1543 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URI; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; -import com.amazonaws.services.dynamodbv2.document.DynamoDB; -import com.amazonaws.services.dynamodbv2.document.Item; -import com.amazonaws.services.dynamodbv2.document.PrimaryKey; -import com.amazonaws.services.dynamodbv2.document.Table; -import com.amazonaws.services.dynamodbv2.model.ListTagsOfResourceRequest; -import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException; -import com.amazonaws.services.dynamodbv2.model.SSEDescription; -import com.amazonaws.services.dynamodbv2.model.TableDescription; -import com.amazonaws.services.dynamodbv2.model.Tag; -import com.amazonaws.services.dynamodbv2.model.TagResourceRequest; -import com.amazonaws.services.dynamodbv2.model.UntagResourceRequest; -import org.apache.hadoop.thirdparty.com.google.common.collect.Lists; -import org.assertj.core.api.Assertions; - -import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.hadoop.fs.PathIOException; -import org.apache.hadoop.fs.contract.s3a.S3AContract; -import org.apache.hadoop.fs.s3a.Constants; -import org.apache.hadoop.fs.s3a.S3ATestConstants; -import org.apache.hadoop.fs.s3a.Tristate; -import org.apache.hadoop.io.IOUtils; -import org.apache.hadoop.util.DurationInfo; - -import org.junit.AfterClass; -import org.junit.Assume; -import org.junit.BeforeClass; -import org.junit.Test; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.apache.hadoop.security.UserGroupInformation; - -import static java.lang.String.valueOf; -import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkNotNull; -import static org.apache.hadoop.fs.s3a.Constants.*; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.*; -import static org.apache.hadoop.fs.s3a.S3AUtils.clearBucketOption; -import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStoreTableManager.E_INCOMPATIBLE_ITEM_VERSION; -import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStoreTableManager.E_INCOMPATIBLE_TAG_VERSION; -import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStoreTableManager.E_NO_VERSION_MARKER_AND_NOT_EMPTY; -import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStoreTableManager.getVersionMarkerFromTags; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.*; -import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore.*; -import static org.apache.hadoop.test.LambdaTestUtils.*; - -/** - * Test that {@link DynamoDBMetadataStore} implements {@link MetadataStore}. - * - * In this integration test, we use a real AWS DynamoDB. A - * {@link DynamoDBMetadataStore} object is created in the @BeforeClass method, - * and shared for all test in the @BeforeClass method. You will be charged - * bills for AWS S3 or DynamoDB when you run these tests. - * - * According to the base class, every test case will have independent contract - * to create a new {@link S3AFileSystem} instance and initializes it. - * A table will be created and shared between the tests; some tests also - * create their own. - * - * Important: Any new test which creates a table must do the following - *
      - *
    1. Enable on-demand pricing.
    2. - *
    3. Always destroy the table, even if an assertion fails.
    4. - *
    - * This is needed to avoid "leaking" DDB tables and running up bills. - */ -public class ITestDynamoDBMetadataStore extends MetadataStoreTestBase { - - public static final int MINUTE = 60_000; - - public ITestDynamoDBMetadataStore() { - super(); - } - - private static final Logger LOG = - LoggerFactory.getLogger(ITestDynamoDBMetadataStore.class); - public static final PrimaryKey - VERSION_MARKER_PRIMARY_KEY = createVersionMarkerPrimaryKey( - DynamoDBMetadataStore.VERSION_MARKER_ITEM_NAME); - - private S3AFileSystem fileSystem; - private S3AContract s3AContract; - private DynamoDBMetadataStoreTableManager tableHandler; - - private URI fsUri; - - private String bucket; - - @SuppressWarnings("StaticNonFinalField") - private static DynamoDBMetadataStore ddbmsStatic; - - @SuppressWarnings("StaticNonFinalField") - private static String testDynamoDBTableName; - - private static final List UNCHANGED_ENTRIES = Collections.emptyList(); - - /** - * Create a path under the test path provided by - * the FS contract. - * @param filepath path string in - * @return a path qualified by the test filesystem - */ - protected Path path(String filepath) { - return getFileSystem().makeQualified( - new Path(s3AContract.getTestPath(), filepath)); - } - - @Override - public void setUp() throws Exception { - Configuration conf = prepareTestConfiguration(new Configuration()); - assumeThatDynamoMetadataStoreImpl(conf); - Assume.assumeTrue("Test DynamoDB table name should be set to run " - + "integration tests.", testDynamoDBTableName != null); - conf.set(S3GUARD_DDB_TABLE_NAME_KEY, testDynamoDBTableName); - enableOnDemand(conf); - s3AContract = new S3AContract(conf); - s3AContract.init(); - - fileSystem = (S3AFileSystem) s3AContract.getTestFileSystem(); - assume("No test filesystem", s3AContract.isEnabled()); - assertNotNull("No test filesystem", fileSystem); - fsUri = fileSystem.getUri(); - bucket = fileSystem.getBucket(); - - try{ - super.setUp(); - } catch (FileNotFoundException e){ - LOG.warn("MetadataStoreTestBase setup failed. Waiting for table to be " - + "deleted before trying again.", e); - try { - ddbmsStatic.getTable().waitForDelete(); - } catch (IllegalArgumentException | InterruptedException ex) { - LOG.warn("When awaiting a table to be cleaned up", e); - } - super.setUp(); - } - tableHandler = getDynamoMetadataStore().getTableHandler(); - } - - @BeforeClass - public static void beforeClassSetup() throws IOException { - Configuration conf = prepareTestConfiguration(new Configuration()); - assumeThatDynamoMetadataStoreImpl(conf); - // S3GUARD_DDB_TEST_TABLE_NAME_KEY and S3GUARD_DDB_TABLE_NAME_KEY should - // be configured to use this test. - testDynamoDBTableName = conf.get( - S3ATestConstants.S3GUARD_DDB_TEST_TABLE_NAME_KEY); - String dynamoDbTableName = conf.getTrimmed(S3GUARD_DDB_TABLE_NAME_KEY); - Assume.assumeTrue("No DynamoDB table name configured in " - + S3GUARD_DDB_TABLE_NAME_KEY, - !StringUtils.isEmpty(dynamoDbTableName)); - - // We should assert that the table name is configured, so the test should - // fail if it's not configured. - assertNotNull("Test DynamoDB table name '" - + S3ATestConstants.S3GUARD_DDB_TEST_TABLE_NAME_KEY + "'" - + " should be set to run integration tests.", - testDynamoDBTableName); - - // We should assert that the test table is not the same as the production - // table, as the test table could be modified and destroyed multiple - // times during the test. - assertNotEquals("Test DynamoDB table name: " - + "'" + S3ATestConstants.S3GUARD_DDB_TEST_TABLE_NAME_KEY + "'" - + " and production table name: " - + "'" + S3GUARD_DDB_TABLE_NAME_KEY + "' can not be the same.", - testDynamoDBTableName, conf.get(S3GUARD_DDB_TABLE_NAME_KEY)); - - // We can use that table in the test if these assertions are valid - conf.set(S3GUARD_DDB_TABLE_NAME_KEY, testDynamoDBTableName); - - // remove some prune delays - conf.setInt(S3GUARD_DDB_BACKGROUND_SLEEP_MSEC_KEY, 0); - - // clear all table tagging config before this test - conf.getPropsWithPrefix(S3GUARD_DDB_TABLE_TAG).keySet().forEach( - propKey -> conf.unset(S3GUARD_DDB_TABLE_TAG + propKey) - ); - - // set the tags on the table so that it can be tested later. - Map tagMap = createTagMap(); - for (Map.Entry tagEntry : tagMap.entrySet()) { - conf.set(S3GUARD_DDB_TABLE_TAG + tagEntry.getKey(), tagEntry.getValue()); - } - LOG.debug("Creating static ddbms which will be shared between tests."); - enableOnDemand(conf); - - ddbmsStatic = new DynamoDBMetadataStore(); - ddbmsStatic.initialize(conf, new S3Guard.TtlTimeProvider(conf)); - } - - @AfterClass - public static void afterClassTeardown() { - LOG.debug("Destroying static DynamoDBMetadataStore."); - destroy(ddbmsStatic); - ddbmsStatic = null; - } - - /** - * Destroy and then close() a metastore instance. - * Exceptions are caught and logged at debug. - * @param ddbms store -may be null. - */ - private static void destroy(final DynamoDBMetadataStore ddbms) { - if (ddbms != null) { - try { - ddbms.destroy(); - IOUtils.closeStream(ddbms); - } catch (IOException e) { - LOG.debug("On ddbms shutdown", e); - } - } - } - - private static void assumeThatDynamoMetadataStoreImpl(Configuration conf){ - Assume.assumeTrue("Test only applies when DynamoDB is used for S3Guard", - conf.get(Constants.S3_METADATA_STORE_IMPL).equals( - Constants.S3GUARD_METASTORE_DYNAMO)); - } - - /** - * This teardown does not call super.teardown() so as to avoid the DDMBS table - * from being destroyed. - *

    - * This is potentially quite slow, depending on DDB IO Capacity and number - * of entries to forget. - */ - @Override - public void tearDown() throws Exception { - LOG.info("Removing data from ddbms table in teardown."); - Thread.currentThread().setName("Teardown"); - // The following is a way to be sure the table will be cleared and there - // will be no leftovers after the test. - try { - deleteAllMetadata(); - } finally { - IOUtils.cleanupWithLogger(LOG, fileSystem); - } - } - - /** - * Forget all metadata in the store. - * This originally did an iterate and forget, but using prune() hands off the - * bulk IO into the metastore itself; the forgetting is used - * to purge anything which wasn't pruned. - */ - private void deleteAllMetadata() throws IOException { - // The following is a way to be sure the table will be cleared and there - // will be no leftovers after the test. - // only executed if there is a filesystem, as failure during test setup - // means that strToPath will NPE. - if (getContract() != null && getContract().getFileSystem() != null) { - deleteMetadataUnderPath(ddbmsStatic, strToPath("/"), true); - } - } - - /** - * Delete all metadata under a path. - * Attempt to use prune first as it scales slightly better. - * @param ms store - * @param path path to prune under - * @param suppressErrors should errors be suppressed? - * @throws IOException if there is a failure and suppressErrors == false - */ - public static void deleteMetadataUnderPath(final DynamoDBMetadataStore ms, - final Path path, final boolean suppressErrors) throws IOException { - ThrottleTracker throttleTracker = new ThrottleTracker(ms); - int forgotten = 0; - try (DurationInfo ignored = new DurationInfo(LOG, true, "forget")) { - PathMetadata meta = ms.get(path); - if (meta != null) { - for (DescendantsIterator desc = new DescendantsIterator(ms, - meta); - desc.hasNext();) { - forgotten++; - ms.forgetMetadata(desc.next().getPath()); - } - LOG.info("Forgot {} entries", forgotten); - } - } catch (FileNotFoundException fnfe) { - // there is no table. - return; - } catch (IOException ioe) { - LOG.warn("Failed to forget entries under {}", path, ioe); - if (!suppressErrors) { - throw ioe; - } - } - LOG.info("Throttle statistics: {}", throttleTracker); - } - - @Override protected String getPathStringForPrune(String path) - throws Exception { - String b = getTestBucketName(getContract().getFileSystem().getConf()); - return "/" + b + "/dir2"; - } - - /** - * Each contract has its own S3AFileSystem and DynamoDBMetadataStore objects. - */ - private class DynamoDBMSContract extends AbstractMSContract { - - DynamoDBMSContract(Configuration conf) { - } - - DynamoDBMSContract() { - this(new Configuration()); - } - - @Override - public S3AFileSystem getFileSystem() { - return ITestDynamoDBMetadataStore.this.fileSystem; - } - - @Override - public DynamoDBMetadataStore getMetadataStore() { - return ITestDynamoDBMetadataStore.ddbmsStatic; - } - } - - @Override - public DynamoDBMSContract createContract() { - return new DynamoDBMSContract(); - } - - @Override - public DynamoDBMSContract createContract(Configuration conf) { - return new DynamoDBMSContract(conf); - } - - @Override - protected S3AFileStatus basicFileStatus(Path path, int size, boolean isDir) - throws IOException { - String owner = UserGroupInformation.getCurrentUser().getShortUserName(); - return isDir - ? new S3AFileStatus(true, path, owner) - : new S3AFileStatus(size, getModTime(), path, BLOCK_SIZE, owner, - null, null); - } - - /** - * Create a directory status entry. - * @param dir directory. - * @return the status - */ - private S3AFileStatus dirStatus(Path dir) throws IOException { - return basicFileStatus(dir, 0, true); - } - - private DynamoDBMetadataStore getDynamoMetadataStore() throws IOException { - return (DynamoDBMetadataStore) getContract().getMetadataStore(); - } - - private S3AFileSystem getFileSystem() { - return this.fileSystem; - } - - /** - * Force the configuration into DDB on demand, so that - * even if a test bucket isn't cleaned up, the cost is $0. - * @param conf configuration to patch. - */ - public static void enableOnDemand(Configuration conf) { - conf.setInt(S3GUARD_DDB_TABLE_CAPACITY_WRITE_KEY, 0); - conf.setInt(S3GUARD_DDB_TABLE_CAPACITY_READ_KEY, 0); - } - - /** - * Get the configuration needed to create a table; extracts - * it from the filesystem then always patches it to be on demand. - * Why the patch? It means even if a cached FS has brought in - * some provisioned values, they get reset. - * @return a new configuration - */ - private Configuration getTableCreationConfig() { - Configuration conf = new Configuration(getFileSystem().getConf()); - enableOnDemand(conf); - return conf; - } - - /** - * This tests that after initialize() using an S3AFileSystem object, the - * instance should have been initialized successfully, and tables are ACTIVE. - */ - @Test - public void testInitialize() throws IOException { - final S3AFileSystem s3afs = this.fileSystem; - final String tableName = - getTestTableName("testInitialize"); - Configuration conf = getFileSystem().getConf(); - enableOnDemand(conf); - conf.set(S3GUARD_DDB_TABLE_NAME_KEY, tableName); - DynamoDBMetadataStore ddbms = new DynamoDBMetadataStore(); - try { - ddbms.initialize(s3afs, new S3Guard.TtlTimeProvider(conf)); - Table table = verifyTableInitialized(tableName, ddbms.getDynamoDB()); - verifyTableSse(conf, table.getDescription()); - assertNotNull(ddbms.getTable()); - assertEquals(tableName, ddbms.getTable().getTableName()); - - String expectedRegion = conf.get(S3GUARD_DDB_REGION_KEY, - s3afs.getBucketLocation(bucket)); - assertEquals("DynamoDB table should be in configured region or the same" + - " region as S3 bucket", - expectedRegion, - ddbms.getRegion()); - } finally { - destroy(ddbms); - } - } - - /** - * This tests that after initialize() using a Configuration object, the - * instance should have been initialized successfully, and tables are ACTIVE. - */ - @Test - public void testInitializeWithConfiguration() throws IOException { - final String tableName = - getTestTableName("testInitializeWithConfiguration"); - final Configuration conf = getTableCreationConfig(); - conf.unset(S3GUARD_DDB_TABLE_NAME_KEY); - String savedRegion = conf.get(S3GUARD_DDB_REGION_KEY, - getFileSystem().getBucketLocation()); - conf.unset(S3GUARD_DDB_REGION_KEY); - try (DynamoDBMetadataStore ddbms = new DynamoDBMetadataStore()) { - ddbms.initialize(conf, new S3Guard.TtlTimeProvider(conf)); - fail("Should have failed because the table name is not set!"); - } catch (IllegalArgumentException ignored) { - } - - // config table name - conf.set(S3GUARD_DDB_TABLE_NAME_KEY, tableName); - try (DynamoDBMetadataStore ddbms = new DynamoDBMetadataStore()) { - ddbms.initialize(conf, new S3Guard.TtlTimeProvider(conf)); - fail("Should have failed because as the region is not set!"); - } catch (IllegalArgumentException ignored) { - } - - // config region - conf.set(S3GUARD_DDB_REGION_KEY, savedRegion); - doTestInitializeWithConfiguration(conf, tableName); - - // config table server side encryption (SSE) - conf.setBoolean(S3GUARD_DDB_TABLE_SSE_ENABLED, true); - doTestInitializeWithConfiguration(conf, tableName); - } - - /** - * Test initialize() using a Configuration object successfully. - */ - private void doTestInitializeWithConfiguration(Configuration conf, - String tableName) throws IOException { - DynamoDBMetadataStore ddbms = new DynamoDBMetadataStore(); - try { - ddbms.initialize(conf, new S3Guard.TtlTimeProvider(conf)); - Table table = verifyTableInitialized(tableName, ddbms.getDynamoDB()); - verifyTableSse(conf, table.getDescription()); - assertNotNull(ddbms.getTable()); - assertEquals(tableName, ddbms.getTable().getTableName()); - assertEquals("Unexpected key schema found!", - keySchema(), - ddbms.getTable().describe().getKeySchema()); - } finally { - destroy(ddbms); - } - } - - /** - * This should really drive a parameterized test run of 5^2 entries, but it - * would require a major refactoring to set things up. - * For now, each source test has its own entry, with the destination written - * to. - * This seems to be enough to stop DDB throttling from triggering test - * timeouts. - */ - private static final int[] NUM_METAS_TO_DELETE_OR_PUT = { - -1, // null - 0, // empty collection - 1, // one path - S3GUARD_DDB_BATCH_WRITE_REQUEST_LIMIT, // exact limit of a batch request - S3GUARD_DDB_BATCH_WRITE_REQUEST_LIMIT + 1 // limit + 1 - }; - - @Test - public void testBatchWrite00() throws IOException { - doBatchWriteForOneSet(0); - } - - @Test - public void testBatchWrite01() throws IOException { - doBatchWriteForOneSet(1); - } - - @Test - public void testBatchWrite02() throws IOException { - doBatchWriteForOneSet(2); - } - - @Test - public void testBatchWrite03() throws IOException { - doBatchWriteForOneSet(3); - } - - @Test - public void testBatchWrite04() throws IOException { - doBatchWriteForOneSet(4); - } - - /** - * Test that for a large batch write request, the limit is handled correctly. - * With cleanup afterwards. - */ - private void doBatchWriteForOneSet(int index) throws IOException { - for (int numNewMetas : NUM_METAS_TO_DELETE_OR_PUT) { - doTestBatchWrite(NUM_METAS_TO_DELETE_OR_PUT[index], - numNewMetas, - getDynamoMetadataStore()); - } - // The following is a way to be sure the table will be cleared and there - // will be no leftovers after the test. - deleteMetadataUnderPath(ddbmsStatic, strToPath("/"), false); - } - - /** - * Test that for a large batch write request, the limit is handled correctly. - */ - private void doTestBatchWrite(int numDelete, int numPut, - DynamoDBMetadataStore ms) throws IOException { - Path path = new Path( - "/ITestDynamoDBMetadataStore_testBatchWrite_" + numDelete + '_' - + numPut); - final Path root = fileSystem.makeQualified(path); - final Path oldDir = new Path(root, "oldDir"); - final Path newDir = new Path(root, "newDir"); - LOG.info("doTestBatchWrite: oldDir={}, newDir={}", oldDir, newDir); - Thread.currentThread() - .setName(String.format("Bulk put=%d; delete=%d", numPut, numDelete)); - - AncestorState putState = checkNotNull(ms.initiateBulkWrite( - BulkOperationState.OperationType.Put, newDir), - "No state from initiateBulkWrite()"); - ms.put(new PathMetadata(dirStatus(oldDir)), putState); - ms.put(new PathMetadata(dirStatus(newDir)), putState); - - final List oldMetas = numDelete < 0 ? null : - new ArrayList<>(numDelete); - for (int i = 0; i < numDelete; i++) { - oldMetas.add(new PathMetadata( - basicFileStatus(new Path(oldDir, "child" + i), i, false))); - } - final List newMetas = numPut < 0 ? null : - new ArrayList<>(numPut); - for (int i = 0; i < numPut; i++) { - newMetas.add(new PathMetadata( - basicFileStatus(new Path(newDir, "child" + i), i, false))); - } - - Collection pathsToDelete = null; - if (oldMetas != null) { - // put all metadata of old paths and verify - ms.put(new DirListingMetadata(oldDir, oldMetas, false), UNCHANGED_ENTRIES, - putState); - assertEquals("Child count", - 0, ms.listChildren(newDir).withoutTombstones().numEntries()); - Assertions.assertThat(ms.listChildren(oldDir).getListing()) - .describedAs("Old Directory listing") - .containsExactlyInAnyOrderElementsOf(oldMetas); - - assertTrue(CollectionUtils - .isEqualCollection(oldMetas, ms.listChildren(oldDir).getListing())); - - pathsToDelete = new ArrayList<>(oldMetas.size()); - for (PathMetadata meta : oldMetas) { - pathsToDelete.add(meta.getFileStatus().getPath()); - } - } - - // move the old paths to new paths and verify - AncestorState state = checkNotNull(ms.initiateBulkWrite( - BulkOperationState.OperationType.Put, newDir), - "No state from initiateBulkWrite()"); - assertEquals("bulk write destination", newDir, state.getDest()); - - ThrottleTracker throttleTracker = new ThrottleTracker(ms); - try(DurationInfo ignored = new DurationInfo(LOG, true, - "Move")) { - ms.move(pathsToDelete, newMetas, state); - } - LOG.info("Throttle status {}", throttleTracker); - assertEquals("Number of children in source directory", - 0, ms.listChildren(oldDir).withoutTombstones().numEntries()); - if (newMetas != null) { - Assertions.assertThat(ms.listChildren(newDir).getListing()) - .describedAs("Directory listing") - .containsAll(newMetas); - if (!newMetas.isEmpty()) { - Assertions.assertThat(state.size()) - .describedAs("Size of ancestor state") - .isGreaterThan(newMetas.size()); - } - } - } - - @Test - public void testInitExistingTable() throws IOException { - final DynamoDBMetadataStore ddbms = getDynamoMetadataStore(); - final String tableName = ddbms.getTable().getTableName(); - verifyTableInitialized(tableName, ddbms.getDynamoDB()); - // create existing table - tableHandler.initTable(); - verifyTableInitialized(tableName, ddbms.getDynamoDB()); - } - - /** - * Test versioning handling. - *

      - *
    1. Create the table.
    2. - *
    3. Verify tag propagation.
    4. - *
    5. Delete the version marker -verify failure.
    6. - *
    7. Reinstate a different version marker -verify failure
    8. - *
    - * Delete the version marker and verify that table init fails. - * This also includes the checks for tagging, which goes against all - * principles of unit tests. - * However, merging the routines saves - */ - @Test - public void testTableVersioning() throws Exception { - String tableName = getTestTableName("testTableVersionRequired"); - Configuration conf = getTableCreationConfig(); - int maxRetries = conf.getInt(S3GUARD_DDB_MAX_RETRIES, - S3GUARD_DDB_MAX_RETRIES_DEFAULT); - conf.setInt(S3GUARD_DDB_MAX_RETRIES, 3); - conf.set(S3GUARD_DDB_TABLE_NAME_KEY, tableName); - tagConfiguration(conf); - DynamoDBMetadataStore ddbms = new DynamoDBMetadataStore(); - try { - ddbms.initialize(conf, new S3Guard.TtlTimeProvider(conf)); - DynamoDBMetadataStoreTableManager localTableHandler = - ddbms.getTableHandler(); - - Table table = verifyTableInitialized(tableName, ddbms.getDynamoDB()); - // check the tagging - verifyStoreTags(createTagMap(), ddbms); - // check version compatibility - checkVerifyVersionMarkerCompatibility(localTableHandler, table); - - conf.setInt(S3GUARD_DDB_MAX_RETRIES, maxRetries); - } finally { - destroy(ddbms); - } - } - - private void checkVerifyVersionMarkerCompatibility( - DynamoDBMetadataStoreTableManager localTableHandler, Table table) - throws Exception { - final AmazonDynamoDB addb - = getDynamoMetadataStore().getAmazonDynamoDB(); - Item originalVersionMarker = table.getItem(VERSION_MARKER_PRIMARY_KEY); - - LOG.info("1/6: remove version marker and tags from table " + - "the table is empty, so it should be initialized after the call"); - deleteVersionMarkerItem(table); - removeVersionMarkerTag(table, addb); - localTableHandler.initTable(); - - final int versionFromItem = extractVersionFromMarker( - localTableHandler.getVersionMarkerItem()); - final int versionFromTag = extractVersionFromMarker( - getVersionMarkerFromTags(table, addb)); - assertEquals("Table should be tagged with the right version.", - VERSION, versionFromTag); - assertEquals("Table should have the right version marker.", - VERSION, versionFromItem); - - LOG.info("2/6: if the table is not empty and there's no version marker " + - "it should fail"); - deleteVersionMarkerItem(table); - removeVersionMarkerTag(table, addb); - String testKey = "coffee"; - Item wrongItem = - createVersionMarker(testKey, VERSION * 2, 0); - table.putItem(wrongItem); - intercept(IOException.class, E_NO_VERSION_MARKER_AND_NOT_EMPTY, - () -> localTableHandler.initTable()); - - LOG.info("3/6: table has only version marker item then it will be tagged"); - table.putItem(originalVersionMarker); - localTableHandler.initTable(); - final int versionFromTag2 = extractVersionFromMarker( - getVersionMarkerFromTags(table, addb)); - assertEquals("Table should have the right version marker tag " + - "if there was a version item.", VERSION, versionFromTag2); - - LOG.info("4/6: table has only version marker tag then the version marker " + - "item will be created."); - deleteVersionMarkerItem(table); - removeVersionMarkerTag(table, addb); - localTableHandler.tagTableWithVersionMarker(); - localTableHandler.initTable(); - final int versionFromItem2 = extractVersionFromMarker( - localTableHandler.getVersionMarkerItem()); - assertEquals("Table should have the right version marker item " + - "if there was a version tag.", VERSION, versionFromItem2); - - LOG.info("5/6: add a different marker tag to the table: init should fail"); - deleteVersionMarkerItem(table); - removeVersionMarkerTag(table, addb); - Item v200 = createVersionMarker(VERSION_MARKER_ITEM_NAME, VERSION * 2, 0); - table.putItem(v200); - intercept(IOException.class, E_INCOMPATIBLE_ITEM_VERSION, - () -> localTableHandler.initTable()); - - LOG.info("6/6: add a different marker item to the table: init should fail"); - deleteVersionMarkerItem(table); - removeVersionMarkerTag(table, addb); - int wrongVersion = VERSION + 3; - tagTableWithCustomVersion(table, addb, wrongVersion); - intercept(IOException.class, E_INCOMPATIBLE_TAG_VERSION, - () -> localTableHandler.initTable()); - - // CLEANUP - table.putItem(originalVersionMarker); - localTableHandler.tagTableWithVersionMarker(); - localTableHandler.initTable(); - } - - private void tagTableWithCustomVersion(Table table, - AmazonDynamoDB addb, - int wrongVersion) { - final Tag vmTag = new Tag().withKey(VERSION_MARKER_TAG_NAME) - .withValue(valueOf(wrongVersion)); - TagResourceRequest tagResourceRequest = new TagResourceRequest() - .withResourceArn(table.getDescription().getTableArn()) - .withTags(vmTag); - addb.tagResource(tagResourceRequest); - } - - private void removeVersionMarkerTag(Table table, AmazonDynamoDB addb) { - addb.untagResource(new UntagResourceRequest() - .withResourceArn(table.describe().getTableArn()) - .withTagKeys(VERSION_MARKER_TAG_NAME)); - } - - /** - * Deletes a version marker; spins briefly to await it disappearing. - * @param table table to delete the key - * @throws Exception failure - */ - private void deleteVersionMarkerItem(Table table) throws Exception { - table.deleteItem(VERSION_MARKER_PRIMARY_KEY); - eventually(30_000, 1_0, () -> - assertNull("Version marker should be null after deleting it " + - "from the table.", table.getItem(VERSION_MARKER_PRIMARY_KEY))); - } - - /** - * Test that initTable fails with IOException when table does not exist and - * table auto-creation is disabled. - */ - @Test - public void testFailNonexistentTable() throws IOException { - final String tableName = - getTestTableName("testFailNonexistentTable"); - final S3AFileSystem s3afs = getFileSystem(); - final Configuration conf = s3afs.getConf(); - enableOnDemand(conf); - conf.set(S3GUARD_DDB_TABLE_NAME_KEY, tableName); - String b = fsUri.getHost(); - clearBucketOption(conf, b, S3GUARD_DDB_TABLE_CREATE_KEY); - clearBucketOption(conf, b, S3_METADATA_STORE_IMPL); - clearBucketOption(conf, b, S3GUARD_DDB_TABLE_NAME_KEY); - conf.unset(S3GUARD_DDB_TABLE_CREATE_KEY); - try (DynamoDBMetadataStore ddbms = new DynamoDBMetadataStore()) { - ddbms.initialize(s3afs, new S3Guard.TtlTimeProvider(conf)); - // if an exception was not raised, a table was created. - // So destroy it before failing. - ddbms.destroy(); - fail("Should have failed as table does not exist and table auto-creation" - + " is disabled"); - } catch (IOException ignored) { - } - } - - /** - * Test cases about root directory as it is not in the DynamoDB table. - */ - @Test - public void testRootDirectory() throws IOException { - final DynamoDBMetadataStore ddbms = getDynamoMetadataStore(); - Path rootPath = new Path(new Path(fsUri), "/"); - verifyRootDirectory(ddbms.get(rootPath), true); - - ddbms.put(new PathMetadata(new S3AFileStatus(true, - new Path(rootPath, "foo"), - UserGroupInformation.getCurrentUser().getShortUserName())), - null); - verifyRootDirectory(ddbms.get(rootPath), false); - } - - private void verifyRootDirectory(PathMetadata rootMeta, boolean isEmpty) { - assertNotNull(rootMeta); - final FileStatus status = rootMeta.getFileStatus(); - assertNotNull(status); - assertTrue(status.isDirectory()); - // UNKNOWN is always a valid option, but true / false should not contradict - if (isEmpty) { - assertNotSame("Should not be marked non-empty", - Tristate.FALSE, - rootMeta.isEmptyDirectory()); - } else { - assertNotSame("Should not be marked empty", - Tristate.TRUE, - rootMeta.isEmptyDirectory()); - } - } - - /** - * Test that when moving nested paths, all its ancestors up to destination - * root will also be created. - * Here is the directory tree before move: - *
    -   * testMovePopulateAncestors
    -   * ├── a
    -   * │   └── b
    -   * │       └── src
    -   * │           ├── dir1
    -   * │           │   └── dir2
    -   * │           └── file1.txt
    -   * └── c
    -   *     └── d
    -   *         └── dest
    -   *
    - * As part of rename(a/b/src, d/c/dest), S3A will enumerate the subtree at - * a/b/src. This test verifies that after the move, the new subtree at - * 'dest' is reachable from the root (i.e. c/ and c/d exist in the table. - * DynamoDBMetadataStore depends on this property to do recursive delete - * without a full table scan. - */ - @Test - public void testMovePopulatesAncestors() throws IOException { - final DynamoDBMetadataStore ddbms = getDynamoMetadataStore(); - final String testRoot = "/testMovePopulatesAncestors"; - final String srcRoot = testRoot + "/a/b/src"; - final String destRoot = testRoot + "/c/d/e/dest"; - - final Path nestedPath1 = strToPath(srcRoot + "/file1.txt"); - AncestorState bulkWrite = ddbms.initiateBulkWrite( - BulkOperationState.OperationType.Put, nestedPath1); - ddbms.put(new PathMetadata(basicFileStatus(nestedPath1, 1024, false)), - bulkWrite); - final Path nestedPath2 = strToPath(srcRoot + "/dir1/dir2"); - ddbms.put(new PathMetadata(basicFileStatus(nestedPath2, 0, true)), - bulkWrite); - - // We don't put the destRoot path here, since put() would create ancestor - // entries, and we want to ensure that move() does it, instead. - - // Build enumeration of src / dest paths and do the move() - final Collection fullSourcePaths = Lists.newArrayList( - strToPath(srcRoot), - strToPath(srcRoot + "/dir1"), - strToPath(srcRoot + "/dir1/dir2"), - strToPath(srcRoot + "/file1.txt")); - final String finalFile = destRoot + "/file1.txt"; - final Collection pathsToCreate = Lists.newArrayList( - new PathMetadata(basicFileStatus(strToPath(destRoot), - 0, true)), - new PathMetadata(basicFileStatus(strToPath(destRoot + "/dir1"), - 0, true)), - new PathMetadata(basicFileStatus(strToPath(destRoot + "/dir1/dir2"), - 0, true)), - new PathMetadata(basicFileStatus(strToPath(finalFile), - 1024, false)) - ); - - ddbms.move(fullSourcePaths, pathsToCreate, bulkWrite); - bulkWrite.close(); - // assert that all the ancestors should have been populated automatically - List paths = Lists.newArrayList( - testRoot + "/c", testRoot + "/c/d", testRoot + "/c/d/e", destRoot, - destRoot + "/dir1", destRoot + "/dir1/dir2"); - for (String p : paths) { - assertCached(p); - verifyInAncestor(bulkWrite, p, true); - } - // Also check moved files while we're at it - assertCached(finalFile); - verifyInAncestor(bulkWrite, finalFile, false); - } - - @Test - public void testAncestorOverwriteConflict() throws Throwable { - final DynamoDBMetadataStore ddbms = getDynamoMetadataStore(); - String testRoot = "/" + getMethodName(); - String parent = testRoot + "/parent"; - Path parentPath = strToPath(parent); - String child = parent + "/child"; - Path childPath = strToPath(child); - String grandchild = child + "/grandchild"; - Path grandchildPath = strToPath(grandchild); - String child2 = parent + "/child2"; - String grandchild2 = child2 + "/grandchild2"; - Path grandchild2Path = strToPath(grandchild2); - AncestorState bulkWrite = ddbms.initiateBulkWrite( - BulkOperationState.OperationType.Put, parentPath); - - // writing a child creates ancestors - ddbms.put( - new PathMetadata(basicFileStatus(childPath, 1024, false)), - bulkWrite); - verifyInAncestor(bulkWrite, child, false); - verifyInAncestor(bulkWrite, parent, true); - - // overwrite an ancestor with a file entry in the same operation - // is an error. - intercept(PathIOException.class, E_INCONSISTENT_UPDATE, - () -> ddbms.put( - new PathMetadata(basicFileStatus(parentPath, 1024, false)), - bulkWrite)); - - // now put a file under the child and expect the put operation - // to fail fast, because the ancestor state includes a file at a parent. - - intercept(PathIOException.class, E_INCONSISTENT_UPDATE, - () -> ddbms.put( - new PathMetadata(basicFileStatus(grandchildPath, 1024, false)), - bulkWrite)); - - // and expect a failure for directory update under the child - DirListingMetadata grandchildListing = new DirListingMetadata( - grandchildPath, - new ArrayList<>(), false); - intercept(PathIOException.class, E_INCONSISTENT_UPDATE, - () -> ddbms.put(grandchildListing, UNCHANGED_ENTRIES, bulkWrite)); - - // but a directory update under another path is fine - DirListingMetadata grandchild2Listing = new DirListingMetadata( - grandchild2Path, - new ArrayList<>(), false); - ddbms.put(grandchild2Listing, UNCHANGED_ENTRIES, bulkWrite); - // and it creates a new entry for its parent - verifyInAncestor(bulkWrite, child2, true); - } - - /** - * Assert that a path has an entry in the ancestor state. - * @param state ancestor state - * @param path path to look for - * @param isDirectory is it a directory - * @return the value - * @throws IOException IO failure - * @throws AssertionError assertion failure. - */ - private DDBPathMetadata verifyInAncestor(AncestorState state, - String path, - final boolean isDirectory) - throws IOException { - final Path p = strToPath(path); - assertTrue("Path " + p + " not found in ancestor state", state.contains(p)); - final DDBPathMetadata md = state.get(p); - assertTrue("Ancestor value for "+ path, - isDirectory - ? md.getFileStatus().isDirectory() - : md.getFileStatus().isFile()); - return md; - } - - @Test - public void testDeleteTable() throws Exception { - final String tableName = getTestTableName("testDeleteTable"); - Path testPath = new Path(new Path(fsUri), "/" + tableName); - final S3AFileSystem s3afs = getFileSystem(); - // patch the filesystem config as this is one read in initialize() - final Configuration conf = s3afs.getConf(); - conf.set(S3GUARD_DDB_TABLE_NAME_KEY, tableName); - enableOnDemand(conf); - DynamoDBMetadataStore ddbms = new DynamoDBMetadataStore(); - try { - ddbms.initialize(s3afs, new S3Guard.TtlTimeProvider(conf)); - // we can list the empty table - ddbms.listChildren(testPath); - DynamoDB dynamoDB = ddbms.getDynamoDB(); - ddbms.destroy(); - verifyTableNotExist(tableName, dynamoDB); - - // delete table once more; the ResourceNotFoundException swallowed - // silently - ddbms.destroy(); - verifyTableNotExist(tableName, dynamoDB); - intercept(IOException.class, "", - "Should have failed after the table is destroyed!", - () -> ddbms.listChildren(testPath)); - ddbms.destroy(); - intercept(FileNotFoundException.class, "", - "Destroyed table should raise FileNotFoundException when pruned", - () -> ddbms.prune(PruneMode.ALL_BY_MODTIME, 0)); - } finally { - destroy(ddbms); - } - } - - protected void verifyStoreTags(final Map tagMap, - final DynamoDBMetadataStore store) { - List tags = listTagsOfStore(store); - Map actual = new HashMap<>(); - tags.forEach(t -> actual.put(t.getKey(), t.getValue())); - Assertions.assertThat(actual) - .describedAs("Tags from DDB table") - .containsAllEntriesOf(tagMap); - - // The version marker is always there in the tags. - // We have a plus one in tags we expect. - assertEquals(tagMap.size() + 1, tags.size()); - } - - protected List listTagsOfStore(final DynamoDBMetadataStore store) { - ListTagsOfResourceRequest listTagsOfResourceRequest = - new ListTagsOfResourceRequest() - .withResourceArn(store.getTable().getDescription() - .getTableArn()); - return store.getAmazonDynamoDB() - .listTagsOfResource(listTagsOfResourceRequest).getTags(); - } - - private static Map createTagMap() { - Map tagMap = new HashMap<>(); - tagMap.put("hello", "dynamo"); - tagMap.put("tag", "youre it"); - return tagMap; - } - - private static void tagConfiguration(Configuration conf) { - // set the tags on the table so that it can be tested later. - Map tagMap = createTagMap(); - for (Map.Entry tagEntry : tagMap.entrySet()) { - conf.set(S3GUARD_DDB_TABLE_TAG + tagEntry.getKey(), tagEntry.getValue()); - } - } - - @Test - public void testGetEmptyDirFlagCanSetTrue() throws IOException { - boolean authoritativeDirectoryListing = true; - testGetEmptyDirFlagCanSetTrueOrUnknown(authoritativeDirectoryListing); - } - - @Test - public void testGetEmptyDirFlagCanSetUnknown() throws IOException { - boolean authoritativeDirectoryListing = false; - testGetEmptyDirFlagCanSetTrueOrUnknown(authoritativeDirectoryListing); - } - - private void testGetEmptyDirFlagCanSetTrueOrUnknown(boolean auth) - throws IOException { - // setup - final DynamoDBMetadataStore ms = getDynamoMetadataStore(); - String rootPath = "/testAuthoritativeEmptyDirFlag-" + UUID.randomUUID(); - String filePath = rootPath + "/file1"; - final Path dirToPut = fileSystem.makeQualified(new Path(rootPath)); - final Path fileToPut = fileSystem.makeQualified(new Path(filePath)); - - // Create non-auth DirListingMetadata - DirListingMetadata dlm = - new DirListingMetadata(dirToPut, new ArrayList<>(), auth); - if(auth){ - assertEquals(Tristate.TRUE, dlm.isEmpty()); - } else { - assertEquals(Tristate.UNKNOWN, dlm.isEmpty()); - } - assertEquals(auth, dlm.isAuthoritative()); - - // Test with non-authoritative listing, empty dir - ms.put(dlm, UNCHANGED_ENTRIES, null); - final PathMetadata pmdResultEmpty = ms.get(dirToPut, true); - if(auth){ - assertEquals(Tristate.TRUE, pmdResultEmpty.isEmptyDirectory()); - } else { - assertEquals(Tristate.UNKNOWN, pmdResultEmpty.isEmptyDirectory()); - } - - // Test with non-authoritative listing, non-empty dir - dlm.put(new PathMetadata(basicFileStatus(fileToPut, 1, false))); - ms.put(dlm, UNCHANGED_ENTRIES, null); - final PathMetadata pmdResultNotEmpty = ms.get(dirToPut, true); - assertEquals(Tristate.FALSE, pmdResultNotEmpty.isEmptyDirectory()); - } - - /** - * This validates the table is created and ACTIVE in DynamoDB. - * - * This should not rely on the {@link DynamoDBMetadataStore} implementation. - * Return the table - */ - private Table verifyTableInitialized(String tableName, DynamoDB dynamoDB) { - final Table table = dynamoDB.getTable(tableName); - final TableDescription td = table.describe(); - assertEquals(tableName, td.getTableName()); - assertEquals("ACTIVE", td.getTableStatus()); - return table; - } - - /** - * Verify the table is created with correct server side encryption (SSE). - */ - private void verifyTableSse(Configuration conf, TableDescription td) { - SSEDescription sseDescription = td.getSSEDescription(); - if (conf.getBoolean(S3GUARD_DDB_TABLE_SSE_ENABLED, false)) { - assertNotNull(sseDescription); - assertEquals("ENABLED", sseDescription.getStatus()); - assertEquals("KMS", sseDescription.getSSEType()); - // We do not test key ARN is the same as configured value, - // because in configuration, the ARN can be specified by alias. - assertNotNull(sseDescription.getKMSMasterKeyArn()); - } else { - if (sseDescription != null) { - assertEquals("DISABLED", sseDescription.getStatus()); - } - } - } - - /** - * This validates the table is not found in DynamoDB. - * - * This should not rely on the {@link DynamoDBMetadataStore} implementation. - */ - private void verifyTableNotExist(String tableName, DynamoDB dynamoDB) throws - Exception{ - intercept(ResourceNotFoundException.class, - () -> dynamoDB.getTable(tableName).describe()); - } - - private String getTestTableName(String suffix) { - return getTestDynamoTablePrefix(s3AContract.getConf()) + suffix; - } - - @Test - public void testPruneAgainstInvalidTable() throws Throwable { - describe("Create an Invalid listing and prune it"); - DynamoDBMetadataStore ms - = ITestDynamoDBMetadataStore.ddbmsStatic; - String base = "/" + getMethodName(); - String subdir = base + "/subdir"; - Path subDirPath = strToPath(subdir); - createNewDirs(base, subdir); - - String subFile = subdir + "/file1"; - Path subFilePath = strToPath(subFile); - putListStatusFiles(subdir, true, - subFile); - final DDBPathMetadata subDirMetadataOrig = ms.get(subDirPath); - Assertions.assertThat(subDirMetadataOrig.isAuthoritativeDir()) - .describedAs("Subdirectory %s", subDirMetadataOrig) - .isTrue(); - - // now let's corrupt the graph by putting a file - // over the subdirectory - - long now = getTime(); - long oldTime = now - MINUTE; - putFile(subdir, oldTime, null); - getFile(subdir); - - Path basePath = strToPath(base); - DirListingMetadata listing = ms.listChildren(basePath); - String childText = listing.prettyPrint(); - LOG.info("Listing {}", childText); - Collection childList = listing.getListing(); - Assertions.assertThat(childList) - .as("listing of %s with %s", basePath, childText) - .hasSize(1); - PathMetadata[] pm = new PathMetadata[0]; - S3AFileStatus status = childList.toArray(pm)[0] - .getFileStatus(); - Assertions.assertThat(status.isFile()) - .as("Entry %s", (Object)pm) - .isTrue(); - getNonNull(subFile); - - LOG.info("Pruning"); - // now prune - ms.prune(PruneMode.ALL_BY_MODTIME, - now + MINUTE, subdir); - ms.get(subFilePath); - - final PathMetadata subDirMetadataFinal = getNonNull(subdir); - - Assertions.assertThat(subDirMetadataFinal.getFileStatus().isFile()) - .describedAs("Subdirectory entry %s is still a file", - subDirMetadataFinal) - .isTrue(); - } - - @Test - public void testPutFileDirectlyUnderTombstone() throws Throwable { - describe("Put a file under a tombstone; verify the tombstone"); - String base = "/" + getMethodName(); - long now = getTime(); - putTombstone(base, now, null); - PathMetadata baseMeta1 = get(base); - Assertions.assertThat(baseMeta1.isDeleted()) - .as("Metadata %s", baseMeta1) - .isTrue(); - String child = base + "/file"; - putFile(child, now, null); - getDirectory(base); - } - - @Test - public void testPruneTombstoneUnderTombstone() throws Throwable { - describe("Put a tombsteone under a tombstone, prune the pair"); - String base = "/" + getMethodName(); - long now = getTime(); - String dir = base + "/dir"; - putTombstone(dir, now, null); - assertIsTombstone(dir); - // parent dir is created - assertCached(base); - String child = dir + "/file"; - String child2 = dir + "/file2"; - - // this will actually mark the parent as a dir, - // so that lists of that dir will pick up the tombstone - putTombstone(child, now, null); - getDirectory(dir); - // tombstone the dir - putTombstone(dir, now, null); - // add another child entry; this will update the dir entry from being - // tombstone to dir - putFile(child2, now, null); - getDirectory(dir); - - // put a tombstone over the directory again - putTombstone(dir, now, null); - // verify - assertIsTombstone(dir); - - //prune all tombstones - getDynamoMetadataStore().prune(PruneMode.TOMBSTONES_BY_LASTUPDATED, - now + MINUTE); - - // the child is gone - assertNotFound(child); - - // *AND* the parent dir has not been created - assertNotFound(dir); - - // the child2 entry is still there, though it's now orphan (the store isn't - // meeting the rule "all entries must have a parent which exists" - getFile(child2); - // a full prune will still find and delete it, as this - // doesn't walk the tree - getDynamoMetadataStore().prune(PruneMode.ALL_BY_MODTIME, - now + MINUTE); - assertNotFound(child2); - assertNotFound(dir); - } - - @Test - public void testPruneFileUnderTombstone() throws Throwable { - describe("Put a file under a tombstone, prune the pair"); - String base = "/" + getMethodName(); - long now = getTime(); - String dir = base + "/dir"; - putTombstone(dir, now, null); - assertIsTombstone(dir); - // parent dir is created - assertCached(base); - String child = dir + "/file"; - - // this will actually mark the parent as a dir, - // so that lists of that dir will pick up the tombstone - putFile(child, now, null); - // dir is reinstated - getDirectory(dir); - - // put a tombstone - putTombstone(dir, now, null); - // prune all entries - getDynamoMetadataStore().prune(PruneMode.ALL_BY_MODTIME, - now + MINUTE); - // the child is gone - assertNotFound(child); - - // *AND* the parent dir has not been created - assertNotFound(dir); - } - - - @Test - public void testPruneFilesNotDirs() throws Throwable { - describe("HADOOP-16725: directories cannot be pruned"); - String base = "/" + getMethodName(); - final long now = getTime(); - // round it off for ease of interpreting results - final long t0 = now - (now % 100_000); - long interval = 1_000; - long t1 = t0 + interval; - long t2 = t1 + interval; - String dir = base + "/dir"; - String dir2 = base + "/dir2"; - String child1 = dir + "/file1"; - String child2 = dir + "/file2"; - final Path basePath = strToPath(base); - // put the dir at age t0 - final DynamoDBMetadataStore ms = getDynamoMetadataStore(); - final AncestorState ancestorState - = ms.initiateBulkWrite( - BulkOperationState.OperationType.Put, - basePath); - putDir(base, t0, ancestorState); - assertLastUpdated(base, t0); - - putDir(dir, t0, ancestorState); - assertLastUpdated(dir, t0); - // base dir is unchanged - assertLastUpdated(base, t0); - - // this directory will not have any children, so - // will be excluded from any ancestor re-creation - putDir(dir2, t0, ancestorState); - - // child1 has age t0 and so will be pruned - putFile(child1, t0, ancestorState); - - // child2 has age t2 - putFile(child2, t2, ancestorState); - - // close the ancestor state - ancestorState.close(); - - // make some assertions about state before the prune - assertLastUpdated(base, t0); - assertLastUpdated(dir, t0); - assertLastUpdated(dir2, t0); - assertLastUpdated(child1, t0); - assertLastUpdated(child2, t2); - - // prune all entries older than t1 must delete child1 but - // not the directory, even though it is of the same age - LOG.info("Starting prune of all entries older than {}", t1); - ms.prune(PruneMode.ALL_BY_MODTIME, t1); - // child1 is gone - assertNotFound(child1); - - // *AND* the parent dir has not been created - assertCached(dir); - assertCached(child2); - assertCached(dir2); - - } - - /** - * A cert that there is an entry for the given key and that its - * last updated timestamp matches that passed in. - * @param key Key to look up. - * @param lastUpdated Timestamp to expect. - * @throws IOException I/O failure. - */ - protected void assertLastUpdated(final String key, final long lastUpdated) - throws IOException { - PathMetadata dirMD = verifyCached(key); - assertEquals("Last updated timestamp in MD " + dirMD, - lastUpdated, dirMD.getLastUpdated()); - } - - /** - * Keep in sync with code changes in S3AFileSystem.finishedWrite() so that - * the production code can be tested here. - */ - @Test - public void testPutFileDeepUnderTombstone() throws Throwable { - describe("Put a file two levels under a tombstone"); - String base = "/" + getMethodName(); - String dir = base + "/dir"; - long now = getTime(); - // creating a file MUST create its parents - String child = dir + "/file"; - Path childPath = strToPath(child); - putFile(child, now, null); - getFile(child); - getDirectory(dir); - getDirectory(base); - - // now put the tombstone - putTombstone(base, now, null); - assertIsTombstone(base); - - /*- --------------------------------------------*/ - /* Begin S3FileSystem.finishedWrite() sequence. */ - /* ---------------------------------------------*/ - AncestorState ancestorState = getDynamoMetadataStore() - .initiateBulkWrite(BulkOperationState.OperationType.Put, - childPath); - S3Guard.addAncestors(getDynamoMetadataStore(), - childPath, - getTtlTimeProvider(), - ancestorState); - // now write the file again. - putFile(child, now, ancestorState); - /* -------------------------------------------*/ - /* End S3FileSystem.finishedWrite() sequence. */ - /* -------------------------------------------*/ - - getFile(child); - // the ancestor will now exist. - getDirectory(dir); - getDirectory(base); - } - - @Test - public void testDumpTable() throws Throwable { - describe("Dump the table contents, but not the S3 Store"); - String target = System.getProperty("test.build.dir", "target"); - File buildDir = new File(target).getAbsoluteFile(); - String name = "ITestDynamoDBMetadataStore"; - File destFile = new File(buildDir, name); - DumpS3GuardDynamoTable.dumpStore( - null, - ddbmsStatic, - getFileSystem().getConf(), - destFile, - fsUri); - File storeFile = new File(buildDir, name + DumpS3GuardDynamoTable.SCAN_CSV); - try (BufferedReader in = new BufferedReader(new InputStreamReader( - new FileInputStream(storeFile), Charset.forName("UTF-8")))) { - for (String line : org.apache.commons.io.IOUtils.readLines(in)) { - LOG.info(line); - } - } - } - - @Test - public void testPurgeTableNoForce() throws Throwable { - describe("Purge the table"); - - putTombstone("/" + getMethodName(), getTime(), null); - Pair r = PurgeS3GuardDynamoTable.purgeStore( - null, - ddbmsStatic, - getFileSystem().getConf(), - fsUri, - false); - - Assertions.assertThat(r.getLeft()). - describedAs("entries found in %s", r) - .isGreaterThanOrEqualTo(1); - Assertions.assertThat(r.getRight()). - describedAs("entries deleted in %s", r) - .isZero(); - } - - @Test - public void testPurgeTableForce() throws Throwable { - describe("Purge the table -force"); - putTombstone("/" + getMethodName(), getTime(), null); - Pair r = PurgeS3GuardDynamoTable.purgeStore( - null, - ddbmsStatic, - getFileSystem().getConf(), - fsUri, - true); - Assertions.assertThat(r.getLeft()). - describedAs("entries found in %s", r) - .isGreaterThanOrEqualTo(1); - Assertions.assertThat(r.getRight()). - describedAs("entries deleted in %s", r) - .isEqualTo(r.getLeft()); - - // second iteration will have zero entries - - r = PurgeS3GuardDynamoTable.purgeStore( - null, - ddbmsStatic, - getFileSystem().getConf(), - fsUri, - true); - Assertions.assertThat(r.getLeft()). - describedAs("entries found in %s", r) - .isZero(); - Assertions.assertThat(r.getRight()). - describedAs("entries deleted in %s", r) - .isZero(); - } - - /** - * Assert that an entry exists and is a directory. - * @param pathStr path - * @throws IOException IO failure. - */ - protected DDBPathMetadata verifyAuthDirStatus(String pathStr, - boolean authDirFlag) - throws IOException { - DDBPathMetadata md = (DDBPathMetadata) getDirectory(pathStr); - assertEquals("isAuthoritativeDir() mismatch in " + md, - authDirFlag, - md.isAuthoritativeDir()); - return md; - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestDynamoDBMetadataStoreAuthoritativeMode.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestDynamoDBMetadataStoreAuthoritativeMode.java deleted file mode 100644 index 3d9715ceb3e..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestDynamoDBMetadataStoreAuthoritativeMode.java +++ /dev/null @@ -1,917 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.IOException; -import java.net.URI; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.TimeUnit; - -import org.assertj.core.api.Assertions; -import org.junit.AfterClass; -import org.junit.Ignore; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.fs.LocatedFileStatus; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.RemoteIterator; -import org.apache.hadoop.fs.contract.ContractTestUtils; -import org.apache.hadoop.fs.s3a.AbstractS3ATestBase; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.apache.hadoop.fs.s3a.S3ATestUtils; -import org.apache.hadoop.fs.s3a.Tristate; -import org.apache.hadoop.io.IOUtils; - -import static org.apache.hadoop.fs.s3a.Constants.AUTHORITATIVE_PATH; -import static org.apache.hadoop.fs.s3a.Constants.METADATASTORE_AUTHORITATIVE; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_BACKGROUND_SLEEP_MSEC_KEY; -import static org.apache.hadoop.fs.s3a.Constants.S3_METADATA_STORE_IMPL; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.assume; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.removeBaseAndBucketOverrides; -import static org.apache.hadoop.fs.s3a.S3AUtils.applyLocatedFiles; -import static org.apache.hadoop.fs.s3a.Statistic.OBJECT_LIST_REQUEST; -import static org.apache.hadoop.fs.s3a.Statistic.S3GUARD_METADATASTORE_AUTHORITATIVE_DIRECTORIES_UPDATED; -import static org.apache.hadoop.fs.s3a.Statistic.S3GUARD_METADATASTORE_RECORD_WRITES; -import static org.apache.hadoop.fs.s3a.s3guard.AuthoritativeAuditOperation.ERROR_PATH_NOT_AUTH_IN_FS; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.authoritativeEmptyDirectoryMarker; -import static org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.Authoritative.CHECK_FLAG; -import static org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.Authoritative.REQUIRE_AUTH; -import static org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.Import.AUTH_FLAG; -import static org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.VERBOSE; -import static org.apache.hadoop.fs.s3a.s3guard.S3GuardToolTestHelper.exec; -import static org.apache.hadoop.fs.s3a.s3guard.S3GuardToolTestHelper.expectExecResult; -import static org.apache.hadoop.service.launcher.LauncherExitCodes.EXIT_NOT_ACCEPTABLE; -import static org.apache.hadoop.service.launcher.LauncherExitCodes.EXIT_NOT_FOUND; -import static org.apache.hadoop.test.LambdaTestUtils.intercept; - -/** - * Test to verify the expected behaviour of DynamoDB and authoritative mode. - * The main testFS is non-auth; we also create a test FS which runs in auth - * mode. - * Making the default FS non-auth means that test path cleanup in the - * superclass isn't going to get mislead by anything authoritative. - * - * For performance boosting we demand create the auth FS and its test - * paths on the first test setup(). - * This also fixes the auth/nonauth paths so that a specific - * bit of the FS is expected to be auth in the FS. - * - * This test is designed to run in parallel mode with other tests which - * may or may not be auth mode. - * - * It shouldn't make any difference -tests here simply must not make - * any assumptions about the state of any path outside the test tree. - */ -@SuppressWarnings("StaticNonFinalField") -public class ITestDynamoDBMetadataStoreAuthoritativeMode - extends AbstractS3ATestBase { - - private static final Logger LOG = LoggerFactory.getLogger( - ITestDynamoDBMetadataStoreAuthoritativeMode.class); - - public static final String AUDIT = S3GuardTool.Authoritative.NAME; - - public static final String IMPORT = S3GuardTool.Import.NAME; - - private String fsUriStr; - - /** - * Authoritative FS. - */ - private static S3AFileSystem authFS; - - /** - * The unguarded file system. - */ - private static S3AFileSystem unguardedFS; - - /** - * Base path in the store for auth and nonauth paths. - */ - private static Path basePath; - - /** - * Path under basePath which will be declared as authoritative. - */ - private static Path authPath; - - /** - * Path under basePath which will be declared as non-authoritative. - */ - private static Path nonauthPath; - - /** - * test method specific auth path. - */ - private Path methodAuthPath; - - /** - * test method specific non-auth path. - */ - private Path methodNonauthPath; - - /** - * Auditor of store state. - */ - private AuthoritativeAuditOperation auditor; - - /** - * Path {@code $methodAuthPath/dir}. - */ - private Path dir; - - /** - * Path {@code $methodAuthPath/dir/file}. - */ - private Path dirFile; - - /** - * List of tools to close in test teardown. - */ - private final List toolsToClose = new ArrayList<>(); - - /** - * The metastore of the auth filesystem. - */ - private DynamoDBMetadataStore metastore; - - /** - * After all tests have run, close the filesystems. - */ - @AfterClass - public static void closeFileSystems() { - IOUtils.cleanupWithLogger(LOG, authFS, unguardedFS); - } - - @Override - protected Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - removeBaseAndBucketOverrides(conf, - S3GUARD_DDB_BACKGROUND_SLEEP_MSEC_KEY, - METADATASTORE_AUTHORITATIVE, - AUTHORITATIVE_PATH); - conf.setTimeDuration( - S3GUARD_DDB_BACKGROUND_SLEEP_MSEC_KEY, - 0, - TimeUnit.MILLISECONDS); - return conf; - } - - /** - * Test case setup will on-demand create the class-level fields - * of the authFS and the auth/non-auth paths. - */ - @Override - public void setup() throws Exception { - super.setup(); - S3AFileSystem fs = getFileSystem(); - Configuration conf = fs.getConf(); - S3ATestUtils.assumeS3GuardState(true, conf); - assume("Filesystem isn't running DDB", - fs.getMetadataStore() instanceof DynamoDBMetadataStore); - URI fsURI = fs.getUri(); - fsUriStr = fsURI.toString(); - if (!fsUriStr.endsWith("/")) { - fsUriStr = fsUriStr + "/"; - } - - - if (authFS == null) { - // creating the test stores - basePath = path("base"); - authPath = new Path(basePath, "auth"); - nonauthPath = new Path(basePath, "nonauth"); - final Configuration authconf = new Configuration(conf); - final URI uri = authPath.toUri(); - authconf.set(AUTHORITATIVE_PATH, uri.toString()); - authFS = (S3AFileSystem) FileSystem.newInstance(uri, authconf); - - // and create the unguarded at the same time - final Configuration unguardedConf = new Configuration(conf); - removeBaseAndBucketOverrides(unguardedConf, - S3_METADATA_STORE_IMPL); - unguardedFS = (S3AFileSystem) FileSystem.newInstance(uri, unguardedConf); - } - metastore = (DynamoDBMetadataStore) authFS.getMetadataStore(); - auditor = new AuthoritativeAuditOperation( - authFS.createStoreContext(), - metastore, - true, - true); - - cleanupMethodPaths(); - dir = new Path(methodAuthPath, "dir"); - dirFile = new Path(dir, "file"); - } - - @Override - public void teardown() throws Exception { - toolsToClose.forEach(t -> IOUtils.cleanupWithLogger(LOG, t)); - try { - cleanupMethodPaths(); - } catch (IOException ignored) { - } - super.teardown(); - } - - /** - * Clean up from other test runs which halted. - * Uses the authfs; no-op if null. - * @throws IOException Failure - */ - private void cleanupMethodPaths() throws IOException { - S3AFileSystem fs = authFS; - if (fs != null) { - methodAuthPath = new Path(authPath, getMethodName()); - fs.delete(methodAuthPath, true); - methodNonauthPath = new Path(nonauthPath, getMethodName()); - fs.delete(methodNonauthPath, true); - } - } - - /** - * Declare that the tool is to be closed in teardown. - * @param tool tool to close - * @return the tool. - */ - protected T toClose(T tool) { - toolsToClose.add(tool); - return tool; - } - - /** - * Get the conf of the auth FS. - * @return the auth FS config. - */ - private Configuration getAuthConf() { - return authFS.getConf(); - } - - @Test - public void testEmptyDirMarkerIsAuth() { - final S3AFileStatus st = new S3AFileStatus(true, dir, "root"); - final DDBPathMetadata md = (DDBPathMetadata) - authoritativeEmptyDirectoryMarker(st); - Assertions.assertThat(md) - .describedAs("Metadata %s", md) - .matches(DDBPathMetadata::isAuthoritativeDir, "is auth dir") - .matches(d -> d.isEmptyDirectory() == Tristate.TRUE, - "isEmptyDirectory"); - } - - @Test - public void testMkDirAuth() throws Throwable { - describe("create an empty dir and assert it is tagged as authoritative"); - authFS.mkdirs(dir); - expectAuthRecursive(dir); - } - - @Test - public void testListStatusMakesEmptyDirAuth() throws Throwable { - describe("Verify listStatus marks an Empty dir as auth"); - mkNonauthDir(dir); - // initial dir is non-auth - expectNonauthNonRecursive(dir); - authFS.listStatus(dir); - // dir is auth; - expectAuthRecursive(dir); - // Next list will not go to s3 - assertListDoesNotUpdateAuth(dir); - } - - @Test - public void testListFilesRecursiveWhenAllListingsAreAuthoritative() - throws Exception { - describe("listFiles does not make further calls to the fs when" - + "all nested directory listings are authoritative"); - Set originalFiles = new HashSet<>(); - - Path parentDir = dir; - Path parentFile = dirFile; - Path nestedDir1 = new Path(dir, "nested1"); - Path nestedFile1 = new Path(nestedDir1, "nestedFile1"); - Path nestedDir2 = new Path(nestedDir1, "nested2/"); - Path nestedFile2 = new Path(nestedDir2, "nestedFile2"); - - originalFiles.add(parentFile); - originalFiles.add(nestedFile1); - originalFiles.add(nestedFile2); - - mkAuthDir(parentDir); - mkAuthDir(nestedDir1); - mkAuthDir(nestedDir2); - touchFile(parentFile); - touchFile(nestedFile1); - touchFile(nestedFile2); - - S3ATestUtils.MetricDiff objListRequests = - new S3ATestUtils.MetricDiff(authFS, OBJECT_LIST_REQUEST); - - RemoteIterator statusIterator = - authFS.listFiles(dir, true); - - List pathsFromStatusIterator = toPaths(statusIterator); - - Assertions.assertThat(pathsFromStatusIterator) - .as("listFiles should return all the items in actual" - + "S3 directory and nothing more") - .hasSameElementsAs(originalFiles) - .hasSameSizeAs(originalFiles); - - objListRequests.assertDiffEquals("There must not be any OBJECT_LIST " - + "requests as all directory listings are authoritative", 0); - } - - @Test - public void testListFilesRecursiveWhenSomePathsAreNotAuthoritative() - throws Exception { - describe("listFiles correctly constructs recursive listing" - + "when authoritative and non-authoritative paths are mixed"); - List originalFiles = new ArrayList<>(); - Path parentDir = dir; - Path parentFile = dirFile; - Path nestedDir1 = new Path(dir, "nested1"); - Path nestedFile1 = new Path(nestedDir1, "nestedFile1"); - Path nestedDir2 = new Path(nestedDir1, "nested2/"); - Path nestedFile2 = new Path(nestedDir2, "nestedFile2"); - - originalFiles.add(parentFile); - originalFiles.add(nestedFile1); - originalFiles.add(nestedFile2); - - mkAuthDir(parentDir); - mkNonauthDir(nestedDir1); - mkAuthDir(nestedDir2); - touchFile(parentFile); - touchFile(nestedFile1); - touchFile(nestedFile2); - - S3ATestUtils.MetricDiff objListRequests = - new S3ATestUtils.MetricDiff(authFS, OBJECT_LIST_REQUEST); - - RemoteIterator statusIterator = - authFS.listFiles(dir, true); - - List pathsFromStatusIterator = toPaths(statusIterator); - - Assertions.assertThat(pathsFromStatusIterator) - .as("listFiles should return all the items in actual" - + "S3 directory and nothing more") - .hasSameElementsAs(originalFiles) - .hasSameSizeAs(originalFiles); - objListRequests.assertDiffEquals("Only 1 OBJECT_LIST call is expected" - + "as a nested directory listing is not authoritative", 1); - } - - @Test - public void testListStatusMakesDirAuth() throws Throwable { - describe("Verify listStatus marks a dir as auth"); - final Path subdir = new Path(dir, "subdir"); - - mkAuthDir(dir); - expectAuthRecursive(dir); - authFS.mkdirs(subdir); - // dir and subdirs are auth - expectAuthRecursive(dir); - expectAuthRecursive(subdir); - // now mark the dir as nonauth - markDirNonauth(dir); - expectNonauthNonRecursive(dir); - expectAuthRecursive(subdir); - - // look at the MD & make sure that the dir and subdir are auth - final DirListingMetadata listing = metastore.listChildren(dir); - Assertions.assertThat(listing) - .describedAs("metadata of %s", dir) - .matches(d -> !d.isAuthoritative(), "is not auth"); - Assertions.assertThat(listing.getListing()) - .describedAs("listing of %s", dir) - .hasSize(1) - .allMatch(md -> ((DDBPathMetadata) md).isAuthoritativeDir(), - "is auth"); - - // directory list makes the dir auth and leaves the child auth - assertListUpdatesAuth(dir); - - // and afterwards, a followup list does not write anything to DDB - // (as the dir is auth, its not going to go near the FS to update...) - expectOperationUpdatesDDB(0, () -> authFS.listStatus(dir)); - // mark the dir nonauth again - markDirNonauth(dir); - // and only one record is written to DDB, the dir marker as auth - // the subdir is not overwritten - expectOperationUpdatesDDB(1, () -> authFS.listStatus(dir)); - } - - @Test - public void testAddFileMarksNonAuth() throws Throwable { - describe("Adding a file marks dir as nonauth but leaves ancestors alone"); - mkAuthDir(methodAuthPath); - touchFile(dirFile); - expectNonauthRecursive(dir); - assertListUpdatesAuth(dir); - expectAuthRecursive(methodAuthPath); - } - - /** - * When you delete the single file in a directory then a fake directory - * marker is added. This must be auth. - */ - @Test - public void testDeleteSingleFileLeavesMarkersAlone() throws Throwable { - describe("Deleting a file with no peers makes no changes to ancestors"); - mkAuthDir(methodAuthPath); - touchFile(dirFile); - assertListUpdatesAuth(dir); - authFS.delete(dirFile, false); - expectAuthRecursive(methodAuthPath); - } - - @Test - public void testDeleteMultipleFileLeavesMarkersAlone() throws Throwable { - describe("Deleting a file from a dir with >1 file makes no changes" - + " to ancestors"); - mkAuthDir(methodAuthPath); - touchFile(dirFile); - Path file2 = new Path(dir, "file2"); - touchFile(file2); - assertListUpdatesAuth(dir); - authFS.delete(dirFile, false); - expectAuthRecursive(methodAuthPath); - } - - @Test - public void testDeleteEmptyDirLeavesParentAuth() throws Throwable { - describe("Deleting a directory retains the auth status " - + "of the parent directory"); - mkAuthDir(dir); - mkAuthDir(dirFile); - expectAuthRecursive(dir); - authFS.delete(dirFile, false); - expectAuthRecursive(dir); - } - - /** - * Assert the number of pruned files matches expectations. - * @param path path to prune - * @param mode prune mode - * @param limit timestamp before which files are deleted - * @param expected number of entries to be pruned - */ - protected void assertPruned(final Path path, - final MetadataStore.PruneMode mode, - final long limit, - final int expected) - throws IOException { - String keyPrefix - = PathMetadataDynamoDBTranslation.pathToParentKey(path); - Assertions.assertThat( - authFS.getMetadataStore().prune( - mode, - limit, - keyPrefix)) - .describedAs("Number of files pruned under %s", keyPrefix) - .isEqualTo(expected); - } - - @Test - public void testPruneFilesMarksNonAuth() throws Throwable { - describe("Pruning a file marks dir as nonauth"); - mkAuthDir(methodAuthPath); - - touchFile(dirFile); - assertListUpdatesAuth(dir); - - assertPruned(dir, - MetadataStore.PruneMode.ALL_BY_MODTIME, - Long.MAX_VALUE, - 1); - expectNonauthRecursive(dir); - } - - @Test - public void testPruneTombstoneRetainsAuth() throws Throwable { - describe("Verify that deleting and then pruning a file does not change" - + " the state of the parent."); - mkAuthDir(methodAuthPath); - - touchFile(dirFile); - assertListUpdatesAuth(dir); - // add a second file to avoid hitting the mkdir-is-nonauth issue that causes - // testDeleteSingleFileLeavesMarkersAlone() to fail - Path file2 = new Path(dir, "file2"); - touchFile(file2); - authFS.delete(dirFile, false); - expectAuthRecursive(dir); - assertPruned(dir, MetadataStore.PruneMode.TOMBSTONES_BY_LASTUPDATED, - Long.MAX_VALUE, 1); - expectAuthRecursive(dir); - } - - @Test - public void testRenameFile() throws Throwable { - describe("renaming a file"); - final Path source = new Path(dir, "source"); - final Path dest = new Path(dir, "dest"); - touchFile(source); - assertListUpdatesAuth(dir); - authFS.rename(source, dest); - expectAuthRecursive(dir); - } - - @Test - public void testRenameDirMarksDestAsAuth() throws Throwable { - describe("renaming a dir must mark dest tree as auth"); - final Path base = methodAuthPath; - mkAuthDir(base); - final Path source = new Path(base, "source"); - final Path dest = new Path(base, "dest"); - mkAuthDir(source); - expectAuthRecursive(base); - Path subdir = new Path(source, "subdir"); - Path f = new Path(subdir, "file"); - touchFile(f); - expectNonauthRecursive(base); - // list the source directories so everything is - // marked as auth - authFS.listStatus(source); - authFS.listStatus(subdir); - expectAuthRecursive(base); - authFS.rename(source, dest); - expectAuthRecursive(base); - } - - @Test - @Ignore("TODO: HADOOP-16465") - public void testListLocatedStatusMarksDirAsAuth() throws Throwable { - describe("validate listLocatedStatus()"); - final Path subdir = new Path(dir, "subdir"); - final Path subdirfile = new Path(subdir, "file"); - touchFile(subdirfile); - // Subdir list makes it auth - expectAuthoritativeUpdate(1, 1, () -> { - final RemoteIterator st - = authFS.listLocatedStatus(subdir); - applyLocatedFiles(st, - f -> LOG.info("{}", f)); - return null; - }); - expectAuthNonRecursive(subdir); - } - - @Test - public void testS3GuardImportMarksDirAsAuth() throws Throwable { - describe("import with authoritive=true marks directories"); - // the base dir is auth - mkAuthDir(methodAuthPath); - int expected = 0; - final Path subdir = new Path(dir, "subdir"); - final Path subdirfile = new Path(subdir, "file"); - ContractTestUtils.touch(authFS, subdirfile); - expected++; - for (int i = 0; i < 5; i++) { - ContractTestUtils.touch(authFS, new Path(subdir, "file-" + i)); - expected++; - } - final Path emptydir = new Path(dir, "emptydir"); - unguardedFS.mkdirs(emptydir); - expected++; - - S3AFileStatus status1 = (S3AFileStatus) authFS.getFileStatus(subdirfile); - final MetadataStore authMS = authFS.getMetadataStore(); - final ImportOperation importer = new ImportOperation(unguardedFS, - authMS, - (S3AFileStatus) unguardedFS.getFileStatus(dir), - true, true); - final Long count = importer.execute(); - expectAuthRecursive(dir); - // the parent dir shouldn't have changed - expectAuthRecursive(methodAuthPath); - - // file entry - S3AFileStatus status2 = (S3AFileStatus) authFS.getFileStatus(subdirfile); - Assertions.assertThat(status2.getETag()) - .describedAs("Etag of %s", status2) - .isEqualTo(status1.getETag()); - // only picked up on versioned stores. - Assertions.assertThat(status2.getVersionId()) - .describedAs("version ID of %s", status2) - .isEqualTo(status1.getVersionId()); - - // the import finds files and empty dirs - Assertions.assertThat(count) - .describedAs("Count of imports under %s", dir) - .isEqualTo(expected); - } - - /** - * Given a flag, add a - prefix. - * @param flag flag to wrap - * @return a flag for use in the CLI - */ - private String f(String flag) { - return "-" + flag; - } - - @Test - public void testAuditS3GuardTool() throws Throwable { - describe("Test the s3guard audit CLI"); - mkNonauthDir(methodAuthPath); - final String path = methodAuthPath.toString(); - // this is non-auth, so the scan is rejected - expectExecResult(EXIT_NOT_ACCEPTABLE, - authTool(), - AUDIT, - f(CHECK_FLAG), - f(REQUIRE_AUTH), - f(VERBOSE), - path); - // a non-auth audit is fine - exec(authTool(), - AUDIT, - f(VERBOSE), - path); - // non-auth import - exec(importTool(), - IMPORT, - f(VERBOSE), - path); - // which will leave the result unchanged - expectExecResult(EXIT_NOT_ACCEPTABLE, - authTool(), - AUDIT, - f(CHECK_FLAG), - f(REQUIRE_AUTH), - f(VERBOSE), - path); - // auth import - exec(importTool(), - IMPORT, - f(AUTH_FLAG), - f(VERBOSE), - path); - // so now the audit succeeds - exec(authTool(), - AUDIT, - f(REQUIRE_AUTH), - path); - } - - /** - * Create an import tool instance with the auth FS Config. - * It will be closed in teardown. - * @return a new instance. - */ - protected S3GuardTool.Import importTool() { - return toClose(new S3GuardTool.Import(getAuthConf())); - } - - /** - * Create an auth tool instance with the auth FS Config. - * It will be closed in teardown. - * @return a new instance. - */ - protected S3GuardTool.Authoritative authTool() { - return toClose(new S3GuardTool.Authoritative(getAuthConf())); - } - - @Test - public void testAuditS3GuardToolNonauthDir() throws Throwable { - describe("Test the s3guard audit -check-conf against a nonauth path"); - mkdirs(methodNonauthPath); - expectExecResult(ERROR_PATH_NOT_AUTH_IN_FS, - authTool(), - AUDIT, - f(CHECK_FLAG), - methodNonauthPath.toString()); - } - - @Test - public void testImportNonauthDir() throws Throwable { - describe("s3guard import against a nonauth path marks the dirs as auth"); - final String path = methodNonauthPath.toString(); - mkdirs(methodNonauthPath); - // auth import - exec(importTool(), - IMPORT, - f(AUTH_FLAG), - f(VERBOSE), - path); - exec(authTool(), - AUDIT, - f(REQUIRE_AUTH), - f(VERBOSE), - path); - } - - @Test - public void testAuditS3GuardTooMissingDir() throws Throwable { - describe("Test the s3guard audit against a missing path"); - expectExecResult(EXIT_NOT_FOUND, - authTool(), - AUDIT, - methodAuthPath.toString()); - } - - /** - * Touch a file in the authoritative fs. - * @param file path of file - * @throws IOException Failure - */ - protected void touchFile(final Path file) throws IOException { - ContractTestUtils.touch(authFS, file); - } - - /** - * Invoke an operation expecting the meta store to have its - * directoryMarkedAuthoritative count to be be updated {@code updates} - * times and S3 LIST requests made {@code lists} times. - * @param Return type - * @param updates Expected count - * @param lists Expected lists - * @param fn Function to invoke - * @return Result of the function call - * @throws Exception Failure - */ - private T expectAuthoritativeUpdate( - int updates, - int lists, - Callable fn) - throws Exception { - S3ATestUtils.MetricDiff authDirsMarked = new S3ATestUtils.MetricDiff(authFS, - S3GUARD_METADATASTORE_AUTHORITATIVE_DIRECTORIES_UPDATED); - S3ATestUtils.MetricDiff listRequests = new S3ATestUtils.MetricDiff(authFS, - OBJECT_LIST_REQUEST); - final T call = fn.call(); - authDirsMarked.assertDiffEquals(updates); - listRequests.assertDiffEquals(lists); - return call; - } - - /** - * Invoke an operation expecting {@code writes} records written to DDB. - * @param Return type - * @param writes Expected count - * @param fn Function to invoke - * @return Result of the function call - * @throws Exception Failure - */ - private T expectOperationUpdatesDDB( - int writes, - Callable fn) - throws Exception { - S3ATestUtils.MetricDiff writeDiff = new S3ATestUtils.MetricDiff(authFS, - S3GUARD_METADATASTORE_RECORD_WRITES); - final T call = fn.call(); - writeDiff.assertDiffEquals(writes); - return call; - } - - /** - * Creates a Path array from all items retrieved from - * {@link RemoteIterator}. - * - * @param remoteIterator iterator - * @return a list of Paths - * @throws IOException - */ - private List toPaths(RemoteIterator remoteIterator) - throws IOException { - List list = new ArrayList<>(); - while (remoteIterator.hasNext()) { - LocatedFileStatus fileStatus = remoteIterator.next(); - list.add(fileStatus.getPath()); - } - return list; - } - - /** - * Assert that a listStatus call increments the - * "s3guard_metadatastore_authoritative_directories_updated" counter. - * Then checks that the directory is recursively authoritative. - * @param path path to scan - */ - private void assertListUpdatesAuth(Path path) throws Exception { - expectAuthoritativeUpdate(1, 1, () -> authFS.listStatus(path)); - expectAuthRecursive(path); - } - - /** - * Assert that a listStatus call does not increment the - * "s3guard_metadatastore_authoritative_directories_updated" counter. - * @param path path to scan - */ - private void assertListDoesNotUpdateAuth(Path path) throws Exception { - expectAuthoritativeUpdate(0, 0, () -> authFS.listStatus(path)); - } - - /** - * Create a directory if needed, force it to be authoritatively listed. - * @param path dir - */ - private void mkAuthDir(Path path) throws IOException { - authFS.mkdirs(path); - } - - /** - * Create a non-auth directory. - * @param path dir - */ - private void mkNonauthDir(Path path) throws IOException { - authFS.mkdirs(path); - // overwrite entry with a nonauth one - markDirNonauth(path); - } - - /** - * Mark a directory as nonauth. - * @param path path to the directory - * @throws IOException failure - */ - private void markDirNonauth(final Path path) throws IOException { - S3Guard.putWithTtl(metastore, - nonAuthEmptyDirectoryMarker((S3AFileStatus) authFS.getFileStatus(path)), - null, null); - } - - /** - * Create an empty dir marker which, when passed to the - * DDB metastore, is considered authoritative. - * @param status file status - * @return path metadata. - */ - private PathMetadata nonAuthEmptyDirectoryMarker( - final S3AFileStatus status) { - return new DDBPathMetadata(status, Tristate.TRUE, - false, false, 0); - } - /** - * Performed a recursive audit of the directory - * -require everything to be authoritative. - * @param path directory - */ - private void expectAuthRecursive(Path path) throws Exception { - auditor.executeAudit(path, true, true); - } - - /** - * Performed a non-recursive audit of the directory - * -require the directory to be authoritative. - * @param path directory - */ - private void expectAuthNonRecursive(Path path) throws Exception { - auditor.executeAudit(path, true, false); - } - - /** - * Performed a recursive audit of the directory - * -expect a failure. - * @param path directory - * @return the path returned by the exception - */ - private Path expectNonauthRecursive(Path path) throws Exception { - return intercept( - AuthoritativeAuditOperation.NonAuthoritativeDirException.class, - () -> auditor.executeAudit(path, true, true)) - .getPath(); - } - - /** - * Performed a recursive audit of the directory - * -expect a failure. - * @param path directory - * @return the path returned by the exception - */ - private Path expectNonauthNonRecursive(Path path) throws Exception { - return intercept( - AuthoritativeAuditOperation.NonAuthoritativeDirException.class, - () -> auditor.executeAudit(path, true, true)) - .getPath(); - } - -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestDynamoDBMetadataStoreScale.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestDynamoDBMetadataStoreScale.java deleted file mode 100644 index 0f6800f4735..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestDynamoDBMetadataStoreScale.java +++ /dev/null @@ -1,691 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import javax.annotation.Nullable; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import com.amazonaws.services.dynamodbv2.document.DynamoDB; -import com.amazonaws.services.dynamodbv2.document.Table; -import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughputDescription; -import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder; -import org.assertj.core.api.Assertions; -import org.junit.Assume; -import org.junit.FixMethodOrder; -import org.junit.Test; -import org.junit.internal.AssumptionViolatedException; -import org.junit.runners.MethodSorters; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.commons.lang3.StringUtils; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.StorageStatistics; -import org.apache.hadoop.fs.contract.ContractTestUtils; -import org.apache.hadoop.fs.s3a.AWSServiceThrottledException; -import org.apache.hadoop.fs.s3a.Invoker; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.apache.hadoop.fs.s3a.S3AStorageStatistics; -import org.apache.hadoop.fs.s3a.Statistic; -import org.apache.hadoop.fs.s3a.scale.AbstractITestS3AMetadataStoreScale; -import org.apache.hadoop.io.IOUtils; -import org.apache.hadoop.test.LambdaTestUtils; -import org.apache.hadoop.util.DurationInfo; - -import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkNotNull; -import static org.apache.hadoop.fs.s3a.Constants.*; -import static org.apache.hadoop.fs.s3a.s3guard.MetadataStoreTestBase.basicFileStatus; -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.PARENT; -import static org.junit.Assume.assumeTrue; - -/** - * Scale test for DynamoDBMetadataStore. - * - * The throttle tests aren't quite trying to verify that throttling can - * be recovered from, because that makes for very slow tests: you have - * to overload the system and them have them back of until they finally complete. - *

    - * With DDB on demand, throttling is very unlikely. - * Here the tests simply run to completion, so act as regression tests of - * parallel invocations on the metastore APIs - */ -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class ITestDynamoDBMetadataStoreScale - extends AbstractITestS3AMetadataStoreScale { - - private static final Logger LOG = LoggerFactory.getLogger( - ITestDynamoDBMetadataStoreScale.class); - - private static final long BATCH_SIZE = 25; - - /** - * IO Units for batch size; this sets the size to use for IO capacity. - * Value: {@value}. - */ - private static final long MAXIMUM_READ_CAPACITY = 10; - private static final long MAXIMUM_WRITE_CAPACITY = 15; - - /** - * Time in milliseconds to sleep after a test throttled: - * {@value}. - * This is to help isolate throttling to the test which failed, - * rather than have it surface in a followup test. - * Also the test reports will record durations more accurately, - * as JUnit doesn't include setup/teardown times in its reports. - * There's a cost: single test runs will sleep, and the last test - * run may throttle when it doesn't need to. - * The last test {}@link {@link #test_999_delete_all_entries()} - * doesn't do the sleep so a full batch run should not suffer here. - */ - public static final int THROTTLE_RECOVER_TIME_MILLIS = 5_000; - - private DynamoDBMetadataStore ddbms; - private DynamoDBMetadataStoreTableManager tableHandler; - - private DynamoDB ddb; - - private Table table; - - private String tableName; - - /** was the provisioning changed in test_001_limitCapacity()? */ - private boolean isOverProvisionedForTest; - - private ProvisionedThroughputDescription originalCapacity; - - private static final int THREADS = 40; - - private static final int OPERATIONS_PER_THREAD = 50; - - private boolean isOnDemandTable; - - /** - * Create the metadata store. The table and region are determined from - * the attributes of the FS used in the tests. - * @return a new metadata store instance - * @throws IOException failure to instantiate - * @throws AssumptionViolatedException if the FS isn't running S3Guard + DDB/ - */ - @Override - public DynamoDBMetadataStore createMetadataStore() throws IOException { - S3AFileSystem fs = getFileSystem(); - assumeTrue("S3Guard is disabled for " + fs.getUri(), - fs.hasMetadataStore()); - MetadataStore store = fs.getMetadataStore(); - assumeTrue("Metadata store for " + fs.getUri() + " is " + store - + " -not DynamoDBMetadataStore", - store instanceof DynamoDBMetadataStore); - DDBCapacities capacities = DDBCapacities.extractCapacities( - store.getDiagnostics()); - isOnDemandTable = capacities.isOnDemandTable(); - - DynamoDBMetadataStore fsStore = (DynamoDBMetadataStore) store; - Configuration conf = new Configuration(fs.getConf()); - - tableName = fsStore.getTableName(); - assertTrue("Null/Empty tablename in " + fsStore, - StringUtils.isNotEmpty(tableName)); - String region = fsStore.getRegion(); - assertTrue("Null/Empty region in " + fsStore, - StringUtils.isNotEmpty(region)); - // create a new metastore configured to fail fast if throttling - // happens. - conf.set(S3GUARD_DDB_TABLE_NAME_KEY, tableName); - conf.set(S3GUARD_DDB_REGION_KEY, region); - conf.set(S3GUARD_DDB_THROTTLE_RETRY_INTERVAL, "50ms"); - conf.set(S3GUARD_DDB_MAX_RETRIES, "1"); - conf.set(MAX_ERROR_RETRIES, "1"); - conf.set(S3GUARD_DDB_BACKGROUND_SLEEP_MSEC_KEY, "5ms"); - - DynamoDBMetadataStore ms = new DynamoDBMetadataStore(); - // init the metastore in a bigger retry loop than the test setup - // in case the previous test case overloaded things - final Invoker fsInvoker = fs.createStoreContext().getInvoker(); - fsInvoker.retry("init metastore", null, true, - () -> ms.initialize(conf, new S3Guard.TtlTimeProvider(conf))); - // wire up the owner FS so that we can make assertions about throttle - // events - ms.bindToOwnerFilesystem(fs); - return ms; - } - - @Override - public void setup() throws Exception { - super.setup(); - ddbms = (DynamoDBMetadataStore) createMetadataStore(); - tableName = ddbms.getTableName(); - tableHandler = ddbms.getTableHandler(); - assertNotNull("table has no name", tableName); - ddb = ddbms.getDynamoDB(); - table = ddb.getTable(tableName); - originalCapacity = table.describe().getProvisionedThroughput(); - - // is this table too big for throttling to surface? - isOverProvisionedForTest = ( - originalCapacity.getReadCapacityUnits() > MAXIMUM_READ_CAPACITY - || originalCapacity.getWriteCapacityUnits() > MAXIMUM_WRITE_CAPACITY); - } - - @Override - public void teardown() throws Exception { - IOUtils.cleanupWithLogger(LOG, ddbms); - super.teardown(); - } - - /** - * Is throttling likely? - * @return true if the DDB table has prepaid IO and is small enough - * to throttle. - */ - private boolean expectThrottling() { - return !isOverProvisionedForTest && !isOnDemandTable; - } - - /** - * Recover from throttling by sleeping briefly. - */ - private void recoverFromThrottling() throws InterruptedException { - LOG.info("Sleeping to recover from throttling for {} ms", - THROTTLE_RECOVER_TIME_MILLIS); - Thread.sleep(THROTTLE_RECOVER_TIME_MILLIS); - } - - /** - * The subclass expects the superclass to be throttled; sometimes it is. - */ - @Test - @Override - public void test_010_Put() throws Throwable { - ThrottleTracker tracker = new ThrottleTracker(ddbms); - try { - // if this doesn't throttle, all is well. - super.test_010_Put(); - } catch (AWSServiceThrottledException ex) { - // if the service was throttled, all is good. - // log and continue - LOG.warn("DDB connection was throttled", ex); - } finally { - LOG.info("Statistics {}", tracker); - } - } - - /** - * The subclass expects the superclass to be throttled; sometimes it is. - */ - @Test - @Override - public void test_020_Moves() throws Throwable { - ThrottleTracker tracker = new ThrottleTracker(ddbms); - try { - // if this doesn't throttle, all is well. - super.test_020_Moves(); - } catch (AWSServiceThrottledException ex) { - // if the service was throttled, all is good. - // log and continue - LOG.warn("DDB connection was throttled", ex); - } finally { - LOG.info("Statistics {}", tracker); - } - } - - /** - * Though the AWS SDK claims in documentation to handle retries and - * exponential backoff, we have witnessed - * com.amazonaws...dynamodbv2.model.ProvisionedThroughputExceededException - * (Status Code: 400; Error Code: ProvisionedThroughputExceededException) - * Hypothesis: - * Happens when the size of a batched write is bigger than the number of - * provisioned write units. This test ensures we handle the case - * correctly, retrying w/ smaller batch instead of surfacing exceptions. - */ - @Test - public void test_030_BatchedWrite() throws Exception { - - final int iterations = 15; - final ArrayList toCleanup = new ArrayList<>(); - toCleanup.ensureCapacity(THREADS * iterations); - - // Fail if someone changes a constant we depend on - assertTrue("Maximum batch size must big enough to run this test", - S3GUARD_DDB_BATCH_WRITE_REQUEST_LIMIT >= BATCH_SIZE); - - - // We know the dynamodb metadata store will expand a put of a path - // of depth N into a batch of N writes (all ancestors are written - // separately up to the root). (Ab)use this for an easy way to write - // a batch of stuff that is bigger than the provisioned write units - try { - describe("Running %d iterations of batched put, size %d", iterations, - BATCH_SIZE); - Path base = path(getMethodName()); - final String pathKey = base.toUri().getPath(); - - ThrottleTracker result = execute("prune", - 1, - expectThrottling(), - () -> { - ThrottleTracker tracker = new ThrottleTracker(ddbms); - long pruneItems = 0; - for (long i = 0; i < iterations; i++) { - Path longPath = pathOfDepth(BATCH_SIZE, - pathKey, String.valueOf(i)); - S3AFileStatus status = basicFileStatus(longPath, 0, false, - 12345); - PathMetadata pm = new PathMetadata(status); - synchronized (toCleanup) { - toCleanup.add(pm); - } - - ddbms.put(pm, null); - - pruneItems++; - - if (pruneItems == BATCH_SIZE) { - describe("pruning files"); - ddbms.prune(MetadataStore.PruneMode.ALL_BY_MODTIME, - Long.MAX_VALUE, pathKey); - pruneItems = 0; - } - if (tracker.probe()) { - // fail fast - break; - } - } - }); - if (expectThrottling() && result.probeThrottlingDetected()) { - recoverFromThrottling(); - } - } finally { - describe("Cleaning up table %s", tableName); - for (PathMetadata pm : toCleanup) { - cleanupMetadata(ddbms, pm); - } - } - } - - /** - * Test Get throttling including using - * {@link MetadataStore#get(Path, boolean)}, - * as that stresses more of the code. - */ - @Test - public void test_040_get() throws Throwable { - // attempt to create many many get requests in parallel. - Path path = new Path("s3a://example.org/get"); - S3AFileStatus status = new S3AFileStatus(true, path, "alice"); - PathMetadata metadata = new PathMetadata(status); - ddbms.put(metadata, null); - try { - execute("get", - OPERATIONS_PER_THREAD, - expectThrottling(), - () -> ddbms.get(path, true) - ); - } finally { - retryingDelete(path); - } - } - - /** - * Ask for the version marker, which is where table init can be overloaded. - */ - @Test - public void test_050_getVersionMarkerItem() throws Throwable { - execute("get", - OPERATIONS_PER_THREAD * 2, - expectThrottling(), - () -> { - try { - tableHandler.getVersionMarkerItem(); - } catch (FileNotFoundException ignored) { - } - } - ); - } - - /** - * Cleanup with an extra bit of retry logic around it, in case things - * are still over the limit. - * @param path path - */ - private void retryingDelete(final Path path) { - try { - ddbms.getInvoker().retry("Delete ", path.toString(), true, - () -> ddbms.delete(path, null)); - } catch (IOException e) { - LOG.warn("Failed to delete {}: ", path, e); - } - } - - @Test - public void test_060_list() throws Throwable { - // attempt to create many many get requests in parallel. - Path path = new Path("s3a://example.org/list"); - S3AFileStatus status = new S3AFileStatus(true, path, "alice"); - PathMetadata metadata = new PathMetadata(status); - ddbms.put(metadata, null); - try { - Path parent = path.getParent(); - execute("list", - OPERATIONS_PER_THREAD, - expectThrottling(), - () -> ddbms.listChildren(parent) - ); - } finally { - retryingDelete(path); - } - } - - @Test - public void test_070_putDirMarker() throws Throwable { - // attempt to create many many get requests in parallel. - Path path = new Path("s3a://example.org/putDirMarker"); - S3AFileStatus status = new S3AFileStatus(true, path, "alice"); - PathMetadata metadata = new PathMetadata(status); - ddbms.put(metadata, null); - DirListingMetadata children = ddbms.listChildren(path.getParent()); - try (DynamoDBMetadataStore.AncestorState state = - ddbms.initiateBulkWrite( - BulkOperationState.OperationType.Put, - path)) { - execute("list", - OPERATIONS_PER_THREAD, - expectThrottling(), - () -> ddbms.put(children, Collections.emptyList(), state)); - } finally { - retryingDelete(path); - } - } - - @Test - public void test_080_fullPathsToPut() throws Throwable { - // attempt to create many many get requests in parallel. - Path base = new Path("s3a://example.org/test_080_fullPathsToPut"); - Path child = new Path(base, "child"); - List pms = new ArrayList<>(); - try { - try (BulkOperationState bulkUpdate - = ddbms.initiateBulkWrite( - BulkOperationState.OperationType.Put, child)) { - ddbms.put(new PathMetadata(makeDirStatus(base)), bulkUpdate); - ddbms.put(new PathMetadata(makeDirStatus(child)), bulkUpdate); - ddbms.getInvoker().retry("set up directory tree", - base.toString(), - true, - () -> ddbms.put(pms, bulkUpdate)); - } - try (BulkOperationState bulkUpdate - = ddbms.initiateBulkWrite( - BulkOperationState.OperationType.Put, child)) { - DDBPathMetadata dirData = ddbms.get(child, true); - execute("put", - OPERATIONS_PER_THREAD, - expectThrottling(), - () -> ddbms.fullPathsToPut(dirData, bulkUpdate) - ); - } - } finally { - ddbms.forgetMetadata(child); - ddbms.forgetMetadata(base); - } - } - - /** - * Try many deletes in parallel; this will create tombstones. - */ - @Test - public void test_090_delete() throws Throwable { - Path path = new Path("s3a://example.org/delete"); - S3AFileStatus status = new S3AFileStatus(true, path, "alice"); - PathMetadata metadata = new PathMetadata(status); - ddbms.put(metadata, null); - ITtlTimeProvider time = checkNotNull(getTtlTimeProvider(), "time provider"); - - try (DurationInfo ignored = new DurationInfo(LOG, true, "delete")) { - execute("delete", - OPERATIONS_PER_THREAD, - expectThrottling(), - () -> { - ddbms.delete(path, null); - }); - } - } - - /** - * Forget Metadata: delete entries without tombstones. - */ - @Test - public void test_100_forgetMetadata() throws Throwable { - Path path = new Path("s3a://example.org/delete"); - try (DurationInfo ignored = new DurationInfo(LOG, true, "delete")) { - execute("delete", - OPERATIONS_PER_THREAD, - expectThrottling(), - () -> ddbms.forgetMetadata(path) - ); - } - } - - @Test - public void test_900_instrumentation() throws Throwable { - describe("verify the owner FS gets updated after throttling events"); - Assume.assumeTrue("No throttling expected", expectThrottling()); - // we rely on the FS being shared - S3AFileSystem fs = getFileSystem(); - String fsSummary = fs.toString(); - - S3AStorageStatistics statistics = fs.getStorageStatistics(); - for (StorageStatistics.LongStatistic statistic : statistics) { - LOG.info("{}", statistic.toString()); - } - String retryKey = Statistic.S3GUARD_METADATASTORE_RETRY.getSymbol(); - assertTrue("No increment of " + retryKey + " in " + fsSummary, - statistics.getLong(retryKey) > 0); - String throttledKey = Statistic.S3GUARD_METADATASTORE_THROTTLED.getSymbol(); - assertTrue("No increment of " + throttledKey + " in " + fsSummary, - statistics.getLong(throttledKey) > 0); - } - - @Test - public void test_999_delete_all_entries() throws Throwable { - describe("Delete all entries from the table"); - S3GuardTableAccess tableAccess = new S3GuardTableAccess(ddbms); - ExpressionSpecBuilder builder = new ExpressionSpecBuilder(); - final String path = "/test/"; - builder.withCondition( - ExpressionSpecBuilder.S(PARENT).beginsWith(path)); - Iterable entries = - ddbms.wrapWithRetries(tableAccess.scanMetadata(builder)); - List list = new ArrayList<>(); - try { - entries.iterator().forEachRemaining(e -> { - Path p = e.getFileStatus().getPath(); - LOG.info("Deleting {}", p); - list.add(p); - }); - } catch (UncheckedIOException e) { - // the iterator may have overloaded; swallow if so. - if (!(e.getCause() instanceof AWSServiceThrottledException)) { - throw e; - } - } - // sending this in one by one for more efficient retries - for (Path p : list) { - ddbms.getInvoker() - .retry("delete", - path, - true, - () -> tableAccess.delete(p)); - } - } - - /** - * Execute a set of operations in parallel, collect throttling statistics - * and return them. - * This execution will complete as soon as throttling is detected. - * This ensures that the tests do not run for longer than they should. - * @param operation string for messages. - * @param operationsPerThread number of times per thread to invoke the action. - * @param expectThrottling is throttling expected (and to be asserted on?) - * @param action action to invoke. - * @return the throttle statistics - */ - public ThrottleTracker execute(String operation, - int operationsPerThread, - final boolean expectThrottling, - LambdaTestUtils.VoidCallable action) - throws Exception { - - final ContractTestUtils.NanoTimer timer = new ContractTestUtils.NanoTimer(); - final ThrottleTracker tracker = new ThrottleTracker(ddbms); - final ExecutorService executorService = Executors.newFixedThreadPool( - THREADS); - final List> tasks = new ArrayList<>(THREADS); - - final AtomicInteger throttleExceptions = new AtomicInteger(0); - for (int i = 0; i < THREADS; i++) { - tasks.add( - () -> { - final ExecutionOutcome outcome = new ExecutionOutcome(); - final ContractTestUtils.NanoTimer t - = new ContractTestUtils.NanoTimer(); - for (int j = 0; j < operationsPerThread; j++) { - if (tracker.isThrottlingDetected() - || throttleExceptions.get() > 0) { - outcome.skipped = true; - return outcome; - } - try { - action.call(); - outcome.completed++; - } catch (AWSServiceThrottledException e) { - // this is possibly OK - LOG.info("Operation [{}] raised a throttled exception " + e, j, e); - LOG.debug(e.toString(), e); - throttleExceptions.incrementAndGet(); - // consider it completed - outcome.throttleExceptions.add(e); - outcome.throttled++; - } catch (Exception e) { - LOG.error("Failed to execute {}", operation, e); - outcome.exceptions.add(e); - break; - } - tracker.probe(); - } - LOG.info("Thread completed {} with in {} ms with outcome {}: {}", - operation, t.elapsedTimeMs(), outcome, tracker); - return outcome; - } - ); - } - final List> futures = - executorService.invokeAll(tasks, - getTestTimeoutMillis(), TimeUnit.MILLISECONDS); - long elapsedMs = timer.elapsedTimeMs(); - LOG.info("Completed {} with {}", operation, tracker); - LOG.info("time to execute: {} millis", elapsedMs); - - Assertions.assertThat(futures) - .describedAs("Futures of all tasks") - .allMatch(Future::isDone); - tracker.probe(); - if (expectThrottling() && tracker.probeThrottlingDetected()) { - recoverFromThrottling(); - } - for (Future future : futures) { - - ExecutionOutcome outcome = future.get(); - if (!outcome.exceptions.isEmpty()) { - throw outcome.exceptions.get(0); - } - if (!outcome.skipped) { - assertEquals("Future did not complete all operations", - operationsPerThread, outcome.completed + outcome.throttled); - } - } - - return tracker; - } - - /** - * Attempt to delete metadata, suppressing any errors, and retrying on - * throttle events just in case some are still surfacing. - * @param ms store - * @param pm path to clean up - */ - private void cleanupMetadata(DynamoDBMetadataStore ms, PathMetadata pm) { - Path path = pm.getFileStatus().getPath(); - try { - ITestDynamoDBMetadataStore.deleteMetadataUnderPath(ms, path, true); - } catch (IOException ioe) { - // Ignore. - LOG.info("Ignoring error while cleaning up {} in database", path, ioe); - } - } - - private Path pathOfDepth(long n, - String name, @Nullable String fileSuffix) { - StringBuilder sb = new StringBuilder(); - for (long i = 0; i < n; i++) { - sb.append(i == 0 ? "/" + name : String.format("level-%03d", i)); - if (i == n - 1 && fileSuffix != null) { - sb.append(fileSuffix); - } - sb.append("/"); - } - return new Path(getFileSystem().getUri().toString(), sb.toString()); - } - - /** - * Outcome of a thread's execution operation. - */ - private static class ExecutionOutcome { - private int completed; - private int throttled; - private boolean skipped; - private final List exceptions = new ArrayList<>(1); - private final List throttleExceptions = new ArrayList<>(1); - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder( - "ExecutionOutcome{"); - sb.append("completed=").append(completed); - sb.append(", skipped=").append(skipped); - sb.append(", throttled=").append(throttled); - sb.append(", exception count=").append(exceptions.size()); - sb.append('}'); - return sb.toString(); - } - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardConcurrentOps.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardConcurrentOps.java deleted file mode 100644 index 0b55e24e5a9..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardConcurrentOps.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.util.ArrayList; -import java.util.List; -import java.util.Random; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.atomic.AtomicInteger; - -import com.amazonaws.services.dynamodbv2.document.DynamoDB; -import com.amazonaws.services.dynamodbv2.document.Table; -import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException; -import org.junit.Assume; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.Timeout; - -import org.apache.commons.lang3.StringUtils; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.contract.ContractTestUtils; -import org.apache.hadoop.fs.s3a.AWSCredentialProviderList; -import org.apache.hadoop.fs.s3a.AbstractS3ATestBase; -import org.apache.hadoop.fs.s3a.Constants; -import org.apache.hadoop.fs.s3a.S3AFileSystem; - -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_REGION_KEY; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_TABLE_CAPACITY_READ_KEY; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_TABLE_CAPACITY_WRITE_KEY; - -/** - * Tests concurrent operations on S3Guard. - */ -public class ITestS3GuardConcurrentOps extends AbstractS3ATestBase { - - @Rule - public final Timeout timeout = new Timeout(5 * 60 * 1000); - - protected Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - //patch the read/write capacity - conf.setInt(S3GUARD_DDB_TABLE_CAPACITY_READ_KEY, 0); - conf.setInt(S3GUARD_DDB_TABLE_CAPACITY_WRITE_KEY, 0); - return conf; - } - - private void failIfTableExists(DynamoDB db, String tableName) { - boolean tableExists = true; - try { - Table table = db.getTable(tableName); - table.describe(); - } catch (ResourceNotFoundException e) { - tableExists = false; - } - if (tableExists) { - fail("Table already exists: " + tableName); - } - } - - private void deleteTable(DynamoDB db, String tableName) throws - InterruptedException { - try { - Table table = db.getTable(tableName); - table.waitForActive(); - table.delete(); - table.waitForDelete(); - } catch (ResourceNotFoundException e) { - LOG.warn("Failed to delete {}, as it was not found", tableName, e); - } - } - - @Test - public void testConcurrentTableCreations() throws Exception { - S3AFileSystem fs = getFileSystem(); - final Configuration conf = fs.getConf(); - Assume.assumeTrue("Test only applies when DynamoDB is used for S3Guard", - conf.get(Constants.S3_METADATA_STORE_IMPL).equals( - Constants.S3GUARD_METASTORE_DYNAMO)); - - AWSCredentialProviderList sharedCreds = - fs.shareCredentials("testConcurrentTableCreations"); - // close that shared copy. - sharedCreds.close(); - // this is the original reference count. - int originalRefCount = sharedCreds.getRefCount(); - - //now init the store; this should increment the ref count. - DynamoDBMetadataStore ms = new DynamoDBMetadataStore(); - ms.initialize(fs, new S3Guard.TtlTimeProvider(conf)); - - // the ref count should have gone up - assertEquals("Credential Ref count unchanged after initializing metastore " - + sharedCreds, - originalRefCount + 1, sharedCreds.getRefCount()); - try { - DynamoDB db = ms.getDynamoDB(); - - String tableName = - getTestTableName("testConcurrentTableCreations" + - new Random().nextInt()); - conf.setBoolean(Constants.S3GUARD_DDB_TABLE_CREATE_KEY, true); - conf.set(Constants.S3GUARD_DDB_TABLE_NAME_KEY, tableName); - - String region = conf.getTrimmed(S3GUARD_DDB_REGION_KEY); - if (StringUtils.isEmpty(region)) { - // no region set, so pick it up from the test bucket - conf.set(S3GUARD_DDB_REGION_KEY, fs.getBucketLocation()); - } - int concurrentOps = 16; - int iterations = 4; - - failIfTableExists(db, tableName); - - for (int i = 0; i < iterations; i++) { - ExecutorService executor = Executors.newFixedThreadPool( - concurrentOps, new ThreadFactory() { - private AtomicInteger count = new AtomicInteger(0); - - public Thread newThread(Runnable r) { - return new Thread(r, - "testConcurrentTableCreations" + count.getAndIncrement()); - } - }); - ((ThreadPoolExecutor) executor).prestartAllCoreThreads(); - Future[] futures = new Future[concurrentOps]; - for (int f = 0; f < concurrentOps; f++) { - final int index = f; - futures[f] = executor.submit(new Callable() { - @Override - public Exception call() throws Exception { - - ContractTestUtils.NanoTimer timer = - new ContractTestUtils.NanoTimer(); - - Exception result = null; - try (DynamoDBMetadataStore store = new DynamoDBMetadataStore()) { - store.initialize(conf, new S3Guard.TtlTimeProvider(conf)); - } catch (Exception e) { - LOG.error(e.getClass() + ": " + e.getMessage()); - result = e; - } - - timer.end("Parallel DynamoDB client creation %d", index); - LOG.info("Parallel DynamoDB client creation {} ran from {} to {}", - index, timer.getStartTime(), timer.getEndTime()); - return result; - } - }); - } - List exceptions = new ArrayList<>(concurrentOps); - for (int f = 0; f < concurrentOps; f++) { - Exception outcome = futures[f].get(); - if (outcome != null) { - exceptions.add(outcome); - } - } - deleteTable(db, tableName); - int exceptionsThrown = exceptions.size(); - if (exceptionsThrown > 0) { - // at least one exception was thrown. Fail the test & nest the first - // exception caught - throw new AssertionError(exceptionsThrown + "/" + concurrentOps + - " threads threw exceptions while initializing on iteration " + i, - exceptions.get(0)); - } - } - } finally { - ms.close(); - } - assertEquals("Credential Ref count unchanged after closing metastore: " - + sharedCreds, - originalRefCount, sharedCreds.getRefCount()); - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardDDBRootOperations.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardDDBRootOperations.java deleted file mode 100644 index f03d4e09247..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardDDBRootOperations.java +++ /dev/null @@ -1,282 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.File; -import java.net.URI; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import org.assertj.core.api.Assertions; -import org.junit.FixMethodOrder; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runners.MethodSorters; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.contract.ContractTestUtils; -import org.apache.hadoop.fs.s3a.AbstractS3ATestBase; -import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.apache.hadoop.fs.s3a.S3ATestUtils; -import org.apache.hadoop.fs.s3a.impl.StoreContext; - -import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkNotNull; -import static org.apache.hadoop.fs.s3a.Constants.DIRECTORY_MARKER_POLICY; -import static org.apache.hadoop.fs.s3a.Constants.DIRECTORY_MARKER_POLICY_DELETE; -import static org.apache.hadoop.fs.s3a.Constants.ENABLE_MULTI_DELETE; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_BACKGROUND_SLEEP_MSEC_KEY; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.assume; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.disableFilesystemCaching; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.getTestBucketName; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.removeBucketOverrides; -import static org.apache.hadoop.fs.s3a.S3AUtils.applyLocatedFiles; - -/** - * This test run against the root of the FS, and operations which span the DDB - * table and the filesystem. - * For this reason, these tests are executed in the sequential phase of the - * integration tests. - *

    - * The tests only run if DynamoDB is the metastore. - *

    - * The marker policy is fixed to "delete" - */ -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class ITestS3GuardDDBRootOperations extends AbstractS3ATestBase { - - private StoreContext storeContext; - - private String fsUriStr; - - private DynamoDBMetadataStore metastore; - - private String metastoreUriStr; - - // this is a switch you can change in your IDE to enable - // or disable those tests which clean up the metastore. - private final boolean cleaning = true; - - /** - * The test timeout is increased in case previous tests have created - * many tombstone markers which now need to be purged. - * @return the test timeout. - */ - @Override - protected int getTestTimeoutMillis() { - return SCALE_TEST_TIMEOUT_SECONDS * 1000; - } - - @Override - protected Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - String bucketName = getTestBucketName(conf); - disableFilesystemCaching(conf); - - removeBucketOverrides(bucketName, conf, - S3GUARD_DDB_BACKGROUND_SLEEP_MSEC_KEY, - ENABLE_MULTI_DELETE, - DIRECTORY_MARKER_POLICY); - conf.set(DIRECTORY_MARKER_POLICY, - DIRECTORY_MARKER_POLICY_DELETE); - // set a sleep time of 0 on pruning, for speedier test runs. - conf.setTimeDuration( - S3GUARD_DDB_BACKGROUND_SLEEP_MSEC_KEY, - 0, - TimeUnit.MILLISECONDS); - return conf; - } - - @Override - public void setup() throws Exception { - super.setup(); - S3AFileSystem fs = getFileSystem(); - Configuration conf = fs.getConf(); - S3ATestUtils.assumeS3GuardState(true, conf); - storeContext = fs.createStoreContext(); - assume("Filesystem isn't running DDB", - storeContext.getMetadataStore() instanceof DynamoDBMetadataStore); - metastore = (DynamoDBMetadataStore) storeContext.getMetadataStore(); - URI fsURI = storeContext.getFsURI(); - fsUriStr = fsURI.toString(); - if (!fsUriStr.endsWith("/")) { - fsUriStr = fsUriStr + "/"; - } - metastoreUriStr = "dynamodb://" + metastore.getTableName() + "/"; - } - - @Override - public void teardown() throws Exception { - Thread.currentThread().setName("teardown"); - super.teardown(); - } - - private void assumeCleaningOperation() { - assume("Cleaning operation skipped", cleaning); - } - - @Test - @Ignore - public void test_050_dump_metastore() throws Throwable { - File destFile = calculateDumpFileBase(); - describe("Dumping S3Guard store under %s", destFile); - DumpS3GuardDynamoTable.dumpStore( - null, - metastore, - getConfiguration(), - destFile, - getFileSystem().getUri()); - } - - @Test - public void test_060_dump_metastore_and_s3() throws Throwable { - File destFile = calculateDumpFileBase(); - describe("Dumping S3Guard store under %s", destFile); - DumpS3GuardDynamoTable.dumpStore( - getFileSystem(), - metastore, - getConfiguration(), - destFile, - getFileSystem().getUri()); - } - - @Test - public void test_100_FilesystemPrune() throws Throwable { - describe("Execute prune against a filesystem URI"); - assumeCleaningOperation(); - S3AFileSystem fs = getFileSystem(); - Configuration conf = fs.getConf(); - int result = S3GuardTool.run(conf, - S3GuardTool.Prune.NAME, - "-seconds", "1", - fsUriStr); - Assertions.assertThat(result) - .describedAs("Result of prune %s", fsUriStr) - .isEqualTo(0); - } - - - @Test - public void test_200_MetastorePruneTombstones() throws Throwable { - describe("Execute prune against a dynamo URL"); - assumeCleaningOperation(); - S3AFileSystem fs = getFileSystem(); - Configuration conf = fs.getConf(); - int result = S3GuardTool.run(conf, - S3GuardTool.Prune.NAME, - "-tombstone", - "-meta", checkNotNull(metastoreUriStr), - "-seconds", "1", - fs.qualify(new Path("/")).toString()); - Assertions.assertThat(result) - .describedAs("Result of prune %s", fsUriStr) - .isEqualTo(0); - } - - @Test - public void test_300_MetastorePrune() throws Throwable { - describe("Execute prune against a dynamo URL"); - assumeCleaningOperation(); - S3AFileSystem fs = getFileSystem(); - Configuration conf = fs.getConf(); - int result = S3GuardTool.run(conf, - S3GuardTool.Prune.NAME, - "-meta", checkNotNull(metastoreUriStr), - "-region", fs.getBucketLocation(), - "-seconds", "1"); - Assertions.assertThat(result) - .describedAs("Result of prune %s", fsUriStr) - .isEqualTo(0); - } - - @Test - public void test_400_rm_root_recursive() throws Throwable { - describe("Remove the root directory"); - assumeCleaningOperation(); - S3AFileSystem fs = getFileSystem(); - Path root = new Path("/"); - Path file = new Path("/test_400_rm_root_recursive-01"); - Path file2 = new Path("/test_400_rm_root_recursive-02"); - // recursive treewalk to delete all files - // does not delete directories. - applyLocatedFiles(fs.listFilesAndEmptyDirectories(root, true), - f -> { - Path p = f.getPath(); - fs.delete(p, true); - assertPathDoesNotExist("expected file to be deleted", p); - }); - ContractTestUtils.deleteChildren(fs, root, true); - // everything must be done by now - StringBuffer sb = new StringBuffer(); - AtomicInteger foundFile = new AtomicInteger(0); - applyLocatedFiles(fs.listFilesAndEmptyDirectories(root, true), - f -> { - foundFile.addAndGet(1); - Path p = f.getPath(); - sb.append(f.isDirectory() - ? "Dir " - : "File ") - .append(p); - if (!f.isDirectory()) { - sb.append("[").append(f.getLen()).append("]"); - } - - fs.delete(p, true); - }); - - assertEquals("Remaining files " + sb, - 0, foundFile.get()); - try { - ContractTestUtils.touch(fs, file); - assertDeleted(file, false); - - - assertFalse("Root directory delete failed", - fs.delete(root, true)); - - ContractTestUtils.touch(fs, file2); - assertFalse("Root directory delete should have failed", - fs.delete(root, true)); - } finally { - fs.delete(file, false); - fs.delete(file2, false); - } - } - - @Test - @Ignore - public void test_600_dump_metastore() throws Throwable { - File destFile = calculateDumpFileBase(); - describe("Dumping S3Guard store under %s", destFile); - DumpS3GuardDynamoTable.dumpStore( - getFileSystem(), - metastore, - getConfiguration(), - destFile, - getFileSystem().getUri()); - } - - protected File calculateDumpFileBase() { - String target = System.getProperty("test.build.dir", "target"); - File buildDir = new File(target, - this.getClass().getSimpleName()).getAbsoluteFile(); - buildDir.mkdirs(); - return new File(buildDir, getMethodName()); - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardFsShell.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardFsShell.java deleted file mode 100644 index 51b23af4dc7..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardFsShell.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.util.Arrays; -import java.util.stream.Collectors; - -import org.junit.Test; - -import org.apache.hadoop.fs.FsShell; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.AbstractS3ATestBase; -import org.apache.hadoop.util.ToolRunner; - -/** - * Test FS shell and S3Guard (and of course, the rest of S3). - */ -public class ITestS3GuardFsShell extends AbstractS3ATestBase { - - /** - * Run a shell command. - * @param args array of arguments. - * @return the exit code. - * @throws Exception any exception raised. - */ - private int fsShell(String[] args) throws Exception { - FsShell shell = new FsShell(getConfiguration()); - try { - return ToolRunner.run(shell, args); - } finally { - shell.close(); - } - } - - /** - * Execute a command and verify that it returned the specific exit code. - * @param expected expected value - * @param args array of arguments. - * @throws Exception any exception raised. - */ - private void exec(int expected, String[] args) throws Exception { - int result = fsShell(args); - String argslist = Arrays.stream(args).collect(Collectors.joining(" ")); - assertEquals("hadoop fs " + argslist, expected, result); - } - - /** - * Execute a shell command expecting a result of 0. - * @param args array of arguments. - * @throws Exception any exception raised. - */ - private void exec(String[] args) throws Exception { - exec(0, args); - } - - /** - * Issue a mkdir without a trailing /. - */ - @Test - public void testMkdirNoTrailing() throws Throwable { - Path dest = path("normal"); - try { - String destStr = dest.toString(); - mkdirs(destStr); - isDir(destStr); - rmdir(destStr); - isNotFound(destStr); - } finally { - getFileSystem().delete(dest, true); - } - } - - /** - * Issue a mkdir with a trailing /. - */ - @Test - public void testMkdirTrailing() throws Throwable { - Path base = path("trailing"); - getFileSystem().delete(base, true); - try { - String destStr = base.toString() + "/"; - mkdirs(destStr); - isDir(destStr); - isDir(base.toString()); - rmdir(destStr); - isNotFound(destStr); - } finally { - getFileSystem().delete(base, true); - } - } - - /** - * Create the destination path and then call mkdir, expect it to still work. - */ - @Test - public void testMkdirTrailingExists() throws Throwable { - Path base = path("trailingexists"); - getFileSystem().mkdirs(base); - try { - String destStr = base.toString() + "/"; - // the path already exists - isDir(destStr); - mkdirs(destStr); - isDir(destStr); - rmdir(base.toString()); - isNotFound(destStr); - } finally { - getFileSystem().delete(base, true); - } - } - - private void isNotFound(final String destStr) throws Exception { - exec(1, new String[]{"-test", "-d", destStr}); - } - - private void mkdirs(final String destStr) throws Exception { - exec(new String[]{"-mkdir", "-p", destStr}); - } - - private void rmdir(final String destStr) throws Exception { - exec(new String[]{"-rmdir", destStr}); - } - - private void isDir(final String destStr) throws Exception { - exec(new String[]{"-test", "-d", destStr}); - } - -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardFsck.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardFsck.java deleted file mode 100644 index bcd1204b1d7..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardFsck.java +++ /dev/null @@ -1,646 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - - -import java.io.IOException; -import java.net.URI; -import java.util.Collections; -import java.util.List; -import java.util.UUID; -import java.util.stream.Collectors; - -import org.assertj.core.api.Assertions; -import org.junit.Before; -import org.junit.Test; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.io.IOUtils; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.AbstractS3ATestBase; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.S3AFileSystem; - -import static org.apache.hadoop.fs.s3a.Constants.AUTHORITATIVE_PATH; -import static org.junit.Assume.assumeTrue; -import static org.apache.hadoop.fs.contract.ContractTestUtils.touch; -import static org.apache.hadoop.fs.s3a.Constants.METADATASTORE_AUTHORITATIVE; -import static org.apache.hadoop.fs.s3a.Constants.S3_METADATA_STORE_IMPL; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.metadataStorePersistsAuthoritativeBit; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.removeBaseAndBucketOverrides; - -/** - * Integration tests for the S3Guard Fsck against a dyamodb backed metadata - * store. - */ -public class ITestS3GuardFsck extends AbstractS3ATestBase { - - private S3AFileSystem guardedFs; - private S3AFileSystem rawFs; - - private MetadataStore metadataStore; - - @Before - public void setup() throws Exception { - super.setup(); - S3AFileSystem fs = getFileSystem(); - // These test will fail if no ms - assumeTrue("FS needs to have a metadatastore.", - fs.hasMetadataStore()); - assumeTrue("Metadatastore should persist authoritative bit", - metadataStorePersistsAuthoritativeBit(fs.getMetadataStore())); - - guardedFs = fs; - metadataStore = fs.getMetadataStore(); - - // create raw fs without s3guard - rawFs = createUnguardedFS(); - assertFalse("Raw FS still has S3Guard " + rawFs, - rawFs.hasMetadataStore()); - } - - @Override - public void teardown() throws Exception { - if (guardedFs != null) { - IOUtils.cleanupWithLogger(LOG, guardedFs); - } - IOUtils.cleanupWithLogger(LOG, rawFs); - super.teardown(); - } - - /** - * Create a test filesystem which is always unguarded. - * This filesystem MUST be closed in test teardown. - * @return the new FS - */ - private S3AFileSystem createUnguardedFS() throws Exception { - S3AFileSystem testFS = getFileSystem(); - Configuration config = new Configuration(testFS.getConf()); - URI uri = testFS.getUri(); - - removeBaseAndBucketOverrides(uri.getHost(), config, - S3_METADATA_STORE_IMPL, METADATASTORE_AUTHORITATIVE, - AUTHORITATIVE_PATH); - S3AFileSystem fs2 = new S3AFileSystem(); - fs2.initialize(uri, config); - return fs2; - } - - @Test - public void testIDetectNoMetadataEntry() throws Exception { - final Path cwd = path("/" + getMethodName() + "-" + UUID.randomUUID()); - final Path file = new Path(cwd, "file"); - try { - touchRawAndWaitRaw(file); - - final S3GuardFsck s3GuardFsck = new S3GuardFsck(rawFs, metadataStore); - final List comparePairs = - s3GuardFsck.compareS3ToMs(cwd); - - assertComparePairsSize(comparePairs, 2); - final S3GuardFsck.ComparePair pair = comparePairs.get(0); - checkForViolationInPairs(file, comparePairs, - S3GuardFsck.Violation.NO_METADATA_ENTRY); - } finally { - // delete the working directory with all of its contents - cleanup(file, cwd); - } - } - - @Test - public void testIDetectNoParentEntry() throws Exception { - final Path cwd = path("/" + getMethodName() + "-" + UUID.randomUUID()); - final Path file = new Path(cwd, "file"); - try { - touchGuardedAndWaitRaw(file); - // delete the parent from the MS - metadataStore.forgetMetadata(cwd); - - final S3GuardFsck s3GuardFsck = new S3GuardFsck(rawFs, metadataStore); - final List comparePairs = - s3GuardFsck.compareS3ToMs(cwd); - - assertComparePairsSize(comparePairs, 2); - // check the parent that it does not exist - checkForViolationInPairs(cwd, comparePairs, - S3GuardFsck.Violation.NO_METADATA_ENTRY); - // check the child that there's no parent entry. - checkForViolationInPairs(file, comparePairs, - S3GuardFsck.Violation.NO_PARENT_ENTRY); - } finally { - cleanup(file, cwd); - } - } - - @Test - public void testIDetectParentIsAFile() throws Exception { - final Path cwd = path("/" + getMethodName() + "-" + UUID.randomUUID()); - final Path file = new Path(cwd, "file"); - try { - touchGuardedAndWaitRaw(file); - // modify the cwd metadata and set that it's not a directory - final S3AFileStatus newParentFile = MetadataStoreTestBase - .basicFileStatus(cwd, 1, false, 1); - metadataStore.put(new PathMetadata(newParentFile)); - - final S3GuardFsck s3GuardFsck = new S3GuardFsck(rawFs, metadataStore); - final List comparePairs = - s3GuardFsck.compareS3ToMs(cwd); - - assertComparePairsSize(comparePairs, 2); - // check the parent that it does not exist - checkForViolationInPairs(cwd, comparePairs, - S3GuardFsck.Violation.DIR_IN_S3_FILE_IN_MS); - // check the child that the parent is a file. - checkForViolationInPairs(file, comparePairs, - S3GuardFsck.Violation.PARENT_IS_A_FILE); - } finally { - cleanup(file, cwd); - } - } - - @Test - public void testIDetectParentTombstoned() throws Exception { - final Path cwd = path("/" + getMethodName() + "-" + UUID.randomUUID()); - final Path file = new Path(cwd, "file"); - try { - touchGuardedAndWaitRaw(file); - // modify the parent metadata and set that it's not a directory - final PathMetadata cwdPmd = metadataStore.get(cwd); - cwdPmd.setIsDeleted(true); - metadataStore.put(cwdPmd); - - final S3GuardFsck s3GuardFsck = new S3GuardFsck(rawFs, metadataStore); - final List comparePairs = - s3GuardFsck.compareS3ToMs(cwd); - - // check the child that the parent is tombstoned - checkForViolationInPairs(file, comparePairs, - S3GuardFsck.Violation.PARENT_TOMBSTONED); - } finally { - cleanup(file, cwd); - } - } - - @Test - public void testIDetectDirInS3FileInMs() throws Exception { - final Path cwd = path("/" + getMethodName() + "-" + UUID.randomUUID()); - try { - // create a file with guarded fs - mkdirs(cwd); - // modify the cwd metadata and set that it's not a directory - final S3AFileStatus newParentFile = MetadataStoreTestBase - .basicFileStatus(cwd, 1, false, 1); - metadataStore.put(new PathMetadata(newParentFile)); - - final S3GuardFsck s3GuardFsck = new S3GuardFsck(rawFs, metadataStore); - final List comparePairs = - s3GuardFsck.compareS3ToMs(cwd); - assertComparePairsSize(comparePairs, 1); - - // check the child that the dir in s3 is a file in the ms - checkForViolationInPairs(cwd, comparePairs, - S3GuardFsck.Violation.DIR_IN_S3_FILE_IN_MS); - } finally { - cleanup(cwd); - } - } - - @Test - public void testIDetectFileInS3DirInMs() throws Exception { - final Path cwd = path("/" + getMethodName() + "-" + UUID.randomUUID()); - final Path file = new Path(cwd, "file"); - try { - touchGuardedAndWaitRaw(file); - // modify the cwd metadata and set that it's not a directory - final S3AFileStatus newFile = MetadataStoreTestBase - .basicFileStatus(file, 1, true, 1); - metadataStore.put(new PathMetadata(newFile)); - - final S3GuardFsck s3GuardFsck = new S3GuardFsck(rawFs, metadataStore); - final List comparePairs = - s3GuardFsck.compareS3ToMs(cwd); - - assertComparePairsSize(comparePairs, 1); - // check the child that the dir in s3 is a file in the ms - checkForViolationInPairs(file, comparePairs, - S3GuardFsck.Violation.FILE_IN_S3_DIR_IN_MS); - } finally { - cleanup(file, cwd); - } - } - - @Test - public void testIAuthoritativeDirectoryContentMismatch() throws Exception { - assumeTrue("Authoritative directory listings should be enabled for this " - + "test", guardedFs.hasAuthoritativeMetadataStore()); - // first dir listing will be correct - final Path cwdCorrect = path("/" + getMethodName() + "-" + UUID.randomUUID()); - final Path fileC1 = new Path(cwdCorrect, "fileC1"); - final Path fileC2 = new Path(cwdCorrect, "fileC2"); - - // second dir listing will be incorrect: missing entry from Dynamo - final Path cwdIncorrect = path("/" + getMethodName() + "-" + UUID.randomUUID()); - final Path fileIc1 = new Path(cwdIncorrect, "fileC1"); - final Path fileIc2 = new Path(cwdIncorrect, "fileC2"); - try { - touchGuardedAndWaitRaw(fileC1); - touchGuardedAndWaitRaw(fileC2); - touchGuardedAndWaitRaw(fileIc1); - - // get listing from ms and set it authoritative - final DirListingMetadata dlmC = metadataStore.listChildren(cwdCorrect); - final DirListingMetadata dlmIc = metadataStore.listChildren(cwdIncorrect); - dlmC.setAuthoritative(true); - dlmIc.setAuthoritative(true); - metadataStore.put(dlmC, Collections.emptyList(), null); - metadataStore.put(dlmIc, Collections.emptyList(), null); - - // add a file raw so the listing will be different. - touchRawAndWaitRaw(fileIc2); - - final S3GuardFsck s3GuardFsck = new S3GuardFsck(rawFs, metadataStore); - final List pairsCorrect = - s3GuardFsck.compareS3ToMs(cwdCorrect); - final List pairsIncorrect = - s3GuardFsck.compareS3ToMs(cwdIncorrect); - - // Assert that the correct dir does not contain the violation. - assertTrue(pairsCorrect.stream() - .noneMatch(p -> p.getPath().equals(cwdCorrect))); - - // Assert that the incorrect listing contains the violation. - checkForViolationInPairs(cwdIncorrect, pairsIncorrect, - S3GuardFsck.Violation.AUTHORITATIVE_DIRECTORY_CONTENT_MISMATCH); - } finally { - cleanup(fileC1, fileC2, fileIc1, fileIc2, cwdCorrect, cwdIncorrect); - } - } - - @Test - public void testIDetectLengthMismatch() throws Exception { - final Path cwd = path("/" + getMethodName() + "-" + UUID.randomUUID()); - final Path file = new Path(cwd, "file"); - try { - // create a file with guarded fs - touchGuardedAndWaitRaw(file); - - // modify the file metadata so the length will not match - final S3AFileStatus newFile = MetadataStoreTestBase - .basicFileStatus(file, 9999, false, 1); - metadataStore.put(new PathMetadata(newFile)); - - final S3GuardFsck s3GuardFsck = new S3GuardFsck(rawFs, metadataStore); - final List comparePairs = - s3GuardFsck.compareS3ToMs(cwd); - - assertComparePairsSize(comparePairs, 1); - // Assert that the correct dir does not contain the violation. - assertTrue(comparePairs.stream() - .noneMatch(p -> p.getPath().equals(cwd))); - // Assert that the incorrect file meta contains the violation. - checkForViolationInPairs(file, comparePairs, - S3GuardFsck.Violation.LENGTH_MISMATCH); - } finally { - cleanup(file, cwd); - } - } - - @Test - public void testIDetectModTimeMismatch() throws Exception { - final Path cwd = path("/" + getMethodName() + "-" + UUID.randomUUID()); - final Path file = new Path(cwd, "file"); - try { - // create a file with guarded fs - touchGuardedAndWaitRaw(file); - // modify the parent meta entry so the MOD_TIME will surely be up to date - final FileStatus oldCwdFileStatus = rawFs.getFileStatus(cwd); - final S3AFileStatus newCwdFileStatus = MetadataStoreTestBase - .basicFileStatus(cwd, 0, true, - oldCwdFileStatus.getModificationTime()); - metadataStore.put(new PathMetadata(newCwdFileStatus)); - - // modify the file metadata so the length will not match - final S3AFileStatus newFileStatus = MetadataStoreTestBase - .basicFileStatus(file, 0, false, 1); - metadataStore.put(new PathMetadata(newFileStatus)); - - final S3GuardFsck s3GuardFsck = new S3GuardFsck(rawFs, metadataStore); - final List comparePairs = - s3GuardFsck.compareS3ToMs(cwd); - - assertComparePairsSize(comparePairs, 1); - // Assert that the correct dir does not contain the violation. - assertTrue(comparePairs.stream() - .noneMatch(p -> p.getPath().equals(cwd))); - // check the file meta that there's a violation. - checkForViolationInPairs(file, comparePairs, - S3GuardFsck.Violation.MOD_TIME_MISMATCH); - } finally { - cleanup(file, cwd); - } - } - - @Test - public void testIEtagMismatch() throws Exception { - final Path cwd = path("/" + getMethodName() + "-" + UUID.randomUUID()); - final Path file = new Path(cwd, "file"); - try { - touchGuardedAndWaitRaw(file); - // modify the file metadata so the etag will not match - final S3AFileStatus newFileStatus = new S3AFileStatus(1, 1, file, 1, "", - "etag", "versionId"); - metadataStore.put(new PathMetadata(newFileStatus)); - - final S3GuardFsck s3GuardFsck = new S3GuardFsck(rawFs, metadataStore); - final List comparePairs = - s3GuardFsck.compareS3ToMs(cwd); - - assertComparePairsSize(comparePairs, 1); - // check the child that there's a BLOCKSIZE_MISMATCH - checkForViolationInPairs(file, comparePairs, - S3GuardFsck.Violation.ETAG_MISMATCH); - } finally { - cleanup(file, cwd); - } - } - - @Test - public void testINoEtag() throws Exception { - final Path cwd = path("/" + getMethodName() + "-" + UUID.randomUUID()); - final Path file1 = new Path(cwd, "file1"); - final Path file2 = new Path(cwd, "file2"); - try { - // create a file1 with guarded fs - touchGuardedAndWaitRaw(file1); - touchGuardedAndWaitRaw(file2); - // modify the file1 metadata so there's no etag - final S3AFileStatus newFile1Status = - new S3AFileStatus(1, 1, file1, 1, "", null, "versionId"); - final S3AFileStatus newFile2Status = - new S3AFileStatus(1, 1, file2, 1, "", "etag", "versionId"); - metadataStore.put(new PathMetadata(newFile1Status)); - metadataStore.put(new PathMetadata(newFile2Status)); - - final S3GuardFsck s3GuardFsck = new S3GuardFsck(rawFs, metadataStore); - final List comparePairs = - s3GuardFsck.compareS3ToMs(cwd); - - assertComparePairsSize(comparePairs, 2); - - // check file 1 that there's NO_ETAG - checkForViolationInPairs(file1, comparePairs, - S3GuardFsck.Violation.NO_ETAG); - // check the child that there's no NO_ETAG violation - checkNoViolationInPairs(file2, comparePairs, - S3GuardFsck.Violation.NO_ETAG); - } finally { - cleanup(file1, file2, cwd); - } - } - - @Test - public void testTombstonedInMsNotDeletedInS3() throws Exception { - final Path cwd = path("/" + getMethodName() + "-" + UUID.randomUUID()); - final Path file = new Path(cwd, "file"); - try { - // create a file with guarded fs - touchGuardedAndWaitRaw(file); - // set isDeleted flag in ms to true (tombstone item) - final PathMetadata fileMeta = metadataStore.get(file); - fileMeta.setIsDeleted(true); - metadataStore.put(fileMeta); - - final S3GuardFsck s3GuardFsck = new S3GuardFsck(rawFs, metadataStore); - final List comparePairs = - s3GuardFsck.compareS3ToMs(cwd); - - assertComparePairsSize(comparePairs, 1); - - // check if the violation is there - checkForViolationInPairs(file, comparePairs, - S3GuardFsck.Violation.TOMBSTONED_IN_MS_NOT_DELETED_IN_S3); - } finally { - cleanup(file, cwd); - } - } - - @Test - public void checkDdbInternalConsistency() throws Exception { - final S3GuardFsck s3GuardFsck = new S3GuardFsck(rawFs, metadataStore); - final DynamoDBMetadataStore ms = - (DynamoDBMetadataStore) guardedFs.getMetadataStore(); - s3GuardFsck.checkDdbInternalConsistency( - new Path("s3a://" + guardedFs.getBucket() + "/")); - } - - @Test - public void testDdbInternalNoLastUpdatedField() throws Exception { - final Path cwd = path("/" + getMethodName() + "-" + UUID.randomUUID()); - final Path file = new Path(cwd, "file"); - try { - final S3AFileStatus s3AFileStatus = new S3AFileStatus(100, 100, file, 100, - "test", "etag", "version"); - final PathMetadata pathMetadata = new PathMetadata(s3AFileStatus); - pathMetadata.setLastUpdated(0); - metadataStore.put(pathMetadata); - - final S3GuardFsck s3GuardFsck = new S3GuardFsck(rawFs, metadataStore); - final List comparePairs = - s3GuardFsck.checkDdbInternalConsistency(cwd); - - assertComparePairsSize(comparePairs, 1); - - // check if the violation is there - checkForViolationInPairs(file, comparePairs, - S3GuardFsck.Violation.NO_LASTUPDATED_FIELD); - } finally { - cleanup(file, cwd); - } - } - - @Test - public void testDdbInternalOrphanEntry() throws Exception { - final Path cwd = path("/" + getMethodName() + "-" + UUID.randomUUID()); - final Path parentDir = new Path(cwd, "directory"); - final Path file = new Path(parentDir, "file"); - try { - final S3AFileStatus s3AFileStatus = new S3AFileStatus(100, 100, file, 100, - "test", "etag", "version"); - final PathMetadata pathMetadata = new PathMetadata(s3AFileStatus); - pathMetadata.setLastUpdated(1000); - metadataStore.put(pathMetadata); - metadataStore.forgetMetadata(parentDir); - - final S3GuardFsck s3GuardFsck = new S3GuardFsck(rawFs, metadataStore); - final List comparePairs = - s3GuardFsck.checkDdbInternalConsistency(cwd); - - // check if the violation is there - assertComparePairsSize(comparePairs, 1); - checkForViolationInPairs(file, comparePairs, - S3GuardFsck.Violation.ORPHAN_DDB_ENTRY); - - // fix the violation - s3GuardFsck.fixViolations( - comparePairs.stream().filter(cP -> cP.getViolations() - .contains(S3GuardFsck.Violation.ORPHAN_DDB_ENTRY)) - .collect(Collectors.toList()) - ); - - // assert that the violation is fixed - final List fixedComparePairs = - s3GuardFsck.checkDdbInternalConsistency(cwd); - checkNoViolationInPairs(file, fixedComparePairs, - S3GuardFsck.Violation.ORPHAN_DDB_ENTRY); - } finally { - cleanup(file, cwd); - } - } - - @Test - public void testDdbInternalParentIsAFile() throws Exception { - final Path cwd = path("/" + getMethodName() + "-" + UUID.randomUUID()); - final Path parentDir = new Path(cwd, "directory"); - final Path file = new Path(parentDir, "file"); - try { - final S3AFileStatus s3AFileStatus = new S3AFileStatus(100, 100, file, 100, - "test", "etag", "version"); - final PathMetadata pathMetadata = new PathMetadata(s3AFileStatus); - pathMetadata.setLastUpdated(1000); - metadataStore.put(pathMetadata); - - final S3AFileStatus dirAsFile = MetadataStoreTestBase - .basicFileStatus(parentDir, 1, false, 1); - final PathMetadata dirAsFilePm = new PathMetadata(dirAsFile); - dirAsFilePm.setLastUpdated(100); - metadataStore.put(dirAsFilePm); - - final S3GuardFsck s3GuardFsck = new S3GuardFsck(rawFs, metadataStore); - final List comparePairs = - s3GuardFsck.checkDdbInternalConsistency(cwd); - - // check if the violation is there - assertComparePairsSize(comparePairs, 1); - checkForViolationInPairs(file, comparePairs, - S3GuardFsck.Violation.PARENT_IS_A_FILE); - } finally { - cleanup(file, cwd); - } - } - - @Test - public void testDdbInternalParentTombstoned() throws Exception { - final Path cwd = path("/" + getMethodName() + "-" + UUID.randomUUID()); - final Path parentDir = new Path(cwd, "directory"); - final Path file = new Path(parentDir, "file"); - try { - final S3AFileStatus s3AFileStatus = new S3AFileStatus(100, 100, file, 100, - "test", "etag", "version"); - final PathMetadata pathMetadata = new PathMetadata(s3AFileStatus); - pathMetadata.setLastUpdated(1000); - metadataStore.put(pathMetadata); - metadataStore.delete(parentDir, null); - - final S3GuardFsck s3GuardFsck = new S3GuardFsck(rawFs, metadataStore); - final List comparePairs = - s3GuardFsck.checkDdbInternalConsistency(cwd); - - // check if the violation is there - assertComparePairsSize(comparePairs, 1); - checkForViolationInPairs(file, comparePairs, - S3GuardFsck.Violation.PARENT_TOMBSTONED); - } finally { - cleanup(file, cwd); - } - } - - protected void assertComparePairsSize( - List comparePairs, int num) { - Assertions.assertThat(comparePairs) - .describedAs("Number of compare pairs") - .hasSize(num); - } - - private void touchGuardedAndWaitRaw(Path file) throws Exception { - touchAndWait(guardedFs, rawFs, file); - } - - private void touchRawAndWaitRaw(Path file) throws Exception { - touchAndWait(rawFs, rawFs, file); - } - - private void touchAndWait(FileSystem forTouch, FileSystem forWait, Path file) - throws IOException { - touch(forTouch, file); - touch(forWait, file); - } - - private void checkForViolationInPairs(Path file, - List comparePairs, - S3GuardFsck.Violation violation) { - final S3GuardFsck.ComparePair childPair = comparePairs.stream() - .filter(p -> p.getPath().equals(file)) - .findFirst().get(); - assertNotNull("The pair should not be null.", childPair); - assertTrue("The pair must contain a violation.", - childPair.containsViolation()); - Assertions.assertThat(childPair.getViolations()) - .describedAs("Violations in the pair") - .contains(violation); - } - - /** - * Check that there is no violation in the pair provided. - * - * @param file the path to filter to in the comparePairs list. - * @param comparePairs the list to validate. - * @param violation the violation that should not be in the list. - */ - private void checkNoViolationInPairs(Path file, - List comparePairs, - S3GuardFsck.Violation violation) { - - if (comparePairs.size() == 0) { - LOG.info("Compare pairs is empty, so there's no violation. (As expected.)"); - return; - } - - final S3GuardFsck.ComparePair comparePair = comparePairs.stream() - .filter(p -> p.getPath().equals(file)) - .findFirst().get(); - assertNotNull("The pair should not be null.", comparePair); - Assertions.assertThat(comparePair.getViolations()) - .describedAs("Violations in the pair") - .doesNotContain(violation); - } - - private void cleanup(Path... paths) { - for (Path path : paths) { - try { - metadataStore.forgetMetadata(path); - rawFs.delete(path, true); - } catch (IOException e) { - LOG.error("Error during cleanup.", e); - } - } - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardToolLocal.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardTool.java similarity index 58% rename from hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardToolLocal.java rename to hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardTool.java index 86fdf9af477..23b14fd3792 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardToolLocal.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardTool.java @@ -21,181 +21,37 @@ package org.apache.hadoop.fs.s3a.s3guard; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.FileNotFoundException; -import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.junit.Test; + +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.s3a.S3AFileSystem; import org.apache.hadoop.test.LambdaTestUtils; import org.apache.hadoop.util.StringUtils; -import org.junit.Assume; -import org.junit.Test; - -import org.apache.hadoop.fs.FSDataOutputStream; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.apache.hadoop.fs.s3a.Tristate; - -import static org.apache.hadoop.fs.s3a.MultipartTestUtils.*; +import static org.apache.hadoop.fs.s3a.MultipartTestUtils.assertNoUploadsAt; +import static org.apache.hadoop.fs.s3a.MultipartTestUtils.clearAnyUploads; +import static org.apache.hadoop.fs.s3a.MultipartTestUtils.countUploadsAt; +import static org.apache.hadoop.fs.s3a.MultipartTestUtils.createPartUpload; import static org.apache.hadoop.fs.s3a.S3ATestUtils.getLandsatCSVFile; -import static org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.*; +import static org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.BucketInfo; +import static org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.E_BAD_STATE; +import static org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.Uploads; import static org.apache.hadoop.fs.s3a.s3guard.S3GuardToolTestHelper.exec; -import static org.apache.hadoop.test.LambdaTestUtils.intercept; /** - * Test S3Guard related CLI commands against a LocalMetadataStore. - * Also responsible for testing the non s3guard-specific commands that, for - * now, live under the s3guard CLI command. + * Test S3Guard Tool CLI commands. */ -public class ITestS3GuardToolLocal extends AbstractS3GuardToolTestBase { +public class ITestS3GuardTool extends AbstractS3GuardToolTestBase { - private static final String LOCAL_METADATA = "local://metadata"; - private static final String[] ABORT_FORCE_OPTIONS = new String[] {"-abort", + private static final String[] ABORT_FORCE_OPTIONS = new String[]{ + "-abort", "-force", "-verbose"}; - @Override - public void setup() throws Exception { - super.setup(); - MetadataStore ms = getMetadataStore(); - Assume.assumeTrue("Test only applies when a local store is used for S3Guard;" - + "Store is " + (ms == null ? "none" : ms.toString()), - ms instanceof LocalMetadataStore); - } - - @Test - public void testImportCommand() throws Exception { - S3AFileSystem fs = getFileSystem(); - MetadataStore ms = getMetadataStore(); - Path parent = path("test-import"); - fs.mkdirs(parent); - Path dir = new Path(parent, "a"); - fs.mkdirs(dir); - Path emptyDir = new Path(parent, "emptyDir"); - fs.mkdirs(emptyDir); - for (int i = 0; i < 10; i++) { - String child = String.format("file-%d", i); - try (FSDataOutputStream out = fs.create(new Path(dir, child))) { - out.write(1); - } - } - - S3GuardTool.Import cmd = toClose(new S3GuardTool.Import(fs.getConf())); - try { - cmd.setStore(ms); - exec(cmd, "import", parent.toString()); - } finally { - cmd.setStore(new NullMetadataStore()); - } - - DirListingMetadata children = - ms.listChildren(dir); - assertEquals("Unexpected number of paths imported", 10, children - .getListing().size()); - assertEquals("Expected 2 items: empty directory and a parent directory", 2, - ms.listChildren(parent).getListing().size()); - assertTrue(children.isAuthoritative()); - } - - @Test - public void testImportCommandRepairsETagAndVersionId() throws Exception { - S3AFileSystem fs = getFileSystem(); - MetadataStore ms = getMetadataStore(); - Path path = path("test-version-metadata"); - try (FSDataOutputStream out = fs.create(path)) { - out.write(1); - } - S3AFileStatus originalStatus = (S3AFileStatus) fs.getFileStatus(path); - - // put in bogus ETag and versionId - S3AFileStatus bogusStatus = S3AFileStatus.fromFileStatus(originalStatus, - Tristate.FALSE, "bogusETag", "bogusVersionId"); - ms.put(new PathMetadata(bogusStatus)); - - // sanity check that bogus status is actually persisted - S3AFileStatus retrievedBogusStatus = (S3AFileStatus) fs.getFileStatus(path); - assertEquals("bogus ETag was not persisted", - "bogusETag", retrievedBogusStatus.getETag()); - assertEquals("bogus versionId was not persisted", - "bogusVersionId", retrievedBogusStatus.getVersionId()); - - // execute the import - S3GuardTool.Import cmd = toClose(new S3GuardTool.Import(fs.getConf())); - cmd.setStore(ms); - try { - exec(cmd, "import", path.toString()); - } finally { - cmd.setStore(new NullMetadataStore()); - } - - // make sure ETag and versionId were corrected - S3AFileStatus updatedStatus = (S3AFileStatus) fs.getFileStatus(path); - assertEquals("ETag was not corrected", - originalStatus.getETag(), updatedStatus.getETag()); - assertEquals("VersionId was not corrected", - originalStatus.getVersionId(), updatedStatus.getVersionId()); - } - - @Test - public void testDestroyBucketExistsButNoTable() throws Throwable { - run(Destroy.NAME, - "-meta", LOCAL_METADATA, - getLandsatCSVFile(getConfiguration())); - } - - @Test - public void testImportNoFilesystem() throws Throwable { - final Import importer = toClose(new S3GuardTool.Import(getConfiguration())); - importer.setStore(getMetadataStore()); - try { - intercept(IOException.class, - () -> importer.run( - new String[]{ - "import", - "-meta", LOCAL_METADATA, - S3A_THIS_BUCKET_DOES_NOT_EXIST - })); - } finally { - importer.setStore(new NullMetadataStore()); - } - } - - @Test - public void testInfoBucketAndRegionNoFS() throws Throwable { - intercept(FileNotFoundException.class, - () -> run(BucketInfo.NAME, "-meta", - LOCAL_METADATA, "-region", - "any-region", S3A_THIS_BUCKET_DOES_NOT_EXIST)); - } - - @Test - public void testInitNegativeRead() throws Throwable { - runToFailure(INVALID_ARGUMENT, - Init.NAME, "-meta", LOCAL_METADATA, "-region", - "eu-west-1", - READ_FLAG, "-10"); - } - - @Test - public void testInit() throws Throwable { - run(Init.NAME, - "-meta", LOCAL_METADATA, - "-region", "us-west-1"); - } - - @Test - public void testInitTwice() throws Throwable { - run(Init.NAME, - "-meta", LOCAL_METADATA, - "-region", "us-west-1"); - run(Init.NAME, - "-meta", LOCAL_METADATA, - "-region", "us-west-1"); - } - @Test public void testLandsatBucketUnguarded() throws Throwable { run(BucketInfo.NAME, @@ -209,7 +65,7 @@ public class ITestS3GuardToolLocal extends AbstractS3GuardToolTestBase { BucketInfo.NAME, "-" + BucketInfo.GUARDED_FLAG, getLandsatCSVFile( - ITestS3GuardToolLocal.this.getConfiguration())); + ITestS3GuardTool.this.getConfiguration())); } @Test @@ -225,38 +81,17 @@ public class ITestS3GuardToolLocal extends AbstractS3GuardToolTestBase { BucketInfo.NAME, "-" + BucketInfo.ENCRYPTION_FLAG, "AES256", getLandsatCSVFile( - ITestS3GuardToolLocal.this.getConfiguration())); + ITestS3GuardTool.this.getConfiguration())); } @Test public void testStoreInfo() throws Throwable { S3GuardTool.BucketInfo cmd = toClose(new S3GuardTool.BucketInfo(getFileSystem().getConf())); - cmd.setStore(getMetadataStore()); - try { - String output = exec(cmd, cmd.getName(), - "-" + BucketInfo.GUARDED_FLAG, - getFileSystem().getUri().toString()); - LOG.info("Exec output=\n{}", output); - } finally { - cmd.setStore(new NullMetadataStore()); - } - } - - @Test - public void testSetCapacity() throws Throwable { - S3GuardTool cmd = toClose( - new S3GuardTool.SetCapacity(getFileSystem().getConf())); - cmd.setStore(getMetadataStore()); - try { - String output = exec(cmd, cmd.getName(), - "-" + READ_FLAG, "100", - "-" + WRITE_FLAG, "100", - getFileSystem().getUri().toString()); - LOG.info("Exec output=\n{}", output); - } finally { - cmd.setStore(new NullMetadataStore()); - } + String output = exec(cmd, cmd.getName(), + "-" + BucketInfo.UNGUARDED_FLAG, + getFileSystem().getUri().toString()); + LOG.info("Exec output=\n{}", output); } private final static String UPLOAD_PREFIX = "test-upload-prefix"; diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardToolDynamoDB.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardToolDynamoDB.java deleted file mode 100644 index 19784ede5ea..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ITestS3GuardToolDynamoDB.java +++ /dev/null @@ -1,403 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.UUID; - -import com.amazonaws.services.dynamodbv2.document.DynamoDB; -import com.amazonaws.services.dynamodbv2.document.Table; -import com.amazonaws.services.dynamodbv2.model.ListTagsOfResourceRequest; -import com.amazonaws.services.dynamodbv2.model.ResourceInUseException; -import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException; -import com.amazonaws.services.dynamodbv2.model.Tag; - -import org.junit.Assert; -import org.junit.Assume; -import org.junit.AssumptionViolatedException; -import org.junit.Test; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.s3a.Constants; -import org.apache.hadoop.fs.s3a.S3AFileSystem; -import org.apache.hadoop.fs.s3a.UnknownStoreException; -import org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.Destroy; -import org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.Init; -import org.apache.hadoop.util.ExitUtil; - -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_REGION_KEY; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_TABLE_NAME_KEY; -import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_DDB_TABLE_TAG; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.removeBucketOverrides; -import static org.apache.hadoop.fs.s3a.S3AUtils.setBucketOption; -import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore.*; -import static org.apache.hadoop.fs.s3a.s3guard.S3GuardTool.*; -import static org.apache.hadoop.fs.s3a.s3guard.S3GuardToolTestHelper.exec; -import static org.apache.hadoop.test.LambdaTestUtils.intercept; - -/** - * Test S3Guard related CLI commands against DynamoDB. - */ -public class ITestS3GuardToolDynamoDB extends AbstractS3GuardToolTestBase { - - @Override - public void setup() throws Exception { - super.setup(); - try { - getMetadataStore(); - } catch (ClassCastException e) { - throw new AssumptionViolatedException( - "Test only applies when DynamoDB is used for S3Guard Store", - e); - } - } - - @Override - protected DynamoDBMetadataStore getMetadataStore() { - return (DynamoDBMetadataStore) super.getMetadataStore(); - } - - // Check the existence of a given DynamoDB table. - private static boolean exist(DynamoDB dynamoDB, String tableName) { - assertNotNull(dynamoDB); - assertNotNull(tableName); - assertFalse("empty table name", tableName.isEmpty()); - try { - Table table = dynamoDB.getTable(tableName); - table.describe(); - } catch (ResourceNotFoundException e) { - return false; - } - return true; - } - - @Test - public void testInvalidRegion() throws Exception { - final String testTableName = - getTestTableName("testInvalidRegion" + new Random().nextInt()); - final String testRegion = "invalidRegion"; - // Initialize MetadataStore - final Init initCmd = toClose(new Init(getFileSystem().getConf())); - intercept(IOException.class, - () -> { - int res = initCmd.run(new String[]{ - "init", - "-region", testRegion, - "-meta", "dynamodb://" + testTableName - }); - return "Use of invalid region did not fail, returning " + res - + "- table may have been " + - "created and not cleaned up: " + testTableName; - }); - } - - @Test - public void testDynamoTableTagging() throws Exception { - Configuration conf = getConfiguration(); - // If the region is not set in conf, skip the test. - String ddbRegion = conf.get(S3GUARD_DDB_REGION_KEY); - Assume.assumeTrue( - S3GUARD_DDB_REGION_KEY + " should be set to run this test", - ddbRegion != null && !ddbRegion.isEmpty() - ); - - // setup - // clear all table tagging config before this test - conf.getPropsWithPrefix(S3GUARD_DDB_TABLE_TAG).keySet().forEach( - propKey -> conf.unset(S3GUARD_DDB_TABLE_TAG + propKey) - ); - - conf.set(S3GUARD_DDB_TABLE_NAME_KEY, - getTestTableName("testDynamoTableTagging-" + UUID.randomUUID())); - String bucket = getFileSystem().getBucket(); - removeBucketOverrides(bucket, conf, - S3GUARD_DDB_TABLE_NAME_KEY, - S3GUARD_DDB_REGION_KEY); - - S3GuardTool.Init cmdR = new S3GuardTool.Init(conf); - Map tagMap = new HashMap<>(); - tagMap.put("hello", "dynamo"); - tagMap.put("tag", "youre it"); - - String[] argsR = new String[]{ - cmdR.getName(), - "-tag", tagMapToStringParams(tagMap), - "s3a://" + bucket + "/" - }; - - // run - cmdR.run(argsR); - - // Check. Should create new metadatastore with the table name set. - try (DynamoDBMetadataStore ddbms = new DynamoDBMetadataStore()) { - ddbms.initialize(conf, new S3Guard.TtlTimeProvider(conf)); - ListTagsOfResourceRequest listTagsOfResourceRequest = new ListTagsOfResourceRequest() - .withResourceArn(ddbms.getTable().getDescription().getTableArn()); - List tags = ddbms.getAmazonDynamoDB().listTagsOfResource(listTagsOfResourceRequest).getTags(); - - // assert - // table version is always there as a plus one tag. - assertEquals(tagMap.size() + 1, tags.size()); - for (Tag tag : tags) { - // skip the version marker tag - if (tag.getKey().equals(VERSION_MARKER_TAG_NAME)) { - continue; - } - Assert.assertEquals(tagMap.get(tag.getKey()), tag.getValue()); - } - // be sure to clean up - delete table - ddbms.destroy(); - } - } - - private String tagMapToStringParams(Map tagMap) { - StringBuilder stringBuilder = new StringBuilder(); - - for (Map.Entry kv : tagMap.entrySet()) { - stringBuilder.append(kv.getKey() + "=" + kv.getValue() + ";"); - } - - return stringBuilder.toString(); - } - - private DDBCapacities getCapacities() throws IOException { - return DDBCapacities.extractCapacities(getMetadataStore().getDiagnostics()); - } - - @Test - public void testDynamoDBInitDestroyCycle() throws Throwable { - String testTableName = - getTestTableName("testDynamoDBInitDestroy" + new Random().nextInt()); - String testS3Url = path(testTableName).toString(); - S3AFileSystem fs = getFileSystem(); - DynamoDB db = null; - try { - try (Init initCmd = new Init(fs.getConf())) { - // Initialize MetadataStore - expectSuccess("Init command did not exit successfully - see output", - initCmd, - Init.NAME, - "-" + READ_FLAG, "0", - "-" + WRITE_FLAG, "0", - "-" + Init.SSE_FLAG, - "-" + META_FLAG, "dynamodb://" + testTableName, - testS3Url); - } - // Verify it exists - MetadataStore ms = getMetadataStore(); - assertTrue("metadata store should be DynamoDBMetadataStore", - ms instanceof DynamoDBMetadataStore); - DynamoDBMetadataStore dynamoMs = (DynamoDBMetadataStore) ms; - db = dynamoMs.getDynamoDB(); - assertTrue(String.format("%s does not exist", testTableName), - exist(db, testTableName)); - - Configuration conf = fs.getConf(); - String bucket = fs.getBucket(); - // force in a new bucket - setBucketOption(conf, bucket, Constants.S3_METADATA_STORE_IMPL, - Constants.S3GUARD_METASTORE_DYNAMO); - try (Init initCmd = new Init(conf)) { - String initOutput = exec(initCmd, - "init", "-meta", "dynamodb://" + testTableName, testS3Url); - assertTrue("No Dynamo diagnostics in output " + initOutput, - initOutput.contains(DESCRIPTION)); - } - - // run a bucket info command and look for - // confirmation that it got the output from DDB diags - String info; - try (S3GuardTool.BucketInfo infocmd = new S3GuardTool.BucketInfo(conf)) { - info = exec(infocmd, BucketInfo.NAME, - "-" + BucketInfo.GUARDED_FLAG, - testS3Url); - assertTrue("No Dynamo diagnostics in output " + info, - info.contains(DESCRIPTION)); - } - - // get the current values to set again - - // play with the set-capacity option - String fsURI = getFileSystem().getUri().toString(); - DDBCapacities original = getCapacities(); - assertTrue("Wrong billing mode in " + info, - info.contains(BILLING_MODE_PER_REQUEST)); - // per-request tables fail here, so expect that - intercept(IOException.class, E_ON_DEMAND_NO_SET_CAPACITY, - () -> exec(toClose(newSetCapacity()), - SetCapacity.NAME, - fsURI)); - - // Destroy MetadataStore - try (Destroy destroyCmd = new Destroy(fs.getConf())){ - String destroyed = exec(destroyCmd, - "destroy", "-meta", "dynamodb://" + testTableName, testS3Url); - // Verify it does not exist - assertFalse(String.format("%s still exists", testTableName), - exist(db, testTableName)); - - // delete again and expect success again - expectSuccess("Destroy command did not exit successfully - see output", - destroyCmd, - "destroy", "-meta", "dynamodb://" + testTableName, testS3Url); - } - } catch (ResourceNotFoundException e) { - throw new AssertionError( - String.format("DynamoDB table %s does not exist", testTableName), - e); - } finally { - LOG.warn("Table may have not been cleaned up: " + - testTableName); - if (db != null) { - Table table = db.getTable(testTableName); - if (table != null) { - try { - table.delete(); - table.waitForDelete(); - } catch (ResourceNotFoundException | ResourceInUseException e) { - /* Ignore */ - } - } - } - } - } - - private S3GuardTool newSetCapacity() { - S3GuardTool setCapacity = new S3GuardTool.SetCapacity( - getFileSystem().getConf()); - setCapacity.setStore(getMetadataStore()); - return setCapacity; - } - - @Test - public void testDestroyUnknownTable() throws Throwable { - run(S3GuardTool.Destroy.NAME, - "-region", "us-west-2", - "-meta", "dynamodb://" + getTestTableName(DYNAMODB_TABLE)); - } - - @Test - public void testCLIFsckWithoutParam() throws Exception { - intercept(ExitUtil.ExitException.class, () -> run(Fsck.NAME)); - } - - @Test - public void testCLIFsckWithParam() throws Exception { - LOG.info("This test serves the purpose to run fsck with the correct " + - "parameters, so there will be no exception thrown."); - final int result = run(S3GuardTool.Fsck.NAME, "-check", - "s3a://" + getFileSystem().getBucket()); - LOG.info("The return value of the run: {}", result); - } - - @Test - public void testCLIFsckWithParamParentOfRoot() throws Exception { - intercept(IOException.class, "Invalid URI", - () -> run(S3GuardTool.Fsck.NAME, "-check", - "s3a://" + getFileSystem().getBucket() + "/..")); - } - - @Test - public void testCLIFsckFailInitializeFs() throws Exception { - intercept(UnknownStoreException.class, - () -> run(S3GuardTool.Fsck.NAME, "-check", - "s3a://this-bucket-does-not-exist-" + UUID.randomUUID())); - } - - @Test - public void testCLIFsckDDbInternalWrongS3APath() throws Exception { - intercept(FileNotFoundException.class, "wrong path", - () -> run(S3GuardTool.Fsck.NAME, "-"+Fsck.DDB_MS_CONSISTENCY_FLAG, - "s3a://" + getFileSystem().getBucket() + "/" + UUID.randomUUID())); - } - - @Test - public void testCLIFsckDDbInternalParam() throws Exception { - describe("This test serves the purpose to run fsck with the correct " + - "parameters, so there will be no exception thrown."); - final int result = run(S3GuardTool.Fsck.NAME, - "-" + Fsck.DDB_MS_CONSISTENCY_FLAG, - "s3a://" + getFileSystem().getBucket()); - LOG.info("The return value of the run: {}", result); - } - - @Test - public void testCLIFsckCheckExclusive() throws Exception { - describe("There should be only one check param when running fsck." + - "If more then one param is passed, the command should fail." + - "This provide exclusive run for checks so the user is able to define " + - "the order of checking."); - intercept(ExitUtil.ExitException.class, "only one parameter", - () -> run(S3GuardTool.Fsck.NAME, - "-" + Fsck.DDB_MS_CONSISTENCY_FLAG, "-" + Fsck.CHECK_FLAG, - "s3a://" + getFileSystem().getBucket())); - } - - @Test - public void testCLIFsckDDbFixOnlyFails() throws Exception { - describe("This test serves the purpose to run fsck with the correct " + - "parameters, so there will be no exception thrown."); - final int result = run(S3GuardTool.Fsck.NAME, - "-" + Fsck.FIX_FLAG, - "s3a://" + getFileSystem().getBucket()); - LOG.info("The return value of the run: {}", result); - assertEquals(ERROR, result); - } - - /** - * Test that the fix flag is accepted by the fsck. - * - * Note that we don't have an assert at the end of this test because - * there maybe some errors found during the check and the returned value - * will be ERROR and not SUCCESS. So if we assert on SUCCESS, then the test - * could (likely) to be flaky. - * If the FIX_FLAG parameter is not accepted here an exception will be thrown - * so the test will break. - * - * @throws Exception - */ - @Test - public void testCLIFsckDDbFixAndInternalSucceed() throws Exception { - describe("This test serves the purpose to run fsck with the correct " + - "parameters, so there will be no exception thrown."); - final int result = run(S3GuardTool.Fsck.NAME, - "-" + Fsck.FIX_FLAG, - "-" + Fsck.DDB_MS_CONSISTENCY_FLAG, - "s3a://" + getFileSystem().getBucket()); - LOG.info("The return value of the run: {}", result); - } - - /** - * Test that when init, the CMK option can not live without SSE enabled. - */ - @Test - public void testCLIInitParamCmkWithoutSse() throws Exception { - intercept(ExitUtil.ExitException.class, "can only be used with", - () -> run(S3GuardTool.Init.NAME, - "-" + S3GuardTool.CMK_FLAG, - "alias/" + UUID.randomUUID(), - "s3a://" + getFileSystem().getBucket() + "/" + UUID.randomUUID())); - } - -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/MetadataStoreTestBase.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/MetadataStoreTestBase.java deleted file mode 100644 index 06148857ea7..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/MetadataStoreTestBase.java +++ /dev/null @@ -1,1342 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.assertj.core.api.Assertions; -import org.junit.After; -import org.junit.Assume; -import org.junit.Before; -import org.junit.Test; -import org.junit.rules.Timeout; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.RemoteIterator; -import org.apache.hadoop.fs.permission.FsPermission; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.S3ATestConstants; -import org.apache.hadoop.fs.s3a.S3ATestUtils; -import org.apache.hadoop.fs.s3a.Tristate; -import org.apache.hadoop.io.IOUtils; -import org.apache.hadoop.test.HadoopTestBase; - -import static org.apache.hadoop.thirdparty.com.google.common.base.Preconditions.checkNotNull; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.metadataStorePersistsAuthoritativeBit; - -/** - * Main test class for MetadataStore implementations. - * Implementations should each create a test by subclassing this and - * overriding {@link #createContract()}. - * If your implementation may return missing results for recently set paths, - * override {@link MetadataStoreTestBase#allowMissing()}. - */ -public abstract class MetadataStoreTestBase extends HadoopTestBase { - - private static final Logger LOG = - LoggerFactory.getLogger(MetadataStoreTestBase.class); - - /** Some dummy values for sanity-checking FileStatus contents. */ - static final long BLOCK_SIZE = 32 * 1024 * 1024; - static final int REPLICATION = 1; - static final String OWNER = "bob"; - private final long modTime = System.currentTimeMillis() - 5000; - - // attributes not supported by S3AFileStatus - static final FsPermission PERMISSION = null; - static final String GROUP = null; - private final long accessTime = 0; - private static ITtlTimeProvider ttlTimeProvider; - - private static final List EMPTY_LIST = Collections.emptyList(); - - /** - * Each test should override this. Will use a new Configuration instance. - * @return Contract which specifies the MetadataStore under test plus config. - */ - public abstract AbstractMSContract createContract() throws IOException; - - /** - * Each test should override this. - * @param conf Base configuration instance to use. - * @return Contract which specifies the MetadataStore under test plus config. - */ - public abstract AbstractMSContract createContract(Configuration conf) - throws IOException; - - /** - * Tests assume that implementations will return recently set results. If - * your implementation does not always hold onto metadata (e.g. LRU or - * time-based expiry) you can override this to return false. - * @return true if the test should succeed when null results are returned - * from the MetadataStore under test. - */ - public boolean allowMissing() { - return false; - } - - /** - * Pruning is an optional feature for metadata store implementations. - * Tests will only check that functionality if it is expected to work. - * @return true if the test should expect pruning to work. - */ - public boolean supportsPruning() { - return true; - } - - /** The MetadataStore contract used to test against. */ - private AbstractMSContract contract; - - protected MetadataStore ms; - - /** - * @return reference to the test contract. - */ - protected AbstractMSContract getContract() { - return contract; - } - - @Before - public void setUp() throws Exception { - Thread.currentThread().setName("setup"); - LOG.debug("== Setup. =="); - contract = createContract(); - ms = contract.getMetadataStore(); - assertNotNull("null MetadataStore", ms); - assertNotNull("null FileSystem", contract.getFileSystem()); - ms.initialize(contract.getFileSystem(), - new S3Guard.TtlTimeProvider(contract.getFileSystem().getConf())); - ttlTimeProvider = - new S3Guard.TtlTimeProvider(contract.getFileSystem().getConf()); - } - - @After - public void tearDown() throws Exception { - Thread.currentThread().setName("teardown"); - LOG.debug("== Tear down. =="); - if (ms != null) { - try { - ms.destroy(); - } catch (Exception e) { - LOG.warn("Failed to destroy tables in teardown", e); - } - IOUtils.closeStream(ms); - ms = null; - } - } - - /** - * Describe a test in the logs. - * @param text text to print - * @param args arguments to format in the printing - */ - protected void describe(String text, Object... args) { - LOG.info("\n\n{}: {}\n", - getMethodName(), - String.format(text, args)); - } - - /** - * Helper function for verifying DescendantsIterator and - * MetadataStoreListFilesIterator behavior. - * @param createNodes List of paths to create - * @param checkNodes List of paths that the iterator should return - */ - private void doTestDescendantsIterator( - Class implementation, String[] createNodes, - String[] checkNodes) throws Exception { - // we set up the example file system tree in metadata store - for (String pathStr : createNodes) { - final S3AFileStatus status = pathStr.contains("file") - ? basicFileStatus(strToPath(pathStr), 100, false) - : basicFileStatus(strToPath(pathStr), 0, true); - ms.put(new PathMetadata(status), null); - } - - final PathMetadata rootMeta = new PathMetadata(makeDirStatus("/")); - RemoteIterator iterator; - if (implementation == DescendantsIterator.class) { - iterator = new DescendantsIterator(ms, rootMeta); - } else if (implementation == MetadataStoreListFilesIterator.class) { - iterator = new MetadataStoreListFilesIterator(ms, rootMeta, false); - } else { - throw new UnsupportedOperationException("Unrecognized class"); - } - - final Set actual = new HashSet<>(); - while (iterator.hasNext()) { - final Path p = iterator.next().getPath(); - actual.add(Path.getPathWithoutSchemeAndAuthority(p).toString()); - } - LOG.info("We got {} by iterating DescendantsIterator", actual); - - if (!allowMissing()) { - Assertions.assertThat(actual) - .as("files listed through DescendantsIterator") - .containsExactlyInAnyOrder(checkNodes); - } - } - - /** - * Test that we can get the whole sub-tree by iterating DescendantsIterator. - * - * The tree is similar to or same as the example in code comment. - */ - @Test - public void testDescendantsIterator() throws Exception { - final String[] tree = new String[] { - "/dir1", - "/dir1/dir2", - "/dir1/dir3", - "/dir1/dir2/file1", - "/dir1/dir2/file2", - "/dir1/dir3/dir4", - "/dir1/dir3/dir5", - "/dir1/dir3/dir4/file3", - "/dir1/dir3/dir5/file4", - "/dir1/dir3/dir6" - }; - doTestDescendantsIterator(DescendantsIterator.class, - tree, tree); - } - - /** - * Test that we can get the correct subset of the tree with - * MetadataStoreListFilesIterator. - * - * The tree is similar to or same as the example in code comment. - */ - @Test - public void testMetadataStoreListFilesIterator() throws Exception { - final String[] wholeTree = new String[] { - "/dir1", - "/dir1/dir2", - "/dir1/dir3", - "/dir1/dir2/file1", - "/dir1/dir2/file2", - "/dir1/dir3/dir4", - "/dir1/dir3/dir5", - "/dir1/dir3/dir4/file3", - "/dir1/dir3/dir5/file4", - "/dir1/dir3/dir6" - }; - final String[] leafNodes = new String[] { - "/dir1/dir2/file1", - "/dir1/dir2/file2", - "/dir1/dir3/dir4/file3", - "/dir1/dir3/dir5/file4" - }; - doTestDescendantsIterator(MetadataStoreListFilesIterator.class, wholeTree, - leafNodes); - } - - @Test - public void testPutNew() throws Exception { - /* create three dirs /da1, /da2, /da3 */ - createNewDirs("/da1", "/da2", "/da3"); - - /* It is caller's responsibility to set up ancestor entries beyond the - * containing directory. We only track direct children of the directory. - * Thus this will not affect entry for /da1. - */ - ms.put(new PathMetadata(makeFileStatus("/da1/db1/fc1", 100)), null); - - assertEmptyDirs("/da2", "/da3"); - assertDirectorySize("/da1/db1", 1); - - /* Check contents of dir status. */ - PathMetadata dirMeta = ms.get(strToPath("/da1")); - if (!allowMissing() || dirMeta != null) { - verifyDirStatus(dirMeta.getFileStatus()); - } - - /* This already exists, and should silently replace it. */ - ms.put(new PathMetadata(makeDirStatus("/da1/db1")), null); - - /* If we had putNew(), and used it above, this would be empty again. */ - assertDirectorySize("/da1", 1); - - assertEmptyDirs("/da2", "/da3"); - - /* Ensure new files update correct parent dirs. */ - ms.put(new PathMetadata(makeFileStatus("/da1/db1/fc1", 100)), null); - ms.put(new PathMetadata(makeFileStatus("/da1/db1/fc2", 200)), null); - assertDirectorySize("/da1", 1); - assertDirectorySize("/da1/db1", 2); - assertEmptyDirs("/da2", "/da3"); - PathMetadata meta = ms.get(strToPath("/da1/db1/fc2")); - if (!allowMissing() || meta != null) { - assertNotNull("Get file after put new.", meta); - verifyFileStatus(meta.getFileStatus(), 200); - } - } - - @Test - public void testPutOverwrite() throws Exception { - final String filePath = "/a1/b1/c1/some_file"; - final String dirPath = "/a1/b1/c1/d1"; - ms.put(new PathMetadata(makeFileStatus(filePath, 100)), null); - ms.put(new PathMetadata(makeDirStatus(dirPath)), null); - PathMetadata meta = ms.get(strToPath(filePath)); - if (!allowMissing() || meta != null) { - verifyFileStatus(meta.getFileStatus(), 100); - } - - ms.put(new PathMetadata(basicFileStatus(strToPath(filePath), 9999, false)), - null); - meta = ms.get(strToPath(filePath)); - if (!allowMissing() || meta != null) { - verifyFileStatus(meta.getFileStatus(), 9999); - } - } - - @Test - public void testRootDirPutNew() throws Exception { - Path rootPath = strToPath("/"); - - ms.put(new PathMetadata(makeFileStatus("/file1", 100)), null); - DirListingMetadata dir = ms.listChildren(rootPath); - if (!allowMissing() || dir != null) { - assertNotNull("Root dir cached", dir); - assertFalse("Root not fully cached", dir.isAuthoritative()); - final Collection listing = dir.getListing(); - Assertions.assertThat(listing) - .describedAs("Root dir listing") - .isNotNull() - .extracting(p -> p.getFileStatus().getPath()) - .containsExactly(strToPath("/file1")); - } - } - - @Test - public void testDelete() throws Exception { - setUpDeleteTest(); - - ms.delete(strToPath("/ADirectory1/db1/file2"), null); - - /* Ensure delete happened. */ - assertDirectorySize("/ADirectory1/db1", 1); - PathMetadata meta = ms.get(strToPath("/ADirectory1/db1/file2")); - assertTrue("File deleted", meta == null || meta.isDeleted()); - } - - @Test - public void testDeleteSubtree() throws Exception { - deleteSubtreeHelper(""); - } - - @Test - public void testDeleteSubtreeHostPath() throws Exception { - deleteSubtreeHelper(contract.getFileSystem().getUri().toString()); - } - - private void deleteSubtreeHelper(String pathPrefix) throws Exception { - - String p = pathPrefix; - setUpDeleteTest(p); - createNewDirs(p + "/ADirectory1/db1/dc1", p + "/ADirectory1/db1/dc1/dd1"); - ms.put(new PathMetadata( - makeFileStatus(p + "/ADirectory1/db1/dc1/dd1/deepFile", 100)), - null); - if (!allowMissing()) { - assertCached(p + "/ADirectory1/db1"); - } - ms.deleteSubtree(strToPath(p + "/ADirectory1/db1/"), null); - - assertEmptyDirectory(p + "/ADirectory1"); - assertDeleted(p + "/ADirectory1/db1"); - assertDeleted(p + "/ADirectory1/file1"); - assertDeleted(p + "/ADirectory1/file2"); - assertDeleted(p + "/ADirectory1/db1/dc1/dd1/deepFile"); - assertEmptyDirectory(p + "/ADirectory2"); - } - - - /* - * Some implementations might not support this. It was useful to test - * correctness of the LocalMetadataStore implementation, but feel free to - * override this to be a no-op. - */ - @Test - public void testDeleteRecursiveRoot() throws Exception { - setUpDeleteTest(); - - ms.deleteSubtree(strToPath("/"), null); - assertDeleted("/ADirectory1"); - assertDeleted("/ADirectory2"); - assertDeleted("/ADirectory2/db1"); - assertDeleted("/ADirectory2/db1/file1"); - assertDeleted("/ADirectory2/db1/file2"); - } - - @Test - public void testDeleteNonExisting() throws Exception { - // Path doesn't exist, but should silently succeed - ms.delete(strToPath("/bobs/your/uncle"), null); - - // Ditto. - ms.deleteSubtree(strToPath("/internets"), null); - } - - - private void setUpDeleteTest() throws IOException { - setUpDeleteTest(""); - } - - private void setUpDeleteTest(String prefix) throws IOException { - createNewDirs(prefix + "/ADirectory1", prefix + "/ADirectory2", - prefix + "/ADirectory1/db1"); - ms.put(new PathMetadata(makeFileStatus(prefix + "/ADirectory1/db1/file1", - 100)), - null); - ms.put(new PathMetadata(makeFileStatus(prefix + "/ADirectory1/db1/file2", - 100)), - null); - - PathMetadata meta = ms.get(strToPath(prefix + "/ADirectory1/db1/file2")); - if (!allowMissing() || meta != null) { - assertNotNull("Found test file", meta); - assertDirectorySize(prefix + "/ADirectory1/db1", 2); - } - } - - @Test - public void testGet() throws Exception { - final String filePath = "/a1/b1/c1/some_file"; - final String dirPath = "/a1/b1/c1/d1"; - ms.put(new PathMetadata(makeFileStatus(filePath, 100)), null); - ms.put(new PathMetadata(makeDirStatus(dirPath)), null); - PathMetadata meta = ms.get(strToPath(filePath)); - if (!allowMissing() || meta != null) { - assertNotNull("Get found file", meta); - verifyFileStatus(meta.getFileStatus(), 100); - } - - if (!(ms instanceof NullMetadataStore)) { - ms.delete(strToPath(filePath), null); - meta = ms.get(strToPath(filePath)); - assertTrue("Tombstone not left for deleted file", meta.isDeleted()); - } - - meta = ms.get(strToPath(dirPath)); - if (!allowMissing() || meta != null) { - assertNotNull("Get found file (dir)", meta); - assertTrue("Found dir", meta.getFileStatus().isDirectory()); - } - - meta = ms.get(strToPath("/bollocks")); - assertNull("Don't get non-existent file", meta); - } - - @Test - public void testGetEmptyDir() throws Exception { - final String dirPath = "/a1/b1/c1/d1"; - // Creates /a1/b1/c1/d1 as an empty dir - setupListStatus(); - - // 1. Tell MetadataStore (MS) that there are zero children - putListStatusFiles(dirPath, true /* authoritative */ - /* zero children */); - - // 2. Request a file status for dir, including whether or not the dir - // is empty. - PathMetadata meta = ms.get(strToPath(dirPath), true); - - // 3. Check that either (a) the MS doesn't track whether or not it is - // empty (which is allowed), or (b) the MS knows the dir is empty. - if (!allowMissing() || meta != null) { - assertNotNull("Get should find meta for dir", meta); - assertNotEquals("Dir is empty or unknown", Tristate.FALSE, - meta.isEmptyDirectory()); - } - } - - @Test - public void testGetNonEmptyDir() throws Exception { - final String dirPath = "/a1/b1/c1"; - // Creates /a1/b1/c1 as an non-empty dir - setupListStatus(); - - // Request a file status for dir, including whether or not the dir - // is empty. - PathMetadata meta = ms.get(strToPath(dirPath), true); - - // MetadataStore knows /a1/b1/c1 has at least one child. It is valid - // for it to answer either (a) UNKNOWN: the MS doesn't track whether - // or not the dir is empty, or (b) the MS knows the dir is non-empty. - if (!allowMissing() || meta != null) { - assertNotNull("Get should find meta for dir", meta); - assertNotEquals("Dir is non-empty or unknown", Tristate.TRUE, - meta.isEmptyDirectory()); - } - } - - @Test - public void testGetDirUnknownIfEmpty() throws Exception { - final String dirPath = "/a1/b1/c1/d1"; - // 1. Create /a1/b1/c1/d1 as an empty dir, but do not tell MetadataStore - // (MS) whether or not it has any children. - setupListStatus(); - - // 2. Request a file status for dir, including whether or not the dir - // is empty. - PathMetadata meta = ms.get(strToPath(dirPath), true); - - // 3. Assert MS reports isEmptyDir as UNKONWN: We haven't told MS - // whether or not the directory has any children. - if (!allowMissing() || meta != null) { - assertNotNull("Get should find meta for dir", meta); - assertEquals("Dir empty is unknown", Tristate.UNKNOWN, - meta.isEmptyDirectory()); - } - } - - @Test - public void testListChildren() throws Exception { - setupListStatus(); - - DirListingMetadata dirMeta; - dirMeta = ms.listChildren(strToPath("/")); - if (!allowMissing()) { - assertNotNull(dirMeta); - /* Cache has no way of knowing it has all entries for root unless we - * specifically tell it via put() with - * DirListingMetadata.isAuthoritative = true */ - assertFalse("Root dir is not cached, or partially cached", - dirMeta.isAuthoritative()); - assertListingsEqual(dirMeta.getListing(), "/a1", "/a2"); - } - - dirMeta = ms.listChildren(strToPath("/a1")); - if (!allowMissing() || dirMeta != null) { - dirMeta = dirMeta.withoutTombstones(); - assertListingsEqual(dirMeta.getListing(), "/a1/b1", "/a1/b2"); - } - - dirMeta = ms.listChildren(strToPath("/a1/b1")); - if (!allowMissing() || dirMeta != null) { - assertListingsEqual(dirMeta.getListing(), "/a1/b1/file1", "/a1/b1/file2", - "/a1/b1/c1"); - } - } - - - - @Test - public void testListChildrenAuthoritative() throws IOException { - Assume.assumeTrue("MetadataStore should be capable for authoritative " - + "storage of directories to run this test.", - metadataStorePersistsAuthoritativeBit(ms)); - - setupListStatus(); - - DirListingMetadata dirMeta = ms.listChildren(strToPath("/a1/b1")); - dirMeta.setAuthoritative(true); - dirMeta.put(new PathMetadata( - makeFileStatus("/a1/b1/file_new", 100))); - ms.put(dirMeta, EMPTY_LIST, null); - - dirMeta = ms.listChildren(strToPath("/a1/b1")); - assertListingsEqual(dirMeta.getListing(), "/a1/b1/file1", "/a1/b1/file2", - "/a1/b1/c1", "/a1/b1/file_new"); - assertTrue(dirMeta.isAuthoritative()); - } - - @Test - public void testDirListingRoot() throws Exception { - commonTestPutListStatus("/"); - } - - @Test - public void testPutDirListing() throws Exception { - commonTestPutListStatus("/a"); - } - - @Test - public void testInvalidListChildren() throws Exception { - setupListStatus(); - assertNull("missing path returns null", - ms.listChildren(strToPath("/a1/b1x"))); - } - - @Test - public void testMove() throws Exception { - // Create test dir structure - createNewDirs("/a1", "/a2", "/a3"); - createNewDirs("/a1/b1", "/a1/b2"); - putListStatusFiles("/a1/b1", false, "/a1/b1/file1", "/a1/b1/file2"); - - // Assert root listing as expected - Collection entries; - DirListingMetadata dirMeta = ms.listChildren(strToPath("/")); - if (!allowMissing() || dirMeta != null) { - dirMeta = dirMeta.withoutTombstones(); - assertNotNull("Listing root", dirMeta); - entries = dirMeta.getListing(); - assertListingsEqual(entries, "/a1", "/a2", "/a3"); - } - - // Assert src listing as expected - dirMeta = ms.listChildren(strToPath("/a1/b1")); - if (!allowMissing() || dirMeta != null) { - assertNotNull("Listing /a1/b1", dirMeta); - entries = dirMeta.getListing(); - assertListingsEqual(entries, "/a1/b1/file1", "/a1/b1/file2"); - } - - // Do the move(): rename(/a1/b1, /b1) - Collection srcPaths = Arrays.asList(strToPath("/a1/b1"), - strToPath("/a1/b1/file1"), strToPath("/a1/b1/file2")); - - ArrayList destMetas = new ArrayList<>(); - destMetas.add(new PathMetadata(makeDirStatus("/b1"))); - destMetas.add(new PathMetadata(makeFileStatus("/b1/file1", 100))); - destMetas.add(new PathMetadata(makeFileStatus("/b1/file2", 100))); - ms.move(srcPaths, destMetas, null); - - // Assert src is no longer there - dirMeta = ms.listChildren(strToPath("/a1")); - if (!allowMissing() || dirMeta != null) { - assertNotNull("Listing /a1", dirMeta); - entries = dirMeta.withoutTombstones().getListing(); - assertListingsEqual(entries, "/a1/b2"); - } - - PathMetadata meta = ms.get(strToPath("/a1/b1/file1")); - assertTrue("Src path deleted", meta == null || meta.isDeleted()); - - // Assert dest looks right - meta = ms.get(strToPath("/b1/file1")); - if (!allowMissing() || meta != null) { - assertNotNull("dest file not null", meta); - verifyFileStatus(meta.getFileStatus(), 100); - } - - dirMeta = ms.listChildren(strToPath("/b1")); - if (!allowMissing() || dirMeta != null) { - assertNotNull("dest listing not null", dirMeta); - entries = dirMeta.getListing(); - assertListingsEqual(entries, "/b1/file1", "/b1/file2"); - } - } - - /** - * Test that the MetadataStore differentiates between the same path in two - * different buckets. - */ - @Test - public void testMultiBucketPaths() throws Exception { - String p1 = "s3a://bucket-a/path1"; - String p2 = "s3a://bucket-b/path2"; - - // Make sure we start out empty - PathMetadata meta = ms.get(new Path(p1)); - assertNull("Path should not be present yet.", meta); - meta = ms.get(new Path(p2)); - assertNull("Path2 should not be present yet.", meta); - - // Put p1, assert p2 doesn't match - ms.put(new PathMetadata(makeFileStatus(p1, 100)), null); - meta = ms.get(new Path(p2)); - assertNull("Path 2 should not match path 1.", meta); - - // Make sure delete is correct as well - if (!allowMissing()) { - ms.delete(new Path(p2), null); - meta = ms.get(new Path(p1)); - assertNotNull("Path should not have been deleted", meta); - } - ms.delete(new Path(p1), null); - } - - @Test - public void testPruneFiles() throws Exception { - Assume.assumeTrue(supportsPruning()); - createNewDirs("/pruneFiles"); - - long oldTime = getTime(); - ms.put(new PathMetadata(makeFileStatus("/pruneFiles/old", 1, oldTime)), - null); - DirListingMetadata ls2 = ms.listChildren(strToPath("/pruneFiles")); - if (!allowMissing()) { - assertListingsEqual(ls2.getListing(), "/pruneFiles/old"); - } - - // It's possible for the Local implementation to get from /pruneFiles/old's - // modification time to here in under 1ms, causing it to not get pruned - Thread.sleep(1); - long cutoff = System.currentTimeMillis(); - long newTime = getTime(); - ms.put(new PathMetadata(makeFileStatus("/pruneFiles/new", 1, newTime)), - null); - - DirListingMetadata ls; - ls = ms.listChildren(strToPath("/pruneFiles")); - if (!allowMissing()) { - assertListingsEqual(ls.getListing(), "/pruneFiles/new", - "/pruneFiles/old"); - } - ms.prune(MetadataStore.PruneMode.ALL_BY_MODTIME, cutoff); - ls = ms.listChildren(strToPath("/pruneFiles")); - if (allowMissing()) { - assertDeleted("/pruneFiles/old"); - } else { - assertListingsEqual(ls.getListing(), "/pruneFiles/new"); - } - } - - @Test - public void testPruneDirs() throws Exception { - Assume.assumeTrue(supportsPruning()); - - // We only test that files, not dirs, are removed during prune. - // We specifically allow directories to remain, as it is more robust - // for DynamoDBMetadataStore's prune() implementation: If a - // file was created in a directory while it was being pruned, it would - // violate the invariant that all ancestors of a file exist in the table. - - createNewDirs("/pruneDirs/dir"); - - long oldTime = getTime(); - ms.put(new PathMetadata(makeFileStatus("/pruneDirs/dir/file", - 1, oldTime)), null); - - // It's possible for the Local implementation to get from the old - // modification time to here in under 1ms, causing it to not get pruned - Thread.sleep(1); - long cutoff = getTime(); - - ms.prune(MetadataStore.PruneMode.ALL_BY_MODTIME, cutoff); - - assertDeleted("/pruneDirs/dir/file"); - } - - @Test - public void testPruneUnsetsAuthoritative() throws Exception { - String rootDir = "/unpruned-root-dir"; - String grandparentDir = rootDir + "/pruned-grandparent-dir"; - String parentDir = grandparentDir + "/pruned-parent-dir"; - String staleFile = parentDir + "/stale-file"; - String freshFile = rootDir + "/fresh-file"; - String[] directories = {rootDir, grandparentDir, parentDir}; - - createNewDirs(rootDir, grandparentDir, parentDir); - long time = System.currentTimeMillis(); - ms.put(new PathMetadata( - basicFileStatus(0, false, 0, time - 1, strToPath(staleFile)), - Tristate.FALSE, false), - null); - ms.put(new PathMetadata( - basicFileStatus(0, false, 0, time + 1, strToPath(freshFile)), - Tristate.FALSE, false), - null); - - // set parent dir as authoritative - if (!allowMissing()) { - DirListingMetadata parentDirMd = ms.listChildren(strToPath(parentDir)); - parentDirMd.setAuthoritative(true); - ms.put(parentDirMd, EMPTY_LIST, null); - } - - ms.prune(MetadataStore.PruneMode.ALL_BY_MODTIME, time); - DirListingMetadata listing; - for (String directory : directories) { - Path path = strToPath(directory); - if (ms.get(path) != null) { - listing = ms.listChildren(path); - assertFalse(listing.isAuthoritative()); - } - } - } - - @Test - public void testPrunePreservesAuthoritative() throws Exception { - String rootDir = "/unpruned-root-dir"; - String grandparentDir = rootDir + "/pruned-grandparent-dir"; - String parentDir = grandparentDir + "/pruned-parent-dir"; - String staleFile = parentDir + "/stale-file"; - String freshFile = rootDir + "/fresh-file"; - String[] directories = {rootDir, grandparentDir, parentDir}; - - // create dirs - createNewDirs(rootDir, grandparentDir, parentDir); - long time = System.currentTimeMillis(); - ms.put(new PathMetadata( - basicFileStatus(0, false, 0, time + 1, strToPath(staleFile)), - Tristate.FALSE, false), - null); - ms.put(new PathMetadata( - basicFileStatus(0, false, 0, time + 1, strToPath(freshFile)), - Tristate.FALSE, false), - null); - - if (!allowMissing()) { - // set parent dir as authoritative - DirListingMetadata parentDirMd = ms.listChildren(strToPath(parentDir)); - parentDirMd.setAuthoritative(true); - ms.put(parentDirMd, EMPTY_LIST, null); - - // prune the ms - ms.prune(MetadataStore.PruneMode.ALL_BY_MODTIME, time); - - // get the directory listings - DirListingMetadata rootDirMd = ms.listChildren(strToPath(rootDir)); - DirListingMetadata grandParentDirMd = - ms.listChildren(strToPath(grandparentDir)); - parentDirMd = ms.listChildren(strToPath(parentDir)); - - // assert that parent dir is still authoritative (no removed elements - // during prune) - assertFalse(rootDirMd.isAuthoritative()); - assertFalse(grandParentDirMd.isAuthoritative()); - assertTrue(parentDirMd.isAuthoritative()); - } - } - - @Test - public void testPutDirListingMetadataPutsFileMetadata() - throws IOException { - boolean authoritative = true; - String[] filenames = {"/dir1/file1", "/dir1/file2", "/dir1/file3"}; - String dirPath = "/dir1"; - - ArrayList metas = new ArrayList<>(filenames.length); - for (String filename : filenames) { - metas.add(new PathMetadata(makeFileStatus(filename, 100))); - } - DirListingMetadata dirMeta = - new DirListingMetadata(strToPath(dirPath), metas, authoritative); - ms.put(dirMeta, EMPTY_LIST, null); - - if (!allowMissing()) { - assertDirectorySize(dirPath, filenames.length); - PathMetadata metadata; - for(String fileName : filenames){ - metadata = ms.get(strToPath(fileName)); - assertNotNull(String.format( - "PathMetadata for file %s should not be null.", fileName), - metadata); - } - } - } - - @Test - public void testPutRetainsIsDeletedInParentListing() throws Exception { - final Path path = strToPath("/a/b"); - final S3AFileStatus fileStatus = basicFileStatus(path, 0, false); - PathMetadata pm = new PathMetadata(fileStatus); - pm.setIsDeleted(true); - ms.put(pm, null); - if(!allowMissing()) { - final PathMetadata pathMetadata = - ms.listChildren(path.getParent()).get(path); - assertTrue("isDeleted should be true on the parent listing", - pathMetadata.isDeleted()); - } - } - - @Test - public void testPruneExpiredTombstones() throws Exception { - List keepFilenames = new ArrayList<>( - Arrays.asList("/dir1/fileK1", "/dir1/fileK2", "/dir1/fileK3")); - List removeFilenames = new ArrayList<>( - Arrays.asList("/dir1/fileR1", "/dir1/fileR2", "/dir1/fileR3")); - - long cutoff = 9001; - - for(String fN : keepFilenames) { - final PathMetadata pathMetadata = new PathMetadata(makeFileStatus(fN, 1)); - pathMetadata.setLastUpdated(9002L); - ms.put(pathMetadata); - } - - for(String fN : removeFilenames) { - final PathMetadata pathMetadata = new PathMetadata(makeFileStatus(fN, 1)); - pathMetadata.setLastUpdated(9000L); - // tombstones are the deleted files! - pathMetadata.setIsDeleted(true); - ms.put(pathMetadata); - } - - ms.prune(MetadataStore.PruneMode.TOMBSTONES_BY_LASTUPDATED, cutoff); - - if (!allowMissing()) { - for (String fN : keepFilenames) { - final PathMetadata pathMetadata = ms.get(strToPath(fN)); - assertNotNull("Kept files should be in the metastore after prune", - pathMetadata); - } - } - - for(String fN : removeFilenames) { - final PathMetadata pathMetadata = ms.get(strToPath(fN)); - assertNull("Expired tombstones should be removed from metastore after " - + "the prune.", pathMetadata); - } - } - - @Test - public void testPruneExpiredTombstonesSpecifiedPath() throws Exception { - List keepFilenames = new ArrayList<>( - Arrays.asList("/dir1/fileK1", "/dir1/fileK2", "/dir1/fileK3")); - List removeFilenames = new ArrayList<>( - Arrays.asList("/dir2/fileR1", "/dir2/fileR2", "/dir2/fileR3")); - - long cutoff = 9001; - - // Both are expired. Difference is it will only delete the specified one. - for (String fN : keepFilenames) { - final PathMetadata pathMetadata = new PathMetadata(makeFileStatus(fN, 1)); - pathMetadata.setLastUpdated(9002L); - ms.put(pathMetadata); - } - - for (String fN : removeFilenames) { - final PathMetadata pathMetadata = new PathMetadata(makeFileStatus(fN, 1)); - pathMetadata.setLastUpdated(9000L); - // tombstones are the deleted files! - pathMetadata.setIsDeleted(true); - ms.put(pathMetadata); - } - - final String prunePath = getPathStringForPrune("/dir2"); - ms.prune(MetadataStore.PruneMode.TOMBSTONES_BY_LASTUPDATED, cutoff, - prunePath); - - if (!allowMissing()) { - for (String fN : keepFilenames) { - final PathMetadata pathMetadata = ms.get(strToPath(fN)); - assertNotNull("Kept files should be in the metastore after prune", - pathMetadata); - } - } - - for (String fN : removeFilenames) { - final PathMetadata pathMetadata = ms.get(strToPath(fN)); - assertNull("Expired tombstones should be removed from metastore after " - + "the prune.", pathMetadata); - } - } - - /* - * Helper functions. - */ - - /** Modifies paths input array and returns it. */ - private String[] buildPathStrings(String parent, String... paths) - throws IOException { - for (int i = 0; i < paths.length; i++) { - Path p = new Path(strToPath(parent), paths[i]); - paths[i] = p.toString(); - } - return paths; - } - - - /** - * The prune operation needs the path with the bucket name as a string in - * {@link DynamoDBMetadataStore}, but not for {@link LocalMetadataStore}. - * This is an implementation detail of the ms, so this should be - * implemented in the subclasses. - */ - protected abstract String getPathStringForPrune(String path) - throws Exception; - - private void commonTestPutListStatus(final String parent) throws IOException { - putListStatusFiles(parent, true, buildPathStrings(parent, "file1", "file2", - "file3")); - DirListingMetadata dirMeta = ms.listChildren(strToPath(parent)); - if (!allowMissing() || dirMeta != null) { - dirMeta = dirMeta.withoutTombstones(); - assertNotNull("list after putListStatus", dirMeta); - Collection entries = dirMeta.getListing(); - assertNotNull("listStatus has entries", entries); - assertListingsEqual(entries, - buildPathStrings(parent, "file1", "file2", "file3")); - } - } - - private void setupListStatus() throws IOException { - createNewDirs("/a1", "/a2", "/a1/b1", "/a1/b2", "/a1/b1/c1", - "/a1/b1/c1/d1"); - ms.put(new PathMetadata(makeFileStatus("/a1/b1/file1", 100)), null); - ms.put(new PathMetadata(makeFileStatus("/a1/b1/file2", 100)), null); - } - - private void assertListingsEqual(Collection listing, - String ...pathStrs) throws IOException { - Set a = new HashSet<>(); - for (PathMetadata meta : listing) { - a.add(meta.getFileStatus().getPath()); - } - - Set b = new HashSet<>(); - for (String ps : pathStrs) { - b.add(strToPath(ps)); - } - Assertions.assertThat(a) - .as("Directory Listing") - .containsExactlyInAnyOrderElementsOf(b); - } - - protected void putListStatusFiles(String dirPath, boolean authoritative, - String... filenames) throws IOException { - ArrayList metas = new ArrayList<>(filenames .length); - for (String filename : filenames) { - metas.add(new PathMetadata(makeFileStatus(filename, 100))); - } - DirListingMetadata dirMeta = - new DirListingMetadata(strToPath(dirPath), metas, authoritative); - ms.put(dirMeta, EMPTY_LIST, null); - } - - protected void createNewDirs(String... dirs) - throws IOException { - for (String pathStr : dirs) { - ms.put(new PathMetadata(makeDirStatus(pathStr)), null); - } - } - - private void assertDirectorySize(String pathStr, int size) - throws IOException { - DirListingMetadata dirMeta = ms.listChildren(strToPath(pathStr)); - if (!allowMissing()) { - assertNotNull("Directory " + pathStr + " is null in cache", dirMeta); - } - if (!allowMissing() || dirMeta != null) { - dirMeta = dirMeta.withoutTombstones(); - Assertions.assertThat(nonDeleted(dirMeta.getListing())) - .as("files in directory %s", pathStr) - .hasSize(size); - } - } - - /** @return only file statuses which are *not* marked deleted. */ - private Collection nonDeleted( - Collection statuses) { - Collection currentStatuses = new ArrayList<>(); - for (PathMetadata status : statuses) { - if (!status.isDeleted()) { - currentStatuses.add(status); - } - } - return currentStatuses; - } - - protected PathMetadata get(final String pathStr) throws IOException { - Path path = strToPath(pathStr); - return ms.get(path); - } - - protected PathMetadata getNonNull(final String pathStr) throws IOException { - return checkNotNull(get(pathStr), "No metastore entry for %s", pathStr); - } - - /** - * Assert that either a path has no entry or that it is marked as deleted. - * @param pathStr path - * @throws IOException IO failure. - */ - protected void assertDeleted(String pathStr) throws IOException { - PathMetadata meta = get(pathStr); - boolean cached = meta != null && !meta.isDeleted(); - assertFalse(pathStr + " should not be cached: " + meta, cached); - } - - protected void assertCached(String pathStr) throws IOException { - verifyCached(pathStr); - } - - /** - * Get an entry which must exist and not be deleted. - * @param pathStr path - * @return the entry - * @throws IOException IO failure. - */ - protected PathMetadata verifyCached(final String pathStr) throws IOException { - PathMetadata meta = getNonNull(pathStr); - assertFalse(pathStr + " was found but marked deleted: "+ meta, - meta.isDeleted()); - return meta; - } - - /** - * Assert that an entry exists and is a file. - * @param pathStr path - * @throws IOException IO failure. - */ - protected PathMetadata verifyIsFile(String pathStr) throws IOException { - PathMetadata md = verifyCached(pathStr); - assertTrue("Not a file: " + md, - md.getFileStatus().isFile()); - return md; - } - - /** - * Assert that an entry exists and is a tombstone. - * @param pathStr path - * @throws IOException IO failure. - */ - protected void assertIsTombstone(String pathStr) throws IOException { - PathMetadata meta = getNonNull(pathStr); - assertTrue(pathStr + " must be a tombstone: " + meta, meta.isDeleted()); - } - - /** - * Assert that an entry does not exist. - * @param pathStr path - * @throws IOException IO failure. - */ - protected void assertNotFound(String pathStr) throws IOException { - PathMetadata meta = get(pathStr); - assertNull("Unexpectedly found entry at path " + pathStr, - meta); - } - - /** - * Get an entry which must be a file. - * @param pathStr path - * @return the entry - * @throws IOException IO failure. - */ - protected PathMetadata getFile(final String pathStr) throws IOException { - PathMetadata meta = verifyCached(pathStr); - assertFalse(pathStr + " is not a file: " + meta, - meta.getFileStatus().isDirectory()); - return meta; - } - - /** - * Get an entry which must be a directory. - * @param pathStr path - * @return the entry - * @throws IOException IO failure. - */ - protected PathMetadata getDirectory(final String pathStr) throws IOException { - PathMetadata meta = verifyCached(pathStr); - assertTrue(pathStr + " is not a directory: " + meta, - meta.getFileStatus().isDirectory()); - return meta; - } - - /** - * Get an entry which must not be marked as an empty directory: - * its empty directory field must be FALSE or UNKNOWN. - * @param pathStr path - * @return the entry - * @throws IOException IO failure. - */ - protected PathMetadata getNonEmptyDirectory(final String pathStr) - throws IOException { - PathMetadata meta = getDirectory(pathStr); - assertNotEquals("Path " + pathStr - + " is considered an empty dir " + meta, - Tristate.TRUE, - meta.isEmptyDirectory()); - return meta; - } - - /** - * Get an entry which must be an empty directory. - * its empty directory field must be TRUE. - * @param pathStr path - * @return the entry - * @throws IOException IO failure. - */ - protected PathMetadata getEmptyDirectory(final String pathStr) - throws IOException { - PathMetadata meta = getDirectory(pathStr); - assertEquals("Path " + pathStr - + " is not considered an empty dir " + meta, - Tristate.TRUE, - meta.isEmptyDirectory()); - return meta; - } - - /** - * Convenience to create a fully qualified Path from string. - */ - protected Path strToPath(String p) throws IOException { - final Path path = new Path(p); - assertTrue("Non-absolute path: " + path, path.isAbsolute()); - return path.makeQualified(contract.getFileSystem().getUri(), null); - } - - protected void assertEmptyDirectory(String pathStr) throws IOException { - assertDirectorySize(pathStr, 0); - } - - protected void assertEmptyDirs(String...dirs) throws IOException { - for (String pathStr : dirs) { - assertEmptyDirectory(pathStr); - } - } - - protected S3AFileStatus basicFileStatus(Path path, int size, boolean isDir) - throws IOException { - return basicFileStatus(path, size, isDir, modTime); - } - - public static S3AFileStatus basicFileStatus(int size, boolean isDir, - long blockSize, long modificationTime, Path path) { - if (isDir) { - return new S3AFileStatus(Tristate.UNKNOWN, path, null); - } else { - return new S3AFileStatus(size, modificationTime, path, blockSize, null, - null, null); - } - } - - public static S3AFileStatus basicFileStatus(Path path, int size, - boolean isDir, long newModTime) throws IOException { - if (isDir) { - return new S3AFileStatus(Tristate.UNKNOWN, path, OWNER); - } else { - return new S3AFileStatus(size, newModTime, path, BLOCK_SIZE, OWNER, - "etag", "version"); - } - } - - protected S3AFileStatus makeFileStatus(String pathStr, int size) throws - IOException { - return makeFileStatus(pathStr, size, modTime); - } - - protected S3AFileStatus makeFileStatus(String pathStr, int size, - long newModTime) throws IOException { - return basicFileStatus(strToPath(pathStr), size, false, - newModTime); - } - - protected void verifyFileStatus(FileStatus status, long size) { - S3ATestUtils.verifyFileStatus(status, size, BLOCK_SIZE, modTime); - } - - protected S3AFileStatus makeDirStatus(String pathStr) throws IOException { - return basicFileStatus(strToPath(pathStr), 0, true, modTime); - } - - /** - * Verify the directory file status. Subclass may verify additional fields. - */ - protected void verifyDirStatus(S3AFileStatus status) { - assertTrue("Is a dir", status.isDirectory()); - assertEquals("zero length", 0, status.getLen()); - } - - long getModTime() { - return modTime; - } - - long getAccessTime() { - return accessTime; - } - - protected static long getTime() { - return System.currentTimeMillis(); - } - - protected static ITtlTimeProvider getTtlTimeProvider() { - return ttlTimeProvider; - } - - /** - * Put a file to the shared DDB table. - * @param key key - * @param time timestamp. - * @param operationState ongoing state - * @return the entry - * @throws IOException IO failure - */ - protected PathMetadata putFile( - final String key, - final long time, - BulkOperationState operationState) throws IOException { - PathMetadata meta = new PathMetadata(makeFileStatus(key, 1, time)); - meta.setLastUpdated(time); - ms.put(meta, operationState); - return meta; - } - - /** - * Put a dir to the shared DDB table. - * @param key key - * @param time timestamp. - * @param operationState ongoing state - * @return the entry - * @throws IOException IO failure - */ - protected PathMetadata putDir( - final String key, - final long time, - BulkOperationState operationState) throws IOException { - PathMetadata meta = new PathMetadata( - basicFileStatus(strToPath(key), 0, true, time)); - meta.setLastUpdated(time); - ms.put(meta, operationState); - return meta; - } - - /** - * Put a tombstone to the shared DDB table. - * @param key key - * @param time timestamp. - * @param operationState ongoing state - * @return the entry - * @throws IOException IO failure - */ - protected PathMetadata putTombstone( - final String key, - final long time, - BulkOperationState operationState) throws IOException { - PathMetadata meta = tombstone(strToPath(key), time); - ms.put(meta, operationState); - return meta; - } - - /** - * Create a tombstone from the timestamp. - * @param path path to tombstone - * @param time timestamp. - * @return the entry. - */ - public static PathMetadata tombstone(Path path, long time) { - S3AFileStatus s3aStatus = new S3AFileStatus(0, - time, path, 0, null, - null, null); - return new PathMetadata(s3aStatus, Tristate.UNKNOWN, true); - } - - @Override - protected Timeout retrieveTestTimeout() { - return Timeout.millis(S3ATestConstants.S3A_TEST_TIMEOUT); - } - -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestDirListingMetadata.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestDirListingMetadata.java deleted file mode 100644 index 5f7a6fbd072..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestDirListingMetadata.java +++ /dev/null @@ -1,348 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.assertj.core.api.Assertions; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3AFileStatus; - -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.junit.Assert.*; - -/** - * Unit tests of {@link DirListingMetadata}. - */ -public class TestDirListingMetadata { - - private static final String TEST_OWNER = "hadoop"; - public static final String TEST_ETAG = "abc"; - public static final String TEST_VERSION_ID = "def"; - - @Rule - public ExpectedException exception = ExpectedException.none(); - - @Test - public void testNullPath() { - exception.expect(NullPointerException.class); - exception.expectMessage(notNullValue(String.class)); - new DirListingMetadata(null, null, false); - } - - @Test - public void testNullListing() { - Path path = new Path("/path"); - DirListingMetadata meta = new DirListingMetadata(path, null, false); - assertEquals(path, meta.getPath()); - assertNotNull(meta.getListing()); - assertTrue(meta.getListing().isEmpty()); - assertFalse(meta.isAuthoritative()); - } - - @Test - public void testEmptyListing() { - Path path = new Path("/path"); - DirListingMetadata meta = new DirListingMetadata(path, - new ArrayList(0), - false); - assertEquals(path, meta.getPath()); - assertNotNull(meta.getListing()); - assertTrue(meta.getListing().isEmpty()); - assertFalse(meta.isAuthoritative()); - } - - @Test - public void testListing() { - Path path = new Path("/path"); - PathMetadata pathMeta1 = new PathMetadata( - new S3AFileStatus(true, new Path(path, "dir1"), TEST_OWNER)); - PathMetadata pathMeta2 = new PathMetadata( - new S3AFileStatus(true, new Path(path, "dir2"), TEST_OWNER)); - PathMetadata pathMeta3 = new PathMetadata( - new S3AFileStatus(123, 456, new Path(path, "file1"), 8192, TEST_OWNER, - TEST_ETAG, TEST_VERSION_ID)); - List listing = Arrays.asList(pathMeta1, pathMeta2, pathMeta3); - DirListingMetadata meta = new DirListingMetadata(path, listing, false); - assertEquals(path, meta.getPath()); - assertNotNull(meta.getListing()); - assertFalse(meta.getListing().isEmpty()); - assertTrue(meta.getListing().contains(pathMeta1)); - assertTrue(meta.getListing().contains(pathMeta2)); - assertTrue(meta.getListing().contains(pathMeta3)); - assertFalse(meta.isAuthoritative()); - } - - @Test - public void testListingUnmodifiable() { - Path path = new Path("/path"); - DirListingMetadata meta = makeTwoDirsOneFile(path); - assertNotNull(meta.getListing()); - exception.expect(UnsupportedOperationException.class); - meta.getListing().clear(); - } - - @Test - public void testAuthoritative() { - Path path = new Path("/path"); - DirListingMetadata meta = new DirListingMetadata(path, null, true); - assertEquals(path, meta.getPath()); - assertNotNull(meta.getListing()); - assertTrue(meta.getListing().isEmpty()); - assertTrue(meta.isAuthoritative()); - } - - @Test - public void testSetAuthoritative() { - Path path = new Path("/path"); - DirListingMetadata meta = new DirListingMetadata(path, null, false); - assertEquals(path, meta.getPath()); - assertNotNull(meta.getListing()); - assertTrue(meta.getListing().isEmpty()); - assertFalse(meta.isAuthoritative()); - meta.setAuthoritative(true); - assertTrue(meta.isAuthoritative()); - } - - @Test - public void testGet() { - Path path = new Path("/path"); - PathMetadata pathMeta1 = new PathMetadata( - new S3AFileStatus(true, new Path(path, "dir1"), TEST_OWNER)); - PathMetadata pathMeta2 = new PathMetadata( - new S3AFileStatus(true, new Path(path, "dir2"), TEST_OWNER)); - PathMetadata pathMeta3 = new PathMetadata( - new S3AFileStatus(123, 456, new Path(path, "file1"), 8192, TEST_OWNER, - TEST_ETAG, TEST_VERSION_ID)); - List listing = Arrays.asList(pathMeta1, pathMeta2, pathMeta3); - DirListingMetadata meta = new DirListingMetadata(path, listing, false); - assertEquals(path, meta.getPath()); - assertNotNull(meta.getListing()); - assertFalse(meta.getListing().isEmpty()); - assertTrue(meta.getListing().contains(pathMeta1)); - assertTrue(meta.getListing().contains(pathMeta2)); - assertTrue(meta.getListing().contains(pathMeta3)); - assertFalse(meta.isAuthoritative()); - assertEquals(pathMeta1, meta.get(pathMeta1.getFileStatus().getPath())); - assertEquals(pathMeta2, meta.get(pathMeta2.getFileStatus().getPath())); - assertEquals(pathMeta3, meta.get(pathMeta3.getFileStatus().getPath())); - assertNull(meta.get(new Path(path, "notfound"))); - } - - @Test - public void testGetNull() { - Path path = new Path("/path"); - DirListingMetadata meta = new DirListingMetadata(path, null, false); - exception.expect(NullPointerException.class); - exception.expectMessage(notNullValue(String.class)); - meta.get(null); - } - - @Test - public void testGetRoot() { - Path path = new Path("/path"); - DirListingMetadata meta = new DirListingMetadata(path, null, false); - exception.expect(IllegalArgumentException.class); - exception.expectMessage(notNullValue(String.class)); - meta.get(new Path("/")); - } - - @Test - public void testGetNotChild() { - Path path = new Path("/path"); - DirListingMetadata meta = new DirListingMetadata(path, null, false); - exception.expect(IllegalArgumentException.class); - exception.expectMessage(notNullValue(String.class)); - meta.get(new Path("/different/ancestor")); - } - - @Test - public void testPut() { - Path path = new Path("/path"); - PathMetadata pathMeta1 = new PathMetadata( - new S3AFileStatus(true, new Path(path, "dir1"), TEST_OWNER)); - PathMetadata pathMeta2 = new PathMetadata( - new S3AFileStatus(true, new Path(path, "dir2"), TEST_OWNER)); - PathMetadata pathMeta3 = new PathMetadata( - new S3AFileStatus(123, 456, new Path(path, "file1"), 8192, TEST_OWNER, - TEST_ETAG, TEST_VERSION_ID)); - List listing = Arrays.asList(pathMeta1, pathMeta2, pathMeta3); - DirListingMetadata meta = new DirListingMetadata(path, listing, false); - assertEquals(path, meta.getPath()); - assertNotNull(meta.getListing()); - assertFalse(meta.getListing().isEmpty()); - assertTrue(meta.getListing().contains(pathMeta1)); - assertTrue(meta.getListing().contains(pathMeta2)); - assertTrue(meta.getListing().contains(pathMeta3)); - assertFalse(meta.isAuthoritative()); - PathMetadata pathMeta4 = new PathMetadata( - new S3AFileStatus(true, new Path(path, "dir3"), TEST_OWNER)); - meta.put(pathMeta4); - assertTrue(meta.getListing().contains(pathMeta4)); - assertEquals(pathMeta4, meta.get(pathMeta4.getFileStatus().getPath())); - } - - @Test - public void testPutNull() { - Path path = new Path("/path"); - DirListingMetadata meta = new DirListingMetadata(path, null, false); - exception.expect(NullPointerException.class); - exception.expectMessage(notNullValue(String.class)); - meta.put(null); - } - - @Test - public void testPutNullPath() { - Path path = new Path("/path"); - DirListingMetadata meta = new DirListingMetadata(path, null, false); - exception.expect(NullPointerException.class); - exception.expectMessage(notNullValue(String.class)); - meta.put(new PathMetadata(new S3AFileStatus(true, null, TEST_OWNER))); - } - - @Test - public void testPutRoot() { - Path path = new Path("/path"); - DirListingMetadata meta = new DirListingMetadata(path, null, false); - exception.expect(IllegalArgumentException.class); - exception.expectMessage(notNullValue(String.class)); - meta.put(new PathMetadata(new S3AFileStatus(true, new Path("/"), - TEST_OWNER))); - } - - @Test - public void testPutNotChild() { - Path path = new Path("/path"); - DirListingMetadata meta = new DirListingMetadata(path, null, false); - exception.expect(IllegalArgumentException.class); - exception.expectMessage(notNullValue(String.class)); - meta.put(new PathMetadata( - new S3AFileStatus(true, new Path("/different/ancestor"), TEST_OWNER))); - } - - @Test - public void testRemove() { - Path path = new Path("/path"); - PathMetadata pathMeta1 = new PathMetadata( - new S3AFileStatus(true, new Path(path, "dir1"), TEST_OWNER)); - PathMetadata pathMeta2 = new PathMetadata( - new S3AFileStatus(true, new Path(path, "dir2"), TEST_OWNER)); - PathMetadata pathMeta3 = new PathMetadata( - new S3AFileStatus(123, 456, new Path(path, "file1"), 8192, TEST_OWNER, - TEST_ETAG, TEST_VERSION_ID)); - List listing = Arrays.asList(pathMeta1, pathMeta2, pathMeta3); - DirListingMetadata meta = new DirListingMetadata(path, listing, false); - assertEquals(path, meta.getPath()); - assertNotNull(meta.getListing()); - assertFalse(meta.getListing().isEmpty()); - assertTrue(meta.getListing().contains(pathMeta1)); - assertTrue(meta.getListing().contains(pathMeta2)); - assertTrue(meta.getListing().contains(pathMeta3)); - assertFalse(meta.isAuthoritative()); - meta.remove(pathMeta1.getFileStatus().getPath()); - assertFalse(meta.getListing().contains(pathMeta1)); - assertNull(meta.get(pathMeta1.getFileStatus().getPath())); - } - - @Test - public void testRemoveNull() { - Path path = new Path("/path"); - DirListingMetadata meta = new DirListingMetadata(path, null, false); - exception.expect(NullPointerException.class); - exception.expectMessage(notNullValue(String.class)); - meta.remove(null); - } - - @Test - public void testRemoveRoot() { - Path path = new Path("/path"); - DirListingMetadata meta = new DirListingMetadata(path, null, false); - exception.expect(IllegalArgumentException.class); - exception.expectMessage(notNullValue(String.class)); - meta.remove(new Path("/")); - } - - @Test - public void testRemoveNotChild() { - Path path = new Path("/path"); - DirListingMetadata meta = new DirListingMetadata(path, null, false); - exception.expect(IllegalArgumentException.class); - exception.expectMessage(notNullValue(String.class)); - meta.remove(new Path("/different/ancestor")); - } - - - @Test - public void testRemoveExpiredEntriesFromListing() { - long ttl = 9; - long oldTime = 100; - long newTime = 110; - long now = 110; - - Path path = new Path("/path"); - PathMetadata pathMeta1 = new PathMetadata( - new S3AFileStatus(true, new Path(path, "dir1"), TEST_OWNER)); - PathMetadata pathMeta2 = new PathMetadata( - new S3AFileStatus(true, new Path(path, "dir2"), TEST_OWNER)); - PathMetadata pathMeta3 = new PathMetadata( - new S3AFileStatus(123, 456, new Path(path, "file1"), 8192, TEST_OWNER, - TEST_ETAG, TEST_VERSION_ID)); - pathMeta1.setLastUpdated(oldTime); - pathMeta2.setLastUpdated(0); - pathMeta3.setLastUpdated(newTime); - - List listing = Arrays.asList(pathMeta1, pathMeta2, pathMeta3); - DirListingMetadata meta = new DirListingMetadata(path, listing, false); - - List expired = meta.removeExpiredEntriesFromListing(ttl, - now); - - Assertions.assertThat(meta.getListing()) - .describedAs("Metadata listing for %s", path) - .doesNotContain(pathMeta1) - .contains(pathMeta2) - .contains(pathMeta3); - Assertions.assertThat(expired) - .describedAs("Expire entries underr %s", path) - .doesNotContain(pathMeta2) - .contains(pathMeta1); - } - - /** - * Create DirListingMetadata with two dirs and one file living in directory - * 'parent'. - */ - private static DirListingMetadata makeTwoDirsOneFile(Path parent) { - PathMetadata pathMeta1 = new PathMetadata( - new S3AFileStatus(true, new Path(parent, "dir1"), TEST_OWNER)); - PathMetadata pathMeta2 = new PathMetadata( - new S3AFileStatus(true, new Path(parent, "dir2"), TEST_OWNER)); - PathMetadata pathMeta3 = new PathMetadata( - new S3AFileStatus(123, 456, new Path(parent, "file1"), 8192, - TEST_OWNER, TEST_ETAG, TEST_VERSION_ID)); - List listing = Arrays.asList(pathMeta1, pathMeta2, pathMeta3); - return new DirListingMetadata(parent, listing, false); - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestDynamoDBMiscOperations.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestDynamoDBMiscOperations.java deleted file mode 100644 index 602a072aac2..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestDynamoDBMiscOperations.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.List; - -import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException; -import com.amazonaws.waiters.WaiterTimedOutException; -import org.junit.Test; - -import org.apache.hadoop.fs.PathIOException; -import org.apache.hadoop.fs.s3a.AWSClientIOException; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.test.HadoopTestBase; -import org.apache.hadoop.fs.Path; - -import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStoreTableManager.translateTableWaitFailure; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import static org.apache.hadoop.test.LambdaTestUtils.intercept; - -/** - * Unit test suite for misc dynamoDB metastore operations. - */ -public class TestDynamoDBMiscOperations extends HadoopTestBase { - - private static final String TIMEOUT_ERROR_MESSAGE - = "Table table-name did not transition into ACTIVE state."; - - @Test - public void testUnwrapTableWaitTimeout() throws Throwable { - final Exception waiterTimedOut = - new WaiterTimedOutException("waiter timed out"); - final AWSClientIOException ex = intercept(AWSClientIOException.class, - TIMEOUT_ERROR_MESSAGE, - () -> { - throw translateTableWaitFailure("example", - new IllegalArgumentException(TIMEOUT_ERROR_MESSAGE, - waiterTimedOut)); - }); - assertEquals(waiterTimedOut, ex.getCause()); - } - - @Test - public void testTranslateIllegalArgumentException() throws Throwable { - final IllegalArgumentException e = - new IllegalArgumentException(TIMEOUT_ERROR_MESSAGE); - final IOException ex = intercept(IOException.class, - TIMEOUT_ERROR_MESSAGE, - () -> { - throw translateTableWaitFailure("example", e); - }); - assertEquals(e, ex.getCause()); - } - - @Test - public void testTranslateWrappedDDBException() throws Throwable { - final Exception inner = new ResourceNotFoundException("ddb"); - final IllegalArgumentException e = - new IllegalArgumentException("outer", inner); - final FileNotFoundException ex = intercept(FileNotFoundException.class, - "outer", - () -> { - throw translateTableWaitFailure("example", e); - }); - assertEquals(inner, ex.getCause()); - } - - @Test - public void testTranslateWrappedOtherException() throws Throwable { - final Exception inner = new NullPointerException("npe"); - final IllegalArgumentException e = - new IllegalArgumentException("outer", inner); - final IOException ex = intercept(IOException.class, - "outer", - () -> { - throw translateTableWaitFailure("example", e); - }); - assertEquals(e, ex.getCause()); - } - - @Test - public void testInnerListChildrenDirectoryNpe() throws Exception { - DynamoDBMetadataStore ddbms = new DynamoDBMetadataStore(); - Path p = mock(Path.class); - List metas = mock(List.class); - - when(metas.isEmpty()).thenReturn(false); - DDBPathMetadata dirPathMeta = null; - - assertNull("The return value should be null.", - ddbms.getDirListingMetadataFromDirMetaAndList(p, metas, dirPathMeta)); - } - - @Test - public void testAncestorStateForDir() throws Throwable { - final DynamoDBMetadataStore.AncestorState ancestorState - = new DynamoDBMetadataStore.AncestorState( - null, BulkOperationState.OperationType.Rename, null); - - // path 1 is a directory - final Path path1 = new Path("s3a://bucket/1"); - final S3AFileStatus status1 = new S3AFileStatus(true, - path1, "hadoop"); - final DDBPathMetadata md1 = new DDBPathMetadata( - status1); - ancestorState.put(md1); - assertTrue("Status not found in ancestors", - ancestorState.contains(path1)); - final DDBPathMetadata result = ancestorState.get(path1); - assertEquals(status1, result.getFileStatus()); - assertTrue("Lookup failed", - ancestorState.findEntry(path1, true)); - final Path path2 = new Path("s3a://bucket/2"); - assertFalse("Lookup didn't fail", - ancestorState.findEntry(path2, true)); - assertFalse("Lookup didn't fail", - ancestorState.contains(path2)); - assertNull("Lookup didn't fail", - ancestorState.get(path2)); - } - - @Test - public void testAncestorStateForFile() throws Throwable { - final DynamoDBMetadataStore.AncestorState ancestorState - = new DynamoDBMetadataStore.AncestorState( - null, BulkOperationState.OperationType.Rename, null); - - // path 1 is a file - final Path path1 = new Path("s3a://bucket/1"); - final S3AFileStatus status1 = new S3AFileStatus( - 1024_1024_1024L, - 0, - path1, - 32_000_000, - "hadoop", - "e4", - "f5"); - final DDBPathMetadata md1 = new DDBPathMetadata( - status1); - ancestorState.put(md1); - assertTrue("Lookup failed", - ancestorState.findEntry(path1, false)); - intercept(PathIOException.class, - DynamoDBMetadataStore.E_INCONSISTENT_UPDATE, - () -> ancestorState.findEntry(path1, true)); - } - - @Test - public void testNoBulkRenameThroughInitiateBulkWrite() throws Throwable { - intercept(IllegalArgumentException.class, - () -> S3Guard.initiateBulkWrite(null, - BulkOperationState.OperationType.Rename, null)); - } - @Test - public void testInitiateBulkWrite() throws Throwable { - assertNull( - S3Guard.initiateBulkWrite(null, - BulkOperationState.OperationType.Put, null)); - } - -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestLocalMetadataStore.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestLocalMetadataStore.java deleted file mode 100644 index 8df61dbe1a8..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestLocalMetadataStore.java +++ /dev/null @@ -1,250 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.IOException; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -import org.apache.hadoop.thirdparty.com.google.common.base.Ticker; -import org.apache.hadoop.thirdparty.com.google.common.cache.Cache; -import org.apache.hadoop.thirdparty.com.google.common.cache.CacheBuilder; -import org.junit.Assume; -import org.junit.Test; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.S3ATestUtils; -import org.apache.hadoop.fs.s3a.Tristate; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * MetadataStore unit test for {@link LocalMetadataStore}. - */ -public class TestLocalMetadataStore extends MetadataStoreTestBase { - - - private final static class LocalMSContract extends AbstractMSContract { - - private FileSystem fs; - - private LocalMSContract() throws IOException { - this(new Configuration()); - } - - private LocalMSContract(Configuration config) throws IOException { - fs = FileSystem.getLocal(config); - } - - @Override - public FileSystem getFileSystem() { - return fs; - } - - @Override - public MetadataStore getMetadataStore() throws IOException { - LocalMetadataStore lms = new LocalMetadataStore(); - return lms; - } - } - - @Override - public AbstractMSContract createContract() throws IOException { - return new LocalMSContract(); - } - - @Override - public AbstractMSContract createContract(Configuration conf) throws - IOException { - return new LocalMSContract(conf); - } - - @Override protected String getPathStringForPrune(String path) - throws Exception{ - return path; - } - - @Test - public void testClearByAncestor() throws Exception { - Cache cache = CacheBuilder.newBuilder().build(); - - // 1. Test paths without scheme/host - assertClearResult(cache, "", "/", 0); - assertClearResult(cache, "", "/dirA/dirB", 2); - assertClearResult(cache, "", "/invalid", 5); - - - // 2. Test paths w/ scheme/host - String p = "s3a://fake-bucket-name"; - assertClearResult(cache, p, "/", 0); - assertClearResult(cache, p, "/dirA/dirB", 2); - assertClearResult(cache, p, "/invalid", 5); - } - - static class TestTicker extends Ticker { - private long myTicker = 0; - @Override public long read() { - return myTicker; - } - public void set(long val) { - this.myTicker = val; - } - - } - - /** - * Test that time eviction in cache used in {@link LocalMetadataStore} - * implementation working properly. - * - * The test creates a Ticker instance, which will be used to control the - * internal clock of the cache to achieve eviction without having to wait - * for the system clock. - * The test creates 3 entry: 2nd and 3rd entry will survive the eviction, - * because it will be created later than the 1st - using the ticker. - */ - @Test - public void testCacheTimedEvictionAfterWrite() { - TestTicker testTicker = new TestTicker(); - final long t0 = testTicker.read(); - final long t1 = t0 + 100; - final long t2 = t1 + 100; - - final long ttl = t1 + 50; // between t1 and t2 - - Cache cache = CacheBuilder.newBuilder() - .expireAfterWrite(ttl, - TimeUnit.NANOSECONDS /* nanos to avoid conversions */) - .ticker(testTicker) - .build(); - - String p = "s3a://fake-bucket-name"; - Path path1 = new Path(p + "/dirA/dirB/file1"); - Path path2 = new Path(p + "/dirA/dirB/file2"); - Path path3 = new Path(p + "/dirA/dirB/file3"); - - // Test time is t0 - populateEntry(cache, path1); - - // set new value on the ticker, so the next two entries will be added later - testTicker.set(t1); // Test time is now t1 - populateEntry(cache, path2); - populateEntry(cache, path3); - - assertEquals("Cache should contain 3 records before eviction", - 3, cache.size()); - LocalMetadataEntry pm1 = cache.getIfPresent(path1); - assertNotNull("PathMetadata should not be null before eviction", pm1); - - // set the ticker to a time when timed eviction should occur - // for the first entry - testTicker.set(t2); - - // call cleanup explicitly, as timed expiration is performed with - // periodic maintenance during writes and occasionally during reads only - cache.cleanUp(); - - assertEquals("Cache size should be 2 after eviction", 2, cache.size()); - pm1 = cache.getIfPresent(path1); - assertNull("PathMetadata should be null after eviction", pm1); - } - - - @Test - public void testUpdateParentLastUpdatedOnPutNewParent() throws Exception { - Assume.assumeTrue("This test only applies if metadatastore does not allow" - + " missing values (skip for NullMS).", !allowMissing()); - - ITtlTimeProvider tp = mock(ITtlTimeProvider.class); - ITtlTimeProvider originalTimeProvider = getTtlTimeProvider(); - - long now = 100L; - - final String parent = "/parentUpdated-" + UUID.randomUUID(); - final String child = parent + "/file1"; - - try { - when(tp.getNow()).thenReturn(now); - - // create a file - ms.put(new PathMetadata(makeFileStatus(child, 100), tp.getNow()), - null); - final PathMetadata fileMeta = ms.get(strToPath(child)); - assertEquals("lastUpdated field of first file should be equal to the " - + "mocked value", now, fileMeta.getLastUpdated()); - - final DirListingMetadata listing = ms.listChildren(strToPath(parent)); - assertEquals("Listing lastUpdated field should be equal to the mocked " - + "time value.", now, listing.getLastUpdated()); - - } finally { - ms.setTtlTimeProvider(originalTimeProvider); - } - - } - - - private static void populateMap(Cache cache, - String prefix) { - populateEntry(cache, new Path(prefix + "/dirA/dirB/")); - populateEntry(cache, new Path(prefix + "/dirA/dirB/dirC")); - populateEntry(cache, new Path(prefix + "/dirA/dirB/dirC/file1")); - populateEntry(cache, new Path(prefix + "/dirA/dirB/dirC/file2")); - populateEntry(cache, new Path(prefix + "/dirA/file1")); - } - - private static void populateEntry(Cache cache, - Path path) { - S3AFileStatus s3aStatus = new S3AFileStatus(Tristate.UNKNOWN, path, null); - cache.put(path, new LocalMetadataEntry(new PathMetadata(s3aStatus))); - } - - private static long sizeOfMap(Cache cache) { - return cache.asMap().values().stream() - .filter(entry -> !entry.getFileMeta().isDeleted()) - .count(); - } - - private static void assertClearResult(Cache cache, - String prefixStr, String pathStr, int leftoverSize) throws IOException { - populateMap(cache, prefixStr); - LocalMetadataStore.deleteEntryByAncestor(new Path(prefixStr + pathStr), - cache, true, getTtlTimeProvider()); - assertEquals(String.format("Cache should have %d entries", leftoverSize), - leftoverSize, sizeOfMap(cache)); - cache.invalidateAll(); - } - - @Override - protected void verifyFileStatus(FileStatus status, long size) { - S3ATestUtils.verifyFileStatus(status, size, REPLICATION, getModTime(), - getAccessTime(), - BLOCK_SIZE, OWNER, GROUP, PERMISSION); - } - - @Override - protected void verifyDirStatus(S3AFileStatus status) { - S3ATestUtils.verifyDirStatus(status, REPLICATION, OWNER); - } - -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestMetastoreChecking.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestMetastoreChecking.java new file mode 100644 index 00000000000..185ed024e46 --- /dev/null +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestMetastoreChecking.java @@ -0,0 +1,100 @@ +/* + * 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.hadoop.fs.s3a.s3guard; + +import java.net.URI; + +import org.assertj.core.api.Assertions; +import org.junit.Before; +import org.junit.Test; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.PathIOException; +import org.apache.hadoop.test.AbstractHadoopTestBase; + +import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_METASTORE_DYNAMO; +import static org.apache.hadoop.fs.s3a.Constants.S3GUARD_METASTORE_LOCAL; +import static org.apache.hadoop.fs.s3a.Constants.S3_METADATA_STORE_IMPL; +import static org.apache.hadoop.fs.s3a.s3guard.S3Guard.NULL_METADATA_STORE; +import static org.apache.hadoop.fs.s3a.s3guard.S3Guard.checkNoS3Guard; +import static org.apache.hadoop.test.LambdaTestUtils.intercept; + +/** + * Verify thqt the metastore checking + * is forgiving for local/null stores, and unforgiving for + * DDB or other bindings. + */ +@SuppressWarnings("deprecation") +public class TestMetastoreChecking extends AbstractHadoopTestBase { + + private final Path root = new Path("/"); + + private URI fsUri; + + private static final String BASE = "s3a://bucket"; + + @Before + public void setup() throws Exception { + fsUri = new URI(BASE +"/"); + } + + private Configuration chooseStore(String classname) { + Configuration conf = new Configuration(false); + if (classname != null) { + conf.set(S3_METADATA_STORE_IMPL, classname, "code"); + } + return conf; + } + + @Test + public void testNoClass() throws Throwable { + checkOutcome(null, false); + } + + @Test + public void testNullClass() throws Throwable { + checkOutcome(NULL_METADATA_STORE, true); + } + + @Test + public void testLocalStore() throws Throwable { + checkOutcome(S3GUARD_METASTORE_LOCAL, true); + } + + @Test + public void testDDBStore() throws Throwable { + intercept(PathIOException.class, () -> + checkOutcome(S3GUARD_METASTORE_DYNAMO, false)); + } + + @Test + public void testUnknownStore() throws Throwable { + intercept(PathIOException.class, "unknownStore", () -> + checkOutcome("unknownStore", false)); + } + + private void checkOutcome(final String classname, final boolean outcome) throws PathIOException { + Configuration conf = chooseStore(classname); + + Assertions.assertThat(checkNoS3Guard(fsUri, conf)) + .describedAs("check with classname %s", classname) + .isEqualTo(outcome); + } +} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestNullMetadataStore.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestNullMetadataStore.java deleted file mode 100644 index 2e0bc4b7e4f..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestNullMetadataStore.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileSystem; - -import java.io.IOException; - -/** - * Run MetadataStore unit tests on the NullMetadataStore implementation. - */ -public class TestNullMetadataStore extends MetadataStoreTestBase { - private static class NullMSContract extends AbstractMSContract { - @Override - public FileSystem getFileSystem() throws IOException { - Configuration config = new Configuration(); - return FileSystem.getLocal(config); - } - - @Override - public MetadataStore getMetadataStore() throws IOException { - return new NullMetadataStore(); - } - } - - /** This MetadataStore always says "I don't know, ask the backing store". */ - @Override - public boolean allowMissing() { - return true; - } - - @Override protected String getPathStringForPrune(String path) - throws Exception { - return path; - } - - @Override - public AbstractMSContract createContract() { - return new NullMSContract(); - } - - @Override - public AbstractMSContract createContract(Configuration conf) { - return createContract(); - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestObjectChangeDetectionAttributes.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestObjectChangeDetectionAttributes.java deleted file mode 100644 index f001262b36a..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestObjectChangeDetectionAttributes.java +++ /dev/null @@ -1,380 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; - -import com.amazonaws.services.s3.Headers; -import com.amazonaws.services.s3.model.CompleteMultipartUploadRequest; -import com.amazonaws.services.s3.model.CompleteMultipartUploadResult; -import com.amazonaws.services.s3.model.GetObjectMetadataRequest; -import com.amazonaws.services.s3.model.GetObjectRequest; -import com.amazonaws.services.s3.model.InitiateMultipartUploadRequest; -import com.amazonaws.services.s3.model.InitiateMultipartUploadResult; -import com.amazonaws.services.s3.model.ListObjectsV2Request; -import com.amazonaws.services.s3.model.ListObjectsV2Result; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.PutObjectRequest; -import com.amazonaws.services.s3.model.PutObjectResult; -import com.amazonaws.services.s3.model.S3Object; -import com.amazonaws.services.s3.model.UploadPartRequest; -import com.amazonaws.services.s3.model.UploadPartResult; -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import org.apache.commons.io.IOUtils; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FSDataInputStream; -import org.apache.hadoop.fs.FSDataOutputStream; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.AbstractS3AMockTest; -import org.apache.hadoop.fs.s3a.Constants; -import org.apache.hadoop.fs.s3a.S3AFileStatus; - -import static org.apache.hadoop.fs.s3a.Constants.CHANGE_DETECT_MODE; -import static org.apache.hadoop.fs.s3a.Constants.CHANGE_DETECT_MODE_SERVER; -import static org.apache.hadoop.fs.s3a.Constants.CHANGE_DETECT_SOURCE; -import static org.apache.hadoop.fs.s3a.Constants.CHANGE_DETECT_SOURCE_ETAG; -import static org.apache.hadoop.fs.s3a.Constants.CHANGE_DETECT_SOURCE_VERSION_ID; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; -import static org.mockito.hamcrest.MockitoHamcrest.argThat; - -/** - * Unit tests to ensure object eTag and versionId are captured on S3 PUT and - * used on GET. - * Further (integration) testing is performed in - * {@link org.apache.hadoop.fs.s3a.ITestS3ARemoteFileChanged}. - */ -@RunWith(Parameterized.class) -public class TestObjectChangeDetectionAttributes extends AbstractS3AMockTest { - private final String changeDetectionSource; - - public TestObjectChangeDetectionAttributes(String changeDetectionSource) { - this.changeDetectionSource = changeDetectionSource; - } - - @Parameterized.Parameters(name = "change={0}") - public static Collection params() { - return Arrays.asList(new Object[][]{ - {CHANGE_DETECT_SOURCE_ETAG}, - {CHANGE_DETECT_SOURCE_VERSION_ID} - }); - } - - @Override - public Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - conf.setClass(Constants.S3_METADATA_STORE_IMPL, - LocalMetadataStore.class, MetadataStore.class); - conf.set(CHANGE_DETECT_SOURCE, changeDetectionSource); - conf.set(CHANGE_DETECT_MODE, CHANGE_DETECT_MODE_SERVER); - return conf; - } - - /** - * Tests a file uploaded with a single PUT to ensure eTag is captured and used - * on file read. - */ - @Test - public void testCreateAndReadFileSinglePart() throws Exception { - String bucket = "s3a://mock-bucket/"; - String file = "single-part-file"; - Path path = new Path(bucket, file); - byte[] content = "content".getBytes(); - String eTag = "abc"; - String versionId = "def"; - - putObject(file, path, content, eTag, versionId); - - // make sure the eTag and versionId were put into the metadataStore - assertVersionAttributes(path, eTag, versionId); - - // Ensure underlying S3 getObject call uses the stored eTag or versionId - // when reading data back. If it doesn't, the read won't work and the - // assert will fail. - assertContent(file, path, content, eTag, versionId); - - // test overwrite - byte[] newConent = "newcontent".getBytes(); - String newETag = "newETag"; - String newVersionId = "newVersionId"; - - putObject(file, path, newConent, newETag, newVersionId); - assertVersionAttributes(path, newETag, newVersionId); - assertContent(file, path, newConent, newETag, newVersionId); - } - - /** - * Tests a file uploaded with multi-part upload to ensure eTag is captured - * and used on file read. - */ - @Test - public void testCreateAndReadFileMultiPart() throws Exception { - String bucket = "s3a://mock-bucket/"; - String file = "multi-part-file"; - Path path = new Path(bucket, file); - byte[] content = new byte[Constants.MULTIPART_MIN_SIZE + 1]; - String eTag = "abc"; - String versionId = "def"; - - multipartUpload(file, path, content, eTag, versionId); - - // make sure the eTag and versionId were put into the metadataStore - assertVersionAttributes(path, eTag, versionId); - - // Ensure underlying S3 getObject call uses the stored eTag or versionId - // when reading data back. If it doesn't, the read won't work and the - // assert will fail. - assertContent(file, path, content, eTag, versionId); - - // test overwrite - byte[] newContent = new byte[Constants.MULTIPART_MIN_SIZE + 1]; - Arrays.fill(newContent, (byte) 1); - String newETag = "newETag"; - String newVersionId = "newVersionId"; - - multipartUpload(file, path, newContent, newETag, newVersionId); - assertVersionAttributes(path, newETag, newVersionId); - assertContent(file, path, newContent, newETag, newVersionId); - } - - private void putObject(String file, Path path, byte[] content, - String eTag, String versionId) throws IOException { - PutObjectResult putObjectResult = new PutObjectResult(); - ObjectMetadata objectMetadata = new ObjectMetadata(); - objectMetadata.setContentLength(content.length); - putObjectResult.setMetadata(objectMetadata); - putObjectResult.setETag(eTag); - putObjectResult.setVersionId(versionId); - - when(s3.getObjectMetadata(any(GetObjectMetadataRequest.class))) - .thenThrow(NOT_FOUND); - when(s3.putObject(argThat(correctPutObjectRequest(file)))) - .thenReturn(putObjectResult); - ListObjectsV2Result emptyListing = new ListObjectsV2Result(); - when(s3.listObjectsV2(argThat(correctListObjectsRequest(file + "/")))) - .thenReturn(emptyListing); - - FSDataOutputStream outputStream = fs.create(path); - outputStream.write(content); - outputStream.close(); - } - - private void multipartUpload(String file, Path path, byte[] content, - String eTag, String versionId) throws IOException { - CompleteMultipartUploadResult uploadResult = - new CompleteMultipartUploadResult(); - uploadResult.setVersionId(versionId); - - when(s3.getObjectMetadata(any(GetObjectMetadataRequest.class))) - .thenThrow(NOT_FOUND); - - InitiateMultipartUploadResult initiateMultipartUploadResult = - new InitiateMultipartUploadResult(); - initiateMultipartUploadResult.setUploadId("uploadId"); - when(s3.initiateMultipartUpload( - argThat(correctInitiateMultipartUploadRequest(file)))) - .thenReturn(initiateMultipartUploadResult); - - UploadPartResult uploadPartResult = new UploadPartResult(); - uploadPartResult.setETag("partETag"); - when(s3.uploadPart(argThat(correctUploadPartRequest(file)))) - .thenReturn(uploadPartResult); - - CompleteMultipartUploadResult multipartUploadResult = - new CompleteMultipartUploadResult(); - multipartUploadResult.setETag(eTag); - multipartUploadResult.setVersionId(versionId); - when(s3.completeMultipartUpload( - argThat(correctMultipartUploadRequest(file)))) - .thenReturn(multipartUploadResult); - - ListObjectsV2Result emptyListing = new ListObjectsV2Result(); - when(s3.listObjectsV2(argThat(correctListObjectsRequest(file + "/")))) - .thenReturn(emptyListing); - - FSDataOutputStream outputStream = fs.create(path); - outputStream.write(content); - outputStream.close(); - } - - private void assertContent(String file, Path path, byte[] content, - String eTag, String versionId) throws IOException { - S3Object s3Object = new S3Object(); - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setHeader(Headers.S3_VERSION_ID, versionId); - metadata.setHeader(Headers.ETAG, eTag); - s3Object.setObjectMetadata(metadata); - s3Object.setObjectContent(new ByteArrayInputStream(content)); - when(s3.getObject(argThat(correctGetObjectRequest(file, eTag, versionId)))) - .thenReturn(s3Object); - FSDataInputStream inputStream = fs.open(path); - byte[] readContent = IOUtils.toByteArray(inputStream); - assertArrayEquals(content, readContent); - } - - private void assertVersionAttributes(Path path, String eTag, String versionId) - throws IOException { - MetadataStore metadataStore = fs.getMetadataStore(); - PathMetadata pathMetadata = metadataStore.get(path); - assertNotNull(pathMetadata); - S3AFileStatus fileStatus = pathMetadata.getFileStatus(); - assertEquals(eTag, fileStatus.getETag()); - assertEquals(versionId, fileStatus.getVersionId()); - } - - private Matcher correctGetObjectRequest(final String key, - final String eTag, final String versionId) { - return new BaseMatcher() { - @Override - public boolean matches(Object item) { - if (item instanceof GetObjectRequest) { - GetObjectRequest getObjectRequest = (GetObjectRequest) item; - if (getObjectRequest.getKey().equals(key)) { - if (changeDetectionSource.equals( - CHANGE_DETECT_SOURCE_ETAG)) { - return getObjectRequest.getMatchingETagConstraints() - .contains(eTag); - } else if (changeDetectionSource.equals( - CHANGE_DETECT_SOURCE_VERSION_ID)) { - return getObjectRequest.getVersionId().equals(versionId); - } - } - } - return false; - } - - @Override - public void describeTo(Description description) { - description.appendText("key and " - + changeDetectionSource - + " matches"); - } - }; - } - - private Matcher correctUploadPartRequest( - final String key) { - return new BaseMatcher() { - @Override - public boolean matches(Object item) { - if (item instanceof UploadPartRequest) { - UploadPartRequest request = (UploadPartRequest) item; - return request.getKey().equals(key); - } - return false; - } - - @Override - public void describeTo(Description description) { - description.appendText("key matches"); - } - }; - } - - private Matcher - correctInitiateMultipartUploadRequest(final String key) { - return new BaseMatcher() { - @Override - public void describeTo(Description description) { - description.appendText("key matches"); - } - - @Override - public boolean matches(Object item) { - if (item instanceof InitiateMultipartUploadRequest) { - InitiateMultipartUploadRequest request = - (InitiateMultipartUploadRequest) item; - return request.getKey().equals(key); - } - return false; - } - }; - } - - private Matcher - correctMultipartUploadRequest(final String key) { - return new BaseMatcher() { - @Override - public boolean matches(Object item) { - if (item instanceof CompleteMultipartUploadRequest) { - CompleteMultipartUploadRequest request = - (CompleteMultipartUploadRequest) item; - return request.getKey().equals(key); - } - return false; - } - - @Override - public void describeTo(Description description) { - description.appendText("key matches"); - } - }; - } - - private Matcher correctListObjectsRequest( - final String key) { - return new BaseMatcher() { - @Override - public boolean matches(Object item) { - if (item instanceof ListObjectsV2Request) { - ListObjectsV2Request listObjectsRequest = - (ListObjectsV2Request) item; - return listObjectsRequest.getPrefix().equals(key); - } - return false; - } - - @Override - public void describeTo(Description description) { - description.appendText("key matches"); - } - }; - } - - private Matcher correctPutObjectRequest( - final String key) { - return new BaseMatcher() { - @Override - public boolean matches(Object item) { - if (item instanceof PutObjectRequest) { - PutObjectRequest putObjectRequest = (PutObjectRequest) item; - return putObjectRequest.getKey().equals(key); - } - return false; - } - - @Override - public void describeTo(Description description) { - description.appendText("key matches"); - } - }; - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestPathMetadataDynamoDBTranslation.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestPathMetadataDynamoDBTranslation.java deleted file mode 100644 index f2d27f333b0..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestPathMetadataDynamoDBTranslation.java +++ /dev/null @@ -1,361 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.IOException; -import java.net.URI; -import java.util.Arrays; -import java.util.Collection; -import java.util.concurrent.Callable; - -import com.amazonaws.services.dynamodbv2.document.Item; -import com.amazonaws.services.dynamodbv2.document.KeyAttribute; -import com.amazonaws.services.dynamodbv2.document.PrimaryKey; -import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; -import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; -import org.apache.hadoop.thirdparty.com.google.common.base.Preconditions; -import org.junit.After; -import org.junit.Assert; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.Timeout; - -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.security.UserGroupInformation; -import org.apache.hadoop.test.LambdaTestUtils; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.mockito.Mockito; - -import static com.amazonaws.services.dynamodbv2.model.KeyType.HASH; -import static com.amazonaws.services.dynamodbv2.model.KeyType.RANGE; -import static com.amazonaws.services.dynamodbv2.model.ScalarAttributeType.S; -import static org.hamcrest.CoreMatchers.anyOf; -import static org.hamcrest.CoreMatchers.is; - -import static org.apache.hadoop.fs.s3a.s3guard.PathMetadataDynamoDBTranslation.*; -import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore.VERSION_MARKER_ITEM_NAME; -import static org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore.VERSION; -import static org.mockito.Mockito.never; - -/** - * Test the PathMetadataDynamoDBTranslation is able to translate between domain - * model objects and DynamoDB items. - */ -@RunWith(Parameterized.class) -public class TestPathMetadataDynamoDBTranslation extends Assert { - - private static final Path TEST_DIR_PATH = new Path("s3a://test-bucket/myDir"); - private static final long TEST_FILE_LENGTH = 100; - private static final long TEST_MOD_TIME = 9999; - private static final long TEST_BLOCK_SIZE = 128; - private static final String TEST_ETAG = "abc"; - private static final String TEST_VERSION_ID = "def"; - private static final Path TEST_FILE_PATH = new Path(TEST_DIR_PATH, "myFile"); - - private final Item testFileItem; - private final DDBPathMetadata testFilePathMetadata; - private final Item testDirItem; - private final DDBPathMetadata testDirPathMetadata; - - @Parameterized.Parameters - public static Collection params() throws IOException { - String username = UserGroupInformation.getCurrentUser().getShortUserName(); - - return Arrays.asList(new Object[][]{ - // with etag and versionId - { - new Item() - .withPrimaryKey(PARENT, - pathToParentKey(TEST_FILE_PATH.getParent()), - CHILD, TEST_FILE_PATH.getName()) - .withBoolean(IS_DIR, false) - .withLong(FILE_LENGTH, TEST_FILE_LENGTH) - .withLong(MOD_TIME, TEST_MOD_TIME) - .withLong(BLOCK_SIZE, TEST_BLOCK_SIZE) - .withString(ETAG, TEST_ETAG) - .withString(VERSION_ID, TEST_VERSION_ID), - new DDBPathMetadata( - new S3AFileStatus(TEST_FILE_LENGTH, TEST_MOD_TIME, - TEST_FILE_PATH, TEST_BLOCK_SIZE, username, TEST_ETAG, - TEST_VERSION_ID)) - }, - // without etag or versionId - { - new Item() - .withPrimaryKey(PARENT, - pathToParentKey(TEST_FILE_PATH.getParent()), - CHILD, TEST_FILE_PATH.getName()) - .withBoolean(IS_DIR, false) - .withLong(FILE_LENGTH, TEST_FILE_LENGTH) - .withLong(MOD_TIME, TEST_MOD_TIME) - .withLong(BLOCK_SIZE, TEST_BLOCK_SIZE), - new DDBPathMetadata( - new S3AFileStatus(TEST_FILE_LENGTH, TEST_MOD_TIME, - TEST_FILE_PATH, TEST_BLOCK_SIZE, username, null, null)) - } - }); - } - - public TestPathMetadataDynamoDBTranslation(Item item, - DDBPathMetadata metadata) throws IOException { - testFileItem = item; - testFilePathMetadata = metadata; - - String username = UserGroupInformation.getCurrentUser().getShortUserName(); - - testDirPathMetadata = new DDBPathMetadata(new S3AFileStatus(false, - TEST_DIR_PATH, username)); - - testDirItem = new Item(); - testDirItem - .withPrimaryKey(PARENT, "/test-bucket", CHILD, TEST_DIR_PATH.getName()) - .withBoolean(IS_DIR, true); - } - - /** - * It should not take long time as it doesn't involve remote server operation. - */ - @Rule - public final Timeout timeout = new Timeout(30 * 1000); - - @Test - public void testKeySchema() { - final Collection keySchema = - PathMetadataDynamoDBTranslation.keySchema(); - assertNotNull(keySchema); - assertEquals("There should be HASH and RANGE key in key schema", - 2, keySchema.size()); - for (KeySchemaElement element : keySchema) { - assertThat(element.getAttributeName(), anyOf(is(PARENT), is(CHILD))); - assertThat(element.getKeyType(), - anyOf(is(HASH.toString()), is(RANGE.toString()))); - } - } - - @After - public void tearDown() { - PathMetadataDynamoDBTranslation.IGNORED_FIELDS.clear(); - } - - @Test - public void testAttributeDefinitions() { - final Collection attrs = - PathMetadataDynamoDBTranslation.attributeDefinitions(); - assertNotNull(attrs); - assertEquals("There should be HASH and RANGE attributes", 2, attrs.size()); - for (AttributeDefinition definition : attrs) { - assertThat(definition.getAttributeName(), anyOf(is(PARENT), is(CHILD))); - assertEquals(S.toString(), definition.getAttributeType()); - } - } - - @Test - public void testItemToPathMetadata() throws IOException { - final String user = - UserGroupInformation.getCurrentUser().getShortUserName(); - assertNull(itemToPathMetadata(null, user)); - - verify(testDirItem, itemToPathMetadata(testDirItem, user)); - verify(testFileItem, itemToPathMetadata(testFileItem, user)); - } - - /** - * Verify that the Item and PathMetadata objects hold the same information. - */ - private static void verify(Item item, PathMetadata meta) { - assertNotNull(meta); - final S3AFileStatus status = meta.getFileStatus(); - final Path path = status.getPath(); - assertEquals(item.get(PARENT), pathToParentKey(path.getParent())); - assertEquals(item.get(CHILD), path.getName()); - boolean isDir = item.hasAttribute(IS_DIR) && item.getBoolean(IS_DIR); - assertEquals(isDir, status.isDirectory()); - long len = item.hasAttribute(FILE_LENGTH) ? item.getLong(FILE_LENGTH) : 0; - assertEquals(len, status.getLen()); - long bSize = item.hasAttribute(BLOCK_SIZE) ? item.getLong(BLOCK_SIZE) : 0; - assertEquals(bSize, status.getBlockSize()); - String eTag = item.getString(ETAG); - assertEquals(eTag, status.getETag()); - String versionId = item.getString(VERSION_ID); - assertEquals(versionId, status.getVersionId()); - - /* - * S3AFileStatue#getModificationTime() reports the current time, so the - * following assertion is failing. - * - * long modTime = item.hasAttribute(MOD_TIME) ? item.getLong(MOD_TIME) : 0; - * assertEquals(modTime, status.getModificationTime()); - */ - } - - @Test - public void testPathMetadataToItem() { - verify(pathMetadataToItem(testDirPathMetadata), testDirPathMetadata); - verify(pathMetadataToItem(testFilePathMetadata), - testFilePathMetadata); - } - - @Test - public void testPathToParentKeyAttribute() { - doTestPathToParentKeyAttribute(TEST_DIR_PATH); - doTestPathToParentKeyAttribute(TEST_FILE_PATH); - } - - private static void doTestPathToParentKeyAttribute(Path path) { - final KeyAttribute attr = pathToParentKeyAttribute(path); - assertNotNull(attr); - assertEquals(PARENT, attr.getName()); - // this path is expected as parent filed - assertEquals(pathToParentKey(path), attr.getValue()); - } - - private static String pathToParentKey(Path p) { - Preconditions.checkArgument(p.isUriPathAbsolute()); - URI parentUri = p.toUri(); - String bucket = parentUri.getHost(); - Preconditions.checkNotNull(bucket); - String s = "/" + bucket + parentUri.getPath(); - // strip trailing slash - if (s.endsWith("/")) { - s = s.substring(0, s.length()-1); - } - return s; - } - - @Test - public void testPathToKey() throws Exception { - LambdaTestUtils.intercept(IllegalArgumentException.class, - () -> pathToKey(new Path("/"))); - doTestPathToKey(TEST_DIR_PATH); - doTestPathToKey(TEST_FILE_PATH); - } - - @Test - public void testPathToKeyTrailing() throws Exception { - doTestPathToKey(new Path("s3a://test-bucket/myDir/trailing//")); - doTestPathToKey(new Path("s3a://test-bucket/myDir/trailing/")); - LambdaTestUtils.intercept(IllegalArgumentException.class, - () -> pathToKey(new Path("s3a://test-bucket//"))); - } - - private static void doTestPathToKey(Path path) { - final PrimaryKey key = pathToKey(path); - assertNotNull(key); - assertEquals("There should be both HASH and RANGE keys", - 2, key.getComponents().size()); - - for (KeyAttribute keyAttribute : key.getComponents()) { - assertThat(keyAttribute.getName(), anyOf(is(PARENT), is(CHILD))); - if (PARENT.equals(keyAttribute.getName())) { - assertEquals(pathToParentKey(path.getParent()), - keyAttribute.getValue()); - } else { - assertEquals(path.getName(), keyAttribute.getValue()); - } - } - } - - @Test - public void testVersionRoundTrip() throws Throwable { - final Item marker = createVersionMarker(VERSION_MARKER_ITEM_NAME, VERSION, 0); - assertEquals("Extracted version from " + marker, - VERSION, extractVersionFromMarker(marker)); - } - - @Test - public void testVersionMarkerNotStatusIllegalPath() throws Throwable { - final Item marker = createVersionMarker(VERSION_MARKER_ITEM_NAME, VERSION, 0); - assertNull("Path metadata fromfrom " + marker, - itemToPathMetadata(marker, "alice")); - } - - /** - * Test when translating an {@link Item} to {@link DDBPathMetadata} works - * if {@code IS_AUTHORITATIVE} flag is ignored. - */ - @Test - public void testIsAuthoritativeCompatibilityItemToPathMetadata() - throws Exception { - Item item = Mockito.spy(testDirItem); - item.withBoolean(IS_AUTHORITATIVE, true); - PathMetadataDynamoDBTranslation.IGNORED_FIELDS.add(IS_AUTHORITATIVE); - - final String user = - UserGroupInformation.getCurrentUser().getShortUserName(); - DDBPathMetadata meta = itemToPathMetadata(item, user); - - Mockito.verify(item, Mockito.never()).getBoolean(IS_AUTHORITATIVE); - assertFalse(meta.isAuthoritativeDir()); - } - - /** - * Test when translating an {@link DDBPathMetadata} to {@link Item} works - * if {@code IS_AUTHORITATIVE} flag is ignored. - */ - @Test - public void testIsAuthoritativeCompatibilityPathMetadataToItem() { - DDBPathMetadata meta = Mockito.spy(testFilePathMetadata); - meta.setAuthoritativeDir(true); - PathMetadataDynamoDBTranslation.IGNORED_FIELDS.add(IS_AUTHORITATIVE); - - Item item = pathMetadataToItem(meta); - - Mockito.verify(meta, never()).isAuthoritativeDir(); - assertFalse(item.hasAttribute(IS_AUTHORITATIVE)); - } - - - /** - * Test when translating an {@link Item} to {@link DDBPathMetadata} works - * if {@code LAST_UPDATED} flag is ignored. - */ - @Test - public void testIsLastUpdatedCompatibilityItemToPathMetadata() - throws Exception { - Item item = Mockito.spy(testDirItem); - item.withLong(LAST_UPDATED, 100); - PathMetadataDynamoDBTranslation.IGNORED_FIELDS.add(LAST_UPDATED); - - final String user = - UserGroupInformation.getCurrentUser().getShortUserName(); - DDBPathMetadata meta = itemToPathMetadata(item, user); - - Mockito.verify(item, Mockito.never()).getLong(LAST_UPDATED); - assertFalse(meta.isAuthoritativeDir()); - } - - /** - * Test when translating an {@link DDBPathMetadata} to {@link Item} works - * if {@code LAST_UPDATED} flag is ignored. - */ - @Test - public void testIsLastUpdatedCompatibilityPathMetadataToItem() { - DDBPathMetadata meta = Mockito.spy(testFilePathMetadata); - meta.setLastUpdated(100); - PathMetadataDynamoDBTranslation.IGNORED_FIELDS.add(LAST_UPDATED); - - Item item = pathMetadataToItem(meta); - - Mockito.verify(meta, never()).getLastUpdated(); - assertFalse(item.hasAttribute(LAST_UPDATED)); - } - -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestPathOrderComparators.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestPathOrderComparators.java deleted file mode 100644 index 9b8e5918efe..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestPathOrderComparators.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.util.Comparator; -import java.util.List; - -import org.junit.Test; - -import org.apache.hadoop.fs.Path; - -import static org.apache.hadoop.thirdparty.com.google.common.collect.Lists.newArrayList; -import static org.apache.hadoop.fs.s3a.s3guard.PathOrderComparators.TOPMOST_PATH_FIRST; -import static org.apache.hadoop.fs.s3a.s3guard.PathOrderComparators.TOPMOST_PATH_LAST; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertEquals; - -/** - * Test ordering of paths with the comparator matches requirements. - */ -public class TestPathOrderComparators { - - private static final Path ROOT = new Path("s3a://bucket/"); - - public static final Path DIR_A = new Path(ROOT, "dirA"); - - public static final Path DIR_B = new Path(ROOT, "dirB"); - - public static final Path DIR_A_FILE_1 = new Path(DIR_A, "file1"); - - public static final Path DIR_A_FILE_2 = new Path(DIR_A, "file2"); - - public static final Path DIR_B_FILE_3 = new Path(DIR_B, "file3"); - - public static final Path DIR_B_FILE_4 = new Path(DIR_B, "file4"); - - @Test - public void testRootEqual() throws Throwable { - assertComparesEqual(ROOT, ROOT); - } - - @Test - public void testRootFirst() throws Throwable { - assertComparesTopmost(ROOT, DIR_A_FILE_1); - } - - @Test - public void testDirOrdering() throws Throwable { - assertComparesTopmost(DIR_A, DIR_B); - } - - @Test - public void testFilesEqual() throws Throwable { - assertComparesEqual(DIR_A_FILE_1, DIR_A_FILE_1); - } - - @Test - public void testFilesInSameDir() throws Throwable { - assertComparesTopmost(ROOT, DIR_A_FILE_1); - assertComparesTopmost(DIR_A, DIR_A_FILE_1); - assertComparesTopmost(DIR_A, DIR_A_FILE_2); - assertComparesTopmost(DIR_A_FILE_1, DIR_A_FILE_2); - } - - @Test - public void testReversedFiles() throws Throwable { - assertReverseOrder(DIR_A_FILE_1, ROOT); - assertReverseOrder(DIR_A_FILE_1, DIR_A); - assertReverseOrder(DIR_A_FILE_2, DIR_A); - assertReverseOrder(DIR_A_FILE_2, DIR_A_FILE_1); - } - - @Test - public void testFilesAndDifferentShallowDir() throws Throwable { - assertComparesTopmost(DIR_B, DIR_A_FILE_1); - assertComparesTopmost(DIR_A, DIR_B_FILE_3); - } - - @Test - public void testOrderRoot() throws Throwable { - verifySorted(ROOT); - } - - @Test - public void testOrderRootDirs() throws Throwable { - verifySorted(ROOT, DIR_A, DIR_B); - } - - @Test - public void testOrderRootDirsAndFiles() throws Throwable { - verifySorted(ROOT, DIR_A, DIR_B, DIR_A_FILE_1, DIR_A_FILE_2); - } - - @Test - public void testOrderRootDirsAndAllFiles() throws Throwable { - verifySorted(ROOT, DIR_A, DIR_B, - DIR_A_FILE_1, DIR_A_FILE_2, - DIR_B_FILE_3, DIR_B_FILE_4); - } - - @Test - public void testSortOrderConstant() throws Throwable { - List sort1 = verifySorted(ROOT, DIR_A, DIR_B, - DIR_A_FILE_1, DIR_A_FILE_2, - DIR_B_FILE_3, DIR_B_FILE_4); - List sort2 = newArrayList(sort1); - assertSortsTo(sort2, sort1, true); - } - - @Test - public void testSortReverse() throws Throwable { - List sort1 = newArrayList( - ROOT, - DIR_A, - DIR_B, - DIR_A_FILE_1, - DIR_A_FILE_2, - DIR_B_FILE_3, - DIR_B_FILE_4); - List expected = newArrayList( - DIR_B_FILE_4, - DIR_B_FILE_3, - DIR_A_FILE_2, - DIR_A_FILE_1, - DIR_B, - DIR_A, - ROOT); - assertSortsTo(expected, sort1, false); - } - - - private List verifySorted(Path... paths) { - List original = newArrayList(paths); - List sorted = newArrayList(paths); - assertSortsTo(original, sorted, true); - return sorted; - } - - private void assertSortsTo( - final List original, - final List sorted, - boolean topmost) { - sorted.sort(topmost ? TOPMOST_PATH_FIRST : TOPMOST_PATH_LAST); - assertThat(sorted) - .as("Sorted paths") - .containsExactlyElementsOf(original); - } - - private void assertComparesEqual(Path l, Path r) { - assertOrder(0, l, r); - } - - private void assertComparesTopmost(Path l, Path r) { - assertOrder(-1, l, r); - assertOrder(1, r, l); - } - - private void assertReverseOrder(Path l, Path r) { - assertComparesTo(-1, TOPMOST_PATH_LAST, l, r); - assertComparesTo(1, TOPMOST_PATH_LAST, r, l); - } - - private void assertOrder(int res, - Path l, Path r) { - assertComparesTo(res, TOPMOST_PATH_FIRST, l, r); - } - - private void assertComparesTo(final int expected, - final Comparator comparator, - final Path l, final Path r) { - int actual = comparator.compare(l, r); - if (actual < -1) { - actual = -1; - } - if (actual > 1) { - actual = 1; - } - assertEquals("Comparing " + l + " to " + r, - expected, actual); - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestS3Guard.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestS3Guard.java deleted file mode 100644 index eaa363bbf19..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestS3Guard.java +++ /dev/null @@ -1,526 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -import org.assertj.core.api.Assertions; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileStatus; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.RemoteIterator; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.S3AUtils; -import org.apache.hadoop.fs.s3a.Tristate; -import org.apache.hadoop.service.launcher.LauncherExitCodes; -import org.apache.hadoop.test.LambdaTestUtils; -import org.apache.hadoop.util.ExitUtil; - -import static org.apache.hadoop.fs.s3a.Constants.DEFAULT_METADATASTORE_METADATA_TTL; -import static org.apache.hadoop.fs.s3a.Constants.METADATASTORE_METADATA_TTL; -import static org.apache.hadoop.fs.s3a.Listing.toProvidedFileStatusIterator; -import static org.apache.hadoop.fs.s3a.s3guard.S3Guard.dirMetaToStatuses; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * Tests for the {@link S3Guard} utility class. - */ -public class TestS3Guard extends Assert { - - public static final String MS_FILE_1 = "s3a://bucket/dir/ms-file1"; - - public static final String MS_FILE_2 = "s3a://bucket/dir/ms-file2"; - - public static final String S3_FILE_3 = "s3a://bucket/dir/s3-file3"; - - public static final String S3_DIR_4 = "s3a://bucket/dir/s3-dir4"; - - public static final Path DIR_PATH = new Path("s3a://bucket/dir"); - - private MetadataStore ms; - - private ITtlTimeProvider timeProvider; - - @Before - public void setUp() throws Exception { - final Configuration conf = new Configuration(false); - ms = new LocalMetadataStore(); - ms.initialize(conf, new S3Guard.TtlTimeProvider(conf)); - timeProvider = new S3Guard.TtlTimeProvider( - DEFAULT_METADATASTORE_METADATA_TTL); - } - - @After - public void tearDown() throws Exception { - if (ms != null) { - ms.destroy(); - } - } - - /** - * Basic test to ensure results from S3 and MetadataStore are merged - * correctly. - */ - @Test - public void testDirListingUnionNonauth() throws Exception { - - // Two files in metadata store listing - PathMetadata m1 = makePathMeta(MS_FILE_1, false); - PathMetadata m2 = makePathMeta(MS_FILE_2, false); - DirListingMetadata dirMeta = new DirListingMetadata(DIR_PATH, - Arrays.asList(m1, m2), false); - - // Two other entries in s3 - final S3AFileStatus s1Status = makeFileStatus(S3_FILE_3, false); - final S3AFileStatus s2Status = makeFileStatus(S3_DIR_4, true); - List s3Listing = Arrays.asList( - s1Status, - s2Status); - RemoteIterator storeItr = toProvidedFileStatusIterator( - s3Listing.toArray(new S3AFileStatus[0])); - RemoteIterator resultItr = S3Guard.dirListingUnion( - ms, DIR_PATH, storeItr, dirMeta, false, - timeProvider, s3AFileStatuses -> - toProvidedFileStatusIterator(dirMetaToStatuses(dirMeta))); - S3AFileStatus[] result = S3AUtils.iteratorToStatuses( - resultItr, new HashSet<>()); - - assertEquals("listing length", 4, result.length); - assertContainsPaths(result, MS_FILE_1, MS_FILE_2, S3_FILE_3, S3_DIR_4); - - // check the MS doesn't contain the s3 entries as nonauth - // unions should block them - assertNoRecord(ms, S3_FILE_3); - assertNoRecord(ms, S3_DIR_4); - - // for entries which do exist, when updated in S3, the metastore is updated - S3AFileStatus f1Status2 = new S3AFileStatus( - 200, System.currentTimeMillis(), new Path(MS_FILE_1), - 1, null, "tag2", "ver2"); - S3AFileStatus[] f1Statuses = new S3AFileStatus[1]; - f1Statuses[0] = f1Status2; - RemoteIterator itr = toProvidedFileStatusIterator( - f1Statuses); - FileStatus[] result2 = S3AUtils.iteratorToStatuses( - S3Guard.dirListingUnion( - ms, DIR_PATH, itr, dirMeta, - false, timeProvider, - s3AFileStatuses -> - toProvidedFileStatusIterator( - dirMetaToStatuses(dirMeta))), - new HashSet<>()); - // the listing returns the new status - Assertions.assertThat(find(result2, MS_FILE_1)) - .describedAs("Entry in listing results for %s", MS_FILE_1) - .isSameAs(f1Status2); - // as does a query of the MS - final PathMetadata updatedMD = verifyRecord(ms, MS_FILE_1); - Assertions.assertThat(updatedMD.getFileStatus()) - .describedAs("Entry in metastore for %s: %s", MS_FILE_1, updatedMD) - .isEqualTo(f1Status2); - } - - /** - * Auth mode unions are different. - */ - @Test - public void testDirListingUnionAuth() throws Exception { - - // Two files in metadata store listing - PathMetadata m1 = makePathMeta(MS_FILE_1, false); - PathMetadata m2 = makePathMeta(MS_FILE_2, false); - DirListingMetadata dirMeta = new DirListingMetadata(DIR_PATH, - Arrays.asList(m1, m2), true); - - // Two other entries in s3 - S3AFileStatus s1Status = makeFileStatus(S3_FILE_3, false); - S3AFileStatus s2Status = makeFileStatus(S3_DIR_4, true); - List s3Listing = Arrays.asList( - s1Status, - s2Status); - - ITtlTimeProvider timeProvider = new S3Guard.TtlTimeProvider( - DEFAULT_METADATASTORE_METADATA_TTL); - - RemoteIterator storeItr = toProvidedFileStatusIterator( - s3Listing.toArray(new S3AFileStatus[0])); - RemoteIterator resultItr = S3Guard - .dirListingUnion(ms, DIR_PATH, storeItr, dirMeta, - true, timeProvider, - s3AFileStatuses -> - toProvidedFileStatusIterator( - dirMetaToStatuses(dirMeta))); - - S3AFileStatus[] result = S3AUtils.iteratorToStatuses( - resultItr, new HashSet<>()); - assertEquals("listing length", 4, result.length); - assertContainsPaths(result, MS_FILE_1, MS_FILE_2, S3_FILE_3, S3_DIR_4); - - // now verify an auth scan added the records - PathMetadata file3Meta = verifyRecord(ms, S3_FILE_3); - PathMetadata dir4Meta = verifyRecord(ms, S3_DIR_4); - - // we can't check auth flag handling because local FS doesn't have one - // so do just check the dir status still all good. - Assertions.assertThat(dir4Meta) - .describedAs("Metastore entry for dir %s", dir4Meta) - .matches(m -> m.getFileStatus().isDirectory()); - - DirListingMetadata dirMeta2 = new DirListingMetadata(DIR_PATH, - Arrays.asList(m1, m2, file3Meta, dir4Meta), true); - // now s1 status is updated on S3 - S3AFileStatus s1Status2 = new S3AFileStatus( - 200, System.currentTimeMillis(), new Path(S3_FILE_3), - 1, null, "tag2", "ver2"); - S3AFileStatus[] f1Statuses = new S3AFileStatus[1]; - f1Statuses[0] = s1Status2; - RemoteIterator itr = - toProvidedFileStatusIterator(f1Statuses); - FileStatus[] result2 = S3AUtils.iteratorToStatuses( - S3Guard.dirListingUnion(ms, DIR_PATH, itr, dirMeta, - true, timeProvider, - s3AFileStatuses -> - toProvidedFileStatusIterator( - dirMetaToStatuses(dirMeta))), - new HashSet<>()); - - // but the result of the listing contains the old entry - // because auth mode doesn't pick up changes in S3 which - // didn't go through s3guard - Assertions.assertThat(find(result2, S3_FILE_3)) - .describedAs("Entry in listing results for %s", S3_FILE_3) - .isSameAs(file3Meta.getFileStatus()); - } - - /** - * Assert there is no record in the store. - * @param ms metastore - * @param path path - * @throws IOException IOError - */ - private void assertNoRecord(MetadataStore ms, String path) - throws IOException { - Assertions.assertThat(lookup(ms, path)) - .describedAs("Metastore entry for %s", path) - .isNull(); - } - - /** - * Assert there is arecord in the store, then return it. - * @param ms metastore - * @param path path - * @return the record. - * @throws IOException IO Error - */ - private PathMetadata verifyRecord(MetadataStore ms, String path) - throws IOException { - final PathMetadata md = lookup(ms, path); - Assertions.assertThat(md) - .describedAs("Metastore entry for %s", path) - .isNotNull(); - return md; - } - - /** - * Look up a record. - * @param ms store - * @param path path - * @return the record or null - * @throws IOException IO Error - */ - private PathMetadata lookup(final MetadataStore ms, final String path) - throws IOException { - return ms.get(new Path(path)); - } - - @Test - public void testPutWithTtlDirListingMeta() throws Exception { - // arrange - DirListingMetadata dlm = new DirListingMetadata(new Path("/"), null, - false); - MetadataStore ms = spy(MetadataStore.class); - ITtlTimeProvider timeProvider = - mock(ITtlTimeProvider.class); - when(timeProvider.getNow()).thenReturn(100L); - - // act - S3Guard.putWithTtl(ms, dlm, Collections.emptyList(), timeProvider, null); - - // assert - assertEquals("last update in " + dlm, 100L, dlm.getLastUpdated()); - verify(timeProvider, times(1)).getNow(); - verify(ms, times(1)).put(dlm, Collections.emptyList(), null); - } - - @Test - public void testPutWithTtlFileMeta() throws Exception { - // arrange - S3AFileStatus fileStatus = mock(S3AFileStatus.class); - when(fileStatus.getPath()).thenReturn(new Path("/")); - PathMetadata pm = new PathMetadata(fileStatus); - MetadataStore ms = spy(MetadataStore.class); - ITtlTimeProvider timeProvider = - mock(ITtlTimeProvider.class); - when(timeProvider.getNow()).thenReturn(100L); - - // act - S3Guard.putWithTtl(ms, pm, timeProvider, null); - - // assert - assertEquals("last update in " + pm, 100L, pm.getLastUpdated()); - verify(timeProvider, times(1)).getNow(); - verify(ms, times(1)).put(pm, null); - } - - @Test - public void testPutWithTtlCollection() throws Exception { - // arrange - S3AFileStatus fileStatus = mock(S3AFileStatus.class); - when(fileStatus.getPath()).thenReturn(new Path("/")); - Collection pmCollection = new ArrayList<>(); - for (int i = 0; i < 10; i++) { - pmCollection.add(new PathMetadata(fileStatus)); - } - MetadataStore ms = spy(MetadataStore.class); - ITtlTimeProvider timeProvider = - mock(ITtlTimeProvider.class); - when(timeProvider.getNow()).thenReturn(100L); - - // act - S3Guard.putWithTtl(ms, pmCollection, timeProvider, null); - - // assert - pmCollection.forEach( - pm -> assertEquals(100L, pm.getLastUpdated()) - ); - verify(timeProvider, times(1)).getNow(); - verify(ms, times(1)).put(pmCollection, null); - } - - @Test - public void testGetWithTtlExpired() throws Exception { - // arrange - S3AFileStatus fileStatus = mock(S3AFileStatus.class); - Path path = new Path("/file"); - when(fileStatus.getPath()).thenReturn(path); - PathMetadata pm = new PathMetadata(fileStatus); - pm.setLastUpdated(100L); - - MetadataStore ms = mock(MetadataStore.class); - when(ms.get(path, false)).thenReturn(pm); - - ITtlTimeProvider timeProvider = - mock(ITtlTimeProvider.class); - when(timeProvider.getNow()).thenReturn(101L); - when(timeProvider.getMetadataTtl()).thenReturn(1L); - - // act - final PathMetadata pmExpired = S3Guard.getWithTtl(ms, path, timeProvider, - false, false); - - // assert - assertNull(pmExpired); - } - - @Test - public void testGetWithTtlNotExpired() throws Exception { - // arrange - S3AFileStatus fileStatus = mock(S3AFileStatus.class); - Path path = new Path("/file"); - when(fileStatus.getPath()).thenReturn(path); - PathMetadata pm = new PathMetadata(fileStatus); - pm.setLastUpdated(100L); - - MetadataStore ms = mock(MetadataStore.class); - when(ms.get(path, false)).thenReturn(pm); - - ITtlTimeProvider timeProvider = - mock(ITtlTimeProvider.class); - when(timeProvider.getNow()).thenReturn(101L); - when(timeProvider.getMetadataTtl()).thenReturn(2L); - - // act - final PathMetadata pmNotExpired = - S3Guard.getWithTtl(ms, path, timeProvider, false, false); - - // assert - assertNotNull(pmNotExpired); - } - - @Test - public void testGetWithZeroLastUpdatedNotExpired() throws Exception { - // arrange - S3AFileStatus fileStatus = mock(S3AFileStatus.class); - Path path = new Path("/file"); - when(fileStatus.getPath()).thenReturn(path); - PathMetadata pm = new PathMetadata(fileStatus); - // we set 0 this time as the last updated: can happen eg. when we use an - // old dynamo table - pm.setLastUpdated(0L); - - MetadataStore ms = mock(MetadataStore.class); - when(ms.get(path, false)).thenReturn(pm); - - ITtlTimeProvider timeProvider = - mock(ITtlTimeProvider.class); - when(timeProvider.getNow()).thenReturn(101L); - when(timeProvider.getMetadataTtl()).thenReturn(2L); - - // act - final PathMetadata pmExpired = S3Guard.getWithTtl(ms, path, timeProvider, - false, false); - - // assert - assertNotNull(pmExpired); - } - - - /** - * Makes sure that all uses of TTL timeouts use a consistent time unit. - * @throws Throwable failure - */ - @Test - public void testTTLConstruction() throws Throwable { - // first one - ITtlTimeProvider timeProviderExplicit = new S3Guard.TtlTimeProvider( - DEFAULT_METADATASTORE_METADATA_TTL); - - // mirror the FS construction, - // from a config guaranteed to be empty (i.e. the code defval) - Configuration conf = new Configuration(false); - long millitime = conf.getTimeDuration(METADATASTORE_METADATA_TTL, - DEFAULT_METADATASTORE_METADATA_TTL, TimeUnit.MILLISECONDS); - assertEquals(15 * 60_000, millitime); - S3Guard.TtlTimeProvider fsConstruction = new S3Guard.TtlTimeProvider( - millitime); - assertEquals("explicit vs fs construction", timeProviderExplicit, - fsConstruction); - assertEquals("first and second constructor", timeProviderExplicit, - new S3Guard.TtlTimeProvider(conf)); - // set the conf to a time without unit - conf.setLong(METADATASTORE_METADATA_TTL, - DEFAULT_METADATASTORE_METADATA_TTL); - assertEquals("first and second time set through long", timeProviderExplicit, - new S3Guard.TtlTimeProvider(conf)); - double timeInSeconds = DEFAULT_METADATASTORE_METADATA_TTL / 1000; - double timeInMinutes = timeInSeconds / 60; - String timeStr = String.format("%dm", (int) timeInMinutes); - assertEquals(":wrong time in minutes from " + timeInMinutes, - "15m", timeStr); - conf.set(METADATASTORE_METADATA_TTL, timeStr); - assertEquals("Time in millis as string from " - + conf.get(METADATASTORE_METADATA_TTL), - timeProviderExplicit, - new S3Guard.TtlTimeProvider(conf)); - } - - @Test - public void testLogS3GuardDisabled() throws Exception { - final Logger localLogger = LoggerFactory.getLogger( - TestS3Guard.class.toString() + UUID.randomUUID()); - S3Guard.logS3GuardDisabled(localLogger, - S3Guard.DisabledWarnLevel.SILENT.toString(), "bucket"); - S3Guard.logS3GuardDisabled(localLogger, - S3Guard.DisabledWarnLevel.INFORM.toString(), "bucket"); - S3Guard.logS3GuardDisabled(localLogger, - S3Guard.DisabledWarnLevel.WARN.toString(), "bucket"); - - // Test that lowercase setting is accepted - S3Guard.logS3GuardDisabled(localLogger, - S3Guard.DisabledWarnLevel.WARN.toString() - .toLowerCase(Locale.US), "bucket"); - - ExitUtil.ExitException ex = LambdaTestUtils.intercept( - ExitUtil.ExitException.class, - String.format(S3Guard.DISABLED_LOG_MSG, "bucket"), - () -> S3Guard.logS3GuardDisabled( - localLogger, S3Guard.DisabledWarnLevel.FAIL.toString(), "bucket")); - if (ex.getExitCode() != LauncherExitCodes.EXIT_BAD_CONFIGURATION) { - throw ex; - } - LambdaTestUtils.intercept(IllegalArgumentException.class, - S3Guard.UNKNOWN_WARN_LEVEL, - () -> S3Guard.logS3GuardDisabled( - localLogger, "FOO_BAR_LEVEL", "bucket")); - } - - void assertContainsPaths(FileStatus[] statuses, String...pathStr) { - for (String s :pathStr) { - assertContainsPath(statuses, s); - } - } - - void assertContainsPath(FileStatus[] statuses, String pathStr) { - find(statuses, pathStr); - } - - /** - * Look up an entry or raise an assertion - * @param statuses list of statuses - * @param pathStr path to search - * @return the entry if found - */ - private FileStatus find(FileStatus[] statuses, String pathStr) { - for (FileStatus s : statuses) { - if (s.getPath().toString().equals(pathStr)) { - return s; - } - } - // no match, fail meaningfully - Assertions.assertThat(statuses) - .anyMatch(s -> s.getPath().toString().equals(pathStr)); - return null; - } - - private PathMetadata makePathMeta(String pathStr, boolean isDir) { - return new PathMetadata(makeFileStatus(pathStr, isDir)); - } - - private S3AFileStatus makeFileStatus(String pathStr, boolean isDir) { - Path p = new Path(pathStr); - S3AFileStatus fileStatus; - if (isDir) { - fileStatus = new S3AFileStatus(Tristate.UNKNOWN, p, null); - } else { - fileStatus = new S3AFileStatus( - 100, System.currentTimeMillis(), p, 1, null, null, null); - } - return fileStatus; - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestS3GuardCLI.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestS3GuardCLI.java index 43256b9f5fb..26ccf1284ef 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestS3GuardCLI.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/TestS3GuardCLI.java @@ -22,7 +22,6 @@ import org.junit.Assert; import org.junit.Test; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.s3a.S3ATestConstants; import org.apache.hadoop.test.LambdaTestUtils; import org.apache.hadoop.util.ExitUtil; @@ -83,39 +82,4 @@ public class TestS3GuardCLI extends Assert { runToFailure(E_USAGE, "unknown"); } - @Test - public void testPruneNoArgs() throws Throwable { - runToFailure(INVALID_ARGUMENT, Prune.NAME); - } - - @Test - public void testDiffNoArgs() throws Throwable { - runToFailure(INVALID_ARGUMENT, Diff.NAME); - } - - @Test - public void testImportNoArgs() throws Throwable { - runToFailure(INVALID_ARGUMENT, Import.NAME); - } - - @Test - public void testDestroyNoArgs() throws Throwable { - runToFailure(INVALID_ARGUMENT, Destroy.NAME); - } - - @Test - public void testDestroyUnknownTableNoRegion() throws Throwable { - runToFailure(INVALID_ARGUMENT, Destroy.NAME, - "-meta", "dynamodb://ireland-team"); - } - - @Test - public void testInitBucketAndRegion() throws Throwable { - runToFailure(INVALID_ARGUMENT, Init.NAME, - "-meta", "dynamodb://ireland-team", - "-region", "eu-west-1", - S3ATestConstants.DEFAULT_CSVTEST_FILE - ); - } - } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ThrottleTracker.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ThrottleTracker.java deleted file mode 100644 index 0dad1bf03d7..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/s3guard/ThrottleTracker.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * 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.hadoop.fs.s3a.s3guard; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Something to track throttles in DynamoDB metastores. - * The constructor sets the counters to the current count in the - * DDB table; a call to {@link #reset()} will set it to the latest values. - * The {@link #probe()} will pick up the latest values to compare them with - * the original counts. - *

    - * The toString value logs the state. - *

    - * This class was originally part of ITestDynamoDBMetadataStoreScale; - * it was converted to a toplevel class for broader use. - */ -class ThrottleTracker { - - private static final Logger LOG = LoggerFactory.getLogger( - ThrottleTracker.class); - private final DynamoDBMetadataStore ddbms; - - private long writeThrottleEventOrig; - - private long readThrottleEventOrig; - - private long batchWriteThrottleCountOrig; - - private long scanThrottleCountOrig; - - private long readThrottles; - - private long writeThrottles; - - private long batchThrottles; - - private long scanThrottles; - - ThrottleTracker(final DynamoDBMetadataStore ddbms) { - this.ddbms = ddbms; - reset(); - } - - /** - * Reset the counters. - */ - public synchronized void reset() { - writeThrottleEventOrig - = ddbms.getWriteThrottleEventCount(); - - readThrottleEventOrig - = ddbms.getReadThrottleEventCount(); - - batchWriteThrottleCountOrig - = ddbms.getBatchWriteCapacityExceededCount(); - - scanThrottleCountOrig - = ddbms.getScanThrottleEventCount(); - } - - /** - * Update the latest throttle count; synchronized. - * @return true if throttling has been detected. - */ - public synchronized boolean probe() { - setReadThrottles( - ddbms.getReadThrottleEventCount() - readThrottleEventOrig); - setWriteThrottles(ddbms.getWriteThrottleEventCount() - - writeThrottleEventOrig); - setBatchThrottles(ddbms.getBatchWriteCapacityExceededCount() - - batchWriteThrottleCountOrig); - setScanThrottles(ddbms.getScanThrottleEventCount() - - scanThrottleCountOrig); - return isThrottlingDetected(); - } - - @Override - public String toString() { - return String.format( - "Tracker with read throttle events = %d;" - + " write throttles = %d;" - + " batch throttles = %d;" - + " scan throttles = %d", - getReadThrottles(), getWriteThrottles(), getBatchThrottles(), - getScanThrottles()); - } - - /** - * Check that throttling was detected; Warn if not. - * @return true if throttling took place. - */ - public boolean probeThrottlingDetected() { - if (!isThrottlingDetected()) { - LOG.warn("No throttling detected in {} against {}", - this, ddbms); - return false; - } - return true; - } - - /** - * Has there been any throttling on an operation? - * @return true if any operations were throttled. - */ - public boolean isThrottlingDetected() { - return getReadThrottles() > 0 - || getWriteThrottles() > 0 - || getBatchThrottles() > 0 - || getScanThrottles() > 0; - } - - public long getReadThrottles() { - return readThrottles; - } - - public void setReadThrottles(long readThrottles) { - this.readThrottles = readThrottles; - } - - public long getWriteThrottles() { - return writeThrottles; - } - - public void setWriteThrottles(long writeThrottles) { - this.writeThrottles = writeThrottles; - } - - public long getBatchThrottles() { - return batchThrottles; - } - - public void setBatchThrottles(long batchThrottles) { - this.batchThrottles = batchThrottles; - } - - public long getScanThrottles() { - return scanThrottles; - } - - public void setScanThrottles(final long scanThrottles) { - this.scanThrottles = scanThrottles; - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/AbstractITestS3AMetadataStoreScale.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/AbstractITestS3AMetadataStoreScale.java deleted file mode 100644 index 8bbec1fdec6..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/AbstractITestS3AMetadataStoreScale.java +++ /dev/null @@ -1,277 +0,0 @@ -/* - * 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.hadoop.fs.s3a.scale; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.s3guard.ITtlTimeProvider; -import org.apache.hadoop.fs.s3a.s3guard.BulkOperationState; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.PathMetadata; -import org.apache.hadoop.fs.s3a.s3guard.S3Guard; - -import org.junit.Before; -import org.junit.FixMethodOrder; -import org.junit.Test; -import org.junit.runners.MethodSorters; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import static org.apache.hadoop.fs.contract.ContractTestUtils.NanoTimer; - -/** - * Test the performance of a MetadataStore. Useful for load testing. - * Could be separated from S3A code, but we're using the S3A scale test - * framework for convenience. - */ -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public abstract class AbstractITestS3AMetadataStoreScale extends - S3AScaleTestBase { - private static final Logger LOG = LoggerFactory.getLogger( - AbstractITestS3AMetadataStoreScale.class); - - /** Some dummy values for FileStatus contents. */ - static final long BLOCK_SIZE = 32 * 1024 * 1024; - static final long SIZE = BLOCK_SIZE * 2; - static final String OWNER = "bob"; - static final long ACCESS_TIME = System.currentTimeMillis(); - - static final Path BUCKET_ROOT = new Path("s3a://fake-bucket/"); - private ITtlTimeProvider ttlTimeProvider; - - @Before - public void initialize() { - ttlTimeProvider = new S3Guard.TtlTimeProvider(new Configuration()); - } - - /** - * Subclasses should override this to provide the MetadataStore they which - * to test. - * @return MetadataStore to test against - * @throws IOException - */ - public abstract MetadataStore createMetadataStore() throws IOException; - - protected ITtlTimeProvider getTtlTimeProvider() { - return ttlTimeProvider; - } - - @Test - public void test_010_Put() throws Throwable { - describe("Test workload of put() operations"); - - // As described in hadoop-aws site docs, count parameter is used for - // width and depth of directory tree - int width = getConf().getInt(KEY_DIRECTORY_COUNT, DEFAULT_DIRECTORY_COUNT); - int depth = width; - - List paths = new ArrayList<>(); - createDirTree(BUCKET_ROOT, depth, width, paths); - - long count = 1; // Some value in case we throw an exception below - try (MetadataStore ms = createMetadataStore()) { - - try { - count = populateMetadataStore(paths, ms); - } finally { - clearMetadataStore(ms, count); - } - } - } - - @Test - public void test_020_Moves() throws Throwable { - describe("Test workload of batched move() operations"); - - // As described in hadoop-aws site docs, count parameter is used for - // width and depth of directory tree - int width = getConf().getInt(KEY_DIRECTORY_COUNT, DEFAULT_DIRECTORY_COUNT); - int depth = width; - - long operations = getConf().getLong(KEY_OPERATION_COUNT, - DEFAULT_OPERATION_COUNT); - - List origMetas = new ArrayList<>(); - createDirTree(BUCKET_ROOT, depth, width, origMetas); - - // Pre-compute source and destination paths for move() loop below - List origPaths = metasToPaths(origMetas); - List movedMetas = moveMetas(origMetas, BUCKET_ROOT, - new Path(BUCKET_ROOT, "moved-here")); - List movedPaths = metasToPaths(movedMetas); - - long count = 1; // Some value in case we throw an exception below - try (MetadataStore ms = createMetadataStore()) { - - try { - // Setup - count = populateMetadataStore(origMetas, ms); - - // Main loop: move things back and forth - describe("Running move workload"); - NanoTimer moveTimer = new NanoTimer(); - LOG.info("Running {} moves of {} paths each", operations, - origMetas.size()); - for (int i = 0; i < operations; i++) { - Collection toDelete; - Collection toCreate; - if (i % 2 == 0) { - toDelete = origPaths; - toCreate = movedMetas; - } else { - toDelete = movedPaths; - toCreate = origMetas; - } - ms.move(toDelete, toCreate, null); - } - moveTimer.end(); - printTiming(LOG, "move", moveTimer, operations); - } finally { - // Cleanup - clearMetadataStore(ms, count); - ms.move(origPaths, null, null); - ms.move(movedPaths, null, null); - } - } - } - - /** - * Create a copy of given list of PathMetadatas with the paths moved from - * src to dest. - */ - protected List moveMetas(List metas, Path src, - Path dest) throws IOException { - List moved = new ArrayList<>(metas.size()); - for (PathMetadata srcMeta : metas) { - S3AFileStatus status = copyStatus((S3AFileStatus)srcMeta.getFileStatus()); - status.setPath(movePath(status.getPath(), src, dest)); - moved.add(new PathMetadata(status)); - } - return moved; - } - - protected Path movePath(Path p, Path src, Path dest) { - String srcStr = src.toUri().getPath(); - String pathStr = p.toUri().getPath(); - // Strip off src dir - pathStr = pathStr.substring(srcStr.length()); - // Prepend new dest - return new Path(dest, pathStr); - } - - protected S3AFileStatus copyStatus(S3AFileStatus status) { - if (status.isDirectory()) { - return new S3AFileStatus(status.isEmptyDirectory(), status.getPath(), - status.getOwner()); - } else { - return new S3AFileStatus(status.getLen(), status.getModificationTime(), - status.getPath(), status.getBlockSize(), status.getOwner(), - status.getETag(), status.getVersionId()); - } - } - - /** @return number of PathMetadatas put() into MetadataStore */ - private long populateMetadataStore(Collection paths, - MetadataStore ms) throws IOException { - long count = 0; - NanoTimer putTimer = new NanoTimer(); - describe("Inserting into MetadataStore"); - try (BulkOperationState operationState = - ms.initiateBulkWrite(BulkOperationState.OperationType.Put, - BUCKET_ROOT)) { - for (PathMetadata p : paths) { - ms.put(p, operationState); - count++; - } - } - putTimer.end(); - printTiming(LOG, "put", putTimer, count); - return count; - } - - protected void clearMetadataStore(MetadataStore ms, long count) - throws IOException { - describe("Recursive deletion"); - NanoTimer deleteTimer = new NanoTimer(); - ms.deleteSubtree(BUCKET_ROOT, null); - deleteTimer.end(); - printTiming(LOG, "delete", deleteTimer, count); - } - - private static void printTiming(Logger log, String op, NanoTimer timer, - long count) { - double msec = (double)timer.duration() / 1000; - double msecPerOp = msec / count; - log.info(String.format("Elapsed %.2f msec. %.3f msec / %s (%d ops)", msec, - msecPerOp, op, count)); - } - - protected static S3AFileStatus makeFileStatus(Path path) throws IOException { - return new S3AFileStatus(SIZE, ACCESS_TIME, path, BLOCK_SIZE, OWNER, - null, null); - } - - protected static S3AFileStatus makeDirStatus(Path p) throws IOException { - return new S3AFileStatus(false, p, OWNER); - } - - protected List metasToPaths(List metas) { - List paths = new ArrayList<>(metas.size()); - for (PathMetadata meta : metas) { - paths.add(meta.getFileStatus().getPath()); - } - return paths; - } - - /** - * Recursively create a directory tree. - * @param parent Parent dir of the paths to create. - * @param depth How many more levels deep past parent to create. - * @param width Number of files (and directories, if depth > 0) per directory. - * @param paths List to add generated paths to. - */ - protected static void createDirTree(Path parent, int depth, int width, - Collection paths) throws IOException { - - // Create files - for (int i = 0; i < width; i++) { - Path p = new Path(parent, String.format("file-%d", i)); - PathMetadata meta = new PathMetadata(makeFileStatus(p)); - paths.add(meta); - } - - if (depth == 0) { - return; - } - - // Create directories if there is depth remaining - for (int i = 0; i < width; i++) { - Path dir = new Path(parent, String.format("dir-%d", i)); - PathMetadata meta = new PathMetadata(makeDirStatus(dir)); - paths.add(meta); - createDirTree(dir, depth-1, width, paths); - } - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/AbstractSTestS3AHugeFiles.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/AbstractSTestS3AHugeFiles.java index 2dd92bbfc8c..15700ce9535 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/AbstractSTestS3AHugeFiles.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/AbstractSTestS3AHugeFiles.java @@ -383,7 +383,7 @@ public abstract class AbstractSTestS3AHugeFiles extends S3AScaleTestBase { /** * This is the set of actions to perform when verifying the file actually - * was created. With the s3guard committer, the file doesn't come into + * was created. With the S3A committer, the file doesn't come into * existence; a different set of assertions must be checked. */ @Test diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ILoadTestS3ABulkDeleteThrottling.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ILoadTestS3ABulkDeleteThrottling.java index 3fa288417c4..f4af12b6eb5 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ILoadTestS3ABulkDeleteThrottling.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ILoadTestS3ABulkDeleteThrottling.java @@ -247,7 +247,7 @@ public class ILoadTestS3ABulkDeleteThrottling extends S3AScaleTestBase { new ContractTestUtils.NanoTimer(); Exception ex = null; try { - fs.removeKeys(fileList, false, null); + fs.removeKeys(fileList, false); } catch (IOException e) { ex = e; } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestLocalMetadataStoreScale.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestLocalMetadataStoreScale.java deleted file mode 100644 index 7477adeeb07..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestLocalMetadataStoreScale.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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.hadoop.fs.s3a.scale; - -import org.apache.hadoop.fs.s3a.s3guard.LocalMetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.S3Guard; - -import java.io.IOException; - -/** - * Scale test for LocalMetadataStore. - */ -public class ITestLocalMetadataStoreScale - extends AbstractITestS3AMetadataStoreScale { - @Override - public MetadataStore createMetadataStore() throws IOException { - MetadataStore ms = new LocalMetadataStore(); - ms.initialize(getFileSystem(), new S3Guard.TtlTimeProvider(getConf())); - return ms; - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestS3ADeleteManyFiles.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestS3ADeleteManyFiles.java index a724b9737c9..d5862bcb335 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestS3ADeleteManyFiles.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestS3ADeleteManyFiles.java @@ -77,7 +77,6 @@ public class ITestS3ADeleteManyFiles extends S3AScaleTestBase { * set too low. Alternatively, consider reducing the * scale.test.operation.count parameter in * getOperationCount(). - * If it is slow: look at the size of any S3Guard Table used. * @see #getOperationCount() */ @Test diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestS3ADirectoryPerformance.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestS3ADirectoryPerformance.java index d5e4788036e..d87af3bac5d 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestS3ADirectoryPerformance.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/scale/ITestS3ADirectoryPerformance.java @@ -53,7 +53,6 @@ import com.amazonaws.services.s3.model.PutObjectResult; import static org.apache.hadoop.fs.s3a.Constants.DIRECTORY_MARKER_POLICY; import static org.apache.hadoop.fs.s3a.Constants.DIRECTORY_MARKER_POLICY_KEEP; -import static org.apache.hadoop.fs.s3a.Constants.S3_METADATA_STORE_IMPL; import static org.apache.hadoop.fs.s3a.Statistic.*; import static org.apache.hadoop.fs.s3a.S3ATestUtils.*; import static org.apache.hadoop.fs.contract.ContractTestUtils.*; @@ -148,9 +147,7 @@ public class ITestS3ADirectoryPerformance extends S3AScaleTestBase { listContinueRequests, listStatusCalls, getFileStatusCalls); - if (!fs.hasMetadataStore()) { - assertEquals(listRequests.toString(), 1, listRequests.diff()); - } + assertEquals(listRequests.toString(), 1, listRequests.diff()); reset(metadataRequests, listRequests, listContinueRequests, @@ -197,7 +194,6 @@ public class ITestS3ADirectoryPerformance extends S3AScaleTestBase { getConfigurationWithConfiguredBatchSize(batchSize); removeBaseAndBucketOverrides(conf, - S3_METADATA_STORE_IMPL, DIRECTORY_MARKER_POLICY); // force directory markers = keep to save delete requests on every // file created. @@ -210,7 +206,6 @@ public class ITestS3ADirectoryPerformance extends S3AScaleTestBase { NanoTimer uploadTimer = new NanoTimer(); try { - assume("Test is only for raw fs", !fs.hasMetadataStore()); fs.create(dir); // create a span for the write operations @@ -321,9 +316,6 @@ public class ITestS3ADirectoryPerformance extends S3AScaleTestBase { .describedAs("Listing results using listLocatedStatus() must" + "match with original list of files") .hasSameElementsAs(originalListOfFiles); - // delete in this FS so S3Guard is left out of it. - // and so that the incremental listing is tested through - // the delete operation. fs.delete(dir, true); } finally { executorService.shutdown(); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/select/ITestS3Select.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/select/ITestS3Select.java index 69189412956..4c173ab07b3 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/select/ITestS3Select.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/select/ITestS3Select.java @@ -496,7 +496,7 @@ public class ITestS3Select extends AbstractS3SelectTest { */ @Test public void testSelectUnsupportedInputFormat() throws Throwable { - describe("Request an unsupported input format"); + describe("Request an Unsupported input format"); FutureDataInputStreamBuilder builder = getFileSystem().openFile(csvPath) .must(SELECT_SQL, SELECT_ODD_ENTRIES) .must(SELECT_INPUT_FORMAT, "pptx"); @@ -510,7 +510,7 @@ public class ITestS3Select extends AbstractS3SelectTest { */ @Test public void testSelectUnsupportedOutputFormat() throws Throwable { - describe("Request a (currently) unsupported output format"); + describe("Request a (currently) Unsupported output format"); FutureDataInputStreamBuilder builder = getFileSystem().openFile(csvPath) .must(SELECT_SQL, SELECT_ODD_ENTRIES) .must(SELECT_INPUT_FORMAT, "csv") @@ -567,7 +567,7 @@ public class ITestS3Select extends AbstractS3SelectTest { FutureDataInputStreamBuilder builder = getFileSystem().openFile(path("/")) .must(SELECT_SQL, SELECT_ODD_ENTRIES); - interceptFuture(FileNotFoundException.class, + interceptFuture(IOException.class, "", builder.build()); } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/statistics/ITestS3AContractStreamIOStatistics.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/statistics/ITestS3AContractStreamIOStatistics.java index 8bed174fd31..0f6b69cd54d 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/statistics/ITestS3AContractStreamIOStatistics.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/statistics/ITestS3AContractStreamIOStatistics.java @@ -27,7 +27,6 @@ import org.apache.hadoop.fs.contract.AbstractFSContract; import org.apache.hadoop.fs.contract.s3a.S3AContract; import org.apache.hadoop.fs.statistics.StreamStatisticNames; -import static org.apache.hadoop.fs.s3a.S3ATestUtils.maybeEnableS3Guard; import static org.apache.hadoop.fs.statistics.StreamStatisticNames.*; /** @@ -36,14 +35,6 @@ import static org.apache.hadoop.fs.statistics.StreamStatisticNames.*; public class ITestS3AContractStreamIOStatistics extends AbstractContractStreamIOStatisticsTest { - @Override - protected Configuration createConfiguration() { - Configuration conf = super.createConfiguration(); - // patch in S3Guard options - maybeEnableS3Guard(conf); - return conf; - } - @Override protected AbstractFSContract createContract(Configuration conf) { return new S3AContract(conf); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/test/MinimalListingOperationCallbacks.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/test/MinimalListingOperationCallbacks.java index 678e5d482d1..2a8cfc663a5 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/test/MinimalListingOperationCallbacks.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/test/MinimalListingOperationCallbacks.java @@ -27,7 +27,6 @@ import org.apache.hadoop.fs.s3a.S3ALocatedFileStatus; import org.apache.hadoop.fs.s3a.S3ListRequest; import org.apache.hadoop.fs.s3a.S3ListResult; import org.apache.hadoop.fs.s3a.impl.ListingOperationCallbacks; -import org.apache.hadoop.fs.s3a.s3guard.ITtlTimeProvider; import org.apache.hadoop.fs.statistics.DurationTrackerFactory; import org.apache.hadoop.fs.store.audit.AuditSpan; @@ -40,8 +39,7 @@ public class MinimalListingOperationCallbacks @Override public CompletableFuture listObjectsAsync( final S3ListRequest request, - final DurationTrackerFactory trackerFactory, AuditSpan span) - throws IOException { + final DurationTrackerFactory trackerFactory, AuditSpan span) { return null; } @@ -49,8 +47,7 @@ public class MinimalListingOperationCallbacks public CompletableFuture continueListObjectsAsync( final S3ListRequest request, final S3ListResult prevResult, - final DurationTrackerFactory trackerFactory, AuditSpan span) - throws IOException { + final DurationTrackerFactory trackerFactory, AuditSpan span) { return null; } @@ -78,14 +75,4 @@ public class MinimalListingOperationCallbacks return 0; } - @Override - public ITtlTimeProvider getUpdatedTtlTimeProvider() { - return null; - } - - @Override - public boolean allowAuthoritative(Path p) { - return false; - } - } diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/test/MinimalOperationCallbacks.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/test/MinimalOperationCallbacks.java index a50b944c798..a2aebc82720 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/test/MinimalOperationCallbacks.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/test/MinimalOperationCallbacks.java @@ -35,7 +35,6 @@ import org.apache.hadoop.fs.s3a.S3ALocatedFileStatus; import org.apache.hadoop.fs.s3a.S3AReadOpContext; import org.apache.hadoop.fs.s3a.S3ObjectAttributes; import org.apache.hadoop.fs.s3a.impl.OperationCallbacks; -import org.apache.hadoop.fs.s3a.s3guard.BulkOperationState; /** * Stub implementation of {@link OperationCallbacks}. @@ -76,8 +75,7 @@ public class MinimalOperationCallbacks public void deleteObjectAtPath( Path path, String key, - boolean isFile, - BulkOperationState operationState) + boolean isFile) throws IOException { } @@ -86,7 +84,6 @@ public class MinimalOperationCallbacks public RemoteIterator listFilesAndDirectoryMarkers( final Path path, final S3AFileStatus status, - final boolean collectTombstones, final boolean includeSelf) throws IOException { return null; } @@ -105,19 +102,12 @@ public class MinimalOperationCallbacks public DeleteObjectsResult removeKeys( List keysToDelete, boolean deleteFakeDir, - List undeletedObjectsOnFailure, - BulkOperationState operationState, boolean quiet) throws MultiObjectDeleteException, AmazonClientException, IOException { return null; } - @Override - public boolean allowAuthoritative(Path p) { - return false; - } - @Override public RemoteIterator listObjects( Path path, diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/test/OperationTrackingStore.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/test/OperationTrackingStore.java deleted file mode 100644 index 1bf0c3e6fc1..00000000000 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/test/OperationTrackingStore.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * 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.hadoop.fs.s3a.test; - -import javax.annotation.Nullable; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.fs.FileSystem; -import org.apache.hadoop.fs.Path; -import org.apache.hadoop.fs.s3a.S3AFileStatus; -import org.apache.hadoop.fs.s3a.impl.StoreContext; -import org.apache.hadoop.fs.s3a.s3guard.BulkOperationState; -import org.apache.hadoop.fs.s3a.s3guard.DirListingMetadata; -import org.apache.hadoop.fs.s3a.s3guard.ITtlTimeProvider; -import org.apache.hadoop.fs.s3a.s3guard.MetadataStore; -import org.apache.hadoop.fs.s3a.s3guard.PathMetadata; -import org.apache.hadoop.fs.s3a.s3guard.RenameTracker; - -/** - * MetadataStore which tracks what is deleted and added. - */ -public class OperationTrackingStore implements MetadataStore { - - private final List deleted = new ArrayList<>(); - - private final List created = new ArrayList<>(); - - @Override - public void initialize(final FileSystem fs, - ITtlTimeProvider ttlTimeProvider) { - } - - @Override - public void initialize(final Configuration conf, - ITtlTimeProvider ttlTimeProvider) { - } - - @Override - public void forgetMetadata(final Path path) { - } - - @Override - public PathMetadata get(final Path path) { - return null; - } - - @Override - public PathMetadata get(final Path path, - final boolean wantEmptyDirectoryFlag) { - return null; - } - - @Override - public DirListingMetadata listChildren(final Path path) { - return null; - } - - @Override - public void put(final PathMetadata meta) { - put(meta, null); - } - - @Override - public void put(final PathMetadata meta, - final BulkOperationState operationState) { - created.add(meta.getFileStatus().getPath()); - } - - @Override - public void put(final Collection metas, - final BulkOperationState operationState) { - metas.stream().forEach(meta -> put(meta, null)); - } - - @Override - public void put(final DirListingMetadata meta, - final List unchangedEntries, - final BulkOperationState operationState) { - created.add(meta.getPath()); - } - - @Override - public void destroy() { - } - - @Override - public void delete(final Path path, - final BulkOperationState operationState) { - deleted.add(path); - } - - @Override - public void deletePaths(final Collection paths, - @Nullable final BulkOperationState operationState) - throws IOException { - deleted.addAll(paths); - } - - @Override - public void deleteSubtree(final Path path, - final BulkOperationState operationState) { - - } - - @Override - public void move(@Nullable final Collection pathsToDelete, - @Nullable final Collection pathsToCreate, - @Nullable final BulkOperationState operationState) { - } - - @Override - public void prune(final PruneMode pruneMode, final long cutoff) { - } - - @Override - public long prune(final PruneMode pruneMode, - final long cutoff, - final String keyPrefix) { - return 0; - } - - @Override - public BulkOperationState initiateBulkWrite( - final BulkOperationState.OperationType operation, - final Path dest) { - return null; - } - - @Override - public void setTtlTimeProvider(ITtlTimeProvider ttlTimeProvider) { - } - - @Override - public Map getDiagnostics() { - return null; - } - - @Override - public void updateParameters(final Map parameters) { - } - - @Override - public void close() { - } - - public List getDeleted() { - return deleted; - } - - public List getCreated() { - return created; - } - - @Override - public RenameTracker initiateRenameOperation( - final StoreContext storeContext, - final Path source, - final S3AFileStatus sourceStatus, - final Path dest) { - throw new UnsupportedOperationException("unsupported"); - } - - @Override - public void addAncestors(final Path qualifiedPath, - @Nullable final BulkOperationState operationState) { - - } -} diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/tools/AbstractMarkerToolTest.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/tools/AbstractMarkerToolTest.java index 797d33c8d90..0c2473a5f61 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/tools/AbstractMarkerToolTest.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/tools/AbstractMarkerToolTest.java @@ -73,12 +73,9 @@ public class AbstractMarkerToolTest extends AbstractS3ATestBase { removeBaseAndBucketOverrides(bucketName, conf, S3A_BUCKET_PROBE, DIRECTORY_MARKER_POLICY, - S3_METADATA_STORE_IMPL, - METADATASTORE_AUTHORITATIVE, AUTHORITATIVE_PATH); // base FS is legacy conf.set(DIRECTORY_MARKER_POLICY, DIRECTORY_MARKER_POLICY_DELETE); - conf.set(S3_METADATA_STORE_IMPL, S3GUARD_METASTORE_NULL); // turn off bucket probes for a bit of speedup in the connectors we create. conf.setInt(S3A_BUCKET_PROBE, 0); @@ -192,7 +189,6 @@ public class AbstractMarkerToolTest extends AbstractS3ATestBase { String bucketName = getTestBucketName(conf); removeBucketOverrides(bucketName, conf, DIRECTORY_MARKER_POLICY, - S3_METADATA_STORE_IMPL, BULK_DELETE_PAGE_SIZE, AUTHORITATIVE_PATH); if (authPath != null) { @@ -201,7 +197,6 @@ public class AbstractMarkerToolTest extends AbstractS3ATestBase { // Use a very small page size to force the paging // code to be tested. conf.setInt(BULK_DELETE_PAGE_SIZE, 2); - conf.set(S3_METADATA_STORE_IMPL, S3GUARD_METASTORE_NULL); conf.set(DIRECTORY_MARKER_POLICY, markerPolicy); S3AFileSystem fs2 = new S3AFileSystem(); fs2.initialize(testFSUri, conf); diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/yarn/ITestS3AMiniYarnCluster.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/yarn/ITestS3AMiniYarnCluster.java index 87160937a18..87b37b7f8ff 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/yarn/ITestS3AMiniYarnCluster.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/yarn/ITestS3AMiniYarnCluster.java @@ -119,7 +119,7 @@ public class ITestS3AMiniYarnCluster extends AbstractS3ATestBase { Path success = new Path(output, _SUCCESS); FileStatus status = fs.getFileStatus(success); - assertTrue("0 byte success file - not a s3guard committer " + success, + assertTrue("0 byte success file - not an S3A committer " + success, status.getLen() > 0); SuccessData successData = SuccessData.load(fs, success); String commitDetails = successData.toString(); diff --git a/hadoop-tools/hadoop-aws/src/test/resources/core-site.xml b/hadoop-tools/hadoop-aws/src/test/resources/core-site.xml index 21e30b6d66c..300bd305fa7 100644 --- a/hadoop-tools/hadoop-aws/src/test/resources/core-site.xml +++ b/hadoop-tools/hadoop-aws/src/test/resources/core-site.xml @@ -43,14 +43,6 @@ it will only create log noise. - - - fs.s3a.bucket.landsat-pds.metadatastore.impl - ${s3guard.null} - The read-only landsat-pds repository isn't - managed by s3guard - - fs.s3a.bucket.landsat-pds.probe 0 @@ -63,16 +55,6 @@ Do not add the referrer header to landsat operations - - - s3guard.null - org.apache.hadoop.fs.s3a.s3guard.NullMetadataStore - - - - s3guard.dynamo - org.apache.hadoop.fs.s3a.s3guard.DynamoDBMetadataStore - - - fs.s3a.s3guard.ddb.table.capacity.read - 0 - - - fs.s3a.s3guard.ddb.table.capacity.write - 0 - - diff --git a/hadoop-tools/hadoop-aws/src/test/resources/log4j.properties b/hadoop-tools/hadoop-aws/src/test/resources/log4j.properties index 653c8a75e69..637d015a091 100644 --- a/hadoop-tools/hadoop-aws/src/test/resources/log4j.properties +++ b/hadoop-tools/hadoop-aws/src/test/resources/log4j.properties @@ -55,17 +55,10 @@ log4j.logger.org.apache.hadoop.ipc.Server=WARN #log4j.logger.org.apache.hadoop.fs.s3a=DEBUG #log4j.logger.org.apache.hadoop.fs.s3a.S3AUtils=INFO #log4j.logger.org.apache.hadoop.fs.s3a.Listing=INFO -# Log S3Guard classes -#log4j.logger.org.apache.hadoop.fs.s3a.s3guard=DEBUG -# if set to debug, this will log the PUT/DELETE operations on a store -log4j.logger.org.apache.hadoop.fs.s3a.s3guard.Operations=DEBUG # Log Committer classes #log4j.logger.org.apache.hadoop.fs.s3a.commit=DEBUG -# Enable debug logging of AWS Dynamo client -#log4j.logger.com.amazonaws.services.dynamodbv2.AmazonDynamoDB=DEBUG - # Log all HTTP requests made; includes S3 interaction. This may # include sensitive information such as account IDs in HTTP headers. #log4j.logger.com.amazonaws.request=DEBUG diff --git a/hadoop-tools/hadoop-azure/src/site/markdown/abfs.md b/hadoop-tools/hadoop-azure/src/site/markdown/abfs.md index dfb7f3f42a5..35d36055604 100644 --- a/hadoop-tools/hadoop-azure/src/site/markdown/abfs.md +++ b/hadoop-tools/hadoop-azure/src/site/markdown/abfs.md @@ -676,8 +676,6 @@ cause problems. As with all Azure storage services, the Azure Datalake Gen 2 store offers a fully consistent view of the store, with complete Create, Read, Update, and Delete consistency for data and metadata. -(Compare and contrast with S3 which only offers Create consistency; -S3Guard adds CRUD to metadata, but not the underlying data). ### Performance and Scalability