Skip to content

feat: Add custom placeholder pages for scale-from-zero scenarios #1303

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

malpou
Copy link

@malpou malpou commented May 30, 2025

Allows HTTPScaledObjects to serve configurable HTML pages while workloads scale up from zero, with support for templates,
custom headers, and automatic refresh.

Description

This PR implements customizable placeholder pages that are displayed to users while applications are scaling up from zero.
This significantly improves the user experience during cold starts by providing immediate feedback instead of connection
timeouts or errors.

Key features:

  • Adds placeholderConfig section to HTTPScaledObject CRD for configuring placeholder pages
  • Supports both inline HTML content and ConfigMap-based templates
  • Includes Go template support with variables like {{.ServiceName}} and {{.RefreshInterval}}
  • Automatic page refresh capability to check when the service becomes available
  • Custom HTTP headers and status codes
  • Configurable timeout for how long to show placeholder pages

Implementation details:

  • New placeholder handler in the interceptor that serves configured HTML when backends are unavailable
  • Graceful fallback to upstream requests once the service is ready
  • Full backward compatibility - disabled by default

Checklist

Fixes #874

@malpou malpou requested a review from a team as a code owner May 30, 2025 10:03
@malpou malpou closed this May 30, 2025
@malpou malpou reopened this May 30, 2025
@malpou malpou force-pushed the placeholder-pages branch from 92f8f69 to 02451ee Compare May 30, 2025 10:33
Copy link
Member

@JorTurFer JorTurFer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great feature! Thanks for the contribution 🙇

<html>
<head>
<title>Service Starting</title>
<meta http-equiv="refresh" content="{{.RefreshInterval}}">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we inject this value instead of expect that users provide it? I mean, this looks as something that we want to enforce. @wozniakjan ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about injecting it but wanted some feedback first.

I think it also would be possible to add some JavaScript instead which calls the URL the browser is on and checks if the X-KEDA-HTTP-Placeholder-Served: true header is there. If it's not we should do a full refresh.
This avoids having the browser do an actual refresh every X seconds. This would mostly be an UX improvement.

Both approaches could be injected into whatever template the user provides, if that's the approach you would prefer.

Example:

<script>
(function() {
    const checkInterval = {{.RefreshInterval}} * 1000; // Convert seconds to milliseconds
    
    async function checkServiceStatus() {
        try {
            // Make a HEAD request to the current URL to check headers
            const response = await fetch(window.location.href, {
                method: 'HEAD',
                cache: 'no-cache'
            });
            
            // Check if the placeholder header is present
            const placeholderHeader = response.headers.get('X-KEDA-HTTP-Placeholder-Served');
            
            if (placeholderHeader !== 'true') {
                // Service is ready! Do a full page refresh
                window.location.reload();
            } else {
                // Still showing placeholder, check again later
                setTimeout(checkServiceStatus, checkInterval);
            }
        } catch (error) {
            // On error, continue checking
            console.error('Error checking service status:', error);
            setTimeout(checkServiceStatus, checkInterval);
        }
    }
    
    // Start checking after the interval
    setTimeout(checkServiceStatus, checkInterval);
})();
</script>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this approach! it's quite elegant IMHO, the only thing is that JS needs to be injected as well, doesn't

Copy link
Member

@JorTurFer JorTurFer Jun 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JorTurFer I've tried to make a take on the injection with a <script> tag and some JS to figure out when to refresh the browser.

malpou added 7 commits June 2, 2025 21:19
Allows HTTPScaledObjects to serve configurable HTML pages while workloads
scale up from zero, with support for templates, custom headers, and
automatic refresh.

Signed-off-by: malpou <[email protected]>
Signed-off-by: malpou <[email protected]>
…should be comptatible in the GHA environment

