Skip to content

Commit f9999d5

Browse files
committed
fix(db): replace oversized upload unique index with tuple hash
1 parent 62e2915 commit f9999d5

12 files changed

Lines changed: 328 additions & 8 deletions

File tree

internal/cache/service.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package cache
33
import (
44
"context"
55
"crypto/rand"
6+
"crypto/sha256"
67
"encoding/base64"
78
"encoding/binary"
9+
"encoding/hex"
810
"errors"
911
"fmt"
1012
"io"
@@ -440,6 +442,7 @@ func ReadScopesByPermission(scope auth.CacheScope) []string {
440442
}
441443

442444
func (s *Service) createUploadRecord(ctx context.Context, key, version, scope, repoID string) (int64, error) {
445+
tupleHash := uploadTupleHash(key, version, scope, repoID)
443446
for i := 0; i < 5; i++ {
444447
id, err := randomPositiveInt64()
445448
if err != nil {
@@ -455,6 +458,7 @@ func (s *Service) createUploadRecord(ctx context.Context, key, version, scope, r
455458
SetVersion(version).
456459
SetScope(scope).
457460
SetRepoId(repoID).
461+
SetTupleHash(tupleHash).
458462
Save(ctx)
459463
if err == nil {
460464
return id, nil
@@ -544,6 +548,17 @@ func uploadTuple(key, version, scope, repoID string) []entpredicate.Upload {
544548
}
545549
}
546550

551+
func uploadTupleHash(key, version, scope, repoID string) string {
552+
hash := sha256.New()
553+
var encodedLength [8]byte
554+
for _, value := range [...]string{key, version, scope, repoID} {
555+
binary.BigEndian.PutUint64(encodedLength[:], uint64(len(value)))
556+
_, _ = hash.Write(encodedLength[:])
557+
_, _ = hash.Write([]byte(value))
558+
}
559+
return hex.EncodeToString(hash.Sum(nil))
560+
}
561+
547562
func (s *Service) isUploadAbandoned(currentUpload *ent.Upload) bool {
548563
lastActivity := currentUpload.CreatedAt
549564
if currentUpload.LastPartUploadedAt != nil {

internal/cache/service_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cache
33
import (
44
"bytes"
55
"context"
6+
"crypto/sha256"
67
"errors"
78
"fmt"
89
"io"
@@ -303,6 +304,18 @@ func TestCreateUploadReservationIsAtomic(t *testing.T) {
303304
count, err := client.Upload.Query().Count(ctx)
304305
require.NoError(t, err)
305306
require.Equal(t, 1, count)
307+
currentUpload := client.Upload.Query().OnlyX(ctx)
308+
require.NotNil(t, currentUpload.TupleHash)
309+
require.Equal(t, uploadTupleHash("key", "version", scope.Scopes[0].Scope, scope.RepoID), *currentUpload.TupleHash)
310+
}
311+
312+
func TestUploadTupleHashPreservesFieldBoundaries(t *testing.T) {
313+
first := uploadTupleHash("ab", "c", "scope", "repo")
314+
second := uploadTupleHash("a", "bc", "scope", "repo")
315+
316+
require.Len(t, first, sha256.Size*2)
317+
require.Equal(t, first, uploadTupleHash("ab", "c", "scope", "repo"))
318+
require.NotEqual(t, first, second)
306319
}
307320

308321
func TestSinglePartUsesPartsObjectForDirectDownload(t *testing.T) {

internal/db/db_test.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ func TestOpenAndMigrateSQLiteFromOriginalSchema(t *testing.T) {
103103
_, err := sqlDB.ExecContext(ctx, stmt)
104104
require.NoError(t, err)
105105
}
106+
_, err = sqlDB.ExecContext(ctx, `insert into uploads (
107+
id, key, version, createdAt, folderName, scope, repoId
108+
) values
109+
(1, 'legacy-key', 'version', 1, 'legacy-1', 'scope', 'repo'),
110+
(2, 'legacy-key', 'version', 2, 'legacy-2', 'scope', 'repo')`)
111+
require.NoError(t, err)
106112
require.NoError(t, sqlDB.Close())
107113

108114
client, err := OpenAndMigrate(ctx, config.DBConfig{
@@ -126,6 +132,10 @@ func TestOpenAndMigrateSQLiteFromOriginalSchema(t *testing.T) {
126132
require.True(t, sqliteColumnExists(ctx, t, sqlDB, "storage_locations", "mergeLeaseToken"))
127133
require.True(t, sqliteColumnExists(ctx, t, sqlDB, "uploads", "lastPartUploadedAt"))
128134
require.True(t, sqliteColumnExists(ctx, t, sqlDB, "uploads", "committedPartCount"))
135+
require.True(t, sqliteColumnExists(ctx, t, sqlDB, "uploads", "tupleHash"))
136+
var legacyUploads int
137+
require.NoError(t, sqlDB.QueryRowContext(ctx, `select count(*) from uploads where tupleHash is null`).Scan(&legacyUploads))
138+
require.Equal(t, 2, legacyUploads)
129139
var storageDeletionsTable string
130140
require.NoError(t, sqlDB.QueryRowContext(
131141
ctx,
@@ -144,6 +154,7 @@ func TestOpenAndMigrateSQLiteFromOriginalSchema(t *testing.T) {
144154
require.False(t, sqliteColumnExists(ctx, t, sqlDB, "uploads", "last_part_uploaded_at"))
145155
require.True(t, sqliteIndexExists(ctx, t, sqlDB, "idx_cache_entries_repo_scope_version_key"))
146156
require.True(t, sqliteIndexExists(ctx, t, sqlDB, "idx_cache_entries_location_updated_at"))
157+
require.True(t, sqliteIndexExists(ctx, t, sqlDB, "idx_uploads_tuple_hash"))
147158
}
148159

149160
func TestUploadIDDoesNotRequireDatabaseIdentity(t *testing.T) {
@@ -168,7 +179,7 @@ func TestGeneratedSchemaMatchesOriginalColumns(t *testing.T) {
168179
"id", "scope", "expiresAt", "storageLocationId",
169180
}, columnNames(migrate.StorageReaderLeasesColumns))
170181
require.Equal(t, []string{
171-
"id", "key", "version", "scope", "repoId", "createdAt", "lastPartUploadedAt", "startedPartUploadCount", "finishedPartUploadCount", "folderName", "committedPartCount",
182+
"id", "key", "version", "scope", "repoId", "createdAt", "lastPartUploadedAt", "startedPartUploadCount", "finishedPartUploadCount", "folderName", "committedPartCount", "tupleHash",
172183
}, columnNames(migrate.UploadsColumns))
173184
}
174185

@@ -202,7 +213,15 @@ func TestGeneratedSchemaMatchesOriginalIndexNames(t *testing.T) {
202213
"idx_uploads_key_version",
203214
"idx_uploads_scope",
204215
"idx_uploads_repoId",
205-
}, indexNames(migrate.UploadsTable.Indexes[:3]))
216+
"idx_uploads_tuple_hash",
217+
}, indexNames(migrate.UploadsTable.Indexes))
218+
}
219+
220+
func TestMySQLUploadUniqueIndexUsesFixedLengthTupleHash(t *testing.T) {
221+
uniqueIndex := migrate.UploadsTable.Indexes[3]
222+
require.True(t, uniqueIndex.Unique)
223+
require.Equal(t, []string{"tupleHash"}, columnNames(uniqueIndex.Columns))
224+
require.Equal(t, "varchar(64)", migrate.UploadsColumns[11].SchemaType[dialect.MySQL])
206225
}
207226

208227
func TestCacheMatchIndexAnnotations(t *testing.T) {

internal/ent/migrate/schema.go

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/ent/mutation.go

Lines changed: 74 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/ent/runtime.go

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/ent/schema/upload.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ func (Upload) Fields() []ent.Field {
4545
Nillable().
4646
NonNegative().
4747
StorageKey("committedPartCount"),
48+
field.String("tupleHash").
49+
MaxLen(64).
50+
Optional().
51+
Nillable().
52+
StorageKey("tupleHash").
53+
SchemaType(originalBoundedStringColumnType(64)),
4854
}
4955
}
5056

@@ -53,6 +59,6 @@ func (Upload) Indexes() []ent.Index {
5359
index.Fields("key", "version").StorageKey("idx_uploads_key_version"),
5460
index.Fields("scope").StorageKey("idx_uploads_scope"),
5561
index.Fields("repoId").StorageKey("idx_uploads_repoId"),
56-
index.Fields("key", "version", "scope", "repoId").Unique(),
62+
index.Fields("tupleHash").Unique().StorageKey("idx_uploads_tuple_hash"),
5763
}
5864
}

internal/ent/upload.go

Lines changed: 16 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)