[
https://issues.apache.org/jira/browse/HDDS-16952?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Andrey Yarovoy updated HDDS-16952:
----------------------------------
Description:
`BasicRootedOzoneFileSystem.delete` for a path below bucket level first calls
`getFileStatus` (one RPC since HDDS-15925) to tell a file from a directory.
What follows depends on the answer.
* *{*}File:{*}* `adapter.deleteObject(key)` → `getBucket()` (`InfoBucket`) →
`DeleteKey`. *{*}3 OM RPCs.{*}*
* *{*}Directory on FSO:{*}* `adapterImpl.getBucket()` to read the layout, then
`adapterImpl.deleteObject(path,
recursive)`, which calls `getBucket()` *{*}again{*}*, then `DeleteKey`, which
OM runs as an atomic recursive
delete. *{*}4 OM RPCs, two of them identical `InfoBucket` calls.{*}*
* *{*}Directory on LEGACY:{*}* `innerDelete` → `DeleteIterator`, which fetches
the bucket once and then lists
and batch-deletes through the bucket-taking `deleteObjects` helper.
Each link hop adds `InfoVolume` + `InfoBucket` to every `getBucket()`. `delete`
is one of the hottest file
system operations: job cleanup, staging directories, Hive overwrite.
The fetch does two jobs:
1. *{*}Layout check.{*}* On delete it is *already done* by the preceding
`getFileStatus`, against any OM:
* An OM ≥ `GET_FILE_STATUS_REJECTS_OBS` (14) rejects OBJECT_STORE server-side,
using the link-resolved layout.
* For an older OM, the adapter's `getFileStatus` falls back to `getBucket()` +
`validateBucketLayout`.
So by the time either `getBucket()` in delete runs, the bucket is known not to
be OBJECT_STORE.
2. *{*}FSO vs LEGACY.{*}* This is only needed for a directory, to choose
between OM's atomic recursive delete
and client-side iteration.
*Part 1 — no protobuf change*
* *{*}File delete: drop the `getBucket()`.{*}* After a successful
`getFileStatus` on OM ≥ 14, the layout check
is redundant. Send `DeleteKey` through a handle built locally
(`OzoneBucket.newBuilder(...)`), or through
a package-private adapter variant used only by `delete()`.
* Keep the public `deleteObject(String)` unchanged, because it can be called
without a preceding
`getFileStatus`.
* 3 → 2 RPCs. *{*}No version gate is needed{*}*, because the `getFileStatus`
that precedes it checked the
layout against an old OM too.
* *{*}FSO directory delete: reuse the fetched bucket.{*}* Pass the
`OzoneBucket` that `delete()` already
fetched into a bucket-taking `deleteObject` variant, as the existing "reduce
calls to getBucket()" helpers
do for `rename` and `deleteObjects`. 4 → 3 RPCs, with no change against any OM.
*Part 2 — opt-in field: an optimistic FSO directory delete*
The directory case still pays one `InfoBucket` to learn FSO vs LEGACY.
Following the rename Jira, add a
request field:
{code:java}
message DeleteKeyRequest {
required KeyArgs keyArgs = 1;
// Set by file system clients that rely on atomic recursive directory
// delete: OM rejects the request with NOT_SUPPORTED_OPERATION unless the
// resolved bucket layout is FILE_SYSTEM_OPTIMIZED.
optional bool requireFileSystemOptimized = 2;
}
{code}
The client sends `DeleteKey` with this field set, and with `recursive` as
requested. On
`NOT_SUPPORTED_OPERATION` it falls back to today's `getBucket()` +
`innerDelete`. FSO: 3 → 2 RPCs.
LEGACY: one extra failed RPC on a path that already lists and batch-deletes.
The rejection has to be opt-in because `DeleteKey` is dual-use.
`OMKeyDeleteRequest` is registered for
`BucketLayout.OBJECT_STORE` (that registration also serves LEGACY), and these
callers send it on any
layout:
* the S3 Gateway's `DeleteObject`;
* `cli-shell` (`DeleteKeyHandler`);
* `freon`;
* `TrashOzoneFileSystem`;
* the public `OzoneBucket.deleteKey` / `deleteDirectory`.
None of them set the field.
*OM.* Check on the leader before Ratis, in `preExecute` or right after
`createClientRequest`. Use the
link-resolved layout `BucketLayoutAwareOMKeyRequestFactory` already read.
Neither delete class uses
`NOT_SUPPORTED_OPERATION` today. Do not put the check in `createRequest`
itself, because that also runs
at apply time.
*Client.* Use the new path when `proxy.getOmVersion() >=` a new
`OzoneManagerVersion` constant. Place it
after `LIST_STATUS_REJECTS_OBS(101)`, never below `ZDU(100)`; the number is
assigned at merge. Against any
older OM, including one that reports ZDU, use the Part 1 path.
*{*}Watch the error handling:{*}* `adapterImpl.deleteObject` catches *every*
`OMException` other than
`DIRECTORY_NOT_EMPTY` / `INVALID_KEY_NAME` and returns `false`. Today the
OBJECT_STORE rejection escapes
that catch only because it is an unchecked `IllegalArgumentException`. The new
`NOT_SUPPORTED_OPERATION` must be handled before the generic catch. Otherwise a
LEGACY directory delete
would silently return `false` and never fall back.
`DeleteKeys` (batch) is *{*}out of scope{*}*. Its file system callers already
pass a fetched bucket
(`DeleteIterator`), or sit on the cold recursive-bucket-delete path
(`DeleteIteratorWithFSO`, one
`InfoBucket` per batch through the public `deleteObjects(List)`). A Part
1-style pass of the iterator's
bucket into the helper fixes the latter without any field. The S3 Gateway's
`DeleteObjects` and
`KeyLifecycleService` use it on all layouts.
*Compatibility*
| |file|FSO dir|LEGACY dir|
|—|—|—|—|
|Today|3|4|2 + iteration|
|Part 1 (any OM)|2|3|2 + iteration|
|Part 2 (new OM)|2|2|3 + iteration|
Old clients, the S3 Gateway, the shell, trash and direct `OzoneBucket` callers
send no field, so their
behavior is unchanged. Part 2 adds one optional proto field and no result code
or configuration.
*{*}Recommendation:{*}* file Part 1 first. It is client-only, has no
compatibility surface, and removes 1–2
RPCs from every delete. Part 2 should follow the rename Jira and use the same
field semantics.
*Testing*
* `BasicRootedOzoneFileSystem` / adapter (Mockito):
* File delete: `getFileStatus` + `DeleteKey`, and no `getBucket()`.
* FSO directory: exactly one `getBucket()` (Part 1), then none (Part 2).
* Part 2 `NOT_SUPPORTED_OPERATION` on LEGACY falls back to `innerDelete` and
does *{*}not{*}* return `false`.
*`DIRECTORY_NOT_EMPTY` → `PathIsNotEmptyDirectoryException`, as today.
* Older OMs, including one at `ZDU`, fall back.
* OM request tests (Part 2):
* With the field set, FSO succeeds and LEGACY / OBJECT_STORE are rejected
before Ratis.
* Without the field, everything succeeds. This guards S3 `DeleteObject`, the
shell, trash and lifecycle.
* `AbstractRootedOzoneFileSystemTest` (`TestOFS`, `TestOFSWithFSO`): the
existing delete suite passes on
both layouts, including orphan-link and bucket deletes. Count RPCs in the
adapter test, not with
OMMetrics deltas (HDDS-16513).
* Benchmark `TestOfsDeleteRpcBenchmark`, same shape as
`TestOfsListStatusRpcBenchmark`:
* Two cases: a file delete, and a non-recursive empty-directory delete on FSO.
* Single-threaded and 10 threads, n=3 per arm, built and run strictly one
after the other.
* Invariants: file 3 → 2, FSO directory 4 → 3 (Part 1) → 2 (Part 2).
was:
`BasicRootedOzoneFileSystem.delete` for a path below bucket level first calls
`getFileStatus` (one RPC since HDDS-15925) to tell a file from a directory.
What follows depends on the answer.
* **File:** `adapter.deleteObject(key)` → `getBucket()` (`InfoBucket`) →
`DeleteKey`. **3 OM RPCs.**
* **Directory on FSO:** `adapterImpl.getBucket()` to read the layout, then
`adapterImpl.deleteObject(path,
recursive)`, which calls `getBucket()` **again**, then `DeleteKey`, which OM
runs as an atomic recursive
delete. **4 OM RPCs, two of them identical `InfoBucket` calls.**
* **Directory on LEGACY:** `innerDelete` → `DeleteIterator`, which fetches the
bucket once and then lists
and batch-deletes through the bucket-taking `deleteObjects` helper.
Each link hop adds `InfoVolume` + `InfoBucket` to every `getBucket()`. `delete`
is one of the hottest file
system operations: job cleanup, staging directories, Hive overwrite.
The fetch does two jobs:
1. **Layout check.** On delete it is *already done* by the preceding
`getFileStatus`, against any OM:
* An OM ≥ `GET_FILE_STATUS_REJECTS_OBS` (14) rejects OBJECT_STORE server-side,
using the link-resolved layout.
* For an older OM, the adapter's `getFileStatus` falls back to `getBucket()` +
`validateBucketLayout`.
So by the time either `getBucket()` in delete runs, the bucket is known not to
be OBJECT_STORE.
2. **FSO vs LEGACY.** This is only needed for a directory, to choose between
OM's atomic recursive delete
and client-side iteration.
### Part 1 — no protobuf change
* **File delete: drop the `getBucket()`.** After a successful `getFileStatus`
on OM ≥ 14, the layout check
is redundant. Send `DeleteKey` through a handle built locally
(`OzoneBucket.newBuilder(...)`), or through
a package-private adapter variant used only by `delete()`.
* Keep the public `deleteObject(String)` unchanged, because it can be called
without a preceding
`getFileStatus`.
* 3 → 2 RPCs. **No version gate is needed**, because the `getFileStatus` that
precedes it checked the
layout against an old OM too.
* **FSO directory delete: reuse the fetched bucket.** Pass the `OzoneBucket`
that `delete()` already
fetched into a bucket-taking `deleteObject` variant, as the existing "reduce
calls to getBucket()" helpers
do for `rename` and `deleteObjects`. 4 → 3 RPCs, with no change against any OM.
### Part 2 — opt-in field: an optimistic FSO directory delete
The directory case still pays one `InfoBucket` to learn FSO vs LEGACY.
Following the rename Jira, add a
request field:
{code}
message DeleteKeyRequest {
required KeyArgs keyArgs = 1;
// Set by file system clients that rely on atomic recursive directory
// delete: OM rejects the request with NOT_SUPPORTED_OPERATION unless the
// resolved bucket layout is FILE_SYSTEM_OPTIMIZED.
optional bool requireFileSystemOptimized = 2;
}
{code}
The client sends `DeleteKey` with this field set, and with `recursive` as
requested. On
`NOT_SUPPORTED_OPERATION` it falls back to today's `getBucket()` +
`innerDelete`. FSO: 3 → 2 RPCs.
LEGACY: one extra failed RPC on a path that already lists and batch-deletes.
The rejection has to be opt-in because `DeleteKey` is dual-use.
`OMKeyDeleteRequest` is registered for
`BucketLayout.OBJECT_STORE` (that registration also serves LEGACY), and these
callers send it on any
layout:
* the S3 Gateway's `DeleteObject`;
* `cli-shell` (`DeleteKeyHandler`);
* `freon`;
* `TrashOzoneFileSystem`;
* the public `OzoneBucket.deleteKey` / `deleteDirectory`.
None of them set the field.
*OM.* Check on the leader before Ratis, in `preExecute` or right after
`createClientRequest`. Use the
link-resolved layout `BucketLayoutAwareOMKeyRequestFactory` already read.
Neither delete class uses
`NOT_SUPPORTED_OPERATION` today. Do not put the check in `createRequest`
itself, because that also runs
at apply time.
*Client.* Use the new path when `proxy.getOmVersion() >=` a new
`OzoneManagerVersion` constant. Place it
after `LIST_STATUS_REJECTS_OBS(101)`, never below `ZDU(100)`; the number is
assigned at merge. Against any
older OM, including one that reports ZDU, use the Part 1 path.
**Watch the error handling:** `adapterImpl.deleteObject` catches *every*
`OMException` other than
`DIRECTORY_NOT_EMPTY` / `INVALID_KEY_NAME` and returns `false`. Today the
OBJECT_STORE rejection escapes
that catch only because it is an unchecked `IllegalArgumentException`. The new
`NOT_SUPPORTED_OPERATION` must be handled before the generic catch. Otherwise a
LEGACY directory delete
would silently return `false` and never fall back.
`DeleteKeys` (batch) is **out of scope**. Its file system callers already pass
a fetched bucket
(`DeleteIterator`), or sit on the cold recursive-bucket-delete path
(`DeleteIteratorWithFSO`, one
`InfoBucket` per batch through the public `deleteObjects(List)`). A Part
1-style pass of the iterator's
bucket into the helper fixes the latter without any field. The S3 Gateway's
`DeleteObjects` and
`KeyLifecycleService` use it on all layouts.
### Compatibility
| | file | FSO dir | LEGACY dir |
|---|---|---|---|
| Today | 3 | 4 | 2 + iteration |
| Part 1 (any OM) | 2 | 3 | 2 + iteration |
| Part 2 (new OM) | 2 | 2 | 3 + iteration |
Old clients, the S3 Gateway, the shell, trash and direct `OzoneBucket` callers
send no field, so their
behavior is unchanged. Part 2 adds one optional proto field and no result code
or configuration.
**Recommendation:** file Part 1 first. It is client-only, has no compatibility
surface, and removes 1–2
RPCs from every delete. Part 2 should follow the rename Jira and use the same
field semantics.
Points for review:
* The BUCKET/READ ACL check from `getBucketDetails` is dropped. `DeleteKey`
keeps its own key/bucket ACL
checks.
* There is a race in Part 1: a bucket deleted and recreated as OBJECT_STORE
between `getFileStatus` and
`DeleteKey` would let one key delete through. That is the same outcome as an
object-store client
deleting that key, so no file system invariant is at stake.
## Testing
* `BasicRootedOzoneFileSystem` / adapter (Mockito):
* File delete: `getFileStatus` + `DeleteKey`, and no `getBucket()`.
* FSO directory: exactly one `getBucket()` (Part 1), then none (Part 2).
* Part 2 `NOT_SUPPORTED_OPERATION` on LEGACY falls back to `innerDelete` and
does **not** return `false`.
*`DIRECTORY_NOT_EMPTY` → `PathIsNotEmptyDirectoryException`, as today.
* Older OMs, including one at `ZDU`, fall back.
* OM request tests (Part 2):
* With the field set, FSO succeeds and LEGACY / OBJECT_STORE are rejected
before Ratis.
* Without the field, everything succeeds. This guards S3 `DeleteObject`, the
shell, trash and lifecycle.
* `AbstractRootedOzoneFileSystemTest` (`TestOFS`, `TestOFSWithFSO`): the
existing delete suite passes on
both layouts, including orphan-link and bucket deletes. Count RPCs in the
adapter test, not with
OMMetrics deltas (HDDS-16513).
* Benchmark `TestOfsDeleteRpcBenchmark`, same shape as
`TestOfsListStatusRpcBenchmark`:
* Two cases: a file delete, and a non-recursive empty-directory delete on FSO.
* Single-threaded and 10 threads, n=3 per arm, built and run strictly one after
the other.
* Invariants: file 3 → 2, FSO directory 4 → 3 (Part 1) → 2 (Part 2).
> Remove the per-call InfoBucket calls from rooted-ofs delete
> -----------------------------------------------------------
>
> Key: HDDS-16952
> URL: https://issues.apache.org/jira/browse/HDDS-16952
> Project: Apache Ozone
> Issue Type: Improvement
> Components: Ozone Client
> Reporter: Andrey Yarovoy
> Assignee: Andrey Yarovoy
> Priority: Major
>
> `BasicRootedOzoneFileSystem.delete` for a path below bucket level first calls
> `getFileStatus` (one RPC since HDDS-15925) to tell a file from a directory.
> What follows depends on the answer.
> * *{*}File:{*}* `adapter.deleteObject(key)` → `getBucket()` (`InfoBucket`) →
> `DeleteKey`. *{*}3 OM RPCs.{*}*
> * *{*}Directory on FSO:{*}* `adapterImpl.getBucket()` to read the layout,
> then `adapterImpl.deleteObject(path,
> recursive)`, which calls `getBucket()` *{*}again{*}*, then `DeleteKey`, which
> OM runs as an atomic recursive
> delete. *{*}4 OM RPCs, two of them identical `InfoBucket` calls.{*}*
> * *{*}Directory on LEGACY:{*}* `innerDelete` → `DeleteIterator`, which
> fetches the bucket once and then lists
> and batch-deletes through the bucket-taking `deleteObjects` helper.
> Each link hop adds `InfoVolume` + `InfoBucket` to every `getBucket()`.
> `delete` is one of the hottest file
> system operations: job cleanup, staging directories, Hive overwrite.
> The fetch does two jobs:
> 1. *{*}Layout check.{*}* On delete it is *already done* by the preceding
> `getFileStatus`, against any OM:
> * An OM ≥ `GET_FILE_STATUS_REJECTS_OBS` (14) rejects OBJECT_STORE
> server-side, using the link-resolved layout.
> * For an older OM, the adapter's `getFileStatus` falls back to `getBucket()`
> + `validateBucketLayout`.
> So by the time either `getBucket()` in delete runs, the bucket is known not
> to be OBJECT_STORE.
> 2. *{*}FSO vs LEGACY.{*}* This is only needed for a directory, to choose
> between OM's atomic recursive delete
> and client-side iteration.
> *Part 1 — no protobuf change*
> * *{*}File delete: drop the `getBucket()`.{*}* After a successful
> `getFileStatus` on OM ≥ 14, the layout check
> is redundant. Send `DeleteKey` through a handle built locally
> (`OzoneBucket.newBuilder(...)`), or through
> a package-private adapter variant used only by `delete()`.
> * Keep the public `deleteObject(String)` unchanged, because it can be called
> without a preceding
> `getFileStatus`.
> * 3 → 2 RPCs. *{*}No version gate is needed{*}*, because the `getFileStatus`
> that precedes it checked the
> layout against an old OM too.
> * *{*}FSO directory delete: reuse the fetched bucket.{*}* Pass the
> `OzoneBucket` that `delete()` already
> fetched into a bucket-taking `deleteObject` variant, as the existing "reduce
> calls to getBucket()" helpers
> do for `rename` and `deleteObjects`. 4 → 3 RPCs, with no change against any
> OM.
> *Part 2 — opt-in field: an optimistic FSO directory delete*
> The directory case still pays one `InfoBucket` to learn FSO vs LEGACY.
> Following the rename Jira, add a
> request field:
> {code:java}
> message DeleteKeyRequest {
> required KeyArgs keyArgs = 1;
> // Set by file system clients that rely on atomic recursive directory
> // delete: OM rejects the request with NOT_SUPPORTED_OPERATION unless the
> // resolved bucket layout is FILE_SYSTEM_OPTIMIZED.
> optional bool requireFileSystemOptimized = 2;
> }
> {code}
> The client sends `DeleteKey` with this field set, and with `recursive` as
> requested. On
> `NOT_SUPPORTED_OPERATION` it falls back to today's `getBucket()` +
> `innerDelete`. FSO: 3 → 2 RPCs.
> LEGACY: one extra failed RPC on a path that already lists and batch-deletes.
> The rejection has to be opt-in because `DeleteKey` is dual-use.
> `OMKeyDeleteRequest` is registered for
> `BucketLayout.OBJECT_STORE` (that registration also serves LEGACY), and these
> callers send it on any
> layout:
> * the S3 Gateway's `DeleteObject`;
> * `cli-shell` (`DeleteKeyHandler`);
> * `freon`;
> * `TrashOzoneFileSystem`;
> * the public `OzoneBucket.deleteKey` / `deleteDirectory`.
> None of them set the field.
> *OM.* Check on the leader before Ratis, in `preExecute` or right after
> `createClientRequest`. Use the
> link-resolved layout `BucketLayoutAwareOMKeyRequestFactory` already read.
> Neither delete class uses
> `NOT_SUPPORTED_OPERATION` today. Do not put the check in `createRequest`
> itself, because that also runs
> at apply time.
> *Client.* Use the new path when `proxy.getOmVersion() >=` a new
> `OzoneManagerVersion` constant. Place it
> after `LIST_STATUS_REJECTS_OBS(101)`, never below `ZDU(100)`; the number is
> assigned at merge. Against any
> older OM, including one that reports ZDU, use the Part 1 path.
> *{*}Watch the error handling:{*}* `adapterImpl.deleteObject` catches *every*
> `OMException` other than
> `DIRECTORY_NOT_EMPTY` / `INVALID_KEY_NAME` and returns `false`. Today the
> OBJECT_STORE rejection escapes
> that catch only because it is an unchecked `IllegalArgumentException`. The new
> `NOT_SUPPORTED_OPERATION` must be handled before the generic catch. Otherwise
> a LEGACY directory delete
> would silently return `false` and never fall back.
> `DeleteKeys` (batch) is *{*}out of scope{*}*. Its file system callers already
> pass a fetched bucket
> (`DeleteIterator`), or sit on the cold recursive-bucket-delete path
> (`DeleteIteratorWithFSO`, one
> `InfoBucket` per batch through the public `deleteObjects(List)`). A Part
> 1-style pass of the iterator's
> bucket into the helper fixes the latter without any field. The S3 Gateway's
> `DeleteObjects` and
> `KeyLifecycleService` use it on all layouts.
> *Compatibility*
> | |file|FSO dir|LEGACY dir|
> |—|—|—|—|
> |Today|3|4|2 + iteration|
> |Part 1 (any OM)|2|3|2 + iteration|
> |Part 2 (new OM)|2|2|3 + iteration|
> Old clients, the S3 Gateway, the shell, trash and direct `OzoneBucket`
> callers send no field, so their
> behavior is unchanged. Part 2 adds one optional proto field and no result
> code or configuration.
> *{*}Recommendation:{*}* file Part 1 first. It is client-only, has no
> compatibility surface, and removes 1–2
> RPCs from every delete. Part 2 should follow the rename Jira and use the same
> field semantics.
> *Testing*
> * `BasicRootedOzoneFileSystem` / adapter (Mockito):
> * File delete: `getFileStatus` + `DeleteKey`, and no `getBucket()`.
> * FSO directory: exactly one `getBucket()` (Part 1), then none (Part 2).
> * Part 2 `NOT_SUPPORTED_OPERATION` on LEGACY falls back to `innerDelete` and
> does *{*}not{*}* return `false`.
> *`DIRECTORY_NOT_EMPTY` → `PathIsNotEmptyDirectoryException`, as today.
> * Older OMs, including one at `ZDU`, fall back.
> * OM request tests (Part 2):
> * With the field set, FSO succeeds and LEGACY / OBJECT_STORE are rejected
> before Ratis.
> * Without the field, everything succeeds. This guards S3 `DeleteObject`, the
> shell, trash and lifecycle.
> * `AbstractRootedOzoneFileSystemTest` (`TestOFS`, `TestOFSWithFSO`): the
> existing delete suite passes on
> both layouts, including orphan-link and bucket deletes. Count RPCs in the
> adapter test, not with
> OMMetrics deltas (HDDS-16513).
> * Benchmark `TestOfsDeleteRpcBenchmark`, same shape as
> `TestOfsListStatusRpcBenchmark`:
> * Two cases: a file delete, and a non-recursive empty-directory delete on
> FSO.
> * Single-threaded and 10 threads, n=3 per arm, built and run strictly one
> after the other.
> * Invariants: file 3 → 2, FSO directory 4 → 3 (Part 1) → 2 (Part 2).
--
This message was sent by Atlassian Jira
(v8.20.10#820010)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]