Skip to content

fix(gofmt): code hygiene — gofmt cleanup, DRY refactor, and FixedWithFallback tests#1022

Closed
itoywh wants to merge 44 commits into
daeuniverse:mainfrom
itoywh:main
Closed

fix(gofmt): code hygiene — gofmt cleanup, DRY refactor, and FixedWithFallback tests#1022
itoywh wants to merge 44 commits into
daeuniverse:mainfrom
itoywh:main

Conversation

@itoywh

@itoywh itoywh commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Code hygiene PR covering three independent fixes:

1. gofmt format fixes

  • dialer_selection_policy.go: struct field comment alignment, blank line between functions
  • connectivity_check.go: fix extra space in latency field log alignment

2. DRY refactor — P3 connectivity check

  • Collapsed shouldSkipTcp6Probes + shouldSkipUdp6Probes into a single shouldSkipIpFamily6 function
  • Both had identical logic; only the config key differed

3. New unit tests for FixedWithFallback policy

  • TestDialerGroup_Select_FixedWithFallback_Alive — alive fixed dialer always selected
  • TestDialerGroup_Select_FixedWithFallback_DeadFallback — retries exhausted → fallback
  • TestDialerGroup_Select_FixedWithFallback_ZeroRetriesNoFallback — retries=0 immediately falls back
  • TestDialerGroup_Select_FixedWithFallback_Recovery — revival returns traffic to fixed dialer
  • TestDialerGroup_Select_FixedWithFallback_RetryTimeoutBehavior — retry within timeout window

Related to: #1009 (fixed_fallback), #1011 (dynamic CheckOpts), #1020 (alive/dead logs), #1021 (CST timezone)

LiYang and others added 30 commits June 15, 2026 16:08
…ilover

Adds DialerSelectionPolicy_FixedWithFallback (fixed_fallback in config),
which behaves like fixed(n) but tracks the fixed dialer's alive state:

- When the fixed dialer is alive: always select it (same as fixed(n))
- When the fixed dialer is dead/backoff: fall back to min_moving_avg
  among all alive dialers in the group
- When the fixed dialer revives (periodic health check restores it):
  traffic automatically returns to the fixed dialer

Unlike fixed() which ignores alive state entirely, fixed_fallback()
requires AliveDialerSet tracking and participates in the health check
lifecycle. This gives users the ability to pin traffic to a preferred
node while still having a safety net when that node goes down.

Related: daeuniverse#1005
syntax: fixed_fallback(n, timeout_seconds, max_retries)
- timeout: how long to wait between retries when node is dead (default 3s)
- retries: how many retries before falling back to min_moving_avg (default 3)

matching passwall2's autoswitch model: connect_timeout + retry_num
…ging

- parseDurationWithUnit: supports 500ms, 5s, 2m, or bare number (seconds)
- logFixedFallback: state-deduplicated, rate-limited logging with log levels
  - WARN: fixed dialer dead, starting retry
  - INFO: retry N/M step (with retry count)
  - WARN: retries exhausted, falling back to min_moving_avg
  - INFO: fixed dialer recovered, traffic returned
- 10s minimum interval between logs; same mark=no duplicate
- Add FallbackPolicy field to DialerSelectionPolicy struct
- Parse optional 4th param: fixed_fallback(n, timeout, retries, policy)
  Supported fallback policies: random, min_moving_avg, min_last_latency, min_avg10
  Default: min_moving_avg (backward compatible)
- Update _select() to use policy.FallbackPolicy instead of hardcoded min_moving_avg
- Add parsePolicyName() helper to map policy name strings
- Update example.dae documentation
- Timeout param now supports unit suffix: 500ms, 5s, 2m (from previous commit)
…cy tracking

Previously, AliveDialerSet for fixed_fallback groups was created with
FixedWithFallback policy, which caused NotifyLatencyChange to skip
latency tracking (FixedWithFallback case only updates alive status).

