Name each database target's archive for its webhook and target (closes #376)
check / check (push) Successful in 3m16s
check / check (push) Successful in 3m16s
Each database target now has its own archive file, archive-WEBHOOKNAME-TARGETNAME-TARGETID.db, instead of one archive-WEBHOOKID.db per webhook. delivery.ArchiveFileName builds the name. A change of webhook or target name renames its archive files under the archive writer's lock, before the new name is saved, and back again if the save fails. Webhook edits, target edits and target creation run one at a time, so no two of them interleave. A rename never replaces a file, and one that fails part way moves back what it moved. Deleting a target evicts only that target's writer. Archive files are never deleted, and nothing looks for files under the old name. Model: opus-5-5
This commit is contained in:
@@ -34,18 +34,23 @@ const (
|
||||
sweepConcurrentWrites = 20
|
||||
)
|
||||
|
||||
// sweeperEnv bundles the pieces an archive sweep test drives:
|
||||
// a main configuration database holding webhooks and targets, a
|
||||
// delivery engine owning the archive writer registry, and the
|
||||
// data directory the archive files live in.
|
||||
type sweeperEnv struct {
|
||||
// archiveTestWebhookName is the name of every webhook
|
||||
// seedDatabaseTarget creates. It is not safe in a file name as it
|
||||
// stands, so every archive test goes through archiveNamePart.
|
||||
const archiveTestWebhookName = "Sweep Test!"
|
||||
|
||||
// archiveEnv bundles the pieces an archive test drives: a main
|
||||
// configuration database holding webhooks and targets, a delivery
|
||||
// engine owning the archive writer registry, the archive sweeper,
|
||||
// and the data directory the archive files live in.
|
||||
type archiveEnv struct {
|
||||
sweeper *delivery.ArchiveSweeper
|
||||
eng *delivery.Engine
|
||||
mainDB *database.Database
|
||||
dataDir string
|
||||
}
|
||||
|
||||
func setupSweeperTest(t *testing.T) *sweeperEnv {
|
||||
func setupArchiveTest(t *testing.T) *archiveEnv {
|
||||
t.Helper()
|
||||
|
||||
dataDir := t.TempDir()
|
||||
@@ -78,7 +83,7 @@ func setupSweeperTest(t *testing.T) *sweeperEnv {
|
||||
1,
|
||||
)
|
||||
|
||||
return &sweeperEnv{
|
||||
return &archiveEnv{
|
||||
sweeper: delivery.NewTestArchiveSweeper(
|
||||
mainDB, eng, log,
|
||||
),
|
||||
@@ -88,25 +93,27 @@ func setupSweeperTest(t *testing.T) *sweeperEnv {
|
||||
}
|
||||
}
|
||||
|
||||
// archivePath returns where the engine keeps a webhook's
|
||||
// archive file.
|
||||
func (env *sweeperEnv) archivePath(webhookID string) string {
|
||||
// archivePath returns where the engine keeps a database target's
|
||||
// archive file, for the names seedDatabaseTarget gave it.
|
||||
func (env *archiveEnv) archivePath(tgt *database.Target) string {
|
||||
return filepath.Join(
|
||||
env.dataDir, fmt.Sprintf("archive-%s.db", webhookID),
|
||||
env.dataDir,
|
||||
delivery.ArchiveFileName(
|
||||
archiveTestWebhookName, tgt.Name, tgt.ID,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// seedDatabaseTarget creates a webhook with one database target
|
||||
// carrying the given target config JSON, and returns the
|
||||
// webhook id.
|
||||
func (env *sweeperEnv) seedDatabaseTarget(
|
||||
// carrying the given target config JSON, and returns the target.
|
||||
func (env *archiveEnv) seedDatabaseTarget(
|
||||
t *testing.T, configJSON string,
|
||||
) string {
|
||||
) *database.Target {
|
||||
t.Helper()
|
||||
|
||||
wh := &database.Webhook{
|
||||
UserID: uuid.New().String(),
|
||||
Name: "sweep-test",
|
||||
Name: archiveTestWebhookName,
|
||||
}
|
||||
require.NoError(
|
||||
t,
|
||||
@@ -115,9 +122,19 @@ func (env *sweeperEnv) seedDatabaseTarget(
|
||||
Create(wh).Error,
|
||||
)
|
||||
|
||||
return env.addDatabaseTarget(t, wh.ID, configJSON)
|
||||
}
|
||||
|
||||
// addDatabaseTarget creates one more database target on an
|
||||
// existing webhook and returns it.
|
||||
func (env *archiveEnv) addDatabaseTarget(
|
||||
t *testing.T, webhookID, configJSON string,
|
||||
) *database.Target {
|
||||
t.Helper()
|
||||
|
||||
tgt := &database.Target{
|
||||
WebhookID: wh.ID,
|
||||
Name: "archive",
|
||||
WebhookID: webhookID,
|
||||
Name: "Archive",
|
||||
Type: database.TargetTypeDatabase,
|
||||
Active: true,
|
||||
Config: configJSON,
|
||||
@@ -129,19 +146,19 @@ func (env *sweeperEnv) seedDatabaseTarget(
|
||||
Create(tgt).Error,
|
||||
)
|
||||
|
||||
return wh.ID
|
||||
return tgt
|
||||
}
|
||||
|
||||
// seedArchiveRows creates the archive file for a webhook and
|
||||
// seedArchiveRows creates the archive file for a target and
|
||||
// inserts one row per supplied archived-at timestamp, returning
|
||||
// the archive path. The handle is closed before returning, so
|
||||
// the archive is idle exactly as it would be with no traffic.
|
||||
func (env *sweeperEnv) seedArchiveRows(
|
||||
t *testing.T, webhookID string, archivedAt ...time.Time,
|
||||
func (env *archiveEnv) seedArchiveRows(
|
||||
t *testing.T, tgt *database.Target, archivedAt ...time.Time,
|
||||
) string {
|
||||
t.Helper()
|
||||
|
||||
path := env.archivePath(webhookID)
|
||||
path := env.archivePath(tgt)
|
||||
|
||||
sqlDB, err := sql.Open(
|
||||
"sqlite", fmt.Sprintf("file:%s?mode=rwc", path),
|
||||
@@ -160,7 +177,7 @@ func (env *sweeperEnv) seedArchiveRows(
|
||||
for i, at := range archivedAt {
|
||||
row := delivery.ExportArchivedEvent{
|
||||
EventID: fmt.Sprintf("ev-%d", i),
|
||||
WebhookID: webhookID,
|
||||
WebhookID: tgt.WebhookID,
|
||||
Method: http.MethodPost,
|
||||
Body: `{"seeded":true}`,
|
||||
ArchivedAt: at,
|
||||
@@ -243,13 +260,13 @@ func TestArchiveSweeper_LoopOutlivesStartHookContext(
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupSweeperTest(t)
|
||||
env := setupArchiveTest(t)
|
||||
|
||||
webhookID := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
|
||||
now := time.Now()
|
||||
path := env.seedArchiveRows(
|
||||
t, webhookID,
|
||||
t, tgt,
|
||||
now.Add(-48*time.Hour),
|
||||
now.Add(-time.Minute),
|
||||
)
|
||||
@@ -287,60 +304,60 @@ func TestArchiveSweeper_LoopOutlivesStartHookContext(
|
||||
}
|
||||
|
||||
// TestArchiveSweep_DoesNotResurrectEvictedWriter covers the
|
||||
// interleaving where a sweep tick has already listed a webhook's
|
||||
// target when the webhook is deleted and its writer evicted. The
|
||||
// sweep must not put a writer back into the registry: nothing
|
||||
// would ever evict it again, which is precisely the leak this
|
||||
// change exists to close.
|
||||
// interleaving where a sweep tick has already listed a target
|
||||
// when the target is deleted and its writer evicted. The sweep
|
||||
// must not put a writer back into the registry: nothing would
|
||||
// ever evict it again, which is precisely the leak this change
|
||||
// exists to close.
|
||||
func TestArchiveSweep_DoesNotResurrectEvictedWriter(
|
||||
t *testing.T,
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupSweeperTest(t)
|
||||
env := setupArchiveTest(t)
|
||||
|
||||
webhookID := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
env.seedArchiveRows(
|
||||
t, webhookID, time.Now().Add(-48*time.Hour),
|
||||
t, tgt, time.Now().Add(-48*time.Hour),
|
||||
)
|
||||
|
||||
// Prime the registry the way a delivery would, then evict as
|
||||
// the deletion path does. The target row is deliberately left
|
||||
// in place: this is the tick that listed the webhook before
|
||||
// in place: this is the tick that listed the target before
|
||||
// the deletion committed.
|
||||
_, err := env.eng.ExportEnsureArchiveWriter(webhookID)
|
||||
_, err := env.eng.ExportEnsureArchiveWriter(tgt.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
env.eng.EvictWebhook(webhookID)
|
||||
require.False(t, env.eng.ExportHasArchiveWriter(webhookID))
|
||||
env.eng.EvictTarget(tgt.ID)
|
||||
require.False(t, env.eng.ExportHasArchiveWriter(tgt.ID))
|
||||
|
||||
env.sweeper.ExportSweep(context.Background())
|
||||
|
||||
assert.False(
|
||||
t, env.eng.ExportHasArchiveWriter(webhookID),
|
||||
"a sweep must never re-register a writer for a webhook "+
|
||||
t, env.eng.ExportHasArchiveWriter(tgt.ID),
|
||||
"a sweep must never re-register a writer for a target "+
|
||||
"whose registry entry has already been released",
|
||||
)
|
||||
}
|
||||
|
||||
// TestArchiveSweep_LeavesNoRegistryEntry states the same
|
||||
// invariant in its general form: sweeping an archive whose
|
||||
// webhook has no cached writer must not leave one behind, so the
|
||||
// target has no cached writer must not leave one behind, so the
|
||||
// registry keeps holding only writers a delivery created and an
|
||||
// eviction can reach.
|
||||
func TestArchiveSweep_LeavesNoRegistryEntry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupSweeperTest(t)
|
||||
env := setupArchiveTest(t)
|
||||
|
||||
webhookID := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
path := env.seedArchiveRows(
|
||||
t, webhookID,
|
||||
t, tgt,
|
||||
time.Now().Add(-48*time.Hour),
|
||||
time.Now().Add(-time.Minute),
|
||||
)
|
||||
|
||||
require.False(t, env.eng.ExportHasArchiveWriter(webhookID))
|
||||
require.False(t, env.eng.ExportHasArchiveWriter(tgt.ID))
|
||||
|
||||
env.sweeper.ExportSweep(context.Background())
|
||||
|
||||
@@ -349,7 +366,7 @@ func TestArchiveSweep_LeavesNoRegistryEntry(t *testing.T) {
|
||||
"the sweep must still prune an idle archive",
|
||||
)
|
||||
assert.False(
|
||||
t, env.eng.ExportHasArchiveWriter(webhookID),
|
||||
t, env.eng.ExportHasArchiveWriter(tgt.ID),
|
||||
"the sweep must release the registry entry it created",
|
||||
)
|
||||
}
|
||||
@@ -364,34 +381,31 @@ func TestArchiveSweep_KeepsWriterAdoptedByDelivery(
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupSweeperTest(t)
|
||||
env := setupArchiveTest(t)
|
||||
|
||||
webhookID := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
env.seedArchiveRows(
|
||||
t, webhookID, time.Now().Add(-48*time.Hour),
|
||||
t, tgt, time.Now().Add(-48*time.Hour),
|
||||
)
|
||||
|
||||
webhookDB := testWebhookDB(t)
|
||||
event := seedEvent(t, webhookDB, `{"n":1}`)
|
||||
event.WebhookID = webhookID
|
||||
d := seedDatabaseTargetDelivery(
|
||||
t, webhookDB, event, `{"expiry":"1h"}`,
|
||||
)
|
||||
d := seedDatabaseTargetDelivery(t, webhookDB, event, tgt)
|
||||
|
||||
env.sweeper.ExportSweep(context.Background())
|
||||
require.False(t, env.eng.ExportHasArchiveWriter(webhookID))
|
||||
require.False(t, env.eng.ExportHasArchiveWriter(tgt.ID))
|
||||
|
||||
env.eng.ExportDeliverDatabase(webhookDB, d)
|
||||
|
||||
assert.True(
|
||||
t, env.eng.ExportHasArchiveWriter(webhookID),
|
||||
t, env.eng.ExportHasArchiveWriter(tgt.ID),
|
||||
"a delivery's writer must stay registered",
|
||||
)
|
||||
|
||||
env.sweeper.ExportSweep(context.Background())
|
||||
|
||||
assert.True(
|
||||
t, env.eng.ExportHasArchiveWriter(webhookID),
|
||||
t, env.eng.ExportHasArchiveWriter(tgt.ID),
|
||||
"a sweep must not drop a writer a delivery owns",
|
||||
)
|
||||
}
|
||||
@@ -423,15 +437,15 @@ func TestArchiveSweep_KeepsWriterAdoptedDuringSweep(
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupSweeperTest(t)
|
||||
env := setupArchiveTest(t)
|
||||
|
||||
webhookID := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
env.seedArchiveRows(
|
||||
t, webhookID, time.Now().Add(-48*time.Hour),
|
||||
t, tgt, time.Now().Add(-48*time.Hour),
|
||||
)
|
||||
|
||||
sweepWriter, created, err := env.eng.ExportSweepWriterFor(
|
||||
webhookID,
|
||||
tgt.ID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.True(
|
||||
@@ -442,37 +456,34 @@ func TestArchiveSweep_KeepsWriterAdoptedDuringSweep(
|
||||
// The delivery lands mid-sweep and adopts the entry.
|
||||
webhookDB := testWebhookDB(t)
|
||||
event := seedEvent(t, webhookDB, `{"n":1}`)
|
||||
event.WebhookID = webhookID
|
||||
d := seedDatabaseTargetDelivery(
|
||||
t, webhookDB, event, `{"expiry":"1h"}`,
|
||||
)
|
||||
d := seedDatabaseTargetDelivery(t, webhookDB, event, tgt)
|
||||
|
||||
env.eng.ExportDeliverDatabase(webhookDB, d)
|
||||
|
||||
adopted := env.eng.ExportArchiveWriterFor(webhookID)
|
||||
adopted := env.eng.ExportArchiveWriterFor(tgt.ID)
|
||||
require.NotNil(t, adopted)
|
||||
require.True(
|
||||
t, sweepWriter.Same(adopted),
|
||||
"the delivery must have adopted the sweep's writer",
|
||||
)
|
||||
require.True(
|
||||
t, env.eng.ExportArchiveHandleOpen(webhookID),
|
||||
t, env.eng.ExportArchiveHandleOpen(tgt.ID),
|
||||
"the delivery leaves the archive handle open",
|
||||
)
|
||||
|
||||
// The sweep finishes.
|
||||
env.eng.ExportReleaseSweepWriter(webhookID, sweepWriter)
|
||||
env.eng.ExportReleaseSweepWriter(tgt.ID, sweepWriter)
|
||||
|
||||
require.True(
|
||||
t, env.eng.ExportHasArchiveWriter(webhookID),
|
||||
t, env.eng.ExportHasArchiveWriter(tgt.ID),
|
||||
"a writer adopted by a delivery during a sweep must "+
|
||||
"stay registered, or its open handle is unreachable",
|
||||
)
|
||||
|
||||
env.eng.EvictWebhook(webhookID)
|
||||
env.eng.EvictTarget(tgt.ID)
|
||||
|
||||
assert.False(
|
||||
t, env.eng.ExportHasArchiveWriter(webhookID),
|
||||
t, env.eng.ExportHasArchiveWriter(tgt.ID),
|
||||
"the adopted writer must still be evictable",
|
||||
)
|
||||
assert.False(
|
||||
@@ -481,34 +492,34 @@ func TestArchiveSweep_KeepsWriterAdoptedDuringSweep(
|
||||
)
|
||||
}
|
||||
|
||||
// TestArchiveSweep_ContinuesAfterPerWebhookFailure proves a
|
||||
// failure for one webhook does not abort the sweep for the
|
||||
// TestArchiveSweep_ContinuesAfterPerTargetFailure proves a
|
||||
// failure for one target does not abort the sweep for the
|
||||
// others: an unparseable expiry and an unreadable archive both
|
||||
// have to be logged and stepped over.
|
||||
func TestArchiveSweep_ContinuesAfterPerWebhookFailure(
|
||||
func TestArchiveSweep_ContinuesAfterPerTargetFailure(
|
||||
t *testing.T,
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupSweeperTest(t)
|
||||
env := setupArchiveTest(t)
|
||||
|
||||
// Seeded first so the sweep reaches them before the healthy
|
||||
// webhook: targets come back in insertion order.
|
||||
badConfigID := env.seedDatabaseTarget(t, `{"expiry":"!!!"}`)
|
||||
// target: targets come back in insertion order.
|
||||
badConfig := env.seedDatabaseTarget(t, `{"expiry":"!!!"}`)
|
||||
env.seedArchiveRows(
|
||||
t, badConfigID, time.Now().Add(-48*time.Hour),
|
||||
t, badConfig, time.Now().Add(-48*time.Hour),
|
||||
)
|
||||
|
||||
corruptID := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
corrupt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
require.NoError(t, os.WriteFile(
|
||||
env.archivePath(corruptID),
|
||||
env.archivePath(corrupt),
|
||||
[]byte("this is not a sqlite database"),
|
||||
0o600,
|
||||
))
|
||||
|
||||
healthyID := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
healthy := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
healthyPath := env.seedArchiveRows(
|
||||
t, healthyID,
|
||||
t, healthy,
|
||||
time.Now().Add(-48*time.Hour),
|
||||
time.Now().Add(-time.Minute),
|
||||
)
|
||||
@@ -518,14 +529,14 @@ func TestArchiveSweep_ContinuesAfterPerWebhookFailure(
|
||||
assert.Equal(
|
||||
t, []string{sweepRowNew},
|
||||
archivedEventIDs(t, healthyPath),
|
||||
"a failure for an earlier webhook must not stop the "+
|
||||
"a failure for an earlier target must not stop the "+
|
||||
"sweep from pruning the ones after it",
|
||||
)
|
||||
}
|
||||
|
||||
// TestArchiveSweep_OpenExistingDoesNotCreateFile pins the second
|
||||
// of the two no-create guards. The first is the stat in
|
||||
// sweepWebhook; this one is the SQLite open mode, which is what
|
||||
// sweepExpired; this one is the SQLite open mode, which is what
|
||||
// protects the window between that stat and the open. Flipping
|
||||
// the sweep's mode to create-if-missing makes this fail.
|
||||
func TestArchiveSweep_OpenExistingDoesNotCreateFile(
|
||||
@@ -561,13 +572,13 @@ func TestArchiveSweep_OpenExistingDoesNotCreateFile(
|
||||
func TestArchiveSweep_PrunesIdleArchive(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupSweeperTest(t)
|
||||
env := setupArchiveTest(t)
|
||||
|
||||
webhookID := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
|
||||
now := time.Now()
|
||||
path := env.seedArchiveRows(
|
||||
t, webhookID,
|
||||
t, tgt,
|
||||
now.Add(-48*time.Hour),
|
||||
now.Add(-time.Minute),
|
||||
)
|
||||
@@ -600,11 +611,11 @@ func TestArchiveSweep_PrunesIdleArchive(t *testing.T) {
|
||||
func TestArchiveSweep_LeavesArchiveClosed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupSweeperTest(t)
|
||||
env := setupArchiveTest(t)
|
||||
|
||||
webhookID := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
path := env.seedArchiveRows(
|
||||
t, webhookID, time.Now().Add(-48*time.Hour),
|
||||
t, tgt, time.Now().Add(-48*time.Hour),
|
||||
)
|
||||
|
||||
w := delivery.NewExportArchiveWriter(
|
||||
@@ -640,35 +651,32 @@ func TestArchiveSweep_ClosesHandleOfRegisteredWriter(
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupSweeperTest(t)
|
||||
env := setupArchiveTest(t)
|
||||
|
||||
webhookID := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
env.seedArchiveRows(
|
||||
t, webhookID, time.Now().Add(-48*time.Hour),
|
||||
t, tgt, time.Now().Add(-48*time.Hour),
|
||||
)
|
||||
|
||||
webhookDB := testWebhookDB(t)
|
||||
event := seedEvent(t, webhookDB, `{"n":1}`)
|
||||
event.WebhookID = webhookID
|
||||
d := seedDatabaseTargetDelivery(
|
||||
t, webhookDB, event, `{"expiry":"1h"}`,
|
||||
)
|
||||
d := seedDatabaseTargetDelivery(t, webhookDB, event, tgt)
|
||||
|
||||
env.eng.ExportDeliverDatabase(webhookDB, d)
|
||||
|
||||
require.True(
|
||||
t, env.eng.ExportArchiveHandleOpen(webhookID),
|
||||
t, env.eng.ExportArchiveHandleOpen(tgt.ID),
|
||||
"the delivery must leave the archive handle open",
|
||||
)
|
||||
|
||||
env.sweeper.ExportSweep(context.Background())
|
||||
|
||||
require.True(
|
||||
t, env.eng.ExportHasArchiveWriter(webhookID),
|
||||
t, env.eng.ExportHasArchiveWriter(tgt.ID),
|
||||
"the delivery's registry entry must survive the sweep",
|
||||
)
|
||||
assert.False(
|
||||
t, env.eng.ExportArchiveHandleOpen(webhookID),
|
||||
t, env.eng.ExportArchiveHandleOpen(tgt.ID),
|
||||
"the sweep must leave the archive closed",
|
||||
)
|
||||
}
|
||||
@@ -684,11 +692,11 @@ func TestArchiveSweep_NeverExpiryUntouched(t *testing.T) {
|
||||
`{"expiry":""}`,
|
||||
"",
|
||||
} {
|
||||
env := setupSweeperTest(t)
|
||||
env := setupArchiveTest(t)
|
||||
|
||||
webhookID := env.seedDatabaseTarget(t, configJSON)
|
||||
tgt := env.seedDatabaseTarget(t, configJSON)
|
||||
path := env.seedArchiveRows(
|
||||
t, webhookID,
|
||||
t, tgt,
|
||||
time.Now().Add(-10000*time.Hour),
|
||||
)
|
||||
|
||||
@@ -699,7 +707,7 @@ func TestArchiveSweep_NeverExpiryUntouched(t *testing.T) {
|
||||
"config %q must keep rows forever", configJSON,
|
||||
)
|
||||
assert.False(
|
||||
t, env.eng.ExportHasArchiveWriter(webhookID),
|
||||
t, env.eng.ExportHasArchiveWriter(tgt.ID),
|
||||
"config %q must leave no registry entry behind",
|
||||
configJSON,
|
||||
)
|
||||
@@ -722,10 +730,10 @@ func TestArchiveSweep_NeverExpirySkipsBeforeOpening(
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupSweeperTest(t)
|
||||
env := setupArchiveTest(t)
|
||||
|
||||
webhookID := env.seedDatabaseTarget(t, `{"expiry":"never"}`)
|
||||
path := env.archivePath(webhookID)
|
||||
tgt := env.seedDatabaseTarget(t, `{"expiry":"never"}`)
|
||||
path := env.archivePath(tgt)
|
||||
|
||||
seedUnmigratedArchive(t, path)
|
||||
require.False(t, archiveTableExists(t, path))
|
||||
@@ -768,16 +776,16 @@ func archiveTableExists(t *testing.T, path string) bool {
|
||||
}
|
||||
|
||||
// TestArchiveSweep_DoesNotCreateArchiveFile proves the sweep
|
||||
// never conjures an archive: a webhook with a database target
|
||||
// that has never received an event must still have no archive
|
||||
// file (nor SQLite sidecar) after a sweep.
|
||||
// never conjures an archive: a database target that has never
|
||||
// received an event must still have no archive file (nor SQLite
|
||||
// sidecar) after a sweep, and no registry entry either.
|
||||
func TestArchiveSweep_DoesNotCreateArchiveFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupSweeperTest(t)
|
||||
env := setupArchiveTest(t)
|
||||
|
||||
webhookID := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
path := env.archivePath(webhookID)
|
||||
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
path := env.archivePath(tgt)
|
||||
|
||||
require.NoFileExists(t, path)
|
||||
|
||||
@@ -789,6 +797,11 @@ func TestArchiveSweep_DoesNotCreateArchiveFile(t *testing.T) {
|
||||
"the sweep must not create an archive file",
|
||||
)
|
||||
}
|
||||
|
||||
assert.False(
|
||||
t, env.eng.ExportHasArchiveWriter(tgt.ID),
|
||||
"the sweep must leave no registry entry behind",
|
||||
)
|
||||
}
|
||||
|
||||
// TestArchiveSweep_DoesNotCreateAfterWriterExists covers the
|
||||
@@ -800,11 +813,11 @@ func TestArchiveSweep_DoesNotCreateAfterWriterExists(
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupSweeperTest(t)
|
||||
env := setupArchiveTest(t)
|
||||
|
||||
webhookID := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
|
||||
path, err := env.eng.ExportEnsureArchiveWriter(webhookID)
|
||||
path, err := env.eng.ExportEnsureArchiveWriter(tgt.ID)
|
||||
require.NoError(t, err)
|
||||
require.NoFileExists(t, path)
|
||||
|
||||
@@ -819,17 +832,17 @@ func TestArchiveSweep_DoesNotCreateAfterWriterExists(
|
||||
func TestArchiveSweep_SkipsDeletedWebhookTargets(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupSweeperTest(t)
|
||||
env := setupArchiveTest(t)
|
||||
|
||||
webhookID := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
path := env.seedArchiveRows(
|
||||
t, webhookID, time.Now().Add(-48*time.Hour),
|
||||
t, tgt, time.Now().Add(-48*time.Hour),
|
||||
)
|
||||
|
||||
require.NoError(
|
||||
t,
|
||||
env.mainDB.DB().
|
||||
Where("webhook_id = ?", webhookID).
|
||||
Where("webhook_id = ?", tgt.WebhookID).
|
||||
Delete(&database.Target{}).Error,
|
||||
)
|
||||
|
||||
@@ -842,14 +855,14 @@ func TestArchiveSweep_SkipsDeletedWebhookTargets(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestArchiveSweep_ConcurrentWrites proves the sweep serialises
|
||||
// against writes through the per-webhook writer mutex. Run
|
||||
// under -race, an unsynchronised sweep would be caught here.
|
||||
// against writes through the target's writer mutex. Run under
|
||||
// -race, an unsynchronised sweep would be caught here.
|
||||
func TestArchiveSweep_ConcurrentWrites(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupSweeperTest(t)
|
||||
env := setupArchiveTest(t)
|
||||
|
||||
webhookID := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
|
||||
webhookDB := testWebhookDB(t)
|
||||
|
||||
@@ -862,13 +875,10 @@ func TestArchiveSweep_ConcurrentWrites(t *testing.T) {
|
||||
|
||||
for range sweepConcurrentWrites {
|
||||
event := seedEvent(t, webhookDB, `{"n":1}`)
|
||||
event.WebhookID = webhookID
|
||||
|
||||
deliveries = append(
|
||||
deliveries,
|
||||
seedDatabaseTargetDelivery(
|
||||
t, webhookDB, event, `{"expiry":"1h"}`,
|
||||
),
|
||||
seedDatabaseTargetDelivery(t, webhookDB, event, tgt),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -894,7 +904,7 @@ func TestArchiveSweep_ConcurrentWrites(t *testing.T) {
|
||||
|
||||
wg.Wait()
|
||||
|
||||
assert.FileExists(t, env.archivePath(webhookID))
|
||||
assert.FileExists(t, env.archivePath(tgt))
|
||||
}
|
||||
|
||||
// TestArchiveSweeper_StopsCleanly proves the background loop
|
||||
@@ -902,11 +912,11 @@ func TestArchiveSweep_ConcurrentWrites(t *testing.T) {
|
||||
func TestArchiveSweeper_StopsCleanly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupSweeperTest(t)
|
||||
env := setupArchiveTest(t)
|
||||
|
||||
webhookID := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
tgt := env.seedDatabaseTarget(t, `{"expiry":"1h"}`)
|
||||
env.seedArchiveRows(
|
||||
t, webhookID, time.Now().Add(-48*time.Hour),
|
||||
t, tgt, time.Now().Add(-48*time.Hour),
|
||||
)
|
||||
|
||||
env.sweeper.ExportSetInterval(time.Millisecond)
|
||||
@@ -930,7 +940,7 @@ func TestArchiveSweeper_StopHookHonoursStopTimeout(
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
env := setupSweeperTest(t)
|
||||
env := setupArchiveTest(t)
|
||||
|
||||
lc := &recordingLifecycle{}
|
||||
env.sweeper.ExportRegisterHooks(lc)
|
||||
|
||||
Reference in New Issue
Block a user