Skip to content

Commit 08fe0a3

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

8 files changed

+219
-76
lines changed

config.js

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

824824
config.NSFS_VERSIONING_ENABLED = true;
825825
config.NSFS_UPDATE_ISSUES_REPORT_ENABLED = true;
826-
config.NSFS_CONTENT_DIRECTORY_VERSIONING_ENABLED = false;
827826

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

docs/design/NsfsVersioning.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,36 @@ 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+
### Directory Object / Content Directory Versioning
58+
Versioning related xattr -
59+
1. `user.noobaa.version_id` - stores the version_id string.
60+
2. `user.noobaa.delete_marker` - boolean.
61+
3. `user.noobaa.content_dir` - stores the size of the directory object.
62+
63+
Directory object / content directory filesystem structure has two modes -
64+
65+
1. versioning disabled mode -
66+
On this mode, the object body and the xattr (extended attributes) are splitted. The data of the directory object will be stored in `<dir_path>/.folder`, while the xattr of the directory object will be stored on the directory itself - <dir_path>. `<dir_path>/.folder` file is created only if the object's data size > 0. When size = 0, only the xattr will be stored in `<dir_path>`.
67+
```
68+
bucket
69+
└── dir <= directory object (dir/), includes xattr
70+
└──.folder <= file containing dir/ object body
71+
```
72+
2. versioning enabled / suspended mode -
73+
On 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
74+
example bucket tree:
75+
```
76+
bucket
77+
└── dir
78+
├── .folder <= contains both body (can be empty) and xattr - dir/
79+
├── key1 <= nested object - dir/key1
80+
└── .versions
81+
├── .folder_mtime-dzdz6vlnbzblk-ino-4hbc <= version of dir/
82+
├── .folder_mtime-dzdz5vlasfsaw-ino-4hia
83+
├── key1_mtime-d6u6vlnbzklc-ino-3ehc <= version of dir/key1
84+
└── key1_mtime-d6u6vnztsiyo-ino-3eik
85+
```
86+
5787
### Posix safe rename
5888

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

src/sdk/namespace_fs.js

Lines changed: 54 additions & 63 deletions
Large diffs are not rendered by 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 {
@@ -635,8 +633,7 @@ describe('test versioning concurrency', () => {
635633
expect(successful_operations).toHaveLength(num_of_concurrency);
636634
expect(failed_operations).toHaveLength(0);
637635
const versions = await nsfs.list_object_versions({ bucket: bucket }, DUMMY_OBJECT_SDK);
638-
//TODO should be num_of_concurrency + 1 (the null version). list-object-version currently ignores the latest .folder file
639-
expect(versions.objects.length).toBe(num_of_concurrency);
636+
expect(versions.objects.length).toBe(num_of_concurrency + 1);
640637
}, TEST_TIMEOUT);
641638
});
642639

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: 134 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const { TMP_PATH, IS_GPFS, is_nc_coretest, set_path_permissions_and_owner, gener
1616
invalid_nsfs_root_permissions, generate_s3_client, get_coretest_path } = require('../system_tests/test_utils');
1717
const { get_process_fs_context } = require('../../util/native_fs_utils');
1818
const _ = require('lodash');
19+
const config = require('../../../config');
1920

2021
const coretest_path = get_coretest_path();
2122
const coretest = require(coretest_path);
@@ -452,6 +453,7 @@ mocha.describe('bucketspace namespace_fs - versioning', function() {
452453
await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: head_object_key, Body: body1 });
453454
await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: get_object_key, Body: body1 });
454455
await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: delete_latest_object_key, Body: body1 });
456+
await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: delete_latest_object_key_suspended, Body: body1 });
455457

