Scope upload idempotency by token
This commit is contained in:
@@ -116,6 +116,7 @@ type uploadJob struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type uploadIdempotencyScope struct {
|
type uploadIdempotencyScope struct {
|
||||||
|
TokenID string
|
||||||
PipelineID string
|
PipelineID string
|
||||||
Key string
|
Key string
|
||||||
}
|
}
|
||||||
@@ -201,7 +202,7 @@ func (coordinator *UploadCoordinator) Submit(ctx context.Context, request Upload
|
|||||||
if err := ingest.ValidateContentType(request.ContentType); err != nil {
|
if err := ingest.ValidateContentType(request.ContentType); err != nil {
|
||||||
return UploadRunRecord{}, err
|
return UploadRunRecord{}, err
|
||||||
}
|
}
|
||||||
scope, hasKey := uploadRequestIdempotencyScope(pipeline.ID, request.IdempotencyKey)
|
scope, hasKey := uploadRequestIdempotencyScope(request.TokenID, pipeline.ID, request.IdempotencyKey)
|
||||||
|
|
||||||
coordinator.mu.Lock()
|
coordinator.mu.Lock()
|
||||||
coordinator.expireLocked(coordinator.now().UTC())
|
coordinator.expireLocked(coordinator.now().UTC())
|
||||||
@@ -287,11 +288,11 @@ func (coordinator *UploadCoordinator) Submit(ctx context.Context, request Upload
|
|||||||
return record, nil
|
return record, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func uploadRequestIdempotencyScope(pipelineID, key string) (uploadIdempotencyScope, bool) {
|
func uploadRequestIdempotencyScope(tokenID, pipelineID, key string) (uploadIdempotencyScope, bool) {
|
||||||
if key == "" {
|
if key == "" {
|
||||||
return uploadIdempotencyScope{}, false
|
return uploadIdempotencyScope{}, false
|
||||||
}
|
}
|
||||||
return uploadIdempotencyScope{PipelineID: pipelineID, Key: key}, true
|
return uploadIdempotencyScope{TokenID: tokenID, PipelineID: pipelineID, Key: key}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (coordinator *UploadCoordinator) Status(runID UploadRunID) (UploadRunRecord, bool) {
|
func (coordinator *UploadCoordinator) Status(runID UploadRunID) (UploadRunRecord, bool) {
|
||||||
|
|||||||
@@ -271,6 +271,7 @@ func TestUploadCoordinatorIdempotencyReturnsOriginalRunForSameManifest(t *testin
|
|||||||
})
|
})
|
||||||
|
|
||||||
first, err := coordinator.Submit(context.Background(), UploadRequest{
|
first, err := coordinator.Submit(context.Background(), UploadRequest{
|
||||||
|
TokenID: "reporter-a",
|
||||||
PipelineID: "reports",
|
PipelineID: "reports",
|
||||||
ContentType: ingest.ContentTypeTar,
|
ContentType: ingest.ContentTypeTar,
|
||||||
Body: strings.NewReader("same"),
|
Body: strings.NewReader("same"),
|
||||||
@@ -282,6 +283,7 @@ func TestUploadCoordinatorIdempotencyReturnsOriginalRunForSameManifest(t *testin
|
|||||||
waitForUploadStatus(t, coordinator, first.ID, UploadStatusSucceeded)
|
waitForUploadStatus(t, coordinator, first.ID, UploadStatusSucceeded)
|
||||||
|
|
||||||
second, err := coordinator.Submit(context.Background(), UploadRequest{
|
second, err := coordinator.Submit(context.Background(), UploadRequest{
|
||||||
|
TokenID: "reporter-a",
|
||||||
PipelineID: "reports",
|
PipelineID: "reports",
|
||||||
ContentType: ingest.ContentTypeTar,
|
ContentType: ingest.ContentTypeTar,
|
||||||
Body: strings.NewReader("same"),
|
Body: strings.NewReader("same"),
|
||||||
@@ -310,6 +312,7 @@ func TestUploadCoordinatorIdempotencyConflictsForDifferentManifest(t *testing.T)
|
|||||||
})
|
})
|
||||||
|
|
||||||
first, err := coordinator.Submit(context.Background(), UploadRequest{
|
first, err := coordinator.Submit(context.Background(), UploadRequest{
|
||||||
|
TokenID: "reporter-a",
|
||||||
PipelineID: "reports",
|
PipelineID: "reports",
|
||||||
ContentType: ingest.ContentTypeTar,
|
ContentType: ingest.ContentTypeTar,
|
||||||
Body: strings.NewReader("one"),
|
Body: strings.NewReader("one"),
|
||||||
@@ -321,6 +324,7 @@ func TestUploadCoordinatorIdempotencyConflictsForDifferentManifest(t *testing.T)
|
|||||||
waitForUploadStatus(t, coordinator, first.ID, UploadStatusSucceeded)
|
waitForUploadStatus(t, coordinator, first.ID, UploadStatusSucceeded)
|
||||||
|
|
||||||
_, err = coordinator.Submit(context.Background(), UploadRequest{
|
_, err = coordinator.Submit(context.Background(), UploadRequest{
|
||||||
|
TokenID: "reporter-a",
|
||||||
PipelineID: "reports",
|
PipelineID: "reports",
|
||||||
ContentType: ingest.ContentTypeTar,
|
ContentType: ingest.ContentTypeTar,
|
||||||
Body: strings.NewReader("two"),
|
Body: strings.NewReader("two"),
|
||||||
@@ -331,6 +335,42 @@ func TestUploadCoordinatorIdempotencyConflictsForDifferentManifest(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUploadCoordinatorIdempotencyIsScopedByToken(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
coordinator := newUploadCoordinator(ctx, uploadCoordinatorConfig(t, uploadCoordinatorConfigOptions{
|
||||||
|
pipelineIDs: []string{"reports"},
|
||||||
|
}), uploadCoordinatorHooks{
|
||||||
|
randomSuffix: uploadTestSuffixes("00000001", "00000002"),
|
||||||
|
stage: manifestUploadStage,
|
||||||
|
run: successfulUploadRun,
|
||||||
|
})
|
||||||
|
|
||||||
|
first, err := coordinator.Submit(context.Background(), UploadRequest{
|
||||||
|
TokenID: "reporter-a",
|
||||||
|
PipelineID: "reports",
|
||||||
|
ContentType: ingest.ContentTypeTar,
|
||||||
|
Body: strings.NewReader("one"),
|
||||||
|
IdempotencyKey: "shared-key",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("first Submit() error = %v", err)
|
||||||
|
}
|
||||||
|
second, err := coordinator.Submit(context.Background(), UploadRequest{
|
||||||
|
TokenID: "reporter-b",
|
||||||
|
PipelineID: "reports",
|
||||||
|
ContentType: ingest.ContentTypeTar,
|
||||||
|
Body: strings.NewReader("two"),
|
||||||
|
IdempotencyKey: "shared-key",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("second Submit() error = %v", err)
|
||||||
|
}
|
||||||
|
if second.ID == first.ID {
|
||||||
|
t.Fatalf("run ids matched across tokens: %q", second.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUploadCoordinatorIdempotencyIsScopedByPipeline(t *testing.T) {
|
func TestUploadCoordinatorIdempotencyIsScopedByPipeline(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -343,6 +383,7 @@ func TestUploadCoordinatorIdempotencyIsScopedByPipeline(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
first, err := coordinator.Submit(context.Background(), UploadRequest{
|
first, err := coordinator.Submit(context.Background(), UploadRequest{
|
||||||
|
TokenID: "reporter-a",
|
||||||
PipelineID: "reports-one",
|
PipelineID: "reports-one",
|
||||||
ContentType: ingest.ContentTypeTar,
|
ContentType: ingest.ContentTypeTar,
|
||||||
Body: strings.NewReader("one"),
|
Body: strings.NewReader("one"),
|
||||||
@@ -352,6 +393,7 @@ func TestUploadCoordinatorIdempotencyIsScopedByPipeline(t *testing.T) {
|
|||||||
t.Fatalf("first Submit() error = %v", err)
|
t.Fatalf("first Submit() error = %v", err)
|
||||||
}
|
}
|
||||||
second, err := coordinator.Submit(context.Background(), UploadRequest{
|
second, err := coordinator.Submit(context.Background(), UploadRequest{
|
||||||
|
TokenID: "reporter-a",
|
||||||
PipelineID: "reports-two",
|
PipelineID: "reports-two",
|
||||||
ContentType: ingest.ContentTypeTar,
|
ContentType: ingest.ContentTypeTar,
|
||||||
Body: strings.NewReader("two"),
|
Body: strings.NewReader("two"),
|
||||||
@@ -408,6 +450,7 @@ func TestUploadCoordinatorIdempotencyReturnsRetryableConflictWhileStaging(t *tes
|
|||||||
firstErr := make(chan error, 1)
|
firstErr := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
_, err := coordinator.Submit(context.Background(), UploadRequest{
|
_, err := coordinator.Submit(context.Background(), UploadRequest{
|
||||||
|
TokenID: "reporter-a",
|
||||||
PipelineID: "reports",
|
PipelineID: "reports",
|
||||||
ContentType: ingest.ContentTypeTar,
|
ContentType: ingest.ContentTypeTar,
|
||||||
Body: strings.NewReader("same"),
|
Body: strings.NewReader("same"),
|
||||||
@@ -419,6 +462,7 @@ func TestUploadCoordinatorIdempotencyReturnsRetryableConflictWhileStaging(t *tes
|
|||||||
|
|
||||||
var reads atomic.Int64
|
var reads atomic.Int64
|
||||||
_, err := coordinator.Submit(context.Background(), UploadRequest{
|
_, err := coordinator.Submit(context.Background(), UploadRequest{
|
||||||
|
TokenID: "reporter-a",
|
||||||
PipelineID: "reports",
|
PipelineID: "reports",
|
||||||
ContentType: ingest.ContentTypeTar,
|
ContentType: ingest.ContentTypeTar,
|
||||||
Body: readerFunc(func(data []byte) (int, error) {
|
Body: readerFunc(func(data []byte) (int, error) {
|
||||||
@@ -440,6 +484,57 @@ func TestUploadCoordinatorIdempotencyReturnsRetryableConflictWhileStaging(t *tes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUploadCoordinatorIdempotencyPendingScopeIncludesToken(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
var calls atomic.Int64
|
||||||
|
entered := make(chan struct{})
|
||||||
|
release := make(chan struct{})
|
||||||
|
coordinator := newUploadCoordinator(ctx, uploadCoordinatorConfig(t, uploadCoordinatorConfigOptions{
|
||||||
|
pipelineIDs: []string{"reports"},
|
||||||
|
}), uploadCoordinatorHooks{
|
||||||
|
randomSuffix: uploadTestSuffixes("00000001", "00000002"),
|
||||||
|
stage: func(ctx context.Context, opts ingest.StageOptions) (ingest.StagedBundle, error) {
|
||||||
|
if calls.Add(1) == 1 {
|
||||||
|
close(entered)
|
||||||
|
<-release
|
||||||
|
}
|
||||||
|
return manifestUploadStage(ctx, opts)
|
||||||
|
},
|
||||||
|
run: successfulUploadRun,
|
||||||
|
})
|
||||||
|
firstErr := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
_, err := coordinator.Submit(context.Background(), UploadRequest{
|
||||||
|
TokenID: "reporter-a",
|
||||||
|
PipelineID: "reports",
|
||||||
|
ContentType: ingest.ContentTypeTar,
|
||||||
|
Body: strings.NewReader("same"),
|
||||||
|
IdempotencyKey: "in-flight",
|
||||||
|
})
|
||||||
|
firstErr <- err
|
||||||
|
}()
|
||||||
|
<-entered
|
||||||
|
|
||||||
|
second, err := coordinator.Submit(context.Background(), UploadRequest{
|
||||||
|
TokenID: "reporter-b",
|
||||||
|
PipelineID: "reports",
|
||||||
|
ContentType: ingest.ContentTypeTar,
|
||||||
|
Body: strings.NewReader("same"),
|
||||||
|
IdempotencyKey: "in-flight",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("second Submit() error = %v", err)
|
||||||
|
}
|
||||||
|
if second.ID == "" {
|
||||||
|
t.Fatal("second run id is empty, want accepted run")
|
||||||
|
}
|
||||||
|
close(release)
|
||||||
|
if err := <-firstErr; err != nil {
|
||||||
|
t.Fatalf("first Submit() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUploadCoordinatorIdempotencyExpiresWithCompletedStatus(t *testing.T) {
|
func TestUploadCoordinatorIdempotencyExpiresWithCompletedStatus(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -455,6 +550,7 @@ func TestUploadCoordinatorIdempotencyExpiresWithCompletedStatus(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
first, err := coordinator.Submit(context.Background(), UploadRequest{
|
first, err := coordinator.Submit(context.Background(), UploadRequest{
|
||||||
|
TokenID: "reporter-a",
|
||||||
PipelineID: "reports",
|
PipelineID: "reports",
|
||||||
ContentType: ingest.ContentTypeTar,
|
ContentType: ingest.ContentTypeTar,
|
||||||
Body: strings.NewReader("same"),
|
Body: strings.NewReader("same"),
|
||||||
@@ -469,6 +565,7 @@ func TestUploadCoordinatorIdempotencyExpiresWithCompletedStatus(t *testing.T) {
|
|||||||
coordinator.Expire()
|
coordinator.Expire()
|
||||||
|
|
||||||
second, err := coordinator.Submit(context.Background(), UploadRequest{
|
second, err := coordinator.Submit(context.Background(), UploadRequest{
|
||||||
|
TokenID: "reporter-a",
|
||||||
PipelineID: "reports",
|
PipelineID: "reports",
|
||||||
ContentType: ingest.ContentTypeTar,
|
ContentType: ingest.ContentTypeTar,
|
||||||
Body: strings.NewReader("same"),
|
Body: strings.NewReader("same"),
|
||||||
|
|||||||
Reference in New Issue
Block a user