LCOV - code coverage report
Current view: top level - proxy/src/bin - local_proxy.rs (source / functions) Coverage Total Hit
Test: 8ff8efadb0253cf618c612650348666c0c564111.info Lines: 0.0 % 276 0
Test Date: 2024-11-20 17:53:50 Functions: 0.0 % 82 0

            Line data    Source code
       1              : use std::net::SocketAddr;
       2              : use std::pin::pin;
       3              : use std::str::FromStr;
       4              : use std::sync::Arc;
       5              : use std::time::Duration;
       6              : 
       7              : use anyhow::{bail, ensure, Context};
       8              : use camino::{Utf8Path, Utf8PathBuf};
       9              : use compute_api::spec::LocalProxySpec;
      10              : use dashmap::DashMap;
      11              : use futures::future::Either;
      12              : use proxy::auth::backend::jwt::JwkCache;
      13              : use proxy::auth::backend::local::{LocalBackend, JWKS_ROLE_MAP};
      14              : use proxy::auth::{self};
      15              : use proxy::cancellation::CancellationHandlerMain;
      16              : use proxy::config::{self, AuthenticationConfig, HttpConfig, ProxyConfig, RetryConfig};
      17              : use proxy::control_plane::locks::ApiLocks;
      18              : use proxy::control_plane::messages::{EndpointJwksResponse, JwksSettings};
      19              : use proxy::http::health_server::AppMetrics;
      20              : use proxy::intern::RoleNameInt;
      21              : use proxy::metrics::{Metrics, ThreadPoolMetrics};
      22              : use proxy::rate_limiter::{
      23              :     BucketRateLimiter, EndpointRateLimiter, LeakyBucketConfig, RateBucketInfo,
      24              : };
      25              : use proxy::scram::threadpool::ThreadPool;
      26              : use proxy::serverless::cancel_set::CancelSet;
      27              : use proxy::serverless::{self, GlobalConnPoolOptions};
      28              : use proxy::types::RoleName;
      29              : use proxy::url::ApiUrl;
      30              : 
      31              : project_git_version!(GIT_VERSION);
      32              : project_build_tag!(BUILD_TAG);
      33              : 
      34              : use clap::Parser;
      35              : use thiserror::Error;
      36              : use tokio::net::TcpListener;
      37              : use tokio::sync::Notify;
      38              : use tokio::task::JoinSet;
      39              : use tokio_util::sync::CancellationToken;
      40              : use tracing::{debug, error, info, warn};
      41              : use utils::sentry_init::init_sentry;
      42              : use utils::{pid_file, project_build_tag, project_git_version};
      43              : 
      44              : #[global_allocator]
      45              : static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
      46              : 
      47              : /// Neon proxy/router
      48            0 : #[derive(Parser)]
      49              : #[command(version = GIT_VERSION, about)]
      50              : struct LocalProxyCliArgs {
      51              :     /// listen for incoming metrics connections on ip:port
      52              :     #[clap(long, default_value = "127.0.0.1:7001")]
      53            0 :     metrics: String,
      54              :     /// listen for incoming http connections on ip:port
      55              :     #[clap(long)]
      56            0 :     http: String,
      57              :     /// timeout for the TLS handshake
      58              :     #[clap(long, default_value = "15s", value_parser = humantime::parse_duration)]
      59            0 :     handshake_timeout: tokio::time::Duration,
      60              :     /// lock for `connect_compute` api method. example: "shards=32,permits=4,epoch=10m,timeout=1s". (use `permits=0` to disable).
      61              :     #[clap(long, default_value = config::ConcurrencyLockOptions::DEFAULT_OPTIONS_CONNECT_COMPUTE_LOCK)]
      62            0 :     connect_compute_lock: String,
      63              :     #[clap(flatten)]
      64              :     sql_over_http: SqlOverHttpArgs,
      65              :     /// User rate limiter max number of requests per second.
      66              :     ///
      67              :     /// Provided in the form `<Requests Per Second>@<Bucket Duration Size>`.
      68              :     /// Can be given multiple times for different bucket sizes.
      69            0 :     #[clap(long, default_values_t = RateBucketInfo::DEFAULT_ENDPOINT_SET)]
      70            0 :     user_rps_limit: Vec<RateBucketInfo>,
      71              :     /// Whether the auth rate limiter actually takes effect (for testing)
      72            0 :     #[clap(long, default_value_t = false, value_parser = clap::builder::BoolishValueParser::new(), action = clap::ArgAction::Set)]
      73            0 :     auth_rate_limit_enabled: bool,
      74              :     /// Authentication rate limiter max number of hashes per second.
      75            0 :     #[clap(long, default_values_t = RateBucketInfo::DEFAULT_AUTH_SET)]
      76            0 :     auth_rate_limit: Vec<RateBucketInfo>,
      77              :     /// The IP subnet to use when considering whether two IP addresses are considered the same.
      78            0 :     #[clap(long, default_value_t = 64)]
      79            0 :     auth_rate_limit_ip_subnet: u8,
      80              :     /// Whether to retry the connection to the compute node
      81              :     #[clap(long, default_value = config::RetryConfig::CONNECT_TO_COMPUTE_DEFAULT_VALUES)]
      82            0 :     connect_to_compute_retry: String,
      83              :     /// Address of the postgres server
      84              :     #[clap(long, default_value = "127.0.0.1:5432")]
      85            0 :     postgres: SocketAddr,
      86              :     /// Address of the compute-ctl api service
      87              :     #[clap(long, default_value = "http://127.0.0.1:3080/")]
      88            0 :     compute_ctl: ApiUrl,
      89              :     /// Path of the local proxy config file
      90              :     #[clap(long, default_value = "./local_proxy.json")]
      91            0 :     config_path: Utf8PathBuf,
      92              :     /// Path of the local proxy PID file
      93              :     #[clap(long, default_value = "./local_proxy.pid")]
      94            0 :     pid_path: Utf8PathBuf,
      95              : }
      96              : 
      97            0 : #[derive(clap::Args, Clone, Copy, Debug)]
      98              : struct SqlOverHttpArgs {
      99              :     /// How many connections to pool for each endpoint. Excess connections are discarded
     100            0 :     #[clap(long, default_value_t = 200)]
     101            0 :     sql_over_http_pool_max_total_conns: usize,
     102              : 
     103              :     /// How long pooled connections should remain idle for before closing
     104              :     #[clap(long, default_value = "5m", value_parser = humantime::parse_duration)]
     105            0 :     sql_over_http_idle_timeout: tokio::time::Duration,
     106              : 
     107            0 :     #[clap(long, default_value_t = 100)]
     108            0 :     sql_over_http_client_conn_threshold: u64,
     109              : 
     110            0 :     #[clap(long, default_value_t = 16)]
     111            0 :     sql_over_http_cancel_set_shards: usize,
     112              : 
     113            0 :     #[clap(long, default_value_t = 10 * 1024 * 1024)] // 10 MiB
     114            0 :     sql_over_http_max_request_size_bytes: usize,
     115              : 
     116            0 :     #[clap(long, default_value_t = 10 * 1024 * 1024)] // 10 MiB
     117            0 :     sql_over_http_max_response_size_bytes: usize,
     118              : }
     119              : 
     120              : #[tokio::main]
     121            0 : async fn main() -> anyhow::Result<()> {
     122            0 :     let _logging_guard = proxy::logging::init_local_proxy()?;
     123            0 :     let _panic_hook_guard = utils::logging::replace_panic_hook_with_tracing_panic_hook();
     124            0 :     let _sentry_guard = init_sentry(Some(GIT_VERSION.into()), &[]);
     125            0 : 
     126            0 :     Metrics::install(Arc::new(ThreadPoolMetrics::new(0)));
     127            0 : 
     128            0 :     // TODO: refactor these to use labels
     129            0 :     debug!("Version: {GIT_VERSION}");
     130            0 :     debug!("Build_tag: {BUILD_TAG}");
     131            0 :     let neon_metrics = ::metrics::NeonMetrics::new(::metrics::BuildInfo {
     132            0 :         revision: GIT_VERSION,
     133            0 :         build_tag: BUILD_TAG,
     134            0 :     });
     135            0 : 
     136            0 :     let jemalloc = match proxy::jemalloc::MetricRecorder::new() {
     137            0 :         Ok(t) => Some(t),
     138            0 :         Err(e) => {
     139            0 :             tracing::error!(error = ?e, "could not start jemalloc metrics loop");
     140            0 :             None
     141            0 :         }
     142            0 :     };
     143            0 : 
     144            0 :     let args = LocalProxyCliArgs::parse();
     145            0 :     let config = build_config(&args)?;
     146            0 :     let auth_backend = build_auth_backend(&args)?;
     147            0 : 
     148            0 :     // before we bind to any ports, write the process ID to a file
     149            0 :     // so that compute-ctl can find our process later
     150            0 :     // in order to trigger the appropriate SIGHUP on config change.
     151            0 :     //
     152            0 :     // This also claims a "lock" that makes sure only one instance
     153            0 :     // of local_proxy runs at a time.
     154            0 :     let _process_guard = loop {
     155            0 :         match pid_file::claim_for_current_process(&args.pid_path) {
     156            0 :             Ok(guard) => break guard,
     157            0 :             Err(e) => {
     158            0 :                 // compute-ctl might have tried to read the pid-file to let us
     159            0 :                 // know about some config change. We should try again.
     160            0 :                 error!(path=?args.pid_path, "could not claim PID file guard: {e:?}");
     161            0 :                 tokio::time::sleep(Duration::from_secs(1)).await;
     162            0 :             }
     163            0 :         }
     164            0 :     };
     165            0 : 
     166            0 :     let metrics_listener = TcpListener::bind(args.metrics).await?.into_std()?;
     167            0 :     let http_listener = TcpListener::bind(args.http).await?;
     168            0 :     let shutdown = CancellationToken::new();
     169            0 : 
     170            0 :     // todo: should scale with CU
     171            0 :     let endpoint_rate_limiter = Arc::new(EndpointRateLimiter::new_with_shards(
     172            0 :         LeakyBucketConfig {
     173            0 :             rps: 10.0,
     174            0 :             max: 100.0,
     175            0 :         },
     176            0 :         16,
     177            0 :     ));
     178            0 : 
     179            0 :     let mut maintenance_tasks = JoinSet::new();
     180            0 : 
     181            0 :     let refresh_config_notify = Arc::new(Notify::new());
     182            0 :     maintenance_tasks.spawn(proxy::signals::handle(shutdown.clone(), {
     183            0 :         let refresh_config_notify = Arc::clone(&refresh_config_notify);
     184            0 :         move || {
     185            0 :             refresh_config_notify.notify_one();
     186            0 :         }
     187            0 :     }));
     188            0 : 
     189            0 :     // trigger the first config load **after** setting up the signal hook
     190            0 :     // to avoid the race condition where:
     191            0 :     // 1. No config file registered when local_proxy starts up
     192            0 :     // 2. The config file is written but the signal hook is not yet received
     193            0 :     // 3. local_proxy completes startup but has no config loaded, despite there being a registerd config.
     194            0 :     refresh_config_notify.notify_one();
     195            0 :     tokio::spawn(refresh_config_loop(args.config_path, refresh_config_notify));
     196            0 : 
     197            0 :     maintenance_tasks.spawn(proxy::http::health_server::task_main(
     198            0 :         metrics_listener,
     199            0 :         AppMetrics {
     200            0 :             jemalloc,
     201            0 :             neon_metrics,
     202            0 :             proxy: proxy::metrics::Metrics::get(),
     203            0 :         },
     204            0 :     ));
     205            0 : 
     206            0 :     let task = serverless::task_main(
     207            0 :         config,
     208            0 :         auth_backend,
     209            0 :         http_listener,
     210            0 :         shutdown.clone(),
     211            0 :         Arc::new(CancellationHandlerMain::new(
     212            0 :             Arc::new(DashMap::new()),
     213            0 :             None,
     214            0 :             proxy::metrics::CancellationSource::Local,
     215            0 :         )),
     216            0 :         endpoint_rate_limiter,
     217            0 :     );
     218            0 : 
     219            0 :     match futures::future::select(pin!(maintenance_tasks.join_next()), pin!(task)).await {
     220            0 :         // exit immediately on maintenance task completion
     221            0 :         Either::Left((Some(res), _)) => match proxy::error::flatten_err(res)? {},
     222            0 :         // exit with error immediately if all maintenance tasks have ceased (should be caught by branch above)
     223            0 :         Either::Left((None, _)) => bail!("no maintenance tasks running. invalid state"),
     224            0 :         // exit immediately on client task error
     225            0 :         Either::Right((res, _)) => res?,
     226            0 :     }
     227            0 : 
     228            0 :     Ok(())
     229            0 : }
     230              : 
     231              : /// ProxyConfig is created at proxy startup, and lives forever.
     232            0 : fn build_config(args: &LocalProxyCliArgs) -> anyhow::Result<&'static ProxyConfig> {
     233              :     let config::ConcurrencyLockOptions {
     234            0 :         shards,
     235            0 :         limiter,
     236            0 :         epoch,
     237            0 :         timeout,
     238            0 :     } = args.connect_compute_lock.parse()?;
     239            0 :     info!(
     240              :         ?limiter,
     241              :         shards,
     242              :         ?epoch,
     243            0 :         "Using NodeLocks (connect_compute)"
     244              :     );
     245            0 :     let connect_compute_locks = ApiLocks::new(
     246            0 :         "connect_compute_lock",
     247            0 :         limiter,
     248            0 :         shards,
     249            0 :         timeout,
     250            0 :         epoch,
     251            0 :         &Metrics::get().proxy.connect_compute_lock,
     252            0 :     )?;
     253              : 
     254            0 :     let http_config = HttpConfig {
     255            0 :         accept_websockets: false,
     256            0 :         pool_options: GlobalConnPoolOptions {
     257            0 :             gc_epoch: Duration::from_secs(60),
     258            0 :             pool_shards: 2,
     259            0 :             idle_timeout: args.sql_over_http.sql_over_http_idle_timeout,
     260            0 :             opt_in: false,
     261            0 : 
     262            0 :             max_conns_per_endpoint: args.sql_over_http.sql_over_http_pool_max_total_conns,
     263            0 :             max_total_conns: args.sql_over_http.sql_over_http_pool_max_total_conns,
     264            0 :         },
     265            0 :         cancel_set: CancelSet::new(args.sql_over_http.sql_over_http_cancel_set_shards),
     266            0 :         client_conn_threshold: args.sql_over_http.sql_over_http_client_conn_threshold,
     267            0 :         max_request_size_bytes: args.sql_over_http.sql_over_http_max_request_size_bytes,
     268            0 :         max_response_size_bytes: args.sql_over_http.sql_over_http_max_response_size_bytes,
     269            0 :     };
     270            0 : 
     271            0 :     Ok(Box::leak(Box::new(ProxyConfig {
     272            0 :         tls_config: None,
     273            0 :         metric_collection: None,
     274            0 :         allow_self_signed_compute: false,
     275            0 :         http_config,
     276            0 :         authentication_config: AuthenticationConfig {
     277            0 :             jwks_cache: JwkCache::default(),
     278            0 :             thread_pool: ThreadPool::new(0),
     279            0 :             scram_protocol_timeout: Duration::from_secs(10),
     280            0 :             rate_limiter_enabled: false,
     281            0 :             rate_limiter: BucketRateLimiter::new(vec![]),
     282            0 :             rate_limit_ip_subnet: 64,
     283            0 :             ip_allowlist_check_enabled: true,
     284            0 :             is_auth_broker: false,
     285            0 :             accept_jwts: true,
     286            0 :             console_redirect_confirmation_timeout: Duration::ZERO,
     287            0 :         },
     288            0 :         proxy_protocol_v2: config::ProxyProtocolV2::Rejected,
     289            0 :         handshake_timeout: Duration::from_secs(10),
     290            0 :         region: "local".into(),
     291            0 :         wake_compute_retry_config: RetryConfig::parse(RetryConfig::WAKE_COMPUTE_DEFAULT_VALUES)?,
     292            0 :         connect_compute_locks,
     293            0 :         connect_to_compute_retry_config: RetryConfig::parse(
     294            0 :             RetryConfig::CONNECT_TO_COMPUTE_DEFAULT_VALUES,
     295            0 :         )?,
     296              :     })))
     297            0 : }
     298              : 
     299              : /// auth::Backend is created at proxy startup, and lives forever.
     300            0 : fn build_auth_backend(
     301            0 :     args: &LocalProxyCliArgs,
     302            0 : ) -> anyhow::Result<&'static auth::Backend<'static, ()>> {
     303            0 :     let auth_backend = proxy::auth::Backend::Local(proxy::auth::backend::MaybeOwned::Owned(
     304            0 :         LocalBackend::new(args.postgres, args.compute_ctl.clone()),
     305            0 :     ));
     306            0 : 
     307            0 :     Ok(Box::leak(Box::new(auth_backend)))
     308            0 : }
     309              : 
     310            0 : #[derive(Error, Debug)]
     311              : enum RefreshConfigError {
     312              :     #[error(transparent)]
     313              :     Read(#[from] std::io::Error),
     314              :     #[error(transparent)]
     315              :     Parse(#[from] serde_json::Error),
     316              :     #[error(transparent)]
     317              :     Validate(anyhow::Error),
     318              : }
     319              : 
     320            0 : async fn refresh_config_loop(path: Utf8PathBuf, rx: Arc<Notify>) {
     321            0 :     let mut init = true;
     322              :     loop {
     323            0 :         rx.notified().await;
     324              : 
     325            0 :         match refresh_config_inner(&path).await {
     326            0 :             Ok(()) => {}
     327              :             // don't log for file not found errors if this is the first time we are checking
     328              :             // for computes that don't use local_proxy, this is not an error.
     329            0 :             Err(RefreshConfigError::Read(e))
     330            0 :                 if init && e.kind() == std::io::ErrorKind::NotFound =>
     331            0 :             {
     332            0 :                 debug!(error=?e, ?path, "could not read config file");
     333              :             }
     334            0 :             Err(e) => {
     335            0 :                 error!(error=?e, ?path, "could not read config file");
     336              :             }
     337              :         }
     338              : 
     339            0 :         init = false;
     340              :     }
     341              : }
     342              : 
     343            0 : async fn refresh_config_inner(path: &Utf8Path) -> Result<(), RefreshConfigError> {
     344            0 :     let bytes = tokio::fs::read(&path).await?;
     345            0 :     let data: LocalProxySpec = serde_json::from_slice(&bytes)?;
     346              : 
     347            0 :     let mut jwks_set = vec![];
     348              : 
     349            0 :     fn parse_jwks_settings(jwks: compute_api::spec::JwksSettings) -> anyhow::Result<JwksSettings> {
     350            0 :         let mut jwks_url = url::Url::from_str(&jwks.jwks_url).context("parsing JWKS url")?;
     351              : 
     352            0 :         ensure!(
     353            0 :             jwks_url.has_authority()
     354            0 :                 && (jwks_url.scheme() == "http" || jwks_url.scheme() == "https"),
     355            0 :             "Invalid JWKS url. Must be HTTP",
     356              :         );
     357              : 
     358            0 :         ensure!(
     359            0 :             jwks_url.host().is_some_and(|h| h != url::Host::Domain("")),
     360            0 :             "Invalid JWKS url. No domain listed",
     361              :         );
     362              : 
     363              :         // clear username, password and ports
     364            0 :         jwks_url
     365            0 :             .set_username("")
     366            0 :             .expect("url can be a base and has a valid host and is not a file. should not error");
     367            0 :         jwks_url
     368            0 :             .set_password(None)
     369            0 :             .expect("url can be a base and has a valid host and is not a file. should not error");
     370            0 :         // local testing is hard if we need to have a specific restricted port
     371            0 :         if cfg!(not(feature = "testing")) {
     372            0 :             jwks_url.set_port(None).expect(
     373            0 :                 "url can be a base and has a valid host and is not a file. should not error",
     374            0 :             );
     375            0 :         }
     376              : 
     377              :         // clear query params
     378            0 :         jwks_url.set_fragment(None);
     379            0 :         jwks_url.query_pairs_mut().clear().finish();
     380            0 : 
     381            0 :         if jwks_url.scheme() != "https" {
     382              :             // local testing is hard if we need to set up https support.
     383            0 :             if cfg!(not(feature = "testing")) {
     384            0 :                 jwks_url
     385            0 :                     .set_scheme("https")
     386            0 :                     .expect("should not error to set the scheme to https if it was http");
     387            0 :             } else {
     388            0 :                 warn!(scheme = jwks_url.scheme(), "JWKS url is not HTTPS");
     389              :             }
     390            0 :         }
     391              : 
     392            0 :         Ok(JwksSettings {
     393            0 :             id: jwks.id,
     394            0 :             jwks_url,
     395            0 :             provider_name: jwks.provider_name,
     396            0 :             jwt_audience: jwks.jwt_audience,
     397            0 :             role_names: jwks
     398            0 :                 .role_names
     399            0 :                 .into_iter()
     400            0 :                 .map(RoleName::from)
     401            0 :                 .map(|s| RoleNameInt::from(&s))
     402            0 :                 .collect(),
     403            0 :         })
     404            0 :     }
     405              : 
     406            0 :     for jwks in data.jwks.into_iter().flatten() {
     407            0 :         jwks_set.push(parse_jwks_settings(jwks).map_err(RefreshConfigError::Validate)?);
     408              :     }
     409              : 
     410            0 :     info!("successfully loaded new config");
     411            0 :     JWKS_ROLE_MAP.store(Some(Arc::new(EndpointJwksResponse { jwks: jwks_set })));
     412            0 : 
     413            0 :     Ok(())
     414            0 : }
        

Generated by: LCOV version 2.1-beta