Skip to content

Commit 0d2edbc

Browse files
authored
Merge pull request #8642 from shirady/nsfs-nc-account-id-cache-with-stat
NC | NSFS | Add `stat` to `account_id_cache`
2 parents 380db50 + 12c6d90 commit 0d2edbc

File tree

5 files changed

+210
-14
lines changed

5 files changed

+210
-14
lines changed

config.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -635,16 +635,24 @@ config.REMOTE_NOOAA_NAMESPACE = `remote-${config.KUBE_APP_LABEL}`;
635635
///////////////////////////////
636636
config.INLINE_MAX_SIZE = 4096;
637637

638+
///////////////////////////////
639+
// CACHE (ACCOUNT, BUCKET) //
640+
///////////////////////////////
641+
638642
// Object SDK bucket cache expiration time
639643
config.OBJECT_SDK_BUCKET_CACHE_EXPIRY_MS = 60000;
640644
// Object SDK account cache expiration time
641645
config.OBJECT_SDK_ACCOUNT_CACHE_EXPIRY_MS = Number(process.env.ACCOUNTS_CACHE_EXPIRY) || 10 * 60 * 1000; // TODO: Decide on a time that we want to invalidate
646+
// Accountspace_fs account id cache expiration time
647+
config.ACCOUNTS_ID_CACHE_EXPIRY = 3 * 60 * 1000; // TODO: Decide on a time that we want to invalidate
642648

643649

644650
// Object SDK bucket_namespace_cache allow stat of the config file
645651
config.NC_ENABLE_BUCKET_NS_CACHE_STAT_VALIDATION = true;
646652
// Object SDK account_cache allow stat of the config file
647653
config.NC_ENABLE_ACCOUNT_CACHE_STAT_VALIDATION = true;
654+
// accountspace_fs allow stat of the config file
655+
config.NC_ENABLE_ACCOUNT_ID_CACHE_STAT_VALIDATION = true;
648656

649657
//////////////////////////////
650658
// OPERATOR RELATED //
@@ -932,7 +940,6 @@ config.NC_DISABLE_HEALTH_ACCESS_CHECK = false;
932940
config.NC_DISABLE_POSIX_MODE_ACCESS_CHECK = true;
933941
config.NC_DISABLE_SCHEMA_CHECK = false;
934942

935-
config.ACCOUNTS_ID_CACHE_EXPIRY = 3 * 60 * 1000;
936943
////////// GPFS //////////
937944
config.GPFS_DOWN_DELAY = 1000;
938945

src/sdk/accountspace_fs.js

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,61 @@ const dummy_service_name = 's3';
2828

2929
const account_id_cache = new LRUCache({
3030
name: 'AccountIDCache',
31-
// TODO: Decide on a time that we want to invalidate
32-
expiry_ms: Number(config.ACCOUNTS_ID_CACHE_EXPIRY),
31+
expiry_ms: config.ACCOUNTS_ID_CACHE_EXPIRY,
3332
make_key: ({ _id }) => _id,
3433
/**
3534
* Accounts are added to the cache based on id, Default value for show_secrets and decrypt_secret_key will be true,
3635
* and show_secrets and decrypt_secret_key `false` only when we load cache from the health script,
3736
* health script doesn't need to fetch or decrypt the secret.
38-
* @param {{ _id: string,
39-
* show_secrets?: boolean,
40-
* decrypt_secret_key?: boolean,
41-
* config_fs: import('./config_fs').ConfigFS }} params
37+
* @param {{
38+
* _id: string,
39+
* show_secrets?: boolean,
40+
* decrypt_secret_key?: boolean,
41+
* config_fs: ConfigFS
42+
* }} params
4243
*/
43-
load: async ({ _id, show_secrets = true, decrypt_secret_key = true, config_fs}) =>
44-
config_fs.get_identity_by_id(_id, CONFIG_TYPES.ACCOUNT, { show_secrets: show_secrets, decrypt_secret_key: decrypt_secret_key }),
44+
load: async ({ _id, show_secrets = true, decrypt_secret_key = true, config_fs }) =>
45+
config_fs.get_identity_by_id_and_stat_file(
46+
_id, CONFIG_TYPES.ACCOUNT, { show_secrets: show_secrets, decrypt_secret_key: decrypt_secret_key }),
47+
validate: (params, data) => _validate_account_id(params, data),
4548
});
4649

