Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions conf/locationConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,12 @@
"legacyAwsBehavior": false,
"isCold": true,
"details": {}
},
"location-crr-source": {
"type": "scality",
"objectId": "location-crr-source",
"legacyAwsBehavior": false,
"isCRR": true,
"details": {}
}
}
7 changes: 3 additions & 4 deletions extensions/gc/tasks/GarbageCollectorTask.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { ObjectMD } = require('arsenal').models;
const BackbeatTask = require('../../../lib/tasks/BackbeatTask');
const { BatchDeleteCommand } = require('@scality/cloudserverclient');
const { GarbageCollectorMetrics } = require('../GarbageCollectorMetrics');
const { clearTransitionAttempt } = require('../../../lib/util/transitionAttempt');
/** @typedef { import('../GarbageCollector.js') } GarbageCollector */

class GarbageCollectorTask extends BackbeatTask {
Expand Down Expand Up @@ -290,10 +291,8 @@ class GarbageCollectorTask extends BackbeatTask {
.setDataStoreName(newLocation)
.setAmzStorageClass(newLocation)
.setOriginOp('s3:LifecycleTransition')
.setTransitionInProgress(false)
.setUserMetadata({
'x-amz-meta-scal-s3-transition-attempt': undefined,
});
.setTransitionInProgress(false);
clearTransitionAttempt(objMD);
this._putMetadata(entry, objMD, log, err => {
GarbageCollectorMetrics.onS3Request(log, 'putMetadata', 'archive', err);
if (!err) {
Expand Down
19 changes: 19 additions & 0 deletions extensions/lifecycle/LifecycleMetrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const LIFECYCLE_LABEL_CONDUCTOR_SCAN_ID = 'conductor_scan_id';

const LIFECYCLE_MARKER_METRICS_LOCATION = '-delete-marker-';

const TRANSITION_TYPE = 'transition';
const LOCALIZATION_TYPE = 'localization';

// Keep per-scan series long enough for scraping and debugging recent overlap,
// but remove them from prom-client after a configurable retention interval.
// We intentionally do not cap the number of tracked scan IDs: if overlapping
Expand Down Expand Up @@ -447,9 +450,25 @@ class LifecycleMetrics {
}
}

/**
* Metrics type of a copyLocation action: clean room localization goes through
* the same pipeline as a lifecycle transition, but is triggered from the oplog
* rather than from a bucket scan, so its latencies are orders of magnitude
* apart and would skew the transition ones if reported under the same type.
*
* @param {ActionQueueEntry} actionEntry - copyLocation action
* @return {string} metrics type
*/
function getCopyLocationMetricsType(actionEntry) {
return actionEntry.getAttribute('metrics.origin') === LOCALIZATION_TYPE ?
LOCALIZATION_TYPE : TRANSITION_TYPE;
}

module.exports = {
DEFAULT_SCAN_METRIC_RETENTION_S,
LifecycleMetrics,
LIFECYCLE_MARKER_METRICS_LOCATION,
LOCALIZATION_TYPE,
getCopyLocationMetricsType,
resetLifecycleScanMetricCleanupTimers,
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';
const assert = require('assert');
const async = require('async');
const { errors } = require('arsenal');

const ColdStorageStatusQueueEntry = require('../../../lib/models/ColdStorageStatusQueueEntry');
const { LifecycleMetrics } = require('../LifecycleMetrics');
Expand All @@ -14,6 +15,9 @@ const { updateCircuitBreakerConfigForImplicitOutputQueue } = require('../../../l
const { LifecycleRetriggerRestoreTask } = require('../tasks/LifecycleRetriggerRestoreTask');
const BackbeatProducer = require('../../../lib/BackbeatProducer');
const GarbageCollectorProducer = require('../../gc/GarbageCollectorProducer');
const VaultClientWrapper = require('../../utils/VaultClientWrapper');
const { AccountIdCache } = require('../../utils/AccountIdCache');
const { authTypeAssumeRole } = require('../../../lib/constants');

class LifecycleObjectTransitionProcessor extends LifecycleObjectProcessor {

Expand Down Expand Up @@ -44,9 +48,78 @@ class LifecycleObjectTransitionProcessor extends LifecycleObjectProcessor {
* @param {Number} s3Config.port - s3 endpoint port
* @param {String} [transport="http"] - transport method ("http"
* or "https")
* @param {Object} [vaultAdminConfig] - vault admin endpoint, used to
* resolve canonical ids into account ids
*/
constructor(zkConfig, kafkaConfig, lcConfig, s3Config, transport = 'http') {
constructor(zkConfig, kafkaConfig, lcConfig, s3Config, transport = 'http',
vaultAdminConfig = undefined) {
super(zkConfig, kafkaConfig, lcConfig, s3Config, transport);

// The transition processor is the only one receiving actions published
// without an account id (clean room localization), so it is the only
// one needing a vault client to resolve them.
const authConfig = this.getAuthConfig(this._lcConfig);
if (authConfig.type === authTypeAssumeRole &&
(vaultAdminConfig || authConfig.vault)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The authConfig.vault fallback means the transition processor initializes a vault client and blocks isReady() on credential arrival in any assumeRole deployment that has a vault field in its auth config — even when clean room localization is not in use and vaultAdminConfig was not explicitly set.

Consider gating solely on the explicit vaultAdminConfig parameter:

Suggested change
(vaultAdminConfig || authConfig.vault)) {
if (authConfig.type === authTypeAssumeRole && vaultAdminConfig) {

(Note: a previous comment raised this same concern but was posted against LifecycleObjectProcessor.js, which was not modified by this PR and may not be visible in the diff view.)

this.vaultClientWrapper = new VaultClientWrapper(
`lifecycle:${this.getProcessorType()}`,
vaultAdminConfig,
authConfig,
this._log,
);
this._accountIdCache = new AccountIdCache(
this._processConfig.concurrency);
}
}

/**
* Resolve the account id of a canonical id. Actions published by the
* lifecycle conductor already carry the account id; those published by the
* queue populator (clean room localization) only know the canonical id.
* @param {String} ownerId - canonical id of the object owner
* @param {Logger} log - logger instance
* @param {Function} cb - callback: cb(err, accountId)
* @return {undefined}
*/
getAccountId(ownerId, log, cb) {
if (this.getAuthConfig(this._lcConfig).type !== authTypeAssumeRole) {
log.debug('skipping: not assume role auth type');
return process.nextTick(cb);
}

if (!this.vaultClientWrapper) {
log.error('cannot resolve canonical id: no vault endpoint configured');
return process.nextTick(cb, errors.InternalError.customizeDescription(
'account id resolution requires a vault endpoint'));
}

// A cached miss must fail like a fresh lookup would: `isKnown()` is also
// true for misses, and `get()` would then hand back `undefined`.
if (this._accountIdCache.isMiss(ownerId)) {
log.error('canonical id does not exist (cached)', { ownerId });
return process.nextTick(cb, errors.NoSuchEntity);
}

if (this._accountIdCache.has(ownerId)) {
return process.nextTick(cb, null, this._accountIdCache.get(ownerId));
}

return this.vaultClientWrapper.getAccountId(ownerId, (err, accountId) => {
if (err) {
if (err.NoSuchEntity) {
log.error('canonical id does not exist', { error: err, ownerId });
this._accountIdCache.miss(ownerId);
} else {
log.error('could not get account id', { error: err, ownerId });
}
return cb(err);
}

this._accountIdCache.set(ownerId, accountId);
this._accountIdCache.expireOldest();

return cb(null, accountId);
});
}

/**
Expand All @@ -56,6 +129,7 @@ class LifecycleObjectTransitionProcessor extends LifecycleObjectProcessor {
* @return {undefined}
*/
start(done) {
this.vaultClientWrapper?.init();
async.waterfall([
next => super.start(next),
next => {
Expand Down Expand Up @@ -226,8 +300,13 @@ class LifecycleObjectTransitionProcessor extends LifecycleObjectProcessor {
...super.getStateVars(),
coldProducer: this._coldProducer,
gcProducer: this._gcProducer,
getAccountId: this.getAccountId.bind(this),
};
}

isReady() {
return super.isReady() && (!this.vaultClientWrapper || this.vaultClientWrapper.tempCredentialsReady());
}
}

module.exports = LifecycleObjectTransitionProcessor;
2 changes: 1 addition & 1 deletion extensions/lifecycle/objectProcessor/task.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ let objectProcessor;
switch (process.env.LIFECYCLE_OBJECT_PROCESSOR_TYPE) {
case 'transition':
objectProcessor = new LifecycleObjectTransitionProcessor(
zkConfig, kafkaConfig, lcConfig, s3Config, transport);
zkConfig, kafkaConfig, lcConfig, s3Config, transport, config.vaultAdmin);
break;
case 'expiration': // fallthrough
default:
Expand Down
7 changes: 3 additions & 4 deletions extensions/lifecycle/tasks/LifecycleColdStatusArchiveTask.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const ObjectMDArchive = require('arsenal').models.ObjectMDArchive;
const ActionQueueEntry = require('../../../lib/models/ActionQueueEntry');
const LifecycleUpdateTransitionTask = require('./LifecycleUpdateTransitionTask');
const { LifecycleMetrics } = require('../LifecycleMetrics');
const { clearTransitionAttempt } = require('../../../lib/util/transitionAttempt');

class SkipMdUpdateError extends Error {}

Expand Down Expand Up @@ -115,10 +116,8 @@ class LifecycleColdStatusArchiveTask extends LifecycleUpdateTransitionTask {
objectMD.setDataStoreName(coldLocation)
.setAmzStorageClass(coldLocation)
.setTransitionInProgress(false)
.setOriginOp('s3:LifecycleTransition')
.setUserMetadata({
'x-amz-meta-scal-s3-transition-attempt': undefined,
});
.setOriginOp('s3:LifecycleTransition');
clearTransitionAttempt(objectMD);
}

this._putMetadata(entry, objectMD, log, err => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const { LifecycleRequeueTask } = require('./LifecycleRequeueTask');
const { setTransitionAttempt } = require('../../../lib/util/transitionAttempt');

class LifecycleResetTransitionInProgressTask extends LifecycleRequeueTask {
/**
Expand All @@ -19,9 +20,7 @@ class LifecycleResetTransitionInProgressTask extends LifecycleRequeueTask {
}
md.setOriginOp('s3:LifecycleTransition:Retry');
md.setTransitionInProgress(false);
md.setUserMetadata({
'x-amz-meta-scal-s3-transition-attempt': try_,
});
setTransitionAttempt(md, try_);
return true;
}

Expand Down
11 changes: 2 additions & 9 deletions extensions/lifecycle/tasks/LifecycleTask.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const ReplicationAPI = require('../../replication/ReplicationAPI');
const { LifecycleMetrics, LIFECYCLE_MARKER_METRICS_LOCATION } = require('../LifecycleMetrics');
const locationsConfig = require('../../../conf/locationConfig.json') || {};
const { rulesSupportTransition } = require('../util/rules');
const { getTransitionAttempt } = require('../../../lib/util/transitionAttempt');
const { stampTraceHeaders } = require('arsenal/build/lib/tracing').kafka;
const { decode } = versioning.VersionID;

Expand Down Expand Up @@ -1176,15 +1177,7 @@ class LifecycleTask extends BackbeatTask {
}

_getTransitionActionEntry(params, objectMD, log, cb) {
let attempt;
const umd = objectMD.getUserMetadata();
if (umd) {
const parsed = JSON.parse(umd);
const rawAttempt = parsed['x-amz-meta-scal-s3-transition-attempt'];
if (rawAttempt) {
attempt = Number.parseInt(rawAttempt, 10);
}
}
const attempt = getTransitionAttempt(objectMD.getUserMetadata());

const entry = ReplicationAPI.createCopyLocationAction({
bucketName: params.bucket,
Expand Down
81 changes: 59 additions & 22 deletions extensions/lifecycle/tasks/LifecycleUpdateTransitionTask.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ const errors = require('arsenal').errors;
const BackbeatTask = require('../../../lib/tasks/BackbeatTask');
const ActionQueueEntry = require('../../../lib/models/ActionQueueEntry');
const ObjectMD = require('arsenal').models.ObjectMD;
const { LifecycleMetrics } = require('../LifecycleMetrics');
const { LifecycleMetrics, getCopyLocationMetricsType } = require('../LifecycleMetrics');
const {
getTransitionAttempt,
setTransitionAttempt,
clearTransitionAttempt,
} = require('../../../lib/util/transitionAttempt');
/** @typedef { import('../objectProcessor/LifecycleObjectProcessor.js') } LifecycleObjectProcessor */

class LifecycleUpdateTransitionTask extends BackbeatTask {
Expand Down Expand Up @@ -71,10 +76,8 @@ class LifecycleUpdateTransitionTask extends BackbeatTask {
.setDataStoreName(newLocationName)
.setAmzStorageClass(newLocationName)
.setOriginOp('s3:LifecycleTransition')
.setUserMetadata({
'x-amz-meta-scal-s3-transition-attempt': undefined,
})
.setTransitionInProgress(false);
clearTransitionAttempt(objMD);
}

_putMetadata(entry, objMD, log, done) {
Expand Down Expand Up @@ -200,7 +203,7 @@ class LifecycleUpdateTransitionTask extends BackbeatTask {
const transitionTime = entry.getAttribute('metrics.transitionTime') ||
objMD.getTransitionTime();
const locationName = entry.getAttribute('toLocation');
LifecycleMetrics.onLifecycleCompleted(log, 'transition',
LifecycleMetrics.onLifecycleCompleted(log, getCopyLocationMetricsType(entry),
locationName, Date.now() - Date.parse(transitionTime));
next(err);
});
Expand Down Expand Up @@ -232,26 +235,53 @@ class LifecycleUpdateTransitionTask extends BackbeatTask {
next(err, objMD);
}),
(objMD, next) => {
const userMDStr = objMD.getUserMetadata() || '{}';
const userMD = JSON.parse(userMDStr);
const tryCount = (getTransitionAttempt(objMD.getUserMetadata()) || 0) + 1;

let tryCount = userMD['x-amz-meta-scal-s3-transition-attempt'];
if (tryCount === undefined) {
tryCount = 1;
} else {
tryCount = parseInt(tryCount, 10) + 1;
}

objMD.setTransitionInProgress(false)
.setUserMetadata({
'x-amz-meta-scal-s3-transition-attempt': tryCount,
});
objMD.setTransitionInProgress(false);
setTransitionAttempt(objMD, tryCount);

return this._putMetadata(entry, objMD, log, next);
},
], done);
}

/**
* Actions published by the lifecycle conductor carry the account id;
* those published by the queue populator (clean room localization) only
* know the object owner's canonical id. Resolve it once, up-front, so the
* rest of the task - and the garbage collection entry it emits - can use
* `target.accountId` as usual.
* @param {ActionQueueEntry} entry - action entry to execute
* @param {Logger} log - logger instance
* @param {Function} cb - callback function
* @return {undefined}
*/
_resolveAccountId(entry, log, cb) {
const { accountId, owner } = this.getTargetAttribute(entry);
if (accountId) {
return process.nextTick(cb);
}

if (!owner) {
// Every publisher sets one or the other, so this is a malformed
// entry: log it, and let the task fail on its own further down
// rather than retrying something that cannot be fixed.
log.error('cannot resolve account id: entry has no account id nor owner');
return process.nextTick(cb);
}

log.debug('no account id in entry, resolving from canonical id', { owner });
return this.getAccountId(owner, log, (err, resolvedAccountId) => {
if (err) {
return cb(err);
}
if (resolvedAccountId) {
entry.setAttribute('target.accountId', resolvedAccountId);
}
return cb();
});
}

/**
*
* @param {ActionQueueEntry} entry - action entry to execute
Expand All @@ -268,11 +298,18 @@ class LifecycleUpdateTransitionTask extends BackbeatTask {
lastModified: 'target.lastModified',
});
log.addDefaultFields(entry.getLogInfo());
if (entry.getStatus() === 'success') {
return this.handleSuccessfullTransition(entry, log, done);
}

return this.handleFailedTransition(entry, log, done);
return this._resolveAccountId(entry, log, err => {
if (err) {
return done(err);
}

if (entry.getStatus() === 'success') {
return this.handleSuccessfullTransition(entry, log, done);
}

return this.handleFailedTransition(entry, log, done);
});
}
}

Expand Down
Loading
Loading