refactor: simplify client initialization and enhance error handling

Kim committed Oct 21, 2025 at 12:06 UTC 5fca4cb03c8e8d74056a5593c5347b47e327bdd1
3 files changed +66 -46
README.md
+2 -3
@@ -90,19 +90,18 @@ import (
90 "context"
91 "time"
92 "github.com/gosuda/relaydns/relaydns"
93 - "github.com/libp2p/go-libp2p"
93 )
94
95 func main() {
96 ctx := context.Background()
98 - h, _ := libp2p.New(libp2p.EnableHolePunching(), libp2p.EnableNATService())
99 - client, _ := relaydns.NewClient(ctx, h, relaydns.ClientConfig{
97 + client, _ := relaydns.NewClient(ctx, relaydns.ClientConfig{
98 Protocol: "/relaydns/http/1.0",
99 Topic: "relaydns.backends",
100 AdvertiseEvery: 5 * time.Second,
101 TargetTCP: "127.0.0.1:8081",
102 Name: "demo-http",
103 })
104 + _ = client.Start(ctx)
105 defer client.Close()
106 select {}
107 }
cmd/example_client/main.go
+4 -14
@@ -72,12 +72,7 @@ func runClient(cmd *cobra.Command, args []string) error {
72 }()
73
74 // 2) libp2p host
75 - h, err := relaydns.MakeHost(ctx, 0, true)
76 - if err != nil {
77 - return fmt.Errorf("make host: %w", err)
78 - }
79 -
80 - client, err := relaydns.NewClient(ctx, h, relaydns.ClientConfig{
75 + client, err := relaydns.NewClient(ctx, relaydns.ClientConfig{
76 Protocol: "/relaydns/http/1.0",
77 Topic: "relaydns.backends",
78 AdvertiseEvery: 3 * time.Second,
@@ -92,15 +87,10 @@ func runClient(cmd *cobra.Command, args []string) error {
87 if err != nil {
88 return fmt.Errorf("new client: %w", err)
89 }
95 - defer client.Close()
96 -
97 - if addrs := h.Addrs(); len(addrs) > 0 {
98 - for _, a := range addrs {
99 - log.Info().Msgf("[client] host addr: %s/p2p/%s", a.String(), h.ID().String())
100 - }
101 - } else {
102 - log.Info().Msgf("[client] host peer: %s (no listen addrs yet)", h.ID().String())
90 + if err := client.Start(ctx); err != nil {
91 + return fmt.Errorf("start client: %w", err)
92 }
93 + defer client.Close()
94
95 // wait for termination
96 sig := make(chan os.Signal, 1)
relaydns/client.go
+60 -29
@@ -56,51 +56,69 @@ type RelayClient struct {
56 stop context.CancelFunc
57 }
58
59 -// NewClient wires a reusable backend node that other apps can embed.
60 -// It registers a stream handler and starts an advertiser loop.
61 -// Call Close() to stop.
62 -func NewClient(ctx context.Context, h host.Host, cfg ClientConfig) (*RelayClient, error) {
59 +// NewClient constructs a client with defaults applied and an initialized libp2p host.
60 +// It does not start networking. Call Start(ctx) to begin handlers, pubsub, and advertising.
61 +func NewClient(ctx context.Context, cfg ClientConfig) (*RelayClient, error) {
62 if cfg.AdvertiseEvery <= 0 {
63 cfg.AdvertiseEvery = 5 * time.Second
64 }
65 if cfg.HTTPTimeout <= 0 {
66 cfg.HTTPTimeout = 3 * time.Second
67 }
68 + if cfg.Protocol == "" {
69 + cfg.Protocol = "/relaydns/http/1.0"
70 + }
71 + if cfg.Topic == "" {
72 + cfg.Topic = "relaydns.backends"
73 + }
74 +
75 + h, err := MakeHost(ctx, 0, true)
76 + if err != nil {
77 + return nil, fmt.Errorf("make host: %w", err)
78 + }
79 b := &RelayClient{
80 h: h,
81 cfg: cfg,
82 protoID: protocol.ID(cfg.Protocol),
83 }
84 + return b, nil
85 +}
86
75 - boot := make([]string, 0, len(cfg.Bootstraps)+4)
76 - if len(cfg.Bootstraps) > 0 {
77 - boot = append(boot, cfg.Bootstraps...)
87 +// Start connects bootstraps, sets stream handler, joins pubsub, and starts advertising.
88 +func (b *RelayClient) Start(ctx context.Context) error {
89 + if b.stop != nil {
90 + return fmt.Errorf("client already started")
91 }
79 - if cfg.ServerURL != "" {
80 - if addrs, err := fetchMultiaddrsFromHealth(cfg.ServerURL, cfg.HTTPTimeout); err != nil {
81 - log.Warn().Err(err).Msgf("relaydns: fetch /health from %s failed", cfg.ServerURL)
92 + // resolve bootstraps (from flags and optional server health)
93 + boot := make([]string, 0, len(b.cfg.Bootstraps)+4)
94 + if len(b.cfg.Bootstraps) > 0 {
95 + boot = append(boot, b.cfg.Bootstraps...)
96 + }
97 + if b.cfg.ServerURL != "" {
98 + if addrs, err := fetchMultiaddrsFromHealth(b.cfg.ServerURL, b.cfg.HTTPTimeout); err != nil {
99 + log.Warn().Err(err).Msgf("relaydns: fetch /health from %s failed", b.cfg.ServerURL)
100 } else {
83 - sortMultiaddrs(addrs, cfg.PreferQUIC, cfg.PreferLocal)
101 + sortMultiaddrs(addrs, b.cfg.PreferQUIC, b.cfg.PreferLocal)
102 boot = append(boot, addrs...)
103 }
104 }
105 boot = uniq(boot)
106 if len(boot) > 0 {
89 - ConnectBootstraps(ctx, h, boot)
107 + ConnectBootstraps(ctx, b.h, boot)
108 } else {
109 log.Warn().Msg("relaydns: no bootstrap sources provided (Bootstraps/ServerURL); discovery may fail")
110 }
111
94 - // 1) stream handler
112 + // stream handler
113 switch {
96 - case cfg.Handler != nil:
97 - h.SetStreamHandler(b.protoID, cfg.Handler)
98 - case cfg.TargetTCP != "":
99 - h.SetStreamHandler(b.protoID, func(s network.Stream) {
114 + case b.cfg.Handler != nil:
115 + b.h.SetStreamHandler(b.protoID, b.cfg.Handler)
116 + case b.cfg.TargetTCP != "":
117 + b.h.SetStreamHandler(b.protoID, func(s network.Stream) {
118 defer s.Close()
101 - c, err := net.Dial("tcp", cfg.TargetTCP)
119 + c, err := net.Dial("tcp", b.cfg.TargetTCP)
120 if err != nil {
103 - log.Error().Err(err).Msgf("relaydns: dial %s", cfg.TargetTCP)
121 + log.Error().Err(err).Msgf("relaydns: dial %s", b.cfg.TargetTCP)
122 return
123 }
124 defer c.Close()
@@ -109,27 +127,27 @@ func NewClient(ctx context.Context, h host.Host, cfg ClientConfig) (*RelayClient
127 io.Copy(s, c)
128 })
129 default:
112 - return nil, fmt.Errorf("relaydns: either Handler or TargetTCP must be set")
130 + return fmt.Errorf("relaydns: either Handler or TargetTCP must be set")
131 }
132
115 - // 2) pubsub join
116 - ps, err := pubsub.NewGossipSub(ctx, h, pubsub.WithMessageSigning(true))
133 + // pubsub join
134 + ps, err := pubsub.NewGossipSub(ctx, b.h, pubsub.WithMessageSigning(true))
135 if err != nil {
118 - return nil, err
136 + return err
137 }
120 - t, err := ps.Join(cfg.Topic)
138 + t, err := ps.Join(b.cfg.Topic)
139 if err != nil {
122 - return nil, err
140 + return err
141 }
142 b.ps, b.t = ps, t
143
126 - // 3) advertiser loop
144 + // advertiser loop
145 advCtx, cancel := context.WithCancel(ctx)
146 b.stop = cancel
147 b.wg.Add(1)
148 go func() {
149 defer b.wg.Done()
132 - ticker := time.NewTicker(cfg.AdvertiseEvery)
150 + ticker := time.NewTicker(b.cfg.AdvertiseEvery)
151 defer ticker.Stop()
152 for {
153 select {
@@ -156,7 +174,19 @@ func NewClient(ctx context.Context, h host.Host, cfg ClientConfig) (*RelayClient
174 }
175 }()
176
159 - return b, nil
177 + if addrs := b.Host().Addrs(); len(addrs) > 0 {
178 + for _, a := range addrs {
179 + log.Info().Msgf("[client] host addr: %s/p2p/%s", a.String(), b.Host().ID().String())
180 + }
181 + } else {
182 + log.Info().Msgf("[client] host peer: %s (no listen addrs yet)", b.Host().ID().String())
183 + }
184 +
185 + return nil
186 +}
187 +
188 +func (b *RelayClient) Host() host.Host {
189 + return b.h
190 }
191
192 func (b *RelayClient) Close() error {
@@ -167,6 +197,7 @@ func (b *RelayClient) Close() error {
197 // leaving topic is optional; libp2p will clean up on host close
198 return nil
199 }
200 +
201 func fetchMultiaddrsFromHealth(base string, timeout time.Duration) ([]string, error) {
202 u, err := url.Parse(base)
203 if err != nil {
@@ -201,7 +232,7 @@ func fetchMultiaddrsFromHealth(base string, timeout time.Duration) ([]string, er
232 }
233 addrs := make([]string, 0, len(payload.Multiaddrs))
234 for _, s := range payload.Multiaddrs {
204 - // 아주 기본적인 sanity check
235 + // sanity check
236 if strings.Contains(s, "/p2p/") && (strings.Contains(s, "/ip4/") || strings.Contains(s, "/ip6/")) {
237 addrs = append(addrs, s)
238 }