456458
await s3_uid6.putBucketVersioning({ Bucket: content_dir_bucket_name, VersioningConfiguration: { MFADelete: 'Disabled', Status: 'Enabled' } });
457459
});
@@ -640,7 +642,135 @@ mocha.describe('bucketspace namespace_fs - versioning', function() {
640642

641643
const get_res = await s3_uid6.getObject({ Bucket: content_dir_bucket_name, Key: key });
642644
assert.equal(get_res.VersionId, res2.VersionId);
645+
});
646+
647+
mocha.it('content directory - object tagging - versioning enabled', async function() {
648+
const dir_tagging_key = 'dir_tagging/';
649+
const tag_set1 = { TagSet: [{ Key: "key1", Value: "Value1" }] };
650+
const tag_set2 = { TagSet: [{ Key: "key2", Value: "Value2" }] };
651+
652+
await s3_uid6.putBucketVersioning({ Bucket: content_dir_bucket_name, VersioningConfiguration: { MFADelete: 'Disabled', Status: 'Enabled' } });
653+
const res_put = await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: dir_tagging_key, Body: body1 });
654+
await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: dir_tagging_key, Body: body1 });
655+
const version_id = res_put.VersionId;
656+
657+
await s3_uid6.putObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key, Tagging: tag_set1 });
658+
let res = await s3_uid6.getObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key });
659+
assert.deepEqual(res.TagSet, tag_set1.TagSet);
660+
661+
await s3_uid6.putObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key,
662+
Tagging: tag_set2, VersionId: version_id });
663+
res = await s3_uid6.getObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key });
664+
assert.notDeepEqual(res.TagSet, tag_set2);
665+
666+
const version_res = await s3_uid6.getObjectTagging({ Bucket: content_dir_bucket_name,
667+
Key: dir_tagging_key, VersionId: version_id });
668+
assert.deepEqual(version_res.TagSet, tag_set2.TagSet);
669+
670+
await s3_uid6.deleteObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key });
671+
res = await s3_uid6.getObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key });
672+
assert.equal(res.TagSet.length, 0);
673+
674+
await s3_uid6.deleteObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key, VersionId: version_id });
675+
res = await s3_uid6.getObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key, VersionId: version_id });
676+
assert.equal(res.TagSet.length, 0);
677+
});
678+
679+
mocha.it('content directory - object tagging - versioning suspended', async function() {
680+
const dir_tagging_key = 'dir_tagging/';
681+
const tag_set1 = { TagSet: [{ Key: "key1", Value: "Value1" }] };
682+
const tag_set2 = { TagSet: [{ Key: "key2", Value: "Value2" }] };
683+
684+
await s3_uid6.putBucketVersioning({ Bucket: content_dir_bucket_name, VersioningConfiguration: { MFADelete: 'Disabled', Status: 'Suspended' } });
685+
const res_put = await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: dir_tagging_key, Body: body1 });
686+
await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: dir_tagging_key, Body: body1 });
687+
const version_id = res_put.VersionId;
688+
689+
await s3_uid6.putObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key, Tagging: tag_set1 });
690+
let res = await s3_uid6.getObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key });
691+
assert.deepEqual(res.TagSet, tag_set1.TagSet);
692+
693+
await s3_uid6.putObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key,
694+
Tagging: tag_set2, VersionId: version_id });
695+
res = await s3_uid6.getObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key });
696+
assert.notDeepEqual(res.TagSet, tag_set2);
643697

