aboutsummaryrefslogtreecommitdiffstats
path: root/api/api_stress_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'api/api_stress_test.go')
-rw-r--r--api/api_stress_test.go134
1 files changed, 134 insertions, 0 deletions
diff --git a/api/api_stress_test.go b/api/api_stress_test.go
index a846f75..4fcbf5e 100644
--- a/api/api_stress_test.go
+++ b/api/api_stress_test.go
@@ -194,6 +194,140 @@ func TestStress_LargeDataset(t *testing.T) {
}
}
+func TestStress_ConcurrentMixedOperations(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping stress test in short mode")
+ }
+
+ setupTestDB(t)
+
+ // Create multiple feeds with items across categories
+ categories := []string{"tech", "news", "science", "art"}
+ for i, cat := range categories {
+ f := &feed.Feed{
+ Url: fmt.Sprintf("http://example.com/mixed/%d", i),
+ Title: fmt.Sprintf("Mixed Feed %d", i),
+ Category: cat,
+ }
+ f.Create()
+ for j := 0; j < 25; j++ {
+ it := &item.Item{
+ Title: fmt.Sprintf("Mixed Item %d-%d", i, j),
+ Url: fmt.Sprintf("http://example.com/mixed/%d/%d", i, j),
+ Description: fmt.Sprintf("<p>Mixed content %d-%d</p>", i, j),
+ PublishDate: "2024-01-01 00:00:00",
+ FeedId: f.Id,
+ }
+ _ = it.Create()
+ }
+ }
+
+ server := newTestServer()
+
+ const goroutines = 40
+ var wg sync.WaitGroup
+ errors := make(chan error, goroutines*2)
+
+ start := time.Now()
+
+ // Mix of reads, filtered reads, updates, and exports
+ for i := 0; i < goroutines; i++ {
+ wg.Add(1)
+ go func(idx int) {
+ defer wg.Done()
+
+ var req *http.Request
+ switch idx % 4 {
+ case 0:
+ // Stream with category filter
+ req = httptest.NewRequest("GET", "/stream?tag="+categories[idx%len(categories)], nil)
+ case 1:
+ // Stream with search
+ req = httptest.NewRequest("GET", "/stream?q=Mixed", nil)
+ case 2:
+ // Feed list
+ req = httptest.NewRequest("GET", "/feed", nil)
+ case 3:
+ // Export
+ req = httptest.NewRequest("GET", "/export/json", nil)
+ }
+
+ rr := httptest.NewRecorder()
+ server.ServeHTTP(rr, req)
+ if rr.Code != http.StatusOK {
+ errors <- fmt.Errorf("op %d (type %d) got status %d", idx, idx%4, rr.Code)
+ }
+ }(i)
+ }
+ wg.Wait()
+ close(errors)
+ elapsed := time.Since(start)
+
+ for err := range errors {
+ t.Errorf("concurrent mixed operation error: %v", err)
+ }
+
+ t.Logf("40 concurrent mixed operations completed in %v", elapsed)
+ if elapsed > 10*time.Second {
+ t.Errorf("concurrent mixed operations took too long: %v (threshold: 10s)", elapsed)
+ }
+}
+
+func TestStress_RapidReadMarkUnmark(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping stress test in short mode")
+ }
+
+ setupTestDB(t)
+
+ f := &feed.Feed{Url: "http://example.com/rapid", Title: "Rapid Feed"}
+ f.Create()
+ it := &item.Item{
+ Title: "Rapid Toggle",
+ Url: "http://example.com/rapid/1",
+ FeedId: f.Id,
+ }
+ _ = it.Create()
+
+ server := newTestServer()
+
+ // Rapidly toggle read state on the same item
+ const iterations = 100
+ var wg sync.WaitGroup
+ errors := make(chan error, iterations)
+
+ start := time.Now()
+ for i := 0; i < iterations; i++ {
+ wg.Add(1)
+ go func(idx int) {
+ defer wg.Done()
+ body, _ := json.Marshal(item.Item{
+ Id: it.Id,
+ ReadState: idx%2 == 0,
+ Starred: idx%3 == 0,
+ })
+ req := httptest.NewRequest("PUT", "/item/"+strconv.FormatInt(it.Id, 10), bytes.NewBuffer(body))
+ rr := httptest.NewRecorder()
+ server.ServeHTTP(rr, req)
+ if rr.Code != http.StatusOK {
+ errors <- fmt.Errorf("rapid update %d got status %d", idx, rr.Code)
+ }
+ }(i)
+ }
+ wg.Wait()
+ close(errors)
+ elapsed := time.Since(start)
+
+ for err := range errors {
+ t.Errorf("rapid toggle error: %v", err)
+ }
+
+ t.Logf("100 concurrent read-state toggles completed in %v", elapsed)
+ if elapsed > 10*time.Second {
+ t.Errorf("rapid toggles took too long: %v (threshold: 10s)", elapsed)
+ }
+}
+
func seedStressData(t *testing.T, count int) {
t.Helper()
f := &feed.Feed{Url: "http://example.com/bench", Title: "Bench Feed", Category: "tech"}