This meant:
- sortingLatency was always 0 for all dialers
- GetMinLatency() returned arbitrary first alive dialer
- check_tolerance had no effect (no latency data to compare)

Fix: when policy is FixedWithFallback, create the AliveDialerSet
with FallbackPolicy instead. The fixed-node path (fixed dialer alive)
bypasses the set entirely, so this change only affects the fallback
path where proper latency-based selection actually matters.

With this fix, fallback selection now correctly:
- Tracks moving average / last latency / avg10 per dialer
- Respects check_tolerance when comparing dialers
- Returns the truly optimal node instead of first alive
Previously, the fallback path always called GetMinLatency() regardless
of the fallback policy, which is wrong for random (should pick a
random alive dialer, not the min-latency one).

Add a branch in the fallback path:
- Random: collect all alive dialers, pick one at random
- min_*: use GetMinLatency() as before (now with proper latency
  tracking thanks to the FallbackPolicy AliveDialerSet fix)
The inner switch block was missing its closing brace for the
case FixedWithFallback block, causing 'unexpected keyword case'
at the next case statement.
The closing brace for case FixedWithFallback and the preceding
return statement had incorrect indentation, causing the compiler
to misinterpret the block structure.
In Go, case blocks don't need closing braces - they are delimited
by the next case or the outer switch's closing brace. The extra }
was closing the entire switch prematurely.
…not s4 recovery

Correct the misleading claim that check_tolerance gates s4 switchback.
In fixed_fallback, s4 recovery is a pure binary alive/dead check:
once alive, traffic returns immediately, with no latency comparison.

check_tolerance only applies to the AliveDialerSet internals during
failover, preventing oscillation between fallback nodes (s2/s5).
parsePolicyName() now accepts official dae policy names only:
- min (was min_last_latency)
- min_moving_avg (unchanged)
- min_avg10 (unchanged)
- random (unchanged)

All docs and examples updated to use official names.
- Add shouldSkipIpv6Probes(): detects if config has only explicit IPv4 addresses
- Add hasUdpDnsConfig(): checks if udp_check_dns is configured
- Build CheckOpts slice dynamically in aliveBackground() instead of hardcoded 4 probes
- Debug log shows which probes are active on dialer start
- Skip NotifyPeriodicCheckResultForType for UDP when not configured

This avoids unnecessary IPv6 connectivity probes when the user's network
has no IPv6 support, and skips UDP DNS probes when udp_check_dns is unset.
- Replace shouldSkipIpv6Probes() with shouldSkipTcp6Probes() and
  shouldSkipUdp6Probes() — each checks only its own config
- tcp_check_url having IPv6 no longer forces udp6CheckDnsOpt
- udp_check_dns defaults no longer affect tcp6 decision
- IPv6 probes are skipped per-protocol when only explicit IPv4
  addresses are configured in the respective check option