Signed-off-by: malpou <[email protected]>
@malpou malpou force-pushed the placeholder-pages branch from 2e3f5f5 to a3d021f Compare June 2, 2025 19:19
malpou and others added 7 commits June 3, 2025 13:43
Signed-off-by: Malthe Poulsen <[email protected]>
Signed-off-by: malpou <[email protected]>
Signed-off-by: Malthe Poulsen <[email protected]>
Signed-off-by: Malthe Poulsen <[email protected]>
Signed-off-by: Malthe Poulsen <[email protected]>
Signed-off-by: Malthe Poulsen <[email protected]>
@malpou malpou requested a review from JorTurFer June 3, 2025 16:53
Signed-off-by: Malthe Poulsen <[email protected]>
malpou and others added 3 commits June 4, 2025 08:54
Co-authored-by: Jorge Turrado Ferrero <[email protected]>
Signed-off-by: Malthe Poulsen <[email protected]>
Co-authored-by: Jorge Turrado Ferrero <[email protected]>
Signed-off-by: Malthe Poulsen <[email protected]>
Signed-off-by: malpou <[email protected]>
@malpou malpou requested a review from JorTurFer June 4, 2025 07:08
@wozniakjan wozniakjan requested a review from Copilot June 10, 2025 12:03
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces configurable placeholder pages for scale-from-zero scenarios, enhancing the user experience during cold starts. Key changes include:

  • Adding a new placeholder handler and supporting API changes in the HTTPScaledObject CRD.
  • Adjusting proxy handler logic and test cases to incorporate placeholder page responses.
  • Updating documentation, examples, and deepcopy functions to reflect the new placeholderConfig functionality.

Reviewed Changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/checks/placeholder_pages/placeholder_pages_test.go Added E2E tests for placeholder page responses and script injection.
operator/apis/http/v1alpha1/zz_generated.deepcopy.go Added DeepCopy functions for the new PlaceholderConfig type.
operator/apis/http/v1alpha1/httpscaledobject_types.go Introduced the PlaceholderConfig type in the CRD schema.
interceptor/proxy_handlers_test.go, proxy_handlers_integration_test.go Updated tests to pass new parameters for placeholder handling.
interceptor/proxy_handlers.go Integrated the placeholder handler into the forwarding logic.
interceptor/main.go, main_test.go Updated server initialization to include the placeholder handler.
interceptor/handler/placeholder.go Implemented the placeholder page rendering and caching logic.
examples/vX.X.X/httpscaledobject.yaml Provided example configuration for placeholder pages.
docs/ref/vX.X.X/http_scaled_object.md Documented the placeholderConfig section details.
config/crd/bases/http.keda.sh_httpscaledobjects.yaml Updated the CRD with placeholderConfig properties.
CHANGELOG.md Added an entry for the custom placeholder pages feature.
Comments suppressed due to low confidence (1)

interceptor/handler/placeholder.go:206

  • [nitpick] Consider logging the error returned from getTemplate before falling back to the inline response to improve debuggability of template parsing issues.
tmpl, err := h.getTemplate(r.Context(), hso)

}

// injectPlaceholderScript injects the placeholder refresh script into a template
func injectPlaceholderScript(templateContent string) string {
Copy link
Preview

Copilot AI Jun 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider checking if the placeholder script is already present in the template content before injecting it to avoid potential duplicate script insertions.

Suggested change
func injectPlaceholderScript(templateContent string) string {
func injectPlaceholderScript(templateContent string) string {
// Check if the placeholder script is already present
if strings.Contains(templateContent, placeholderScript) {
// Return the original content if the script is already present
return templateContent
}

Copilot uses AI. Check for mistakes.

Copy link
Member

@JorTurFer JorTurFer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome job! Only one small nit inline and it's okey to merge ❤️

Comment on lines +248 to +256
h.cacheMutex.RUnlock()

injectedContent := injectPlaceholderScript(config.Content)
tmpl, err := template.New("inline").Parse(injectedContent)
if err != nil {
return nil, err
}

h.cacheMutex.Lock()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case of high pressure, I'd say that more than 1 placeholder can be init. WDYT about something like?

Suggested change
h.cacheMutex.RUnlock()
injectedContent := injectPlaceholderScript(config.Content)
tmpl, err := template.New("inline").Parse(injectedContent)
if err != nil {
return nil, err
}
h.cacheMutex.Lock()
h.cacheMutex.RUnlock()
h.cacheMutex.Lock()
injectedContent := injectPlaceholderScript(config.Content)
tmpl, err := template.New("inline").Parse(injectedContent)
if err != nil {
return nil, err
}

return nil, err
}

h.cacheMutex.Lock()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

"Please hold" landing pages for slow scale from zero scenarios
2 participants