698+
const version_res = await s3_uid6.getObjectTagging({ Bucket: content_dir_bucket_name,
699+
Key: dir_tagging_key, VersionId: version_id });
700+
assert.deepEqual(version_res.TagSet, tag_set2.TagSet);
701+
702+
await s3_uid6.deleteObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key });
703+
res = await s3_uid6.getObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key });
704+
assert.equal(res.TagSet.length, 0);
705+
706+
await s3_uid6.deleteObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key, VersionId: version_id });
707+
res = await s3_uid6.getObjectTagging({ Bucket: content_dir_bucket_name, Key: dir_tagging_key, VersionId: version_id });
708+
assert.equal(res.TagSet.length, 0);
709+
});
710+
711+
mocha.it('content directory - list object', async function() {
712+
const res = await s3_uid6.listObjects({ Bucket: content_dir_bucket_name });
713+
assert.equal(res.Contents.length, 10);
714+
});
715+
716+
mocha.it('content directory - list object versions - enabled mode', async function() {
717+
await s3_uid6.putBucketVersioning({ Bucket: content_dir_bucket_name, VersioningConfiguration: { MFADelete: 'Disabled', Status: 'Enabled' } });
718+
const key = "list_key/";
719+
const version_ids = new Set();
720+
let push_res = await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: key, Body: body1 });
721+
version_ids.add(push_res.VersionId);
722+
push_res = await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: key, Body: body1 });
723+
version_ids.add(push_res.VersionId);
724+
push_res = await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: key, Body: body1 });
725+
version_ids.add(push_res.VersionId);
726+
const list_res = await s3_uid6.listObjectVersions({ Bucket: content_dir_bucket_name });
727+
const key_list_versions = list_res.Versions.filter(v => v.Key === key);
728+
assert.equal(key_list_versions.length, version_ids.size);
729+
key_list_versions.forEach(v => {
730+
assert(version_ids.has(v.VersionId));
731+
if (v.VersionId === push_res.VersionId) {
732+
assert.equal(v.IsLatest, true);
733+
assert(!v.Key.includes(config.NSFS_FOLDER_OBJECT_NAME));
734+
assert(!v.VersionId.includes(config.NSFS_FOLDER_OBJECT_NAME));
735+
}
736+
});
737+
});
738+
739+
mocha.it('content directory - list object versions - delete marker', async function() {
740+
const key = "list_key/";
741+
const delete_res = await s3_uid6.deleteObject({ Bucket: content_dir_bucket_name, Key: key });
742+
const list_res = await s3_uid6.listObjectVersions({ Bucket: content_dir_bucket_name });
743+
const key_list_versions = list_res.Versions.filter(v => v.Key === key);
744+
assert.equal(key_list_versions.length, 3);
745+
assert.equal(list_res.DeleteMarkers.length, 3); // 2 previous delete markers + 1 new delete marker
746+
let is_delete_marker_present = false;
747+
for (const delete_marker of list_res.DeleteMarkers) {
748+
if (delete_marker.versionId === delete_res.created_version_id) {
749+
is_delete_marker_present = true;
750+
assert.equal(delete_marker.versionId, delete_res.created_version_id);
751+
assert.equal(delete_marker.IsLatest, true);
752+
assert(!delete_marker.Key.includes(config.NSFS_FOLDER_OBJECT_NAME));
753+
assert(!delete_marker.VersionId.includes(config.NSFS_FOLDER_OBJECT_NAME));
754+
}
755+
}
756+
assert(is_delete_marker_present);
757+
});
758+
759+
mocha.it('content directory - list object versions - suspended mode', async function() {
760+
const key = "list_key/";
761+
await s3_uid6.putBucketVersioning({ Bucket: content_dir_bucket_name, VersioningConfiguration: { MFADelete: 'Disabled', Status: 'Suspended' } });
762+
const push_res = await s3_uid6.putObject({ Bucket: content_dir_bucket_name, Key: key, Body: body1 });
763+
const list_res = await s3_uid6.listObjectVersions({ Bucket: content_dir_bucket_name });
764+
const key_list_versions = list_res.Versions.filter(v => v.Key === key);
765+
assert.equal(key_list_versions.length, 4); //3 from before + 1 new key
766+
key_list_versions.forEach(v => {
767+
if (v.VersionId === push_res.VersionId) {
768+
assert.equal(v.VersionId, 'null');
769+
assert.equal(v.IsLatest, true);
770+
assert(!v.key.includes(config.NSFS_FOLDER_OBJECT_NAME));
771+
assert(!v.VersionId.includes(config.NSFS_FOLDER_OBJECT_NAME));
772+
}
773+
});
644774
});
645775
});
646776

@@ -3419,10 +3549,10 @@ function _extract_version_info_from_xattr(version_id_str) {
34193549

34203550
/**
34213551
* version_file_exists returns path of version in .versions
3422-
* @param {String} full_path
3423-
* @param {String} key
3424-
* @param {String} dir
3425-
* @param {String} version_id
3552+
* @param {String} full_path
3553+
* @param {String} key
3554+
* @param {String} dir
3555+
* @param {String} version_id
34263556
* @returns {Promise<Boolean>}
34273557
*/
34283558
async function version_file_exists(full_path, key, dir, version_id) {

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)