50+
/** _validate_account_id is an additional layer (to expiry_ms)
51+
* to checks the stat of the config file
52+
* @param {object} data
53+
* @param {object} params
54+
* @returns Promise<{boolean>}
55+
*/
56+
async function _validate_account_id(params, data) {
57+
if (config.NC_ENABLE_ACCOUNT_ID_CACHE_STAT_VALIDATION) {
58+
const same_stat = await check_same_stat_account(params._id, params.name, params.stat, data.config_fs);
59+
if (!same_stat) { // config file of account was changed
60+
return false;
61+
}
62+
}
63+
return true;
64+
}
65+
66+
/**
67+
* check_same_stat_account will return true the config file was not changed
68+
* in case we had any issue (for example error during stat) the returned value will be undefined
69+
* @param {string} id
70+
* @param {string} account_name
71+
* @param {nb.NativeFSStats} account_stat
72+
* @param {ConfigFS} config_fs
73+
* @returns Promise<{boolean|undefined>}
74+
*/
75+
async function check_same_stat_account(id, account_name, account_stat, config_fs) {
76+
if (!config_fs) return;
77+
try {
78+
const current_stat = await config_fs.stat_account_config_file_by_identity(id, account_name);
79+
if (current_stat) {
80+
return current_stat.ino === account_stat.ino && current_stat.mtimeNsBigint === account_stat.mtimeNsBigint;
81+
}
82+
} catch (err) {
83+
dbg.warn('check_same_stat_account: current_stat got an error', err, 'ignoring...');
84+
}
85+
}
4786

4887
/**
4988
* @param {Object} requested_account

src/sdk/config_fs.js

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,29 @@ class ConfigFS {
378378
return identity;
379379
}
380380

381+
/**
382+
* get_identity_by_id_and_stat_file returns the full account/user data and stat the file:
383+
* @param {string} id
384+
* @param {string} [type]
385+
* @param {{show_secrets?: boolean, decrypt_secret_key?: boolean, silent_if_missing?: boolean}} [options]
386+
* @returns {Promise<Object>}
387+
*/
388+
async get_identity_by_id_and_stat_file(id, type, options = {}) {
389+
let identity;
390+
try {
391+
identity = await this.get_identity_by_id(id, type, options);
392+
if (!identity && options.silent_if_missing) {
393+
return undefined; // we don't have the identity, so we can't add the property of stat to it
394+
}
395+
const stat = await this.stat_account_config_file_by_identity(id, identity.name);
396+
identity.stat = stat;
397+
return identity; // this identity object should have also a stat property
398+
} catch (err) {
399+
dbg.error('get_identity_by_id_and_stat_file: got an error', err);
400+
throw err;
401+
}
402+
}
403+
381404
/**
382405
* search_accounts_by_id searches old accounts directory and finds an account that its _id matches the given id param
383406
* @param {string} id
@@ -450,7 +473,7 @@ class ConfigFS {
450473
}
451474

452475
/**
453-
* stat_account_config_file will return the stat output on account config file
476+
* stat_account_config_file will return the stat output on account config file by access key
454477
* please notice that stat might throw an error - you should wrap it with try-catch and handle the error
455478
* Note: access_key type of anonymous_access_key is a symbol, otherwise it is a string (not SensitiveString)
456479
* @param {Symbol|string} access_key
@@ -468,6 +491,38 @@ class ConfigFS {
468491
return nb_native().fs.stat(this.fs_context, path_for_account_or_user_config_file);
469492
}
470493

494+
/**
495+
* stat_account_config_file_by_identity will return the stat output on account config file by id or by account name
496+
* 1. try by identity path
497+
* 2. if not found - try by account name with new accounts path
498+
* 3. if not found - try by account name with old accounts path
499+
* 4. else throw an error
500+
* @param {string} id
501+
* @param {string} account_name
502+
* @returns {Promise<nb.NativeFSStats>}
503+
*/
504+
async stat_account_config_file_by_identity(id, account_name) {
505+
const options = { silent_if_missing: true };
506+
507+
const identity_path = this.get_identity_path_by_id(id);
508+
dbg.log2('stat_account_config_file_by_identity: will try to stat by identity (identities path)');
509+
let stat = await this.stat_config_file_general_use(identity_path, options);
510+
if (stat) return stat;
511+
512+
dbg.log2('stat_account_config_file_by_identity: will try to stat by account name (new accounts path)');
513+
const account_name_new_path = this.get_account_path_by_name(account_name);
514+
stat = await this.stat_config_file_general_use(account_name_new_path, options);
515+
if (stat) return stat;
516+
517+
dbg.log2('stat_account_config_file_by_identity: will try to stat by account name (old accounts path)');
518+
const account_name_old_path = this._get_old_account_path_by_name(account_name);
519+
stat = await this.stat_config_file_general_use(account_name_old_path, options);
520+
if (stat) return stat;
521+
522+
dbg.error(`stat_account_config_file_by_identity: could not stat identity by id ${id} or by account name ${account_name} (new and old accounts path)`);
523+
throw new Error('Could not stat account (identities path, new accounts path and old accounts path)');
524+
}
525+
471526
/**
472527
* is_account_exists_by_name returns true if account config path exists in config dir
473528
* if account does not exist and it's a regular account (not an IAM user)
@@ -1255,6 +1310,24 @@ class ConfigFS {
12551310
get_hosts_data(system_data) {
12561311
return _.omit(system_data, 'config_directory');
12571312
}
1313+
1314+
/** stat_config_file_general_use will stat a file by a path
1315+
* if the file is not found:
1316+
* - using the options with silent_if_missing true on ENOENT it would return undefined
1317+
* - else it would throw an error
1318+
* @param {string} path_to_stat
1319+
* @param {{ silent_if_missing: any; }} options
1320+
*/
1321+
async stat_config_file_general_use(path_to_stat, options) {
1322+
try {
1323+
const stat_by_identity_path = await nb_native().fs.stat(this.fs_context, path_to_stat);
1324+
return stat_by_identity_path;
1325+
} catch (err) {
1326+
if (err.code === 'ENOENT' && options.silent_if_missing) return;
1327+
dbg.error('stat_account_config_file_by_identity: could not stat by identity ID, got an error', err);
1328+
throw err;
1329+
}
1330+
}
12581331
}
12591332

