Skip to content

Commit ccbf9f6

Browse files
committed
NSFS | versioning | content directory versioning - list-object + tagging operations
Signed-off-by: nadav mizrahi <[email protected]>
1 parent 1e32fa3 commit ccbf9f6

File tree

8 files changed

+113
-50
lines changed

8 files changed

+113
-50
lines changed

config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -818,7 +818,6 @@ config.NSFS_RANDOM_DELAY_BASE = 70;
818818

819819
config.NSFS_VERSIONING_ENABLED = true;
820820
config.NSFS_UPDATE_ISSUES_REPORT_ENABLED = true;
821-
config.NSFS_CONTENT_DIRECTORY_VERSIONING_ENABLED = false;
822821

823822
config.NSFS_EXIT_EVENTS_TIME_FRAME_MIN = 24 * 60; // per day
824823
config.NSFS_MAX_EXIT_EVENTS_PER_TIME_FRAME = 10; // allow max 10 failed forks per day

docs/design/NsfsVersioning.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,29 @@ In the figure, each original directory contains a hidden .versions/ sub- directo
5454
* When A delete marker is the latest version of an object, it indicates that the object is deleted.
5555
* A unique version ID will be allocated to the delete marker as for regular versions.
5656

57+
### content Dir versioning
58+
content dir structure has two modes:
59+
1. versioning disabled mode,at this mode xattr are located at the directory. we create .folder file inside the directory only if the object has data in it. in that case .folder file contains the objects body.
60+
example bucket tree:
61+
```
62+
bucket
63+
└── dir <= directory object (dir/), includes xattr
64+
└──.folder <= file containing dir/ object body
65+
```
66+
2. versioning enabled / suspended mode. at this mode we always create .folder file. xatrr are located at the .folder file. the transition between the disable and enabled/disabled is lazy. meaning that we only change the key structure after a put operation for that key. non-latest versions and delete markers are saved in the directories .versions directory as `.folder-<version-id>`. versioning handling is the same as for non-directory objects. only that the object file used is the `.folder` file
67+
example bucket tree:
68+
```
69+
bucket
70+
└── dir
71+
├── .folder <= contains both body (can be empty) and xattr - dir/
72+
├── key1 <= nested object - dir/key1
73+
└── .versions
74+
├── .folder_mtime-dzdz6vlnbzblk-ino-4hbc <= version of dir/
75+
├── .folder_mtime-dzdz5vlasfsaw-ino-4hia
76+
├── key1_mtime-d6u6vlnbzklc-ino-3ehc <= version of dir/key1
77+
└── key1_mtime-d6u6vnztsiyo-ino-3eik
78+
```
79+
5780
### Posix safe rename
5881

5982
#### In the following cases NooBaa will move files between a directory and its .versions/ directory:

src/sdk/namespace_fs.js

