| 794 | } |
| 795 | |
| 796 | void CloudTunnel::ThreadMain() { |
| 797 | pthread_setname_np(pthread_self(), "cloud-tunnel"); |
| 798 | // Exponential backoff for reconnect attempts. Caps at 60s so a flaky |
| 799 | // network or temporary cloud outage does not keep the journal at the |
| 800 | // previous 1-attempt-per-second cadence (which on a host with broken |
| 801 | // IPv6 + AF_UNSPEC produced ~30k log lines/day for nothing). Reset |
| 802 | // only after the link has been *up* for kBackoffResetThreshold so a |
| 803 | // pathological connect-then-immediately-disconnect loop still backs |
| 804 | // off instead of hot-spinning at 1Hz. |
| 805 | using Clock = std::chrono::steady_clock; |
| 806 | constexpr auto kBackoffMin = std::chrono::seconds(1); |
| 807 | constexpr auto kBackoffMax = std::chrono::seconds(60); |
| 808 | constexpr auto kBackoffResetThreshold = std::chrono::seconds(30); |
| 809 | auto backoff = kBackoffMin; |
| 810 | auto bump_backoff = [&]() { |
| 811 | backoff = std::min(backoff * 2, kBackoffMax); |
| 812 | }; |
| 813 | // Sleep `delay` but wake early on shutdown so SIGTERM doesn't have |
| 814 | // to wait out a full 60s slot during the backoff phase. |
| 815 | auto interruptible_sleep = [this](std::chrono::seconds delay) { |
| 816 | const auto deadline = Clock::now() + delay; |
| 817 | while (running_ && Clock::now() < deadline) { |
| 818 | const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>( |
| 819 | deadline - Clock::now()); |
| 820 | const auto slice = std::min<std::chrono::milliseconds>( |
| 821 | remaining, std::chrono::milliseconds(200)); |
| 822 | std::this_thread::sleep_for(slice); |
| 823 | } |
| 824 | }; |
| 825 | |
| 826 | while (running_) { |
| 827 | // Refresh the token snapshot at the top of each connect attempt so |
| 828 | // a successful pair (which writes device.token mid-session) takes |
| 829 | // effect on the very next reconnect without a daemon restart. |
| 830 | { |
| 831 | std::lock_guard<std::mutex> lock(pair_mu_); |
| 832 | device_token_ = LoadDeviceToken(config_.data_dir); |
| 833 | if (device_token_.empty()) { |
| 834 | if (pair_code_.empty()) pair_code_ = GeneratePairCode(); |
| 835 | } else { |
| 836 | pair_code_.clear(); |
| 837 | } |
| 838 | } |
| 839 | |
| 840 | std::string error; |
| 841 | if (!Connect(&error)) { |
| 842 | std::cerr << "cloud tunnel connect failed: " << error |
| 843 | << " (retrying in " << backoff.count() << "s)\n"; |
| 844 | interruptible_sleep(backoff); |
| 845 | bump_backoff(); |
| 846 | continue; |
| 847 | } |
| 848 | |
| 849 | const auto connected_at = Clock::now(); |
| 850 | std::cerr << "cloud tunnel connected as " << host_id_ << "\n"; |
| 851 | SendJson(HelloPayload()); |
| 852 | // Once the WS upgrade succeeds and we know we still need pairing, |
| 853 | // surface the URL so an operator running `curl ... | sh` sees it |
nothing calls this directly
no test coverage detected