12601333
// EXPORTS

src/sdk/object_sdk.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const BucketSpaceNB = require('./bucketspace_nb');
2727
const { RpcError } = require('../rpc');
2828

2929
const anonymous_access_key = Symbol('anonymous_access_key');
30+
3031
const bucket_namespace_cache = new LRUCache({
3132
name: 'ObjectSDK-Bucket-Namespace-Cache',
3233
// This is intentional. Cache entry expiration is handled by _validate_bucket_namespace().
@@ -93,7 +94,7 @@ async function _validate_account(data, params) {
9394
const bs_allow_stat_account = Boolean(bs.check_same_stat_account);
9495
if (bs_allow_stat_account && config.NC_ENABLE_ACCOUNT_CACHE_STAT_VALIDATION) {
9596
const same_stat = await bs.check_same_stat_account(params.access_key, data.stat);
96-
if (!same_stat) { // config file of bucket was changed
97+
if (!same_stat) { // config file of account was changed
9798
return false;
9899
}
99100
}

src/test/unit_tests/test_nc_with_a_couple_of_forks.js

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ const P = require('../../util/promise');
99
const mocha = require('mocha');
1010
const assert = require('assert');
1111
const fs_utils = require('../../util/fs_utils');
12-
const { TMP_PATH, generate_nsfs_account, get_new_buckets_path_by_test_env, generate_s3_client,
13-
get_coretest_path, exec_manage_cli } = require('../system_tests/test_utils');
12+
const { TMP_PATH, generate_nsfs_account, get_new_buckets_path_by_test_env, generate_s3_client, get_coretest_path, exec_manage_cli } = require('../system_tests/test_utils');
1413
const { TYPES, ACTIONS } = require('../../manage_nsfs/manage_nsfs_constants');
1514
const ManageCLIResponse = require('../../manage_nsfs/manage_nsfs_cli_responses').ManageCLIResponse;
1615

1716
const coretest_path = get_coretest_path();
1817
const coretest = require(coretest_path);
1918
const setup_options = { forks: 2, debug: 5 };
2019
coretest.setup(setup_options);
21-
const { rpc_client, EMAIL, get_current_setup_options, stop_nsfs_process, start_nsfs_process, config_dir_name } = coretest;
20+
const { rpc_client, EMAIL, get_current_setup_options, stop_nsfs_process, start_nsfs_process,
21+
config_dir_name, NC_CORETEST_CONFIG_FS, NC_CORETEST_STORAGE_PATH } = coretest;
2222

2323
const CORETEST_ENDPOINT = coretest.get_http_address();
2424

@@ -52,6 +52,7 @@ mocha.describe('operations with a couple of forks', async function() {
5252

5353
mocha.after(async () => {
5454
fs_utils.folder_delete(`${config_root}`);
55+
fs_utils.folder_delete(`${new_bucket_path_param}`);
5556
});
5657

5758
mocha.it('versioning change with a couple of forks', async function() {
@@ -147,4 +148,79 @@ mocha.describe('operations with a couple of forks', async function() {
147148
// cleanup
148149
await s3_uid5_after_access_keys_update.deleteBucket({ Bucket: bucket_name2 });
149150
});
151+
152+
mocha.it('head a bucket after account update (change fs_backend)', async function() {
153+
// create additional account
154+
const account_name = 'Oliver';
155+
const account_options_create = { account_name, uid: 6001, gid: 6001, config_root: config_dir_name };
156+
await fs_utils.create_fresh_path(new_bucket_path_param);
157+
await fs.promises.chown(new_bucket_path_param, account_options_create.uid, account_options_create.gid);
158+
await fs.promises.chmod(new_bucket_path_param, 0o700);
159+
const access_details = await generate_nsfs_account(rpc_client, EMAIL, new_bucket_path_param, account_options_create);
160+
// check the account status
161+
const account_options_status = { config_root: config_dir_name, name: account_name};
162+
const res_account_status = await exec_manage_cli(TYPES.ACCOUNT, ACTIONS.STATUS, account_options_status);
163+
assert.equal(JSON.parse(res_account_status).response.code, ManageCLIResponse.AccountStatus.code);
164+
// generate the s3 client
165+
const s3_uid6001 = generate_s3_client(access_details.access_key,
166+
access_details.secret_key, CORETEST_ENDPOINT);
167+
// check the connection for the new account (can be any of the forks)
168+
const res_list_buckets = await s3_uid6001.listBuckets({});
169+
assert.equal(res_list_buckets.$metadata.httpStatusCode, 200);
170+
// create a bucket
171+
const bucket_name3 = 'bucket3';
172+
const res_bucket_create = await s3_uid6001.createBucket({ Bucket: bucket_name3 });
173+
assert.equal(res_bucket_create.$metadata.httpStatusCode, 200);
174+
// head the bucket
175+
const res_head_bucket1 = await s3_uid6001.headBucket({Bucket: bucket_name3});
176+
assert.equal(res_head_bucket1.$metadata.httpStatusCode, 200);
177+
// update the account
178+
const account_options_update = { config_root: config_dir_name, name: account_name, fs_backend: 'GPFS'};
179+
const res_account_update = await exec_manage_cli(TYPES.ACCOUNT, ACTIONS.UPDATE, account_options_update);
180+
assert.equal(JSON.parse(res_account_update).response.code, ManageCLIResponse.AccountUpdated.code);
181+
// head the bucket (again)
182+
const res_head_bucket2 = await s3_uid6001.headBucket({Bucket: bucket_name3});
183+
assert.equal(res_head_bucket2.$metadata.httpStatusCode, 200);
184+
185+
// cleanup
186+
await s3_uid6001.deleteBucket({ Bucket: bucket_name3 });
187+
});
188+
189+
mocha.it('create a bucket after account update (change buckets_path)', async function() {
190+
// create an additional account
191+
const account_name = 'John';
192+
const account_options_create = { account_name, uid: 7001, gid: 7001, config_root: config_dir_name };
193+
// reuse NC_CORETEST_STORAGE_PATH as new_buckets_path (no need for create fresh path, chmod and chown)
194+
const access_details = await generate_nsfs_account(rpc_client, EMAIL, NC_CORETEST_STORAGE_PATH, account_options_create);
195+
// check the account status
196+
const account_options_status = { config_root: config_dir_name, name: account_name};
197+
const res_account_status = await exec_manage_cli(TYPES.ACCOUNT, ACTIONS.STATUS, account_options_status);
198+
assert.equal(JSON.parse(res_account_status).response.code, ManageCLIResponse.AccountStatus.code);
199+
// generate the s3 client
200+
const s3_uid6001 = generate_s3_client(access_details.access_key,
201+
access_details.secret_key, CORETEST_ENDPOINT);
202+
// check the connection for the new account (can be any of the forks)
203+
const res_list_buckets = await s3_uid6001.listBuckets({});
204+
assert.equal(res_list_buckets.$metadata.httpStatusCode, 200);
205+
// update the account - change its new_bucket_path
206+
const new_bucket_path_param2 = path.join(TMP_PATH, 'nc_coretest_storage_root_path2/');
207+
await fs_utils.create_fresh_path(new_bucket_path_param2);
208+
await fs.promises.chown(new_bucket_path_param2, account_options_create.uid, account_options_create.gid);
209+
await fs.promises.chmod(new_bucket_path_param2, 0o700);
210+
const account_options_update = { config_root: config_dir_name, name: account_name, new_buckets_path: new_bucket_path_param2};
211+
const res_account_update = await exec_manage_cli(TYPES.ACCOUNT, ACTIONS.UPDATE, account_options_update);
212+
assert.equal(JSON.parse(res_account_update).response.code, ManageCLIResponse.AccountUpdated.code);
213+
// create a bucket
214+
const bucket_name4 = 'bucket4';
215+
const res_bucket_create = await s3_uid6001.createBucket({ Bucket: bucket_name4 });
216+
assert.equal(res_bucket_create.$metadata.httpStatusCode, 200);
217+
// validate the bucket was created in the updated path
218+
const bucket4 = await NC_CORETEST_CONFIG_FS.get_bucket_by_name(bucket_name4);
219+
const expected_bucket_path = path.join(new_bucket_path_param2, bucket_name4);
220+
assert.equal(bucket4.path, expected_bucket_path);
221+
222+
// cleanup
223+
await s3_uid6001.deleteBucket({ Bucket: bucket_name4 });
224+
await fs.promises.rm(new_bucket_path_param2, { recursive: true });
225+
});
150226
});

0 commit comments

Comments
 (0)