Merge pull request #2950 from GSA/hotfix/remove-all-polling

Hotfix/remove all polling
This commit is contained in:
Beverly Nguyen
2025-09-26 15:49:27 -07:00
committed by GitHub
6 changed files with 75 additions and 113 deletions

View File

@@ -90,7 +90,7 @@ class Config(object):
],
}
FEATURE_SOCKET_ENABLED = getenv("FEATURE_SOCKET_ENABLED", "true") == "true"
FEATURE_SOCKET_ENABLED = getenv("FEATURE_SOCKET_ENABLED", "false") == "true"
def _s3_credentials_from_env(bucket_prefix):

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
import json
import os
import time
from functools import partial
from flask import (
@@ -22,7 +20,6 @@ from markupsafe import Markup
from app import (
current_service,
format_datetime_table,
job_api_client,
notification_api_client,
service_api_client,
)
@@ -113,46 +110,50 @@ def cancel_job(service_id, job_id):
@user_has_permissions()
def view_job_status_poll(service_id, job_id):
"""
Poll status endpoint using new lightweight cached API endpoint.
Returns minimal data needed for polling.
DISABLED: Lightweight polling endpoint no longer supported.
Use manual page refresh instead.
"""
start_time = time.time()
current_app.logger.info(f"Disabled lightweight polling endpoint accessed for job {job_id[:8]} - returning 410 Gone")
abort(410, "Lightweight polling endpoint disabled. Please refresh the page manually.")
# Use new lightweight status endpoint
try:
status_data = job_api_client.get_job_status(service_id, job_id)
# Validate the response has expected fields
required_fields = ["sent_count", "failed_count", "pending_count", "total_count", "processing_finished"]
if all(key in status_data for key in required_fields):
response_data = {
"sent_count": status_data["sent_count"],
"failed_count": status_data["failed_count"],
"pending_count": status_data["pending_count"],
"total_count": status_data["total_count"],
"finished": status_data["processing_finished"],
}
processed_count = status_data["sent_count"] + status_data["failed_count"]
else:
current_app.logger.error(f"Status endpoint returned invalid response for job {job_id[:8]}: {status_data}")
abort(500, "Invalid status response from API")
except Exception as e:
current_app.logger.error(f"Status endpoint failed for job {job_id[:8]}: {e}")
abort(500, "Status endpoint unavailable")
response_time_ms = round((time.time() - start_time) * 1000, 2)
response_json = json.dumps(response_data)
response_size_bytes = len(response_json.encode("utf-8"))
current_app.logger.info(
f"Poll status request - job_id={job_id[:8]} "
f"response_size={response_size_bytes}b "
f"response_time={response_time_ms}ms "
f"progress={processed_count}/{response_data['total_count']}"
)
return jsonify(response_data)
# Original polling code - commented out for manual refresh mode
# start_time = time.time()
#
# # Use new lightweight status endpoint
# try:
# status_data = job_api_client.get_job_status(service_id, job_id)
#
# # Validate the response has expected fields
# required_fields = ["sent_count", "failed_count", "pending_count", "total_count", "processing_finished"]
# if all(key in status_data for key in required_fields):
# response_data = {
# "sent_count": status_data["sent_count"],
# "failed_count": status_data["failed_count"],
# "pending_count": status_data["pending_count"],
# "total_count": status_data["total_count"],
# "finished": status_data["processing_finished"],
# }
# processed_count = status_data["sent_count"] + status_data["failed_count"]
# else:
# current_app.logger.error(f"Status endpoint returned invalid response for job {job_id[:8]}: {status_data}")
# abort(500, "Invalid status response from API")
#
# except Exception as e:
# current_app.logger.error(f"Status endpoint failed for job {job_id[:8]}: {e}")
# abort(500, "Status endpoint unavailable")
#
# response_time_ms = round((time.time() - start_time) * 1000, 2)
# response_json = json.dumps(response_data)
# response_size_bytes = len(response_json.encode("utf-8"))
#
# current_app.logger.info(
# f"Poll status request - job_id={job_id[:8]} "
# f"response_size={response_size_bytes}b "
# f"response_time={response_time_ms}ms "
# f"progress={processed_count}/{response_data['total_count']}"
# )
#
# return jsonify(response_data)
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>.json")

View File

@@ -12,7 +12,13 @@
{{ page_header("Message status") }}
{% if not job.finished_processing %}
<p class="max-width-full">This page refreshes automatically to show the latest message activity delivery rates, details, and reports.<br>You can watch it in progress or check back later.</p>
<p class="max-width-full">This page no longer refreshes automatically. Use the refresh button below to check the latest message status.</p>
<form method="get" style="display: inline-block;">
<button type="submit" class="usa-button usa-button--outline">
Refresh Status
</button>
</form>
<br><br>
{% endif %}
<div data-job-id="{{ job.id }}" data-host="{{ api_public_url }}">
{% if not job.finished_processing and FEATURE_SOCKET_ENABLED %}

View File

@@ -79,7 +79,7 @@ const javascripts = () => {
paths.src + 'javascripts/activityChart.js',
paths.src + 'javascripts/sidenav.js',
paths.src + 'javascripts/validation.js',
paths.src + 'javascripts/job-status-polling.js',
// paths.src + 'javascripts/job-status-polling.js', // Disabled for manual refresh mode
paths.src + 'javascripts/scrollPosition.js',
])
.pipe(plugins.prettyerror())
@@ -105,6 +105,7 @@ const copySetTimezone = () => {
return src(paths.src + 'js/setTimezone.js').pipe(dest(paths.dist + 'js/'));
};
// Task to copy images
const copyImages = () => {
return src(paths.src + 'images/**/*', { encoding: false }).pipe(

View File

@@ -1,15 +1,11 @@
"""
Tests for job notification update logic during polling.
Tests for disabled job polling endpoint.
These tests verify the poll status endpoint behavior and document
the JavaScript notification refresh logic:
1. Notifications update for first 50 messages
2. Notifications stop updating after 50 messages (to prevent performance issues)
3. Notifications always update when job finishes
These tests verify that the poll status endpoint is properly disabled
and returns 410 Gone status. The JavaScript notification refresh logic
is no longer used as polling has been replaced with manual refresh.
"""
import json
import pytest
@@ -58,31 +54,14 @@ def test_poll_status_notification_update_logic(
"main.view_job_status_poll",
service_id=service_one["id"],
job_id=fake_uuid,
_expected_status=410,
)
assert response.status_code == 200
data = json.loads(response.get_data(as_text=True))
assert response.status_code == 410
# Endpoint is disabled, so no data to verify
# Verify the response
assert data["sent_count"] == delivered
assert data["failed_count"] == failed
assert data["pending_count"] == pending
assert data["total_count"] == total
assert data["finished"] is finished
processed_count = delivered + failed
if js_should_update_notifications:
# JavaScript would call: await updateNotifications()
if finished:
assert finished, f"JS updates notifications: {reason}"
else:
assert processed_count <= 50, f"JS updates notifications: {reason}"
assert not finished, f"JS updates notifications: {reason}"
else:
# JavaScript would NOT update notifications
assert processed_count > 50, f"JS skips notification update: {reason}"
assert not finished, f"JS skips notification update: {reason}"
# Since the polling endpoint is disabled, the JavaScript logic being tested
# is no longer relevant. The test now verifies the endpoint is disabled.
def test_poll_status_provides_required_fields(
@@ -107,12 +86,7 @@ def test_poll_status_provides_required_fields(
"main.view_job_status_poll",
service_id=service_one["id"],
job_id=fake_uuid,
_expected_status=410,
)
data = json.loads(response.get_data(as_text=True))
required_fields = {"sent_count", "failed_count", "finished", "pending_count", "total_count"}
assert set(data.keys()) == required_fields
response_size = len(response.get_data(as_text=True))
assert response_size < 200, f"Response too large: {response_size} bytes"
assert response.status_code == 410

View File

@@ -358,7 +358,11 @@ def test_should_show_scheduled_job(
assert page.select("main p a")[0]["href"] == url_for(
"main.message_status",
)
assert page.select_one("main button[type=submit]").text.strip() == "Cancel sending"
# Test that both buttons are present
buttons = page.select("main button[type=submit]")
button_texts = [b.text.strip() for b in buttons]
assert "Refresh Status" in button_texts
assert "Cancel sending" in button_texts
def test_should_cancel_job(
@@ -523,25 +527,10 @@ def test_poll_status_endpoint(
"main.view_job_status_poll",
service_id=service_one["id"],
job_id=fake_uuid,
_expected_status=410,
)
assert response.status_code == 200
data = json.loads(response.get_data(as_text=True))
expected_keys = {
"sent_count",
"failed_count",
"pending_count",
"total_count",
"finished",
}
assert set(data.keys()) == expected_keys
assert data["sent_count"] == 90
assert data["failed_count"] == 10
assert data["pending_count"] == 0
assert data["total_count"] == 100
assert data["finished"] is True
assert response.status_code == 410
def test_poll_status_with_zero_notifications(
@@ -566,13 +555,10 @@ def test_poll_status_with_zero_notifications(
"main.view_job_status_poll",
service_id=service_one["id"],
job_id=fake_uuid,
_expected_status=410,
)
assert response.status_code == 200
data = json.loads(response.get_data(as_text=True))
assert data["total_count"] == 0
assert data["finished"] is True
assert response.status_code == 410
def test_poll_status_endpoint_does_not_query_notifications_table(
@@ -601,16 +587,10 @@ def test_poll_status_endpoint_does_not_query_notifications_table(
"main.view_job_status_poll",
service_id=service_one["id"],
job_id=fake_uuid,
_expected_status=410,
)
assert response.status_code == 200
assert response.status_code == 410
# Verify no notifications were fetched
# Verify no notifications were fetched (since endpoint is disabled)
mock_get_notifications.assert_not_called()
data = json.loads(response.get_data(as_text=True))
assert data["total_count"] == 500
assert data["sent_count"] == 300
assert data["failed_count"] == 50
assert data["pending_count"] == 150
assert data["finished"] is False