3
import (
4
"bytes"
5
"encoding/json"
6
+ "fmt"
7
"net/http"
8
"net/http/httptest"
9
"strings"
609
})
610
}
611
612
+// runResumeTests validates Provide.DHT.ResumeEnabled behavior for SweepingProvider.
613
+//
614
+// Background: The provider tracks current_time_offset = (now - cycleStart) % interval
615
+// where cycleStart is the timestamp marking the beginning of the reprovide cycle.
616
+// With ResumeEnabled=true, cycleStart persists in the datastore across restarts.
617
+// With ResumeEnabled=false, cycleStart resets to 'now' on each startup.
618
+func runResumeTests(t *testing.T, apply cfgApplier) {
619
+ t.Helper()
620
+
621
+ const (
622
+ reprovideInterval = 30 * time.Second
623
+ initialRuntime = 10 * time.Second // Let cycle progress
624
+ downtime = 5 * time.Second // Simulated offline period
625
+ restartTime = 2 * time.Second // Daemon restart stabilization
626
+
627
+ // Thresholds account for timing jitter (~2-3s margin)
628
+ minOffsetBeforeRestart = 8 * time.Second // Expect ~10s
629
+ minOffsetAfterResume = 12 * time.Second // Expect ~17s (10s + 5s + 2s)
630
+ maxOffsetAfterReset = 5 * time.Second // Expect ~2s (fresh start)
631
+ )
632
+
633
+ setupNode := func(t *testing.T, resumeEnabled bool) *harness.Node {
634
+ node := harness.NewT(t).NewNode().Init()
635
+ apply(node) // Sets Provide.DHT.SweepEnabled=true
636
+ node.SetIPFSConfig("Provide.DHT.ResumeEnabled", resumeEnabled)
637
+ node.SetIPFSConfig("Provide.DHT.Interval", reprovideInterval.String())
638
+ node.SetIPFSConfig("Bootstrap", []string{})
639
+ node.StartDaemon()
640
+ return node
641
+ }
642
+
643
+ t.Run("preserves cycle state across restart", func(t *testing.T) {
644
+ t.Parallel()
645
+
646
+ node := setupNode(t, true)
647
+ defer node.StopDaemon()
648
+
649
+ for i := 0; i < 10; i++ {
650
+ node.IPFSAddStr(fmt.Sprintf("resume-test-%d-%d", i, time.Now().UnixNano()))
651
+ }
652
+
653
+ time.Sleep(initialRuntime)
654
+
655
+ beforeRestart := node.IPFS("provide", "stat", "--enc=json")
656
+ offsetBeforeRestart, _, err := parseProvideStatJSON(beforeRestart.Stdout.String())
657
+ require.NoError(t, err)
658
+ require.Greater(t, offsetBeforeRestart, minOffsetBeforeRestart,
659
+ "cycle should have progressed")
660
+
661
+ node.StopDaemon()
662
+ time.Sleep(downtime)
663
+ node.StartDaemon()
664
+ time.Sleep(restartTime)
665
+
666
+ afterRestart := node.IPFS("provide", "stat", "--enc=json")
667
+ offsetAfterRestart, _, err := parseProvideStatJSON(afterRestart.Stdout.String())
668
+ require.NoError(t, err)
669
+
670
+ assert.GreaterOrEqual(t, offsetAfterRestart, minOffsetAfterResume,
671
+ "offset should account for downtime")
672
+ })
673
+
674
+ t.Run("resets cycle when disabled", func(t *testing.T) {
675
+ t.Parallel()
676
+
677
+ node := setupNode(t, false)
678
+ defer node.StopDaemon()
679
+
680
+ for i := 0; i < 10; i++ {
681
+ node.IPFSAddStr(fmt.Sprintf("no-resume-%d-%d", i, time.Now().UnixNano()))
682
+ }
683
+
684
+ time.Sleep(initialRuntime)
685
+
686
+ beforeRestart := node.IPFS("provide", "stat", "--enc=json")
687
+ offsetBeforeRestart, _, err := parseProvideStatJSON(beforeRestart.Stdout.String())
688
+ require.NoError(t, err)
689
+ require.Greater(t, offsetBeforeRestart, minOffsetBeforeRestart,
690
+ "cycle should have progressed")
691
+
692
+ node.StopDaemon()
693
+ time.Sleep(downtime)
694
+ node.StartDaemon()
695
+ time.Sleep(restartTime)
696
+
697
+ afterRestart := node.IPFS("provide", "stat", "--enc=json")
698
+ offsetAfterRestart, _, err := parseProvideStatJSON(afterRestart.Stdout.String())
699
+ require.NoError(t, err)
700
+
701
+ assert.Less(t, offsetAfterRestart, maxOffsetAfterReset,
702
+ "offset should reset to near zero")
703
+ })
704
+}
705
+
706
+type provideStatJSON struct {
707
+ Sweep struct {
708
+ Timing struct {
709
+ CurrentTimeOffset int64 `json:"current_time_offset"` // nanoseconds
710
+ } `json:"timing"`
711
+ Schedule struct {
712
+ NextReprovidePrefix string `json:"next_reprovide_prefix"`
713
+ } `json:"schedule"`
714
+ } `json:"Sweep"`
715
+}
716
+
717
+// parseProvideStatJSON extracts timing and schedule information from
718
+// the JSON output of 'ipfs provide stat --enc=json'.
719
+// Note: prefix is unused in current tests but kept for potential future use.
720
+func parseProvideStatJSON(output string) (offset time.Duration, prefix string, err error) {
721
+ var stat provideStatJSON
722
+ if err := json.Unmarshal([]byte(output), &stat); err != nil {
723
+ return 0, "", err
724
+ }
725
+ offset = time.Duration(stat.Sweep.Timing.CurrentTimeOffset)
726
+ prefix = stat.Sweep.Schedule.NextReprovidePrefix
727
+ return offset, prefix, nil
728
+}
729
+
730
func TestProvider(t *testing.T) {
731
t.Parallel()
732
756
t.Run(v.name, func(t *testing.T) {
757
// t.Parallel()
758
runProviderSuite(t, v.reprovide, v.apply)
759
+
760
+ // Resume tests only apply to SweepingProvider
761
+ if v.name == "SweepingProvider" {
762
+ runResumeTests(t, v.apply)
763
+ }
764
})
765
}
766
}