11const { isMasterKey } = require ( 'arsenal' ) . versioning ;
2+ const { encode } = require ( 'arsenal' ) . versioning . VersionID ;
23const { usersBucket, mpuBucketPrefix } = require ( 'arsenal' ) . constants ;
34
45const QueuePopulatorExtension =
56 require ( '../../lib/queuePopulator/QueuePopulatorExtension' ) ;
67const ObjectQueueEntry = require ( '../../lib/models/ObjectQueueEntry' ) ;
8+ const ReplicationAPI = require ( './ReplicationAPI' ) ;
9+ const { LifecycleMetrics, LOCALIZATION_TYPE } = require ( '../lifecycle/LifecycleMetrics' ) ;
10+ const config = require ( '../../lib/Config' ) ;
711const locationsConfig = require ( '../../conf/locationConfig.json' ) || { } ;
812const safeJsonParse = require ( '../../lib/util/safeJsonParse' ) ;
13+ const { getTransitionAttempt } = require ( '../../lib/util/transitionAttempt' ) ;
914const { traceHeadersFromEntry } = require ( 'arsenal/build/lib/tracing' ) . kafka ;
1015
16+ const { transitionTasksTopic } = config . extensions . lifecycle ;
17+
18+ // Where clean room objects are localized when their metadata does not name a
19+ // usable target. Cold and source (isCRR) locations can never hold localized
20+ // data, any other one is a valid local destination.
21+ const defaultLocalLocation = Object . keys ( locationsConfig ) . find (
22+ name => ! locationsConfig [ name ] . isCold && ! locationsConfig [ name ] . isCRR ) ;
23+
1124class ReplicationQueuePopulator extends QueuePopulatorExtension {
1225 constructor ( params ) {
1326 super ( params ) ;
@@ -73,6 +86,15 @@ class ReplicationQueuePopulator extends QueuePopulatorExtension {
7386 if ( sanityCheckRes ) {
7487 return ;
7588 }
89+ const locationConfig = locationsConfig [ queueEntry . getDataStoreName ( ) ] || { } ;
90+ // Clean room: the object data still lives on the source (isCRR)
91+ // location and first needs to be localized. This is unrelated to
92+ // replicationInfo, which tracks replication of a *local* object to
93+ // remote sites, hence the check before any replication condition.
94+ if ( locationConfig . isCRR ) {
95+ this . _publishLocalizationAction ( entry , queueEntry , value ) ;
96+ return ;
97+ }
7698 // Allow a non-versioned object if being replicated from an NFS bucket.
7799 // Or if the master key is of a non versioned object
78100 if ( ! this . _entryCanBeReplicated ( queueEntry ) ) {
@@ -81,11 +103,8 @@ class ReplicationQueuePopulator extends QueuePopulatorExtension {
81103 if ( queueEntry . getReplicationStatus ( ) !== 'PENDING' ) {
82104 return ;
83105 }
84- const dataStoreName = queueEntry . getDataStoreName ( ) ;
85- const isObjectCold = dataStoreName && locationsConfig [ dataStoreName ]
86- && locationsConfig [ dataStoreName ] . isCold ;
87106 // We do not replicate cold objects.
88- if ( isObjectCold ) {
107+ if ( locationConfig . isCold ) {
89108 return ;
90109 }
91110
@@ -124,6 +143,132 @@ class ReplicationQueuePopulator extends QueuePopulatorExtension {
124143 traceHeaders ) ;
125144 }
126145
146+ /**
147+ * Queue a copyLocation action for an object whose data still lives on the
148+ * source (isCRR) location, so the data mover copies it to the local
149+ * location and the transition processor merges the new location back into
150+ * the object metadata.
151+ *
152+ * Duplicates are expected (and harmless): the same object may show up
153+ * several times in the oplog, and the copy is idempotent.
154+ *
155+ * @param {Object } entry - raw metadata log entry
156+ * @param {ObjectQueueEntry } queueEntry - parsed entry
157+ * @param {Object } value - parsed entry metadata
158+ * @return {undefined }
159+ */
160+ _publishLocalizationAction ( entry , queueEntry , value ) {
161+ // Clean room buckets are versioned: the master key is repaired by the
162+ // metadata layer once the version has been localized.
163+ if ( isMasterKey ( queueEntry . getObjectVersionedKey ( ) ) ) {
164+ return ;
165+ }
166+ if ( queueEntry . getIsDeleteMarker ( ) ) {
167+ return ;
168+ }
169+ const locations = queueEntry . getLocation ( ) ;
170+ if ( ! locations || locations . length === 0 ) {
171+ // Empty objects hold no data, there is nothing to localize. Any
172+ // other object without location information is inconsistent.
173+ if ( queueEntry . getContentLength ( ) > 0 ) {
174+ this . log . error ( 'non-empty object without location, skipping localization' , {
175+ method : 'ReplicationQueuePopulator._publishLocalizationAction' ,
176+ ...queueEntry . getLogInfo ( ) ,
177+ dataStoreName : queueEntry . getDataStoreName ( ) ,
178+ contentLength : queueEntry . getContentLength ( ) ,
179+ } ) ;
180+ }
181+ return ;
182+ }
183+
184+ const bucket = queueEntry . getBucket ( ) ;
185+ const objectKey = queueEntry . getObjectKey ( ) ;
186+ const contentLength = queueEntry . getContentLength ( ) ;
187+ const targetLocation = this . _getLocalizationTarget ( queueEntry , locations ) ;
188+ if ( ! targetLocation ) {
189+ return ;
190+ }
191+ const transitionTime = new Date ( entry . overheadFields ?. commitTimestamp ?? Date . now ( ) ) ;
192+ const action = ReplicationAPI . createCopyLocationAction ( {
193+ bucketName : bucket ,
194+ objectKey,
195+ owner : queueEntry . getOwnerId ( ) ,
196+ versionId : value . versionId ? encode ( value . versionId ) : undefined ,
197+ eTag : `"${ queueEntry . getContentMd5 ( ) } "` ,
198+ lastModified : queueEntry . getLastModified ( ) ,
199+ toLocation : targetLocation ,
200+ originLabel : 'localization' ,
201+ fromLocation : queueEntry . getDataStoreName ( ) ,
202+ contentLength,
203+ resultsTopic : transitionTasksTopic ,
204+ transitionTime : transitionTime . toISOString ( ) ,
205+ attempt : getTransitionAttempt ( queueEntry . getUserMetadata ( ) ) ,
206+ } ) ;
207+ // 'transition' is what the lifecycle transition processor dispatches
208+ // on to pick up the copyLocation result.
209+ action . addContext ( {
210+ origin : 'localization' ,
211+ ruleType : 'transition' ,
212+ bucketName : bucket ,
213+ objectKey,
214+ versionId : value . versionId ,
215+ } ) ;
216+ action . setAttribute ( 'source' , {
217+ bucket,
218+ objectKey,
219+ storageClass : queueEntry . getDataStoreName ( ) ,
220+ } ) ;
221+
222+ LifecycleMetrics . onLifecycleTriggered ( this . log , 'queuePopulator' ,
223+ LOCALIZATION_TYPE , targetLocation , Date . now ( ) - transitionTime . getTime ( ) ) ;
224+
225+ this . log . trace ( 'publishing object localization entry' , { entry : queueEntry . getLogInfo ( ) } ) ;
226+ this . publish ( ReplicationAPI . getDataMoverTopic ( ) ,
227+ `${ bucket } /${ objectKey } ` ,
228+ action . toKafkaMessage ( ) ,
229+ undefined ,
230+ traceHeadersFromEntry ( value ) ) ;
231+ }
232+
233+ /**
234+ * Local location the object data must be copied to.
235+ *
236+ * It is named in the source location entry itself, next to the bucket and
237+ * role the copy needs: the rewrite pipeline resolves it from the bucket
238+ * when it synthesizes that entry, which keeps this populator -a single
239+ * threaded oplog reader- from having to look the bucket up per object.
240+ *
241+ * @param {ObjectQueueEntry } queueEntry - parsed entry
242+ * @param {Object[] } locations - object data locations
243+ * @return {String|undefined } target location, undefined if there is none
244+ */
245+ _getLocalizationTarget ( queueEntry , locations ) {
246+ const { targetLocation } = locations [ 0 ] ;
247+ if ( locationsConfig [ targetLocation ] ) {
248+ return targetLocation ;
249+ }
250+ // Either the object predates the rewrite pipeline naming a target, or
251+ // the location was deleted since the metadata was written. Neither is
252+ // recoverable here, so fall back to the default location: localizing
253+ // elsewhere beats leaving the data on the source forever.
254+ if ( ! defaultLocalLocation ) {
255+ this . log . error ( 'invalid localization target and no local location ' +
256+ 'to fall back to, skipping localization' , {
257+ method : 'ReplicationQueuePopulator._getLocalizationTarget' ,
258+ ...queueEntry . getLogInfo ( ) ,
259+ targetLocation,
260+ } ) ;
261+ return undefined ;
262+ }
263+ this . log . error ( 'invalid localization target in object metadata' , {
264+ method : 'ReplicationQueuePopulator._getLocalizationTarget' ,
265+ ...queueEntry . getLogInfo ( ) ,
266+ targetLocation,
267+ fallbackLocation : defaultLocalLocation ,
268+ } ) ;
269+ return defaultLocalLocation ;
270+ }
271+
127272 /**
128273 * Filter if the entry is considered a valid master key entry.
129274 * There is a case where a single null entry looks like a master key and
0 commit comments