carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "context"
7 "strings"
8 "sync"
9 "testing"
10 "time"
11
12 "github.com/carlosframework/platform/internal/activator"
13 "github.com/carlosframework/platform/internal/lease"
14 "github.com/carlosframework/platform/internal/moves"
15 "github.com/carlosframework/platform/internal/provision"
16)
17
18// The pre-copy (offline-staging), through offlineRound over the seal world:
19// the S3 wire protocol, real litestream replicas, the real lease package and
20// the production edges-file writer. A stage round copies the replica into
21// the target's staging prefix with the instance serving; the seal's round
22// then copies only what changed since; abort from staging removes it all.
23
24// stagerEdges publishes b's edges file as an agent with an armed, capable
25// moves worker does: the hibernate coordinates and the offline capabilities
26// (offline-move-1, offline-stage-1). The stager is chosen from these.
27func (sw *sealWorld) stagerEdges(b *offBox) {
28 sw.t.Helper()
29 ident := edgesFile{Box: b.name, Placement: b.region, DataBucket: sealIE, DataBucketRegion: sealIERegion,
30 HibernationConfigured: true, ServesSharedPool: true, Capabilities: withOfflineCapability(nil, b.w.cfg.OfflineCapable)}
31 if err := edgesFileWrite(context.Background(), sw.s, b.reg, ident, "", &b.last, false); err != nil {
32 sw.t.Fatal(err)
33 }
34}
35
36// stagePass is sw.pass with the edges file stagerEdges writes.
37func (sw *sealWorld) stagePass(b *offBox) error {
38 sw.t.Helper()
39 b.reconcile()
40 sw.stagerEdges(b)
41 ctx := context.Background()
42 refreshOfflineView(ctx, sw.s, b.w.cfg.Offline)
43 return b.w.offlineRound(ctx, sw.s, sw.record())
44}
45
46func (sw *sealWorld) mustStagePass(b *offBox) {
47 sw.t.Helper()
48 if err := sw.stagePass(b); err != nil {
49 sw.t.Fatal(err)
50 }
51}
52
53func (sw *sealWorld) stage() { sw.stageBy("ie-a") }
54
55// stageBy stages with the stager the door pinned.
56func (sw *sealWorld) stageBy(stager string) {
57 sw.move(provision.OfflineStage(offID, "", "uk", "op@example.com", provision.StageTarget{Stager: stager, Bucket: sealUK, Region: sealUKRegion}, sw.clock()))
58}
59
60// restage is a second carlos move offline stage: one more round.
61func (sw *sealWorld) restage() {
62 sw.advance(time.Second)
63 sw.move(provision.OfflineRestage(offID, sw.clock()))
64}
65
66// copySpy records the source key of every server-side copy the fake S3
67// performs from now on.
68type copySpy struct {
69 mu sync.Mutex
70 keys []string
71}
72
73func (sw *sealWorld) spyCopies() *copySpy {
74 c := &copySpy{}
75 sw.fake.FailOn = func(op, key string) error {
76 if op == "copy" {
77 c.mu.Lock()
78 c.keys = append(c.keys, key)
79 c.mu.Unlock()
80 }
81 return nil
82 }
83 sw.t.Cleanup(func() { sw.fake.FailOn = nil })
84 return c
85}
86
87func (c *copySpy) take() []string {
88 c.mu.Lock()
89 defer c.mu.Unlock()
90 out := c.keys
91 c.keys = nil
92 return out
93}
94
95// replicaRel is the replica's listing by key relative to its prefix, lock
96// aside; stagedRel the staging prefix's the same way.
97func (sw *sealWorld) replicaRel() map[string]lease.MirrorObject {
98 prefix := sealPrefix + "/" + offHost + "/"
99 out := map[string]lease.MirrorObject{}
100 for k, o := range sw.list(sealIE, prefix) {
101 if rel := strings.TrimPrefix(k, prefix); rel != "lock.json" {
102 out[rel] = o
103 }
104 }
105 return out
106}
107
108func (sw *sealWorld) stagedRel() map[string]lease.MirrorObject {
109 out := map[string]lease.MirrorObject{}
110 for k, o := range sw.list(sealUK, activator.MovesPrefix(offHost)) {
111 out[strings.TrimPrefix(k, activator.MovesPrefix(offHost))] = o
112 }
113 return out
114}
115
116func sameSizes(t *testing.T, staged, replica map[string]lease.MirrorObject) {
117 t.Helper()
118 if len(staged) != len(replica) {
119 t.Fatalf("staged %d objects, replica %d", len(staged), len(replica))
120 }
121 for rel, o := range replica {
122 if s, ok := staged[rel]; !ok || s.Size != o.Size {
123 t.Fatalf("%s: staged %+v (%v), replica %+v", rel, s, ok, o)
124 }
125 }
126}
127
128func TestOfflineStageThenStartThenSealSkipsThePreCopy(t *testing.T) {
129 sw := newSealWorld(t)
130 ie := sw.ie
131 ieB := sw.sourceBox("ie-b")
132 sw.instance(ie)
133 ctx := context.Background()
134 // A stale staged prefix an earlier aborted move left behind: a file
135 // under a replica key with other bytes, and one the replica never had.
136 // Neither may survive into the seal.
137 for rel := range sw.replicaRel() {
138 sw.fake.Put(sealUK, activator.MovesPrefix(offHost)+rel, []byte("stale bytes of an earlier move"))
139 break
140 }
141 sw.fake.Put(sealUK, activator.MovesPrefix(offHost)+"0000/stale-extra.ltx", []byte("stale"))
142 owner0, expires0, gen0 := sw.lock()
143
144 sw.stage()
145 spy := sw.spyCopies()
146 sw.stagerEdges(ie) // both boxes publish the capability,
147 sw.mustStagePass(ieB) // and ie-b is not the stager: it does nothing
148 if _, ok := sw.reportIf(moves.OfflineRoleStager, "ie-b"); ok || len(spy.take()) != 0 {
149 t.Fatal("a box that is not the stager staged")
150 }
151 sw.mustStagePass(ie)
152 first := spy.take()
153 replica := sw.replicaRel()
154 sameSizes(t, sw.stagedRel(), replica)
155 r := sw.report(moves.OfflineRoleStager, "ie-a")
156 if r.Phase != moves.OfflinePhaseStaged || r.ReplicaObjects != len(replica) || r.StagedObjects != len(replica) || r.RoundBytes == 0 || r.StagedBytes != r.ReplicaBytes || r.StageError != "" {
157 t.Fatalf("stager report %+v (replica %d objects)", r, len(replica))
158 }
159 if len(first) != len(replica) {
160 t.Fatalf("the first round made %d copies, want %d", len(first), len(replica))
161 }
162 if staged := sw.record().Move.StagedAt; staged == nil || !r.StageRequest.Equal(*staged) {
163 t.Fatalf("the round answered %v, the request is %v", r.StageRequest, staged)
164 }
165 // One round per request: an idle pass copies, lists and writes nothing.
166 snap := sw.fake.Snapshot()
167 sw.mustStagePass(ie)
168 if len(spy.take()) != 0 || sw.fake.Snapshot() != snap {
169 t.Fatal("a pass with no new stage request ran a round")
170 }
171 if again := sw.report(moves.OfflineRoleStager, "ie-a"); !again.At.Equal(r.At) {
172 t.Fatal("a pass with no new stage request rewrote the stager report")
173 }
174
175 // Staging held, fenced and leased nothing: the row serves, no fence,
176 // the lease is as it was, and a wake takes it.
177 if rt := ie.row(offHost); rt == nil || rt.HoldOp != "" {
178 t.Fatalf("row %+v during staging", rt)
179 }
180 if ie.svc.Fenced(offHost) {
181 t.Fatal("fenced during staging")
182 }
183 if owner, expires, gen := sw.lock(); owner != owner0 || !expires.Equal(expires0) || gen != gen0 {
184 t.Fatalf("the lease changed during staging: %s %s %d, was %s %s %d", owner, expires, gen, owner0, expires0, gen0)
185 }
186 h, err := acquireAsAWake(ctx)
187 if err != nil {
188 t.Fatalf("a wake during staging: %v", err)
189 }
190 h.Release(ctx)
191
192 // The instance keeps writing; a second round copies only what is new.
193 sw.writeRows(ie, sw.dbPath(ie), 20, 40, true)
194 before := sw.stagedRel()
195 sw.restage()
196 sw.mustStagePass(ie)
197 second := spy.take()
198 fresh := 0
199 for rel, o := range sw.replicaRel() {
200 if s, ok := before[rel]; !ok || s.Size != o.Size {
201 fresh++
202 }
203 }
204 if fresh == 0 || len(second) != fresh {
205 t.Fatalf("the second round made %d copies, want the %d new objects", len(second), fresh)
206 }
207 sameSizes(t, sw.stagedRel(), sw.replicaRel())
208
209 // A replica file that came and went between rounds (retention): the
210 // seal's round removes it from staging.
211 retired := sealPrefix + "/" + offHost + "/scratch/retired.bin"
212 sw.fake.Put(sealIE, retired, []byte("gone before the seal"))
213 sw.restage()
214 sw.mustStagePass(ie)
215 spy.take()
216 if _, ok := sw.stagedRel()["scratch/retired.bin"]; !ok {
217 t.Fatal("fixture: the retired file was not staged")
218 }
219 sw.fake.BareDelete(sealIE, retired)
220
221 // Start: the stage's objects are in place, and the seal's round copies
222 // only what changed since the last stage round.
223 preStaged := sw.stagedRel()
224 preReplica := sw.replicaRel()
225 sw.move(provision.OfflineStartStaged(offID, "op@example.com", sw.clock()))
226 ie.tick()
227 ieB.tick()
228 sw.move(provision.OfflineSeal(offID, "ie-a", []string{"ie-a", "ie-b"}))
229 spy.take()
230 sw.mustPass(ie)
231 if st := sw.record().Move.State; st != provision.OfflineSealed {
232 t.Fatalf("state %s, want sealed", st)
233 }
234 sealCopies := spy.take()
235 // Litestream's compaction decides how many files the checkpoint adds,
236 // so the count of the seal's copies is not fixed. What is: at least one
237 // object the stage copied is still in the replica, unchanged, and the
238 // seal copied none of those (the loop below).
239 kept := 0
240 for rel, o := range sw.replicaRel() {
241 if s, ok := preStaged[rel]; ok && s.Size == o.Size && preReplica[rel].ETag == o.ETag {
242 kept++
243 }
244 }
245 if kept == 0 {
246 t.Fatal("fixture: no pre-copied object survived to the seal, so nothing shows a skip")
247 }
248 t.Logf("the seal copied %d of %d objects; %d were pre-copied and skipped", len(sealCopies), len(sw.replicaRel()), kept)
249 for _, k := range sealCopies {
250 rel := strings.TrimPrefix(k, sealPrefix+"/"+offHost+"/")
251 s, staged := preStaged[rel]
252 o, listed := preReplica[rel]
253 if staged && listed && s.Size == o.Size && s.ETag == o.ETag {
254 t.Fatalf("the seal recopied %s, which the stage had copied", rel)
255 }
256 }
257 seal, _, ok := sw.sealRecord()
258 if !ok {
259 t.Fatal("no seal")
260 }
261 if _, ok := seal.Staged["scratch/retired.bin"]; ok {
262 t.Fatal("the seal kept a staged file the replica no longer has")
263 }
264 if _, ok := seal.Staged["0000/stale-extra.ltx"]; ok {
265 t.Fatal("the seal kept the stale prefix's extra file")
266 }
267 sameSizes(t, sw.stagedRel(), sw.replicaRel())
268 // The staged copy restores to every row written while staging.
269 restored := t.TempDir() + "/restored.db"
270 if ok, err := lease.Restore(ctx, restored, sealUK, strings.TrimSuffix(activator.MovesPrefix(offHost), "/"), sealUKRegion); err != nil || !ok {
271 t.Fatalf("restore the staged copy: %v %v", ok, err)
272 }
273 if n := countRows(t, restored); n != 40 {
274 t.Fatalf("the staged copy holds %d rows, want 40", n)
275 }
276}
277
278// Abort from staging: the stager deletes the staged copy and the receipts
279// before it reports reverted, every other source box reports reverted as
280// for any abort, and finish ends a move that was never offline. Nothing the
281// move did not write is touched.
282func TestOfflineAbortFromStagingRemovesTheStagedCopy(t *testing.T) {
283 sw := newSealWorld(t)
284 ie := sw.ie
285 sw.instance(ie)
286 sw.stage()
287 sw.mustStagePass(ie)
288 if len(sw.stagedRel()) == 0 {
289 t.Fatal("fixture: nothing staged")
290 }
291 // Receipts, as a round over a multipart source leaves them, and
292 // neighbours the abort must not touch.
293 sw.fake.Put(sealIE, activator.MoveReceiptsKey(offHost), []byte(`{"v":1}`))
294 sw.fake.Put(sealUK, "moves/other.example.com/0000/x.ltx", []byte("x"))
295 replica := sw.replicaRel()
296
297 sw.abort()
298 sw.mustStagePass(ie)
299 if n := len(sw.stagedRel()); n != 0 {
300 t.Fatalf("%d staged objects left after the abort", n)
301 }
302 if _, ok := sw.fake.Current(sealIE, activator.MoveReceiptsKey(offHost)); ok {
303 t.Fatal("the receipts are still there")
304 }
305 if _, ok := sw.fake.Current(sealUK, "moves/other.example.com/0000/x.ltx"); !ok {
306 t.Fatal("the abort deleted another host's staging")
307 }
308 if got := sw.replicaRel(); len(got) != len(replica) {
309 t.Fatalf("the replica changed: %d objects, was %d", len(got), len(replica))
310 }
311 if r := sw.report(moves.OfflineRoleStager, "ie-a"); r.Phase != moves.OfflinePhaseReverted {
312 t.Fatalf("stager report %+v", r)
313 }
314 if r := sw.report(moves.OfflineRoleSource, "ie-a"); r.Phase != moves.OfflinePhaseReverted {
315 t.Fatalf("source report %+v", r)
316 }
317 // Again: nothing in the buckets changes.
318 snap := sw.fake.Snapshot()
319 sw.mustStagePass(ie)
320 if sw.fake.Snapshot() != snap {
321 t.Fatal("a second revert pass wrote to the buckets")
322 }
323 rec := sw.move(provision.OfflineFinish(offID, sw.clock()))
324 if rec.Move != nil || rec.LastMove == nil || rec.LastMove.Outcome != provision.OfflineOutcomeAborted || rec.LastMove.OfflineS != 0 {
325 t.Fatalf("finish: %+v", rec.LastMove)
326 }
327 if rt := ie.row(offHost); rt == nil || rt.HoldOp != "" {
328 t.Fatalf("row %+v after the abort", rt)
329 }
330}
331
332// A staging bucket that is the source's own would have the serving instance
333// restore from the staged copy (the activator's staging wins). The stage
334// door refuses it (MV-O191); a record that names it anyway (written by
335// hand) gets a refusing round that writes nothing, and a cleanup that
336// deletes and acknowledges nothing.
337func TestOfflineStageRefusesTheSourcesOwnBucket(t *testing.T) {
338 sw := newSealWorld(t)
339 ie := sw.ie
340 sw.instance(ie)
341 sw.publishTarget(sealIE, sealIERegion)
342 sw.move(provision.OfflineStage(offID, "", "uk", "op@example.com", provision.StageTarget{Stager: "ie-a", Bucket: sealIE, Region: sealIERegion}, sw.clock()))
343 if err := sw.stagePass(ie); err == nil {
344 t.Fatal("staged into the source's own bucket")
345 }
346 if n := len(sw.list(sealIE, "moves/")); n != 0 {
347 t.Fatalf("%d objects staged into the source's bucket", n)
348 }
349 r := sw.report(moves.OfflineRoleStager, "ie-a")
350 if r.Phase != moves.OfflinePhaseFailed || !strings.Contains(r.StageError, "own bucket") {
351 t.Fatalf("stager report %+v", r)
352 }
353 sw.abort()
354 if err := sw.stagePass(ie); err == nil {
355 t.Fatal("an abort acknowledged a cleanup of the source's own bucket")
356 }
357 if r := sw.report(moves.OfflineRoleStager, "ie-a"); r.Phase == moves.OfflinePhaseReverted {
358 t.Fatal("reverted written for a staging bucket that was never cleaned")
359 }
360}
361
362// Astra round 1, finding 1: a round that copied and then never wrote its
363// report (the box crashed, or the report write failed) still leaves its
364// copy to the abort. Cleanup follows the record's staged_at, never the
365// reports.
366func TestOfflineAbortCleansARoundThatNeverReported(t *testing.T) {
367 sw := newSealWorld(t)
368 ie := sw.ie
369 sw.instance(ie)
370 sw.stage()
371 sw.stagerEdges(ie)
372 // The round's copies land; the box dies before its report.
373 if _, err := ie.w.offlineStageRound(context.Background(), sw.s, sw.record()); err != nil {
374 t.Fatal(err)
375 }
376 if len(sw.stagedRel()) == 0 {
377 t.Fatal("fixture: nothing staged")
378 }
379 if _, ok := sw.reportIf(moves.OfflineRoleStager, "ie-a"); ok {
380 t.Fatal("fixture: the round reported")
381 }
382 sw.abort()
383 sw.mustStagePass(ie)
384 if n := len(sw.stagedRel()); n != 0 {
385 t.Fatalf("%d staged objects left by a round that never reported", n)
386 }
387 if r := sw.report(moves.OfflineRoleStager, "ie-a"); r.Phase != moves.OfflinePhaseReverted {
388 t.Fatalf("stager report %+v", r)
389 }
390}
391
392// Astra round 1, finding 2: a box that joins mid-move, lexically before the
393// stager, never stages and never cleans for it. Only the pinned stager
394// writes the staging prefix, so its cleanup, after its own last round, is
395// the one that counts.
396func TestOfflineAbortWaitsForThePinnedStager(t *testing.T) {
397 sw := newSealWorld(t)
398 sw.instance(sw.ie)
399 ieB := sw.sourceBox("ie-b")
400 sw.stageBy("ie-b")
401 sw.mustStagePass(ieB)
402 if r := sw.report(moves.OfflineRoleStager, "ie-b"); r.Phase != moves.OfflinePhaseStaged {
403 t.Fatalf("stager report %+v", r)
404 }
405 // ie-a joins, publishing the capability, and the operator stages again.
406 sw.stagerEdges(sw.ie)
407 sw.restage()
408 spy := sw.spyCopies()
409 sw.mustStagePass(sw.ie)
410 if _, ok := sw.reportIf(moves.OfflineRoleStager, "ie-a"); ok || len(spy.take()) != 0 {
411 t.Fatal("a box that is not the pinned stager staged")
412 }
413 sw.abort()
414 sw.mustStagePass(sw.ie) // ie-a's revert: not its copy to remove
415 if _, ok := sw.reportIf(moves.OfflineRoleStager, "ie-a"); ok {
416 t.Fatal("a box that is not the pinned stager acknowledged the cleanup")
417 }
418 // ie-b's round, which began before the abort, lands its copies now.
419 sw.fake.Put(sealIE, sealPrefix+"/"+offHost+"/scratch/late.bin", []byte("late"))
420 if _, err := ieB.w.offlineStageRound(context.Background(), sw.s, sw.record()); err != nil {
421 t.Fatal(err)
422 }
423 if len(sw.stagedRel()) == 0 {
424 t.Fatal("fixture: the late round staged nothing")
425 }
426 sw.mustStagePass(ieB) // the pinned stager's revert, after its last round
427 if n := len(sw.stagedRel()); n != 0 {
428 t.Fatalf("%d staged objects left after the pinned stager's cleanup", n)
429 }
430 if r := sw.report(moves.OfflineRoleStager, "ie-b"); r.Phase != moves.OfflinePhaseReverted {
431 t.Fatalf("stager report %+v", r)
432 }
433}
434
435// ---- Astra round 2 ----
436
437// publishTarget republishes the target box's edges file with bucket in
438// region: a target replaced or reconfigured after the stage.
439func (sw *sealWorld) publishTarget(bucket, region string) {
440 sw.t.Helper()
441 var last []byte
442 if err := edgesFileWrite(context.Background(), sw.s, sw.uk.reg, edgesFile{Box: "uk-box", Placement: "uk", DataBucket: bucket, DataBucketRegion: region}, "", &last, false); err != nil {
443 sw.t.Fatal(err)
444 }
445}
446
447// Finding 1: once the move has left staging, the stager never starts a
448// round, even on a pass that read the record while it was still staging.
449func TestOfflineStageNoRoundAfterTheMoveLeftStaging(t *testing.T) {
450 sw := newSealWorld(t)
451 ie := sw.ie
452 sw.instance(ie)
453 sw.stage()
454 sw.mustStagePass(ie)
455 sw.restage()
456 stale := sw.record() // a pass's listing, read while still staging
457 sw.move(provision.OfflineStartStaged(offID, "op@example.com", sw.clock()))
458 sw.fake.Put(sealIE, sealPrefix+"/"+offHost+"/scratch/new.bin", []byte("new"))
459 spy := sw.spyCopies()
460 if err := ie.w.offlineRound(context.Background(), sw.s, stale); err != nil {
461 t.Fatal(err)
462 }
463 if n := len(spy.take()); n != 0 {
464 t.Fatalf("a round made %d copies after the move left staging", n)
465 }
466 if r := sw.report(moves.OfflineRoleStager, "ie-a"); r.StageRequest.Equal(*stale.Move.StagedAt) {
467 t.Fatal("the stager answered a request after the move left staging")
468 }
469}
470
471// Finding 1: a round the start overtakes deletes nothing. The delete phase
472// reads the record again first, and a move that has left staging stops the
473// round there: an object the seal's checkpoint added must never be removed
474// by a stage round.
475func TestOfflineStageRoundDeletesNothingOnceTheMoveLeftStaging(t *testing.T) {
476 sw := newSealWorld(t)
477 ie := sw.ie
478 sw.instance(ie)
479 sw.stage()
480 sw.mustStagePass(ie)
481 // An object only the staging prefix has (the round would delete it),
482 // and a new replica object (the round copies it, and the start lands
483 // during that copy).
484 extra := activator.MovesPrefix(offHost) + "0000/added-by-the-seal.ltx"
485 sw.fake.Put(sealUK, extra, []byte("the seal's"))
486 sw.fake.Put(sealIE, sealPrefix+"/"+offHost+"/scratch/new.bin", []byte("new"))
487 sw.restage()
488 var once sync.Once
489 sw.fake.FailOn = func(op, key string) error {
490 if op == "copy" {
491 once.Do(func() {
492 go func() { // the operator's start, while the copy is in flight
493 _, _ = provision.SetRecordMove(context.Background(), sw.s, sw.acct, "app", offHost,
494 provision.OfflineStartStaged(offID, "op@example.com", time.Now()))
495 }()
496 time.Sleep(200 * time.Millisecond)
497 })
498 }
499 return nil
500 }
501 t.Cleanup(func() { sw.fake.FailOn = nil })
502 _ = ie.w.offlineRound(context.Background(), sw.s, sw.record())
503 if sw.record().Move.State != provision.OfflineStopping {
504 t.Fatal("fixture: the start did not land during the round")
505 }
506 if _, ok := sw.fake.Current(sealUK, extra); !ok {
507 t.Fatal("a stage round deleted from the staging prefix after the move left staging")
508 }
509}
510
511// Finding 1: in stopping and on, the pinned stager says, on every pass and
512// under its pass lock, that no round is in flight and none will start.
513func TestOfflineStagerAcknowledgesItHasStopped(t *testing.T) {
514 sw := newSealWorld(t)
515 ie := sw.ie
516 sw.instance(ie)
517 sw.stage()
518 sw.mustStagePass(ie)
519 sw.move(provision.OfflineStartStaged(offID, "op@example.com", sw.clock()))
520 ie.tick()
521 r := sw.report(moves.OfflineRoleStager, "ie-a")
522 if r.Phase != moves.OfflinePhaseStopped || !r.At.Equal(sw.clock()) || r.StagedObjects == 0 {
523 t.Fatalf("stager report %+v, want stopped now with its progress kept", r)
524 }
525}
526
527// Finding 2: cleanup deletes from the destination the stage fixed on the
528// record, whatever the target publishes now, and says reverted only then.
529func TestOfflineAbortCleansTheStoredDestination(t *testing.T) {
530 for _, c := range []struct {
531 name string
532 bucket, region string
533 }{
534 {"a replaced target", "data-uk-new", sealUKRegion},
535 {"a target that now publishes the source's own bucket", sealIE, sealIERegion},
536 } {
537 t.Run(c.name, func(t *testing.T) {
538 sw := newSealWorld(t)
539 sw.fake.AddBucket("data-uk-new")
540 ie := sw.ie
541 sw.instance(ie)
542 sw.stage()
543 sw.mustStagePass(ie)
544 if len(sw.stagedRel()) == 0 {
545 t.Fatal("fixture: nothing staged")
546 }
547 sw.publishTarget(c.bucket, c.region)
548 sw.abort()
549 sw.mustStagePass(ie)
550 if n := len(sw.stagedRel()); n != 0 {
551 t.Fatalf("%d objects left in the stored staging bucket %s", n, sealUK)
552 }
553 if r := sw.report(moves.OfflineRoleStager, "ie-a"); r.Phase != moves.OfflinePhaseReverted {
554 t.Fatalf("stager report %+v", r)
555 }
556 if got := sw.replicaRel(); len(got) == 0 {
557 t.Fatal("the cleanup touched the source's replica")
558 }
559 })
560 }
561}
562
563// Finding 2: a stage round never follows a target that now publishes other
564// coordinates than the stored ones. It refuses and writes nowhere.
565func TestOfflineStageRoundRefusesChangedCoordinates(t *testing.T) {
566 sw := newSealWorld(t)
567 sw.fake.AddBucket("data-uk-new")
568 ie := sw.ie
569 sw.instance(ie)
570 sw.stage()
571 sw.publishTarget("data-uk-new", sealUKRegion)
572 if err := sw.stagePass(ie); err == nil {
573 t.Fatal("staged with changed coordinates")
574 }
575 if n := len(sw.list("data-uk-new", "moves/")); n != 0 {
576 t.Fatalf("%d objects staged into the new bucket", n)
577 }
578 if n := len(sw.stagedRel()); n != 0 {
579 t.Fatalf("%d objects staged at all", n)
580 }
581 if r := sw.report(moves.OfflineRoleStager, "ie-a"); r.Phase != moves.OfflinePhaseFailed {
582 t.Fatalf("stager report %+v", r)
583 }
584}
585