Lines changed: 32 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -736,12 +736,12 @@ class NamespaceFS {
736736
/**
737737
* @param {fs.Dirent} ent
738738
*/
739-
const process_entry = async ent => {
739+
const process_entry = async (ent, is_disabled_dir_content) => {
740740
// dbg.log0('process_entry', dir_key, ent.name);
741-
if ((!ent.name.startsWith(prefix_ent) ||
741+
if (!ent.name.startsWith(prefix_ent) ||
742742
ent.name < marker_curr ||
743743
ent.name === this.get_bucket_tmpdir_name() ||
744-
ent.name === config.NSFS_FOLDER_OBJECT_NAME) ||
744+
(ent.name === config.NSFS_FOLDER_OBJECT_NAME && is_disabled_dir_content) ||
745745
this._is_hidden_version_path(ent.name)) {
746746
return;
747747
}
@@ -756,7 +756,7 @@ class NamespaceFS {
756756
};
757757
} else {
758758
r = {
759-
key: this._get_entry_key(dir_key, ent, isDir),
759+
key: this._get_entry_key(dir_key, ent, isDir, is_disabled_dir_content),
760760
common_prefix: isDir,
761761
is_latest: true
762762
};
@@ -781,8 +781,8 @@ class NamespaceFS {
781781

782782
// insert dir object to objects list if its key is lexicographicly bigger than the key marker &&
783783
// no delimiter OR prefix is the current directory entry
784-
const is_dir_content = cached_dir.stat.xattr && cached_dir.stat.xattr[XATTR_DIR_CONTENT];
785-
if (is_dir_content && dir_key > key_marker && (!delimiter || dir_key === prefix)) {
784+
const is_disabled_dir_content = cached_dir.stat.xattr && cached_dir.stat.xattr[XATTR_DIR_CONTENT];
785+
if (is_disabled_dir_content && dir_key > key_marker && (!delimiter || dir_key === prefix)) {
786786
const r = { key: dir_key, common_prefix: false };
787787
await insert_entry_to_results_arr(r);
788788
}
@@ -835,7 +835,7 @@ class NamespaceFS {
835835
if (ent.name === config.NSFS_FOLDER_OBJECT_NAME && dir_key === marker_dir) {
836836
continue;
837837
}
838-
await process_entry(ent);
838+
await process_entry(ent, is_disabled_dir_content);
839839
// since we traverse entries in sorted order,
840840
// we can break as soon as enough keys are collected.
841841
if (is_truncated) break;
@@ -883,6 +883,17 @@ class NamespaceFS {
883883
}
884884
};
885885

886+
const format_key_name = obj_info => {
887+
if (this._is_hidden_version_path(obj_info.key)) {
888+
obj_info.key = path.normalize(obj_info.key.replace(HIDDEN_VERSIONS_PATH + '/', ''));
889+
obj_info.key = _get_filename(obj_info.key);
890+
set_latest_delete_marker(obj_info);
891+
}
892+
if (obj_info.key.endsWith(config.NSFS_FOLDER_OBJECT_NAME)) {
893+
obj_info.key = obj_info.key.slice(0, -config.NSFS_FOLDER_OBJECT_NAME.length);
894+
}
895+
};
896+
886897
const prefix_dir_key = prefix.slice(0, prefix.lastIndexOf('/') + 1);
887898
await process_dir(prefix_dir_key);
888899
await Promise.all(results.map(async r => {
@@ -908,11 +919,7 @@ class NamespaceFS {
908919
if (!list_versions && obj_info.delete_marker) {
909920
continue;
910921
}
911-
if (this._is_hidden_version_path(obj_info.key)) {
912-
obj_info.key = path.normalize(obj_info.key.replace(HIDDEN_VERSIONS_PATH + '/', ''));
913-
obj_info.key = _get_filename(obj_info.key);
914-
set_latest_delete_marker(obj_info);
915-
}
922+
format_key_name(obj_info);
916923
res.objects.push(obj_info);
917924
previous_key = obj_info.key;
918925
}
@@ -1215,7 +1222,7 @@ class NamespaceFS {
12151222
try {
12161223
await this._check_path_in_bucket_boundaries(fs_context, file_path);
12171224

1218-
if (this.should_use_empty_content_dir_optimization() && this.empty_dir_content_flow(file_path, params)) {
1225+
if (this._is_versioning_disabled() && this.empty_dir_content_flow(file_path, params)) {
12191226
const content_dir_info = await this._create_empty_dir_content(fs_context, params, file_path);
12201227
return content_dir_info;
12211228
}
@@ -1353,8 +1360,7 @@ class NamespaceFS {
13531360
const part_upload = file_path === upload_path;
13541361
const same_inode = params.copy_source && copy_res === COPY_STATUS_ENUM.SAME_INODE;
13551362
const should_replace_xattr = params.copy_source ? copy_res === COPY_STATUS_ENUM.FALLBACK : true;
1356-
const is_dir_content_optimized_flow = this._is_directory_content(file_path, params.key) &&
1357-
this.should_use_empty_content_dir_optimization();
1363+
const is_disabled_dir_content = this._is_directory_content(file_path, params.key) && this._is_versioning_disabled();
13581364

13591365
const stat = await target_file.stat(fs_context);
13601366
this._verify_encryption(params.encryption, this._get_encryption_info(stat));
@@ -1397,21 +1403,20 @@ class NamespaceFS {
13971403
});
13981404
}
13991405
}
1400-
if (fs_xattr && !is_dir_content_optimized_flow && should_replace_xattr) {
1406+
if (fs_xattr && !is_disabled_dir_content && should_replace_xattr) {
14011407
await target_file.replacexattr(fs_context, fs_xattr);
14021408
}
14031409
// fsync
14041410
if (config.NSFS_TRIGGER_FSYNC) await target_file.fsync(fs_context);
14051411
dbg.log1('NamespaceFS._finish_upload:', open_mode, file_path, upload_path, fs_xattr);
14061412

14071413
if (!same_inode && !part_upload) {
1408-
await this._move_to_dest(fs_context, upload_path, file_path, target_file, open_mode, params.key,
1409-
is_dir_content_optimized_flow);
1414+
await this._move_to_dest(fs_context, upload_path, file_path, target_file, open_mode, params.key);
14101415
}
14111416

14121417
// when object is a dir, xattr are set on the folder itself and the content is in .folder file
14131418
// we still should put the xattr if copy is link/same inode because we put the xattr on the directory
1414-
if (is_dir_content_optimized_flow) {
1419+
if (is_disabled_dir_content) {
14151420
await this._assign_dir_content_to_xattr(fs_context, fs_xattr, { ...params, size: stat.size }, copy_xattr);
14161421
}
14171422
stat.xattr = { ...stat.xattr, ...fs_xattr };
@@ -1439,14 +1444,14 @@ class NamespaceFS {
14391444
}
14401445

14411446
// move to dest GPFS (wt) / POSIX (w / undefined) - non part upload
1442-
async _move_to_dest(fs_context, source_path, dest_path, target_file, open_mode, key, is_dir_content_optimized_flow) {
1443-
dbg.log2('_move_to_dest', fs_context, source_path, dest_path, target_file, open_mode, key, is_dir_content_optimized_flow);
1447+
async _move_to_dest(fs_context, source_path, dest_path, target_file, open_mode, key) {
1448+
dbg.log2('_move_to_dest', fs_context, source_path, dest_path, target_file, open_mode, key);
14441449
let retries = config.NSFS_RENAME_RETRIES;
14451450
// will retry renaming a file in case of parallel deleting of the destination path
14461451
for (;;) {
14471452
try {
14481453
await native_fs_utils._make_path_dirs(dest_path, fs_context);
1449-
if (this._is_versioning_disabled() || is_dir_content_optimized_flow) {
1454+
if (this._is_versioning_disabled()) {
14501455
if (open_mode === 'wt') {
14511456
await target_file.linkfileat(fs_context, dest_path);
14521457
} else {
@@ -1989,7 +1994,7 @@ class NamespaceFS {
19891994
if (is_key_dir_path && !params.key.endsWith('/')) {
19901995
return {};
19911996
}
1992-
if (this._is_versioning_disabled() || (is_key_dir_path && this.should_use_empty_content_dir_optimization())) {
1997+
if (this._is_versioning_disabled()) {
19931998
// TODO- Directory object (key/) is currently can't co-exist while key (without slash) exists. see -https://github.com/noobaa/noobaa-core/issues/8320
19941999
await this._delete_single_object(fs_context, file_path, params);
19952000
} else {
@@ -2111,13 +2116,8 @@ class NamespaceFS {
21112116

21122117
async delete_object_tagging(params, object_sdk) {
21132118
dbg.log0('NamespaceFS.delete_object_tagging:', params);
2114-
let file_path;
2115-
if (params.version_id && this._is_versioning_enabled()) {
2116-
file_path = this._get_version_path(params.key, params.version_id);
2117-
} else {
2118-
file_path = this._get_file_path(params);
2119-
}
21202119
const fs_context = this.prepare_fs_context(object_sdk);
2120+
const file_path = await this._find_version_path(fs_context, params, true);
21212121
try {
21222122
await this._clear_user_xattr(fs_context, file_path, XATTR_TAG);
21232123
} catch (err) {
@@ -2133,13 +2133,8 @@ class NamespaceFS {
21332133
for (const [xattr_key, xattr_value] of Object.entries(tagging)) {
21342134
fs_xattr[XATTR_TAG + xattr_key] = xattr_value;
21352135
}
2136-
let file_path;
2137-
if (params.version_id && this._is_versioning_enabled()) {
2138-
file_path = this._get_version_path(params.key, params.version_id);
2139-
} else {
2140-
file_path = this._get_file_path(params);
2141-
}
21422136
const fs_context = this.prepare_fs_context(object_sdk);
2137+
const file_path = await this._find_version_path(fs_context, params, true);
21432138
dbg.log0('NamespaceFS.put_object_tagging: fs_xattr ', fs_xattr, 'file_path :', file_path);
21442139
try {
21452140
// remove existng tag before putting new tags
@@ -2439,8 +2434,8 @@ class NamespaceFS {
24392434
* @param {fs.Dirent} ent
24402435
* @returns {string}
24412436
*/
2442-
_get_entry_key(dir_key, ent, isDir) {
2443-
if (ent.name === config.NSFS_FOLDER_OBJECT_NAME) return dir_key;
2437+
_get_entry_key(dir_key, ent, isDir, is_disabled_dir_content) {
2438+
if (ent.name === config.NSFS_FOLDER_OBJECT_NAME && is_disabled_dir_content) return dir_key;
24442439
return dir_key + ent.name + (isDir ? '/' : '');
24452440
}
24462441

@@ -2450,7 +2445,6 @@ class NamespaceFS {
24502445
* @returns {string}
24512446
*/
24522447
_get_version_entry_key(dir_key, ent) {
2453-
if (ent.name === config.NSFS_FOLDER_OBJECT_NAME) return dir_key;
24542448
return dir_key + HIDDEN_VERSIONS_PATH + '/' + ent.name;
24552449
}
24562450

@@ -2765,9 +2759,6 @@ class NamespaceFS {
27652759
return is_dir_content && params.size === 0;
27662760
}
27672761

2768-
should_use_empty_content_dir_optimization() {
2769-
return this._is_versioning_disabled() || !config.NSFS_CONTENT_DIRECTORY_VERSIONING_ENABLED;
2770-
}
27712762
/**
27722763
* returns if should force md5 calculation for the bucket/account.
27732764
* first check if defined for bucket / account, if not use global default

src/test/unit_tests/coretest.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ config.test_mode = true;
2727
config.NODES_FREE_SPACE_RESERVE = 10 * 1024 * 1024;
2828
config.NSFS_VERSIONING_ENABLED = true;
2929
config.OBJECT_SDK_BUCKET_CACHE_EXPIRY_MS = 1;
30-
config.NSFS_CONTENT_DIRECTORY_VERSIONING_ENABLED = true;
3130

3231
config.ROOT_KEY_MOUNT = '/tmp/noobaa-server/root_keys';
3332

src/test/unit_tests/jest_tests/test_versioning_concurrency.test.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ const buffer_utils = require('../../../util/buffer_utils');
1010
const { TMP_PATH, IS_GPFS, TEST_TIMEOUT } = require('../../system_tests/test_utils');
1111
const { crypto_random_string } = require('../../../util/string_utils');
1212
const endpoint_stats_collector = require('../../../sdk/endpoint_stats_collector');
13-
const config = require('../../../../config');
14-
config.NSFS_CONTENT_DIRECTORY_VERSIONING_ENABLED = true;
1513

1614
function make_dummy_object_sdk(nsfs_config, uid, gid) {
1715
return {
@@ -580,8 +578,7 @@ describe('test versioning concurrency', () => {
580578
expect(successful_operations).toHaveLength(num_of_concurrency);
581579
expect(failed_operations).toHaveLength(0);
582580
const versions = await nsfs.list_object_versions({ bucket: bucket }, DUMMY_OBJECT_SDK);
583-
//TODO should be num_of_concurrency + 1 (the null version). list-object-version currently ignores the latest .folder file
584-
expect(versions.objects.length).toBe(num_of_concurrency);
581+
expect(versions.objects.length).toBe(num_of_concurrency + 1);
585582
}, TEST_TIMEOUT);
586583
});
587584

src/test/unit_tests/nc_coretest.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,6 @@ async function config_dir_setup() {
205205
// DO NOT CHANGE - setting VACCUM_ANALYZER_INTERVAL=1 needed for failing the tests
206206
// in case where vaccumAnalyzer is being called before setting process.env.NC_NSFS_NO_DB_ENV = 'true' on nsfs.js
207207
VACCUM_ANALYZER_INTERVAL: 1,
208-
NSFS_CONTENT_DIRECTORY_VERSIONING_ENABLED: true
209208
}));
210209
await fs.promises.mkdir(FIRST_BUCKET_PATH, { recursive: true });
211210
}

src/test/unit_tests/test_bucketspace_versioning.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ mocha.describe('bucketspace namespace_fs - versioning', function() {
451451
await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: head_object_key, Body: body1 });
452452
await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: get_object_key, Body: body1 });
453453
await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: delete_latest_object_key, Body: body1 });
454+
await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: delete_latest_object_key_suspended, Body: body1 });
454455

455456
await s3_uid6.putBucketVersioning({ Bucket: content_dir_bucket_name, VersioningConfiguration: { MFADelete: 'Disabled', Status: 'Enabled' } });
456457
});
@@ -639,7 +640,63 @@ mocha.describe('bucketspace namespace_fs - versioning', function() {
639640

640641
const get_res = await s3_uid6.getObject({ Bucket: content_dir_bucket_name, Key: key });
641642
assert.equal(get_res.VersionId, res2.VersionId);
643+
});
644+
645+
mocha.it('content directory - object tagging', async function() {
646+
const dir_tagging_key = 'dir_tagging/';
647+
const tag_set1 = { TagSet: [{ Key: "key1", Value: "Value1" }] };
648+
const tag_set2 = { TagSet: [{ Key: "key2", Value: "Value2" }] };
642649

650+
await s3_uid6.putBucketVersioning({ Bucket: content_dir_bucket_name, VersioningConfiguration: { MFADelete: 'Disabled', Status: 'Enabled' } });
651+
const res_put = await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: dir_tagging_key, Body: body1 });
652+
await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: dir_tagging_key, Body: body1 });
653+
const version_id = res_put.VersionId;
654+
655+
await s3_uid6.putObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key, Tagging: tag_set1 });
656+
let res = await s3_uid6.getObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key });
657+
assert.deepEqual(res.TagSet, tag_set1.TagSet);
658+
659+
await s3_uid6.putObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key,
660+
Tagging: tag_set2, VersionId: version_id });
661+
res = await s3_uid6.getObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key });
662+
assert.notDeepEqual(res.TagSet, tag_set2);
663+
664+
const version_res = await s3_uid6.getObjectTagging({ Bucket: content_dir_bucket_name,
665+
Key: dir_tagging_key, VersionId: version_id });
666+
assert.deepEqual(version_res.TagSet, tag_set2.TagSet);
667+
668+
await s3_uid6.deleteObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key });
669+
res = await s3_uid6.getObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key });
670+
assert.equal(res.TagSet.length, 0);
671+
672+
await s3_uid6.deleteObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key, VersionId: version_id });
673+
res = await s3_uid6.getObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key, VersionId: version_id });
674+
assert.equal(res.TagSet.length, 0);
675+
});
676+
677+
mocha.it('content directory - list object', async function() {
678+
const res = await s3_uid6.listObjects({ Bucket: content_dir_bucket_name });
679+
assert.equal(res.Contents.length, 10);
680+
});
681+
682+
mocha.it('content directory - list object versions', async function() {
683+
const key = "list_key/";
684+
const version_ids = new Set();
685+
let push_res = await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: key, Body: body1 });
686+
version_ids.add(push_res.VersionId);
687+
push_res = await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: key, Body: body1 });
688+
version_ids.add(push_res.VersionId);
689+
push_res = await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: key, Body: body1 });
690+
version_ids.add(push_res.VersionId);
691+
const list_res = await s3_uid6.listObjectVersions({ Bucket: content_dir_bucket_name });
692+
const key_list_versions = list_res.Versions.filter(v => v.Key === key);
693+
assert.equal(key_list_versions.length, version_ids.size);
694+
key_list_versions.forEach(v => {
695+
assert(version_ids.has(v.VersionId));
696+
if (v.VersionId === push_res.VersionId) {
697+
assert.equal(v.IsLatest, true);
698+
}
699+
});
643700
});
644701
});
645702

src/test/unit_tests/test_namespace_fs.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ const DEFAULT_FS_CONFIG = get_process_fs_context();
4242
const empty_data = crypto.randomBytes(0);
4343
const empty_stream = () => buffer_utils.buffer_to_read_stream(empty_data);
4444

45-
config.NSFS_CONTENT_DIRECTORY_VERSIONING_ENABLED = true;
46-
4745
function make_dummy_object_sdk(config_root) {
4846
return {
4947
requesting_account: {

0 commit comments

Comments
 (0)