- Add section 3 'Dynamic CheckOpts' to both README.md and README_zh.md
- Document core rule: only check addresses explicitly written in config
- Add probe matrix table, tcp/udp independent evaluation, recommended configs
- Update upstream PR references (add PR daeuniverse#1011)
- Restore README_zh.md (was lost during upstream merge)
- Remove default values for tcp_check_url, udp_check_dns, and check_interval
- If check_interval=0 or not configured, skip connectivity check entirely
- Only add TCP checks if tcp_check_url is configured
- Only add UDP DNS checks if udp_check_dns is configured
- Users can now completely disable health checks by omitting these options
- Add notes about tcp_check_url, udp_check_dns, check_interval being optional
- Users can now completely disable health checks by omitting these options
- Set check_interval to 0 to disable all checks
- Section 3: rewrite to reflect new opt-in behavior
- All health check options are now fully optional
- check_interval=0 or not set → no checks at all
- Add zero-check config example for maximum performance
- Add migration note for users upgrading from stock dae
- Add WARN log when node becomes DEAD in markUnavailableInternal
- Add INFO log when node becomes ALIVE in markAvailable
- Logs include node name and network type for easy debugging
- Helps monitor node health and fixed_fallback behavior in production
LiYang and others added 11 commits June 15, 2026 16:08
On minimal OpenWrt/ImmortalWrt without tzdata setup,
Go defaults to UTC. This makes dae logs show CST (Beijing time)
by setting time.Local in the logger package init().

Try Asia/Shanghai first for proper DST; fall back to FixedZone.
Add update-release job to build.yml that:
- Downloads the x86_64 artifact after successful build
- Deletes the old dae-linux-x86_64 asset from pre-release (ID 338955206)
- Uploads the new binary to the pre-release
- Updates the release body with the latest commit SHA

Uses scripts/update-release.py for the GitHub API calls.
update-release.py now:
- Runs 'git log' to collect recent commits
- Detects whether the release body already has a changelog section
- Appends only new commits (no duplicates)
- Rebuilds the entire ## Changelog section each time for consistency
- Uses a sentinel marker (---) to separate header content from changelog
When config is unchanged and a reload is triggered, the staged handoff
path shares BPF objects between old and new generations. The old
generation's closeTail() calls core.Close() synchronously within a 5s
timeout window. core.Close() runs TC filter detachment
(netlink.FilterDel) which can exceed the timeout, causing "control
plane close tail timed out" warnings (dae#1013).

In staged handoff, by the time closeTail() runs, EjectBpf() has already
transferred BPF ownership to the new generation. core.Close() only
needs to detach TC filters and release the UDP tracker — neither is
time-critical because the new generation already has its own TC
filters attached.

Run core.Close() in a background goroutine when isBpfEjected() is true.
This check covers all staged handoff scenarios including the first
reload (P0→P1) where sharedBpfReload is false but EjectBpf has been
called. Using bpfEjected rather than sharedBpfReload ensures the
async path is taken based on actual BPF ownership state, not creation
history.

This preserves zero-downtime staged handoff while eliminating the
closeTail timeout.

Reported-by: @itoywh
See: dae#1013
- Run gofmt on dialer_selection_policy.go (struct field align, blank line)
- Run gofmt on connectivity_check.go (latency field align)
- DRY: collapse shouldSkipTcp6Probes + shouldSkipUdp6Probes into one
  shouldSkipIpFamily6 (identical logic, different config keys)
- Add unit tests for FixedWithFallback policy:
  * TestDialerGroup_Select_FixedWithFallback_Alive
  * TestDialerGroup_Select_FixedWithFallback_DeadFallback
  * TestDialerGroup_Select_FixedWithFallback_ZeroRetriesNoFallback
  * TestDialerGroup_Select_FixedWithFallback_Recovery
  * TestDialerGroup_Select_FixedWithFallback_RetryTimeoutBehavior
@itoywh itoywh requested review from a team as code owners June 18, 2026 08:55
LiYang added 3 commits June 18, 2026 16:58
- Rename shouldSkipIpFamily6 → shouldSkipIpFamily (returns '4'/'6'/'' instead of bool)
- Add skipTcp4/skipUdp4 to complement existing skipTcp6/skipUdp6
- When tcp_check_url has only IPv6 addr, only probe tcp6 (skip useless tcp4)
- When udp_check_dns has only IPv6 addr, only probe udp6_dns (skip useless udp4)
- Same logic applies symmetrically for IPv4-only configs
- connectivity_check.go: Debugln → Warnln for check_interval=0 and
  no checks configured (production-critical, users must see this)
- control_plane.go: add global WARN at startup if all three health check
  config fields (check_interval, tcp_check_url, udp_check_dns) are unset
@itoywh itoywh closed this Jun 18, 2026
@itoywh

itoywh commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Content merged into #1009 (fixed_fallback + gofmt/tests). Closing in favor of #1009.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant