diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-18 08:00:18 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-18 13:01:00 +0000 |
| commit | 10f5dd5f48659d6e99abc762d46be397d24b48d9 (patch) | |
| tree | 1c344dfabb9b0416dfcc165f54b1b2ab94d21308 | |
| parent | 3d4918fea3280fd6653ad138160aa7327ab9b36e (diff) | |
poll grasp servers for readiness instead of fixed 5s wait
Replace arbitrary 5-second countdown with active polling of grasp
servers during ngit init. Servers are checked every 1s with a 7s
timeout (2s in tests). Shows progress like '1/2 grasp servers ready'.
Grasp servers return 404 until the repo is provisioned, so we poll
until they accept git connections. Proceeds anyway on timeout rather
than failing, since some servers may still be initializing.
| -rw-r--r-- | src/bin/ngit/sub_commands/init.rs | 318 |
1 files changed, 294 insertions, 24 deletions
diff --git a/src/bin/ngit/sub_commands/init.rs b/src/bin/ngit/sub_commands/init.rs index 158ff29..ae6a202 100644 --- a/src/bin/ngit/sub_commands/init.rs +++ b/src/bin/ngit/sub_commands/init.rs | |||
| @@ -3,14 +3,18 @@ use std::{ | |||
| 3 | env, | 3 | env, |
| 4 | process::{Command, Stdio}, | 4 | process::{Command, Stdio}, |
| 5 | str::FromStr, | 5 | str::FromStr, |
| 6 | sync::Arc, | 6 | sync::{ |
| 7 | thread, | 7 | Arc, Mutex, |
| 8 | atomic::{AtomicBool, AtomicU64, Ordering}, | ||
| 9 | }, | ||
| 8 | time::Duration, | 10 | time::Duration, |
| 9 | }; | 11 | }; |
| 10 | 12 | ||
| 11 | use anyhow::{Context, Result, bail}; | 13 | use anyhow::{Context, Result, bail}; |
| 12 | use console::{Style, Term}; | 14 | use console::{Style, Term}; |
| 15 | use futures::future::join_all; | ||
| 13 | use git2::Oid; | 16 | use git2::Oid; |
| 17 | use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; | ||
| 14 | use ngit::{ | 18 | use ngit::{ |
| 15 | cli_interactor::{ | 19 | cli_interactor::{ |
| 16 | PromptChoiceParms, PromptConfirmParms, cli_error, multi_select_with_custom_value, | 20 | PromptChoiceParms, PromptConfirmParms, cli_error, multi_select_with_custom_value, |
| @@ -1391,17 +1395,13 @@ async fn publish_and_finalize( | |||
| 1391 | if fields.selected_grasp_servers.is_empty() { | 1395 | if fields.selected_grasp_servers.is_empty() { |
| 1392 | println!("running `ngit push` to publish your repository data"); | 1396 | println!("running `ngit push` to publish your repository data"); |
| 1393 | } else { | 1397 | } else { |
| 1394 | let countdown_start = 5; | 1398 | wait_for_grasp_servers( |
| 1395 | println!( | 1399 | git_repo, |
| 1396 | "waiting {countdown_start}s for grasp servers to create your repo before we push your data" | 1400 | &fields.selected_grasp_servers, |
| 1397 | ); | 1401 | &user_ref.public_key, |
| 1398 | let term = Term::stdout(); | 1402 | &fields.identifier, |
| 1399 | for i in (1..=countdown_start).rev() { | 1403 | ) |
| 1400 | term.write_line(format!("\rrunning `git push` in {i}s").as_str())?; | 1404 | .await?; |
| 1401 | thread::sleep(Duration::new(1, 0)); // Sleep for 1 second | ||
| 1402 | term.clear_last_lines(1)?; | ||
| 1403 | } | ||
| 1404 | term.flush().unwrap(); // Ensure the output is flushed to the terminal | ||
| 1405 | } | 1405 | } |
| 1406 | 1406 | ||
| 1407 | if let Err(err) = push_main_or_master_branch(git_repo) { | 1407 | if let Err(err) = push_main_or_master_branch(git_repo) { |
| @@ -1416,17 +1416,13 @@ async fn publish_and_finalize( | |||
| 1416 | "running `ngit sync` to ensure your repository data is available on repository git servers" | 1416 | "running `ngit sync` to ensure your repository data is available on repository git servers" |
| 1417 | ); | 1417 | ); |
| 1418 | } else { | 1418 | } else { |
| 1419 | let countdown_start = 5; | 1419 | wait_for_grasp_servers( |
| 1420 | println!( | 1420 | git_repo, |
| 1421 | "waiting {countdown_start}s for any new grasp servers to create your repo before we sync your data" | 1421 | &fields.selected_grasp_servers, |
| 1422 | ); | 1422 | &user_ref.public_key, |
| 1423 | let term = Term::stdout(); | 1423 | &fields.identifier, |
| 1424 | for i in (1..=countdown_start).rev() { | 1424 | ) |
| 1425 | term.write_line(format!("\rrunning `ngit sync` in {i}s").as_str())?; | 1425 | .await?; |
| 1426 | thread::sleep(Duration::new(1, 0)); // Sleep for 1 second | ||
| 1427 | term.clear_last_lines(1)?; | ||
| 1428 | } | ||
| 1429 | term.flush().unwrap(); // Ensure the output is flushed to the terminal | ||
| 1430 | } | 1426 | } |
| 1431 | 1427 | ||
| 1432 | if let Err(err) = run_ngit_sync() { | 1428 | if let Err(err) = run_ngit_sync() { |
| @@ -1690,3 +1686,277 @@ fn run_ngit_sync() -> Result<()> { | |||
| 1690 | bail!("ngit sync process exited with an error: {exit_status}"); | 1686 | bail!("ngit sync process exited with an error: {exit_status}"); |
| 1691 | } | 1687 | } |
| 1692 | } | 1688 | } |
| 1689 | |||
| 1690 | fn check_git_server_ready(git_repo_path: &std::path::Path, git_server_url: &str) -> bool { | ||
| 1691 | let Ok(git_repo) = git2::Repository::open(git_repo_path) else { | ||
| 1692 | return false; | ||
| 1693 | }; | ||
| 1694 | let Ok(mut remote) = git_repo.remote_anonymous(git_server_url) else { | ||
| 1695 | return false; | ||
| 1696 | }; | ||
| 1697 | match remote.connect(git2::Direction::Fetch) { | ||
| 1698 | Ok(()) => { | ||
| 1699 | let _ = remote.disconnect(); | ||
| 1700 | true | ||
| 1701 | } | ||
| 1702 | Err(_) => false, | ||
| 1703 | } | ||
| 1704 | } | ||
| 1705 | |||
| 1706 | /// Holds the final style+message for a bar that completed before the detail | ||
| 1707 | /// view was revealed. | ||
| 1708 | struct DeferredServerFinish { | ||
| 1709 | bar: ProgressBar, | ||
| 1710 | style: ProgressStyle, | ||
| 1711 | message: String, | ||
| 1712 | } | ||
| 1713 | |||
| 1714 | /// Coordinates the delayed reveal of per-server detail bars. | ||
| 1715 | /// Bars that finish before the expand timer fires store their final | ||
| 1716 | /// style+message here. The timer applies them all at reveal time so | ||
| 1717 | /// every bar — completed or still waiting — appears in the expanded view. | ||
| 1718 | struct ServerRevealState { | ||
| 1719 | revealed: AtomicBool, | ||
| 1720 | deferred: Mutex<Vec<DeferredServerFinish>>, | ||
| 1721 | } | ||
| 1722 | |||
| 1723 | fn finish_server_bar( | ||
| 1724 | bar: &ProgressBar, | ||
| 1725 | style: ProgressStyle, | ||
| 1726 | message: String, | ||
| 1727 | reveal_state: &Arc<ServerRevealState>, | ||
| 1728 | ) { | ||
| 1729 | let mut deferred = reveal_state.deferred.lock().unwrap(); | ||
| 1730 | if reveal_state.revealed.load(Ordering::Acquire) { | ||
| 1731 | drop(deferred); | ||
| 1732 | bar.set_style(style); | ||
| 1733 | bar.finish_with_message(message); | ||
| 1734 | } else { | ||
| 1735 | // Style is set now so the timer can drain it correctly; finish is | ||
| 1736 | // deferred until the detail view becomes visible. | ||
| 1737 | bar.set_style(style.clone()); | ||
| 1738 | deferred.push(DeferredServerFinish { | ||
| 1739 | bar: bar.clone(), | ||
| 1740 | style, | ||
| 1741 | message, | ||
| 1742 | }); | ||
| 1743 | } | ||
| 1744 | } | ||
| 1745 | |||
| 1746 | /// Poll grasp servers in parallel until all are ready or timeout is reached. | ||
| 1747 | /// | ||
| 1748 | /// Shows a concise spinner with `x/y - complete` progress. After 5s without | ||
| 1749 | /// all servers responding, expands to show per-server status bars (including | ||
| 1750 | /// any that already finished). Times out after 15s (2s in tests) and proceeds | ||
| 1751 | /// anyway rather than failing. | ||
| 1752 | async fn wait_for_grasp_servers( | ||
| 1753 | git_repo: &Repo, | ||
| 1754 | grasp_servers: &[String], | ||
| 1755 | public_key: &PublicKey, | ||
| 1756 | identifier: &str, | ||
| 1757 | ) -> Result<()> { | ||
| 1758 | let clone_urls: Vec<String> = grasp_servers | ||
| 1759 | .iter() | ||
| 1760 | .filter_map(|gs| format_grasp_server_url_as_clone_url(gs, public_key, identifier).ok()) | ||
| 1761 | .collect(); | ||
| 1762 | |||
| 1763 | if clone_urls.is_empty() { | ||
| 1764 | return Ok(()); | ||
| 1765 | } | ||
| 1766 | |||
| 1767 | let is_test = std::env::var("NGITTEST").is_ok(); | ||
| 1768 | let timeout_secs: u64 = if is_test { 2 } else { 15 }; | ||
| 1769 | let expand_delay_ms: u64 = if is_test { 500 } else { 5000 }; | ||
| 1770 | let total = clone_urls.len() as u64; | ||
| 1771 | |||
| 1772 | // Spinner shown immediately with x/y count | ||
| 1773 | let spinner_multi = MultiProgress::new(); | ||
| 1774 | let spinner_pb = spinner_multi.add( | ||
| 1775 | ProgressBar::new_spinner() | ||
| 1776 | .with_style( | ||
| 1777 | ProgressStyle::with_template("{spinner} {msg}") | ||
| 1778 | .unwrap() | ||
| 1779 | .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈"), | ||
| 1780 | ) | ||
| 1781 | .with_message(format!( | ||
| 1782 | "waiting for servers to create bare git repo... (0/{total} - complete)" | ||
| 1783 | )), | ||
| 1784 | ); | ||
| 1785 | spinner_pb.enable_steady_tick(Duration::from_millis(100)); | ||
| 1786 | |||
| 1787 | // Detail MultiProgress starts hidden; revealed after expand_delay_ms. | ||
| 1788 | // A heading bar is pre-added at position 0 so it holds its slot before | ||
| 1789 | // any per-server bars are added. | ||
| 1790 | let detail_multi = MultiProgress::with_draw_target(ProgressDrawTarget::hidden()); | ||
| 1791 | let heading_bar = detail_multi | ||
| 1792 | .add(ProgressBar::new(0).with_style(ProgressStyle::with_template("{msg}").unwrap())); | ||
| 1793 | |||
| 1794 | let ready_count = Arc::new(AtomicU64::new(0)); | ||
| 1795 | let reveal_state = Arc::new(ServerRevealState { | ||
| 1796 | revealed: AtomicBool::new(false), | ||
| 1797 | deferred: Mutex::new(Vec::new()), | ||
| 1798 | }); | ||
| 1799 | |||
| 1800 | // Per-server spinner bars (added to hidden detail_multi) | ||
| 1801 | let waiting_style = ProgressStyle::with_template(" {spinner} {msg}") | ||
| 1802 | .unwrap() | ||
| 1803 | .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈"); | ||
| 1804 | let server_bars: Vec<ProgressBar> = clone_urls | ||
| 1805 | .iter() | ||
| 1806 | .map(|url| { | ||
| 1807 | let name = url | ||
| 1808 | .trim_start_matches("https://") | ||
| 1809 | .trim_start_matches("http://") | ||
| 1810 | .to_string(); | ||
| 1811 | detail_multi.add( | ||
| 1812 | ProgressBar::new_spinner() | ||
| 1813 | .with_style(waiting_style.clone()) | ||
| 1814 | .with_message( | ||
| 1815 | console::style(format!("{name} - waiting")) | ||
| 1816 | .for_stderr() | ||
| 1817 | .dim() | ||
| 1818 | .to_string(), | ||
| 1819 | ), | ||
| 1820 | ) | ||
| 1821 | }) | ||
| 1822 | .collect(); | ||
| 1823 | |||
| 1824 | // Background timer: after expand_delay_ms reveal the detail view and | ||
| 1825 | // flush any bars that already finished (the BarRevealState pattern). | ||
| 1826 | let detail_multi_for_timer = detail_multi.clone(); | ||
| 1827 | let spinner_for_timer = spinner_pb.clone(); | ||
| 1828 | let reveal_state_for_timer = reveal_state.clone(); | ||
| 1829 | let server_bars_for_timer = server_bars.clone(); | ||
| 1830 | let heading_bar_for_timer = heading_bar.clone(); | ||
| 1831 | let timer_handle = tokio::spawn(async move { | ||
| 1832 | tokio::time::sleep(Duration::from_millis(expand_delay_ms)).await; | ||
| 1833 | spinner_for_timer.finish_and_clear(); | ||
| 1834 | detail_multi_for_timer.set_draw_target(ProgressDrawTarget::stderr()); | ||
| 1835 | // Show the heading in the expanded view. | ||
| 1836 | heading_bar_for_timer.finish_with_message("waiting for servers to create bare git repo..."); | ||
| 1837 | // Lock deferred list, mark revealed, and flush bars that already | ||
| 1838 | // finished. Must hold the lock across the revealed.store so that | ||
| 1839 | // finish_server_bar cannot push after the drain. | ||
| 1840 | let mut deferred = reveal_state_for_timer.deferred.lock().unwrap(); | ||
| 1841 | reveal_state_for_timer | ||
| 1842 | .revealed | ||
| 1843 | .store(true, Ordering::Release); | ||
| 1844 | for df in deferred.drain(..) { | ||
| 1845 | df.bar.set_style(df.style); | ||
| 1846 | df.bar.finish_with_message(df.message); | ||
| 1847 | } | ||
| 1848 | // Kick still-waiting bars into drawing by enabling their tick. | ||
| 1849 | for bar in &server_bars_for_timer { | ||
| 1850 | if !bar.is_finished() { | ||
| 1851 | bar.enable_steady_tick(Duration::from_millis(100)); | ||
| 1852 | } | ||
| 1853 | } | ||
| 1854 | }); | ||
| 1855 | |||
| 1856 | // Poll each server in parallel | ||
| 1857 | let git_repo_path = git_repo.get_path()?.to_path_buf(); | ||
| 1858 | let futures: Vec<_> = clone_urls | ||
| 1859 | .iter() | ||
| 1860 | .enumerate() | ||
| 1861 | .map(|(i, url)| { | ||
| 1862 | let url = url.clone(); | ||
| 1863 | let ready_count = ready_count.clone(); | ||
| 1864 | let spinner_pb = spinner_pb.clone(); | ||
| 1865 | let bar = server_bars[i].clone(); | ||
| 1866 | let git_repo_path = git_repo_path.clone(); | ||
| 1867 | let reveal_state = reveal_state.clone(); | ||
| 1868 | async move { | ||
| 1869 | let poll_interval = Duration::from_millis(500); | ||
| 1870 | let deadline = tokio::time::Instant::now() + Duration::from_secs(timeout_secs); | ||
| 1871 | let mut ready = false; | ||
| 1872 | loop { | ||
| 1873 | let is_ready = tokio::task::spawn_blocking({ | ||
| 1874 | let url = url.clone(); | ||
| 1875 | let path = git_repo_path.clone(); | ||
| 1876 | move || check_git_server_ready(&path, &url) | ||
| 1877 | }) | ||
| 1878 | .await | ||
| 1879 | .unwrap_or(false); | ||
| 1880 | |||
| 1881 | if is_ready { | ||
| 1882 | ready = true; | ||
| 1883 | break; | ||
| 1884 | } | ||
| 1885 | |||
| 1886 | if tokio::time::Instant::now() >= deadline { | ||
| 1887 | break; | ||
| 1888 | } | ||
| 1889 | |||
| 1890 | tokio::time::sleep(poll_interval).await; | ||
| 1891 | } | ||
| 1892 | |||
| 1893 | let count = if ready { | ||
| 1894 | ready_count.fetch_add(1, Ordering::Relaxed) + 1 | ||
| 1895 | } else { | ||
| 1896 | ready_count.load(Ordering::Relaxed) | ||
| 1897 | }; | ||
| 1898 | |||
| 1899 | // Update spinner message | ||
| 1900 | spinner_pb.set_message(format!( | ||
| 1901 | "waiting for servers to create bare git repo... ({count}/{total} - complete)" | ||
| 1902 | )); | ||
| 1903 | |||
| 1904 | // Finish per-server bar (deferred if detail not yet visible) | ||
| 1905 | let name = url | ||
| 1906 | .trim_start_matches("https://") | ||
| 1907 | .trim_start_matches("http://") | ||
| 1908 | .to_string(); | ||
| 1909 | if ready { | ||
| 1910 | let style = ProgressStyle::with_template(&format!( | ||
| 1911 | " {} {{msg}}", | ||
| 1912 | console::style("✔").for_stderr().green() | ||
| 1913 | )) | ||
| 1914 | .unwrap(); | ||
| 1915 | let msg = console::style(format!("{name} - ready")) | ||
| 1916 | .for_stderr() | ||
| 1917 | .green() | ||
| 1918 | .to_string(); | ||
| 1919 | finish_server_bar(&bar, style, msg, &reveal_state); | ||
| 1920 | } else { | ||
| 1921 | let style = ProgressStyle::with_template(&format!( | ||
| 1922 | " {} {{msg}}", | ||
| 1923 | console::style("✘").for_stderr().red() | ||
| 1924 | )) | ||
| 1925 | .unwrap(); | ||
| 1926 | let msg = console::style(format!("{name} - timeout")) | ||
| 1927 | .for_stderr() | ||
| 1928 | .red() | ||
| 1929 | .to_string(); | ||
| 1930 | finish_server_bar(&bar, style, msg, &reveal_state); | ||
| 1931 | } | ||
| 1932 | |||
| 1933 | ready | ||
| 1934 | } | ||
| 1935 | }) | ||
| 1936 | .collect(); | ||
| 1937 | |||
| 1938 | let results = join_all(futures).await; | ||
| 1939 | let final_ready = ready_count.load(Ordering::Relaxed); | ||
| 1940 | |||
| 1941 | // Cancel the expand timer if it hasn't fired yet. | ||
| 1942 | timer_handle.abort(); | ||
| 1943 | |||
| 1944 | // If detail view was revealed, clear the detail bars. | ||
| 1945 | if reveal_state.revealed.load(Ordering::Acquire) { | ||
| 1946 | let _ = detail_multi.clear(); | ||
| 1947 | } | ||
| 1948 | |||
| 1949 | let all_ready = results.iter().all(|&r| r); | ||
| 1950 | if all_ready { | ||
| 1951 | // Success — erase the spinner line entirely, leave nothing behind. | ||
| 1952 | spinner_pb.finish_and_clear(); | ||
| 1953 | } else { | ||
| 1954 | // Partial timeout — leave a message so the user knows we proceeded. | ||
| 1955 | spinner_pb.set_style(ProgressStyle::with_template("{msg}").unwrap()); | ||
| 1956 | spinner_pb.finish_with_message(format!( | ||
| 1957 | "timed out waiting for servers to create bare git repo ({final_ready}/{total} - complete), proceeding anyway" | ||
| 1958 | )); | ||
| 1959 | } | ||
| 1960 | |||
| 1961 | Ok(()) | ||
| 1962 | } | ||