master
go 156 lines 4.67 KB
Raw
1 package cli
2
3 import (
4 "bytes"
5 "crypto/rand"
6 "fmt"
7 "io"
8 "net/http"
9 "os/exec"
10 "testing"
11 "time"
12
13 "github.com/ipfs/kubo/config"
14 "github.com/ipfs/kubo/test/cli/harness"
15 "github.com/multiformats/go-multiaddr"
16 manet "github.com/multiformats/go-multiaddr/net"
17 "github.com/stretchr/testify/require"
18 )
19
20 func TestDaemon(t *testing.T) {
21 t.Parallel()
22
23 t.Run("daemon starts if api is set to null", func(t *testing.T) {
24 t.Parallel()
25 node := harness.NewT(t).NewNode().Init()
26 node.SetIPFSConfig("Addresses.API", nil)
27 node.Runner.MustRun(harness.RunRequest{
28 Path: node.IPFSBin,
29 Args: []string{"daemon"},
30 RunFunc: (*exec.Cmd).Start, // Start without waiting for completion.
31 })
32
33 node.StopDaemon()
34 })
35
36 t.Run("daemon shuts down gracefully with active operations", func(t *testing.T) {
37 t.Parallel()
38
39 // Start daemon with multiple components active via config
40 node := harness.NewT(t).NewNode().Init()
41
42 // Enable experimental features and pubsub via config
43 node.UpdateConfig(func(cfg *config.Config) {
44 cfg.Pubsub.Enabled = config.True // Instead of --enable-pubsub-experiment
45 cfg.Experimental.P2pHttpProxy = true // Enable P2P HTTP proxy
46 cfg.Experimental.GatewayOverLibp2p = true // Enable gateway over libp2p
47 })
48
49 node.StartDaemon("--enable-gc")
50
51 // Start background operations to simulate real daemon workload:
52 // 1. "ipfs add" simulates content onboarding/ingestion work
53 // 2. Gateway request simulates content retrieval and gateway processing work
54
55 // Background operation 1: Continuous add of random data to simulate onboarding
56 addDone := make(chan struct{})
57 go func() {
58 defer close(addDone)
59
60 // Start the add command asynchronously
61 res := node.Runner.Run(harness.RunRequest{
62 Path: node.IPFSBin,
63 Args: []string{"add", "--progress=false", "-"},
64 RunFunc: (*exec.Cmd).Start,
65 CmdOpts: []harness.CmdOpt{
66 harness.RunWithStdin(&infiniteReader{}),
67 },
68 })
69
70 // Wait for command to finish (when daemon stops)
71 if res.Cmd != nil {
72 _ = res.Cmd.Wait() // Ignore error, expect command to be killed during shutdown
73 }
74 }()
75
76 // Background operation 2: Gateway CAR request to simulate retrieval work
77 gatewayDone := make(chan struct{})
78 go func() {
79 defer close(gatewayDone)
80
81 // First add a file sized to ensure gateway request takes ~1 minute
82 largeData := make([]byte, 512*1024) // 512KB of data
83 _, _ = rand.Read(largeData) // Always succeeds for crypto/rand
84 testCID := node.IPFSAdd(bytes.NewReader(largeData))
85
86 // Get gateway address from config
87 cfg := node.ReadConfig()
88 gatewayMaddr, err := multiaddr.NewMultiaddr(cfg.Addresses.Gateway[0])
89 if err != nil {
90 return
91 }
92 gatewayAddr, err := manet.ToNetAddr(gatewayMaddr)
93 if err != nil {
94 return
95 }
96
97 // Request CAR but slow reading to simulate heavy gateway load
98 gatewayURL := fmt.Sprintf("http://%s/ipfs/%s?format=car", gatewayAddr, testCID)
99
100 client := &http.Client{Timeout: 90 * time.Second}
101 resp, err := client.Get(gatewayURL)
102 if err == nil {
103 defer resp.Body.Close()
104 // Read response slowly: 512KB ÷ 1KB × 125ms = ~64 seconds (1+ minute) total
105 // This ensures operation is still active when we shutdown at 2 seconds
106 buf := make([]byte, 1024) // 1KB buffer
107 for {
108 if _, err := io.ReadFull(resp.Body, buf); err != nil {
109 return
110 }
111 time.Sleep(125 * time.Millisecond) // 125ms delay = ~64s total for 512KB
112 }
113 }
114 }()
115
116 // Let operations run for 2 seconds to ensure they're active
117 time.Sleep(2 * time.Second)
118
119 // Trigger graceful shutdown
120 shutdownStart := time.Now()
121 node.StopDaemon()
122 shutdownDuration := time.Since(shutdownStart)
123
124 // Verify clean shutdown:
125 // - Daemon should stop within reasonable time (not hang)
126 require.Less(t, shutdownDuration, 10*time.Second, "daemon should shut down within 10 seconds")
127
128 // Wait for background operations to complete (with timeout)
129 select {
130 case <-addDone:
131 // Good, add operation terminated
132 case <-time.After(5 * time.Second):
133 t.Error("add operation did not terminate within 5 seconds after daemon shutdown")
134 }
135
136 select {
137 case <-gatewayDone:
138 // Good, gateway operation terminated
139 case <-time.After(5 * time.Second):
140 t.Error("gateway operation did not terminate within 5 seconds after daemon shutdown")
141 }
142
143 // Verify we can restart with same repo (no lock issues)
144 node.StartDaemon()
145 node.StopDaemon()
146 })
147 }
148
149 // infiniteReader provides an infinite stream of random data
150 type infiniteReader struct{}
151
152 func (r *infiniteReader) Read(p []byte) (n int, err error) {
153 _, _ = rand.Read(p) // Always succeeds for crypto/rand
154 time.Sleep(50 * time.Millisecond) // Rate limit to simulate steady stream
155 return len(p), nil
156 }