187 lines
5 KiB
Go
187 lines
5 KiB
Go
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"sync"
|
||
|
|
"sync/atomic"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/maddalax/htmgo/framework/h"
|
||
|
|
"github.com/maddalax/htmgo/framework/h/cache"
|
||
|
|
)
|
||
|
|
|
||
|
|
// This example demonstrates the atomic guarantees of GetOrCompute,
|
||
|
|
// showing how it prevents duplicate expensive computations when
|
||
|
|
// multiple goroutines request the same uncached key simultaneously.
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
fmt.Println("=== Atomic Cache Example ===")
|
||
|
|
|
||
|
|
// Demonstrate the problem without atomic guarantees
|
||
|
|
demonstrateProblem()
|
||
|
|
|
||
|
|
fmt.Println("\n=== Now with GetOrCompute atomic guarantees ===")
|
||
|
|
|
||
|
|
// Show the solution with GetOrCompute
|
||
|
|
demonstrateSolution()
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
// demonstrateProblem shows what happens without atomic guarantees
|
||
|
|
func demonstrateProblem() {
|
||
|
|
fmt.Println("Without atomic guarantees (simulated):")
|
||
|
|
fmt.Println("Multiple goroutines checking cache and computing...")
|
||
|
|
|
||
|
|
var computeCount int32
|
||
|
|
var wg sync.WaitGroup
|
||
|
|
|
||
|
|
// Simulate 10 goroutines trying to get the same uncached value
|
||
|
|
for i := range 10 {
|
||
|
|
wg.Add(1)
|
||
|
|
go func(id int) {
|
||
|
|
defer wg.Done()
|
||
|
|
|
||
|
|
// Simulate checking cache (not found)
|
||
|
|
time.Sleep(time.Millisecond) // Small delay to increase collision chance
|
||
|
|
|
||
|
|
// All goroutines think the value is not cached
|
||
|
|
// so they all compute it
|
||
|
|
atomic.AddInt32(&computeCount, 1)
|
||
|
|
fmt.Printf("Goroutine %d: Computing expensive value...\n", id)
|
||
|
|
|
||
|
|
// Simulate expensive computation
|
||
|
|
time.Sleep(50 * time.Millisecond)
|
||
|
|
}(i)
|
||
|
|
}
|
||
|
|
|
||
|
|
wg.Wait()
|
||
|
|
fmt.Printf("\nResult: Computed %d times (wasteful!)\n", computeCount)
|
||
|
|
}
|
||
|
|
|
||
|
|
// demonstrateSolution shows how GetOrCompute solves the problem
|
||
|
|
func demonstrateSolution() {
|
||
|
|
// Create a cache store
|
||
|
|
store := cache.NewTTLStore[string, string]()
|
||
|
|
defer store.Close()
|
||
|
|
|
||
|
|
var computeCount int32
|
||
|
|
var wg sync.WaitGroup
|
||
|
|
|
||
|
|
fmt.Println("With GetOrCompute atomic guarantees:")
|
||
|
|
fmt.Println("Multiple goroutines requesting the same key...")
|
||
|
|
|
||
|
|
startTime := time.Now()
|
||
|
|
|
||
|
|
// Launch 10 goroutines trying to get the same value
|
||
|
|
for i := range 10 {
|
||
|
|
wg.Add(1)
|
||
|
|
go func(id int) {
|
||
|
|
defer wg.Done()
|
||
|
|
|
||
|
|
// All goroutines call GetOrCompute at the same time
|
||
|
|
result := store.GetOrCompute("expensive-key", func() string {
|
||
|
|
// Only ONE goroutine will execute this function
|
||
|
|
count := atomic.AddInt32(&computeCount, 1)
|
||
|
|
fmt.Printf("Goroutine %d: Computing expensive value (computation #%d)\n", id, count)
|
||
|
|
|
||
|
|
// Simulate expensive computation
|
||
|
|
time.Sleep(50 * time.Millisecond)
|
||
|
|
|
||
|
|
return fmt.Sprintf("Expensive result computed by goroutine %d", id)
|
||
|
|
}, 1*time.Hour)
|
||
|
|
|
||
|
|
fmt.Printf("Goroutine %d: Got result: %s\n", id, result)
|
||
|
|
}(i)
|
||
|
|
}
|
||
|
|
|
||
|
|
wg.Wait()
|
||
|
|
elapsed := time.Since(startTime)
|
||
|
|
|
||
|
|
fmt.Printf("\nResult: Computed only %d time (efficient!)\n", computeCount)
|
||
|
|
fmt.Printf("Total time: %v (vs ~500ms if all computed)\n", elapsed)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Example with htmgo cached components
|
||
|
|
func ExampleCachedComponent() {
|
||
|
|
fmt.Println("\n=== Real-world htmgo Example ===")
|
||
|
|
|
||
|
|
var renderCount int32
|
||
|
|
|
||
|
|
// Create a cached component that simulates fetching user data
|
||
|
|
UserProfile := h.CachedPerKeyT(5*time.Minute, func(userID int) (int, h.GetElementFunc) {
|
||
|
|
return userID, func() *h.Element {
|
||
|
|
count := atomic.AddInt32(&renderCount, 1)
|
||
|
|
fmt.Printf("Fetching and rendering user %d (render #%d)\n", userID, count)
|
||
|
|
|
||
|
|
// Simulate database query
|
||
|
|
time.Sleep(100 * time.Millisecond)
|
||
|
|
|
||
|
|
return h.Div(
|
||
|
|
h.H2(h.Text(fmt.Sprintf("User Profile #%d", userID))),
|
||
|
|
h.P(h.Text("This was expensive to compute!")),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
// Simulate multiple concurrent requests for the same user
|
||
|
|
var wg sync.WaitGroup
|
||
|
|
for i := range 5 {
|
||
|
|
wg.Add(1)
|
||
|
|
go func(requestID int) {
|
||
|
|
defer wg.Done()
|
||
|
|
|
||
|
|
// All requests are for user 123
|
||
|
|
html := h.Render(UserProfile(123))
|
||
|
|
fmt.Printf("Request %d: Received %d bytes of HTML\n", requestID, len(html))
|
||
|
|
}(i)
|
||
|
|
}
|
||
|
|
|
||
|
|
wg.Wait()
|
||
|
|
fmt.Printf("\nTotal renders: %d (only one, despite 5 concurrent requests!)\n", renderCount)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Example showing cache stampede prevention
|
||
|
|
func ExampleCacheStampedePrevention() {
|
||
|
|
fmt.Println("\n=== Cache Stampede Prevention ===")
|
||
|
|
|
||
|
|
store := cache.NewLRUStore[string, string](100)
|
||
|
|
defer store.Close()
|
||
|
|
|
||
|
|
var dbQueries int32
|
||
|
|
|
||
|
|
// Simulate a popular cache key expiring
|
||
|
|
fetchPopularData := func(key string) string {
|
||
|
|
return store.GetOrCompute(key, func() string {
|
||
|
|
queries := atomic.AddInt32(&dbQueries, 1)
|
||
|
|
fmt.Printf("Database query #%d for key: %s\n", queries, key)
|
||
|
|
|
||
|
|
// Simulate slow database query
|
||
|
|
time.Sleep(200 * time.Millisecond)
|
||
|
|
|
||
|
|
return fmt.Sprintf("Popular data for %s", key)
|
||
|
|
}, 100*time.Millisecond) // Short TTL to simulate expiration
|
||
|
|
}
|
||
|
|
|
||
|
|
// First, populate the cache
|
||
|
|
_ = fetchPopularData("trending-posts")
|
||
|
|
fmt.Println("Cache populated")
|
||
|
|
|
||
|
|
// Wait for it to expire
|
||
|
|
time.Sleep(150 * time.Millisecond)
|
||
|
|
fmt.Println("\nCache expired, simulating traffic spike...")
|
||
|
|
|
||
|
|
// Simulate 20 concurrent requests right after expiration
|
||
|
|
var wg sync.WaitGroup
|
||
|
|
for i := 0; i < 20; i++ {
|
||
|
|
wg.Add(1)
|
||
|
|
go func(id int) {
|
||
|
|
defer wg.Done()
|
||
|
|
data := fetchPopularData("trending-posts")
|
||
|
|
fmt.Printf("Request %d: Got data: %s\n", id, data)
|
||
|
|
}(i)
|
||
|
|
}
|
||
|
|
|
||
|
|
wg.Wait()
|
||
|
|
fmt.Printf("\nTotal database queries: %d (prevented 19 redundant queries!)\n", dbQueries)
|
||
|
|
}
|