* fix: remove optimistic UI updates in external actions to prevent race condition (#4711) When adding/deleting external actions, the UI would sometimes show duplicated or cloned rows due to a race condition between: 1. The optimistic setActions() call updating local state 2. The save() callback updating externalActions in context 3. The useEffect syncing actions from externalActions This fix removes the optimistic updates and lets the server response be the single source of truth, updating the UI only after the context is updated via the save() success callback. Fixes #4347 * fix(ap): additional outbound ActivityPub delivery optimizations - check circuit breaker before signing any messages or any other work - create outbound messages in batches and not all at once - add small delay between outbound messages to spread it out a bit * fix(ap): increase ap queue buffer size, reduce outbound http client timeout * fix(ap): handle malformed inbox URIs * fix(ap): batch outbox based on work performed not index * feat(ap): add retryable activitypub outbound delivery client --------- Co-authored-by: John Costa <jcosta@execonline.com>
134 lines
3.7 KiB
Go
134 lines
3.7 KiB
Go
package workerpool
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// resetCircuitBreakerForTesting clears all failed domain state for testing purposes.
|
|
// This function provides test isolation by resetting the global circuit breaker state.
|
|
func resetCircuitBreakerForTesting() {
|
|
failedDomainsMutex.Lock()
|
|
defer failedDomainsMutex.Unlock()
|
|
failedDomains = make(map[string]*domainFailure)
|
|
}
|
|
|
|
func TestCircuitBreaker(t *testing.T) {
|
|
// Ensure clean state before test
|
|
resetCircuitBreakerForTesting()
|
|
defer resetCircuitBreakerForTesting() // Clean up after test
|
|
|
|
testDomain := "failing.example.com"
|
|
|
|
// Initially, domain should not be skipped
|
|
if ShouldSkipDomain(testDomain) {
|
|
t.Error("Domain should not be skipped initially")
|
|
}
|
|
|
|
// Record failures
|
|
recordDomainFailure(testDomain)
|
|
recordDomainFailure(testDomain)
|
|
recordDomainFailure(testDomain)
|
|
|
|
// Domain should now be skipped
|
|
if !ShouldSkipDomain(testDomain) {
|
|
t.Error("Domain should be skipped after failures")
|
|
}
|
|
|
|
// After successful delivery, domain should be reset
|
|
resetDomainFailure(testDomain)
|
|
if ShouldSkipDomain(testDomain) {
|
|
t.Error("Domain should not be skipped after reset")
|
|
}
|
|
}
|
|
|
|
func TestHTTPTimeouts(t *testing.T) {
|
|
// Ensure clean state before test
|
|
resetCircuitBreakerForTesting()
|
|
defer resetCircuitBreakerForTesting() // Clean up after test
|
|
|
|
// Initialize HTTP client
|
|
InitOutboundWorkerPool(1)
|
|
|
|
if httpClient == nil {
|
|
t.Error("HTTP client should be initialized")
|
|
}
|
|
|
|
if httpClient.Timeout != 8*time.Second {
|
|
t.Errorf("HTTP client should have 8 second timeout, got %v", httpClient.Timeout)
|
|
}
|
|
}
|
|
|
|
func TestWorkerPoolSizing(t *testing.T) {
|
|
// Ensure clean state before test
|
|
resetCircuitBreakerForTesting()
|
|
defer resetCircuitBreakerForTesting() // Clean up after test
|
|
|
|
// Test that queue buffer is at least the minimum (500) even for small worker pools
|
|
InitOutboundWorkerPool(5)
|
|
|
|
if cap(queue) < 500 {
|
|
t.Errorf("Queue capacity should be at least 500, got %d", cap(queue))
|
|
}
|
|
|
|
// Test that larger worker pools get proportionally larger buffers
|
|
InitOutboundWorkerPool(100)
|
|
if cap(queue) != 1000 {
|
|
t.Errorf("Queue capacity should be 1000 for 100 workers, got %d", cap(queue))
|
|
}
|
|
}
|
|
|
|
func TestBackoffDurations(t *testing.T) {
|
|
// Test that backoff durations are properly configured
|
|
expectedDurations := []time.Duration{
|
|
1 * time.Minute,
|
|
5 * time.Minute,
|
|
15 * time.Minute,
|
|
30 * time.Minute,
|
|
60 * time.Minute,
|
|
}
|
|
|
|
if len(circuitBreakerBackoffDurations) != len(expectedDurations) {
|
|
t.Errorf("Expected %d backoff durations, got %d", len(expectedDurations), len(circuitBreakerBackoffDurations))
|
|
}
|
|
|
|
for i, expected := range expectedDurations {
|
|
if circuitBreakerBackoffDurations[i] != expected {
|
|
t.Errorf("Backoff duration at index %d: expected %v, got %v", i, expected, circuitBreakerBackoffDurations[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCircuitBreakerIsolation(t *testing.T) {
|
|
// Test that multiple tests don't interfere with each other
|
|
resetCircuitBreakerForTesting()
|
|
defer resetCircuitBreakerForTesting()
|
|
|
|
domain1 := "test1.example.com"
|
|
domain2 := "test2.example.com"
|
|
|
|
// Neither domain should be blocked initially
|
|
if ShouldSkipDomain(domain1) || ShouldSkipDomain(domain2) {
|
|
t.Error("Domains should not be blocked initially")
|
|
}
|
|
|
|
// Record failures for domain1 only
|
|
recordDomainFailure(domain1)
|
|
recordDomainFailure(domain1)
|
|
recordDomainFailure(domain1)
|
|
|
|
// Only domain1 should be blocked
|
|
if !ShouldSkipDomain(domain1) {
|
|
t.Error("Domain1 should be blocked after failures")
|
|
}
|
|
if ShouldSkipDomain(domain2) {
|
|
t.Error("Domain2 should not be blocked")
|
|
}
|
|
|
|
// Reset and verify clean state
|
|
resetCircuitBreakerForTesting()
|
|
if ShouldSkipDomain(domain1) || ShouldSkipDomain(domain2) {
|
|
t.Error("Both domains should be unblocked after reset")
|
|
}
|
|
}
|