@@ -27,6 +27,9 @@ class Static_Site_Importer_URL_Site_Collector {
2727 private const MAX_RESPONSE_BYTES = 10485760 ;
2828 private const MAX_SITEMAP_DOCUMENTS = 100 ;
2929 private const MAX_DISCOVERED_ROUTES = 5000 ;
30+ // The hardened socket transport is blocking, so one PHP invocation can safely admit one request at a time.
31+ private const SAME_ORIGIN_CONCURRENCY = 1 ;
32+ private const CROSS_ORIGIN_CONCURRENCY = 1 ;
3033
3134 /**
3235 * Collect a public static site.
@@ -45,26 +48,8 @@ public static function collect( string $url, array $args = array(), ?callable $f
4548 $ max_pages = min ( self ::MAX_PAGES , max ( 1 , (int ) ( $ args ['max_pages ' ] ?? self ::DEFAULT_MAX_PAGES ) ) );
4649 $ max_assets = min ( self ::MAX_ASSETS , max ( 0 , (int ) ( $ args ['max_assets ' ] ?? self ::DEFAULT_MAX_ASSETS ) ) );
4750 $ max_total_bytes = min ( self ::MAX_TOTAL_BYTES , max ( 1 , (int ) ( $ args ['max_total_bytes ' ] ?? self ::DEFAULT_MAX_TOTAL_BYTES ) ) );
48- $ request_delay = min ( 2000 , max ( 0 , (int ) ( $ args ['request_delay_ms ' ] ?? 100 ) ) );
4951 $ fetcher = $ fetcher ?? static fn ( string $ resource_url , array $ fetch_args ) => Static_Site_Importer_URL_Fetcher::fetch ( $ resource_url , $ fetch_args );
50- $ fetch_attempts = min ( 3 , max ( 1 , (int ) ( $ args ['fetch_attempts ' ] ?? 2 ) ) );
51- $ fetch_resource = $ fetcher ;
52- $ fetcher = static function ( string $ resource_url , array $ fetch_args ) use ( $ fetch_resource , $ fetch_attempts ) {
53- $ response = null ;
54- for ( $ attempt = 0 ; $ attempt < $ fetch_attempts ; $ attempt ++ ) {
55- $ response = $ fetch_resource ( $ resource_url , $ fetch_args );
56- if ( ! is_wp_error ( $ response ) ) {
57- return $ response ;
58- }
59- }
60- $ data = is_wp_error ( $ response ) && is_array ( $ response ->get_error_data () ) ? $ response ->get_error_data () : array ();
61- if ( ! empty ( $ data ['_static_site_importer_cache_aware ' ] ) ) {
62- unset( $ data ['_static_site_importer_cache_aware ' ] );
63- $ response = new WP_Error ( $ response ->get_error_code (), $ response ->get_error_message (), $ data ?: null );
64- $ fetch_resource ( $ resource_url , $ fetch_args + array ( '_static_site_importer_cache_failure ' => $ response ) );
65- }
66- return $ response ;
67- };
52+ $ fetcher = self ::scheduled_fetcher ( $ fetcher , $ args );
6853 $ fetch_args = array_intersect_key ( $ args , array_flip ( array ( 'timeout ' ) ) );
6954 $ fetch_args ['max_bytes ' ] = min ( self ::MAX_RESPONSE_BYTES , $ max_total_bytes , max ( 1 , (int ) ( $ args ['max_bytes ' ] ?? 5242880 ) ) );
7055
@@ -105,7 +90,6 @@ public static function collect( string $url, array $args = array(), ?callable $f
10590 while ( $ page_queue && count ( array_filter ( $ resources , static fn ( array $ resource ): bool => 'html ' === $ resource ['kind ' ] ) ) < $ max_pages ) {
10691 $ page_url = array_shift ( $ page_queue );
10792 $ response = $ fetcher ( $ page_url , array_merge ( $ fetch_args , array ( 'content_types ' => array ( 'text/html ' , 'application/xhtml+xml ' ) ) ) );
108- self ::delay_after_fetch ( $ response , $ request_delay , $ args );
10993 $ response = self ::without_cache_marker ( $ response );
11094 if ( is_wp_error ( $ response ) ) {
11195 if ( $ page_url === $ entry_url ) {
@@ -191,7 +175,6 @@ public static function collect( string $url, array $args = array(), ?callable $f
191175 while ( $ asset_queue && self ::resource_count ( $ resources , 'asset ' ) < $ max_assets ) {
192176 $ asset_url = array_shift ( $ asset_queue );
193177 $ response = $ fetcher ( $ asset_url , array_merge ( $ fetch_args , array ( 'content_types ' => array () ) ) );
194- self ::delay_after_fetch ( $ response , $ request_delay , $ args );
195178 $ response = self ::without_cache_marker ( $ response );
196179 if ( is_wp_error ( $ response ) ) {
197180 if ( $ preserve_failed_assets ) {
@@ -350,6 +333,7 @@ public static function collect( string $url, array $args = array(), ?callable $f
350333 'source_exclusions ' => $ source_exclusions ,
351334 'truncated ' => array_keys ( $ truncated ),
352335 'sitemap_urls ' => count ( $ sitemap_urls ),
336+ 'fetch_scheduling ' => self ::scheduling_limits ( $ args ),
353337 'external_asset_retained ' => array (
354338 'count ' => count ( $ external_assets ),
355339 'samples ' => array_slice ( array_map ( static fn ( string $ url , string $ reason ): array => array (
@@ -373,22 +357,7 @@ public static function discover_routes( string $url, array $args = array(), ?cal
373357 return new WP_Error ( 'static_site_importer_site_collection_invalid_url ' , 'Enter a valid public site URL. ' );
374358 }
375359 $ fetcher = $ fetcher ?? static fn ( string $ resource_url , array $ fetch_args ) => Static_Site_Importer_URL_Fetcher::fetch ( $ resource_url , $ fetch_args );
376- $ fetch_attempts = min ( 3 , max ( 1 , (int ) ( $ args ['fetch_attempts ' ] ?? 2 ) ) );
377- $ fetch_resource = $ fetcher ;
378- $ fetcher = static function ( string $ resource_url , array $ fetch_args ) use ( $ fetch_resource , $ fetch_attempts ) {
379- for ( $ attempt = 0 ; $ attempt < $ fetch_attempts ; $ attempt ++ ) {
380- $ response = $ fetch_resource ( $ resource_url , $ fetch_args );
381- if ( ! is_wp_error ( $ response ) ) {
382- return $ response ; }
383- }
384- $ data = is_wp_error ( $ response ) && is_array ( $ response ->get_error_data () ) ? $ response ->get_error_data () : array ();
385- if ( ! empty ( $ data ['_static_site_importer_cache_aware ' ] ) ) {
386- unset( $ data ['_static_site_importer_cache_aware ' ] );
387- $ response = new WP_Error ( $ response ->get_error_code (), $ response ->get_error_message (), $ data ?: null );
388- $ fetch_resource ( $ resource_url , $ fetch_args + array ( '_static_site_importer_cache_failure ' => $ response ) );
389- }
390- return $ response ;
391- };
360+ $ fetcher = self ::scheduled_fetcher ( $ fetcher , $ args );
392361 $ fetch_args = array_intersect_key ( $ args , array_flip ( array ( 'timeout ' ) ) );
393362 $ fetch_args ['max_bytes ' ] = min ( 10485760 , max ( 1 , (int ) ( $ args ['max_bytes ' ] ?? 5242880 ) ) );
394363 $ routes = self ::sitemap_urls ( $ entry_url , $ fetcher , $ fetch_args );
@@ -884,20 +853,44 @@ private static function failure( string $url, WP_Error $error, string $kind = 'a
884853 );
885854 }
886855
887- private static function delay_after_fetch ( $ response , int $ milliseconds , array $ args ): void {
888- $ error_data = is_wp_error ( $ response ) ? $ response ->get_error_data () : null ;
889- if ( is_array ( $ error_data ) && ! empty ( $ error_data ['_static_site_importer_negative_cache_hit ' ] ) ) {
890- return ;
891- }
892- if ( is_array ( $ response ) && ! empty ( $ response ['metadata ' ]['_static_site_importer_cache_hit ' ] ) ) {
893- unset( $ response ['metadata ' ]['_static_site_importer_cache_hit ' ] );
894- return ;
895- }
896- if ( isset ( $ args ['_static_site_importer_delay_callback ' ] ) && is_callable ( $ args ['_static_site_importer_delay_callback ' ] ) ) {
897- call_user_func ( $ args ['_static_site_importer_delay_callback ' ] );
898- return ;
899- }
900- self ::delay ( $ milliseconds );
856+ /** @return callable */
857+ private static function scheduled_fetcher ( callable $ fetch_resource , array $ args ): callable {
858+ $ fetch_attempts = min ( 3 , max ( 1 , (int ) ( $ args ['fetch_attempts ' ] ?? 2 ) ) );
859+ $ retry_delay = min ( 2000 , max ( 0 , (int ) ( $ args ['request_delay_ms ' ] ?? 0 ) ) );
860+ $ clock = isset ( $ args ['_static_site_importer_scheduler_clock ' ] ) && is_callable ( $ args ['_static_site_importer_scheduler_clock ' ] ) ? $ args ['_static_site_importer_scheduler_clock ' ] : static fn (): float => microtime ( true );
861+ return static function ( string $ resource_url , array $ fetch_args ) use ( $ fetch_resource , $ fetch_attempts , $ retry_delay , $ clock , $ args ) {
862+ $ response = null ;
863+ $ next_allowed = array ();
864+ $ origin = self ::origin ( $ resource_url );
865+ for ( $ attempt = 0 ; $ attempt < $ fetch_attempts ; $ attempt ++ ) {
866+ $ wait = max ( 0 , ( $ next_allowed [ $ origin ] ?? 0 ) - (float ) call_user_func ( $ clock ) );
867+ if ( $ wait > 0 ) {
868+ self ::delay ( (int ) ceil ( $ wait * 1000 ), $ args );
869+ }
870+ $ response = $ fetch_resource ( $ resource_url , $ fetch_args );
871+ if ( ! is_wp_error ( $ response ) ) {
872+ return $ response ;
873+ }
874+ // Successful and cached responses are immediately eligible; only a retry is paced.
875+ $ next_allowed [ $ origin ] = (float ) call_user_func ( $ clock ) + ( $ retry_delay / 1000 );
876+ }
877+ $ data = is_array ( $ response ->get_error_data () ) ? $ response ->get_error_data () : array ();
878+ if ( ! empty ( $ data ['_static_site_importer_cache_aware ' ] ) ) {
879+ unset( $ data ['_static_site_importer_cache_aware ' ] );
880+ $ response = new WP_Error ( $ response ->get_error_code (), $ response ->get_error_message (), $ data ?: null );
881+ $ fetch_resource ( $ resource_url , $ fetch_args + array ( '_static_site_importer_cache_failure ' => $ response ) );
882+ }
883+ return $ response ;
884+ };
885+ }
886+
887+ /** @return array<string,int> */
888+ private static function scheduling_limits ( array $ args ): array {
889+ return array (
890+ 'same_origin_concurrency ' => self ::SAME_ORIGIN_CONCURRENCY ,
891+ 'cross_origin_concurrency ' => self ::CROSS_ORIGIN_CONCURRENCY ,
892+ 'retry_delay_ms ' => min ( 2000 , max ( 0 , (int ) ( $ args ['request_delay_ms ' ] ?? 0 ) ) ),
893+ );
901894 }
902895
903896 private static function without_cache_marker ( $ response ) {
@@ -912,8 +905,12 @@ private static function without_cache_marker( $response ) {
912905 return new WP_Error ( $ response ->get_error_code (), $ response ->get_error_message (), $ data ?: null );
913906 }
914907
915- private static function delay ( int $ milliseconds ): void {
908+ private static function delay ( int $ milliseconds, array $ args = array () ): void {
916909 if ( $ milliseconds > 0 ) {
910+ if ( isset ( $ args ['_static_site_importer_delay_callback ' ] ) && is_callable ( $ args ['_static_site_importer_delay_callback ' ] ) ) {
911+ call_user_func ( $ args ['_static_site_importer_delay_callback ' ], $ milliseconds );
912+ return ;
913+ }
917914 usleep ( $ milliseconds * 1000 );
918915 }
919916 }
0 commit comments