webmazta commited on
Commit
74a35c2
·
1 Parent(s): 4a78eed

Fix Hostinger pull relay heartbeat

Browse files
app.py CHANGED
@@ -1114,13 +1114,12 @@ def _wp_relay_authorized() -> bool:
1114
  return bool(expected) and hmac.compare_digest(supplied, f"Bearer {expected}")
1115
 
1116
 
1117
- @app.route("/api/wp-upload-relay/next", methods=["GET"])
1118
  def api_wp_relay_next():
1119
  if not _wp_relay_authorized():
1120
  return jsonify({"error": "Unauthorized"}), 401
1121
- from upload_pull_queue import claim_next, record_poll
1122
 
1123
- record_poll()
1124
  item = claim_next()
1125
  if item is None:
1126
  response = Response(status=204)
 
1114
  return bool(expected) and hmac.compare_digest(supplied, f"Bearer {expected}")
1115
 
1116
 
1117
+ @app.route("/api/wp-upload-relay/next", methods=["POST"])
1118
  def api_wp_relay_next():
1119
  if not _wp_relay_authorized():
1120
  return jsonify({"error": "Unauthorized"}), 401
1121
+ from upload_pull_queue import claim_next
1122
 
 
1123
  item = claim_next()
1124
  if item is None:
1125
  response = Response(status=204)
hostinger-upload-relay/server.js CHANGED
@@ -15,10 +15,14 @@ try {
15
  const spaceUrl = (process.env.SPACE_URL || relayConfig.spaceUrl || "").replace(/\/$/, "");
16
  const relayToken = process.env.WP_PULL_RELAY_TOKEN || relayConfig.relayToken || "";
17
  const relayState = {
18
- last_poll_at: null,
 
 
 
19
  last_claim_at: null,
20
  last_success_at: null,
21
  last_error: null,
 
22
  in_flight: null,
23
  };
24
 
@@ -144,19 +148,30 @@ async function pollForUpload() {
144
  polling = true;
145
  let item;
146
  try {
147
- relayState.last_poll_at = new Date().toISOString();
148
  const next = await fetch(
149
  `${spaceUrl}/api/wp-upload-relay/next?ts=${Date.now()}`,
150
  {
 
151
  headers: {
152
  authorization: `Bearer ${relayToken}`,
153
- "cache-control": "no-cache",
 
 
154
  },
155
- signal: AbortSignal.timeout(30000),
 
 
 
 
156
  },
157
  );
158
- if (next.status === 204) return;
 
159
  if (!next.ok) throw new Error(`Queue endpoint returned ${next.status}`);
 
 
 
160
  item = await next.json();
161
  relayState.last_claim_at = new Date().toISOString();
162
  relayState.in_flight = item.id;
@@ -209,6 +224,7 @@ async function pollForUpload() {
209
  relayState.last_error = null;
210
  console.log(`Completed pulled upload ${item.id}: ${upstream.status}`);
211
  } catch (error) {
 
212
  relayState.last_error = `${new Date().toISOString()} ${error.message}`;
213
  console.error(`Pull relay error: ${error.message}`);
214
  if (item?.id) {
 
15
  const spaceUrl = (process.env.SPACE_URL || relayConfig.spaceUrl || "").replace(/\/$/, "");
16
  const relayToken = process.env.WP_PULL_RELAY_TOKEN || relayConfig.relayToken || "";
17
  const relayState = {
18
+ process_started_at: new Date().toISOString(),
19
+ last_poll_attempt_at: null,
20
+ last_poll_response_at: null,
21
+ last_poll_status: null,
22
  last_claim_at: null,
23
  last_success_at: null,
24
  last_error: null,
25
+ consecutive_poll_errors: 0,
26
  in_flight: null,
27
  };
28
 
 
148
  polling = true;
149
  let item;
150
  try {
151
+ relayState.last_poll_attempt_at = new Date().toISOString();
152
  const next = await fetch(
153
  `${spaceUrl}/api/wp-upload-relay/next?ts=${Date.now()}`,
154
  {
155
+ method: "POST",
156
  headers: {
157
  authorization: `Bearer ${relayToken}`,
158
+ "cache-control": "no-store",
159
+ pragma: "no-cache",
160
+ "content-type": "application/json",
161
  },
162
+ body: JSON.stringify({
163
+ relay_id: "hostinger-primary",
164
+ sent_at: new Date().toISOString(),
165
+ }),
166
+ signal: AbortSignal.timeout(15000),
167
  },
168
  );
169
+ relayState.last_poll_response_at = new Date().toISOString();
170
+ relayState.last_poll_status = next.status;
171
  if (!next.ok) throw new Error(`Queue endpoint returned ${next.status}`);
172
+ relayState.consecutive_poll_errors = 0;
173
+ relayState.last_error = null;
174
+ if (next.status === 204) return;
175
  item = await next.json();
176
  relayState.last_claim_at = new Date().toISOString();
177
  relayState.in_flight = item.id;
 
224
  relayState.last_error = null;
225
  console.log(`Completed pulled upload ${item.id}: ${upstream.status}`);
226
  } catch (error) {
227
+ relayState.consecutive_poll_errors += 1;
228
  relayState.last_error = `${new Date().toISOString()} ${error.message}`;
229
  console.error(`Pull relay error: ${error.message}`);
230
  if (item?.id) {
test_wordpress_transport.py CHANGED
@@ -3,7 +3,11 @@ import unittest
3
  from types import SimpleNamespace
4
  from unittest.mock import patch
5
 
6
- from wordpress_transport import origin_candidates, post_multipart_to_wordpress_origin
 
 
 
 
7
 
8
 
9
  class OriginCandidatesTests(unittest.TestCase):
@@ -68,7 +72,14 @@ class OriginCandidatesTests(unittest.TestCase):
68
  self.assertEqual(result[3], "pull")
69
  submit.assert_called_once()
70
 
71
- @patch.dict("os.environ", {"WP_PULL_RELAY_ENABLED": "true"}, clear=False)
 
 
 
 
 
 
 
72
  @patch("wordpress_transport.requests.post")
73
  @patch("upload_pull_queue.submit")
74
  def test_pull_mode_falls_back_to_direct_on_failure(self, submit, post):
@@ -86,6 +97,29 @@ class OriginCandidatesTests(unittest.TestCase):
86
  self.assertEqual(result[3], "relay.example")
87
  post.assert_called_once()
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  if __name__ == "__main__":
91
  unittest.main()
 
3
  from types import SimpleNamespace
4
  from unittest.mock import patch
5
 
6
+ from wordpress_transport import (
7
+ WordPressUnavailableError,
8
+ origin_candidates,
9
+ post_multipart_to_wordpress_origin,
10
+ )
11
 
12
 
13
  class OriginCandidatesTests(unittest.TestCase):
 
72
  self.assertEqual(result[3], "pull")
73
  submit.assert_called_once()
74
 
75
+ @patch.dict(
76
+ "os.environ",
77
+ {
78
+ "WP_PULL_RELAY_ENABLED": "true",
79
+ "WP_PULL_DIRECT_FALLBACK": "true",
80
+ },
81
+ clear=False,
82
+ )
83
  @patch("wordpress_transport.requests.post")
84
  @patch("upload_pull_queue.submit")
85
  def test_pull_mode_falls_back_to_direct_on_failure(self, submit, post):
 
97
  self.assertEqual(result[3], "relay.example")
98
  post.assert_called_once()
99
 
100
+ @patch.dict(
101
+ "os.environ",
102
+ {
103
+ "WP_PULL_RELAY_ENABLED": "true",
104
+ "WP_PULL_DIRECT_FALLBACK": "false",
105
+ },
106
+ clear=False,
107
+ )
108
+ @patch("wordpress_transport.requests.post")
109
+ @patch("upload_pull_queue.submit")
110
+ def test_pull_mode_fails_fast_without_direct_fallback(self, submit, post):
111
+ submit.side_effect = TimeoutError("pull relay unavailable")
112
+
113
+ with self.assertRaisesRegex(
114
+ WordPressUnavailableError, "Hostinger pull relay failed"
115
+ ):
116
+ post_multipart_to_wordpress_origin(
117
+ "https://relay.example/upload",
118
+ {"file": ("x.jpg", b"data", "image/jpeg")},
119
+ )
120
+
121
+ post.assert_not_called()
122
+
123
 
124
  if __name__ == "__main__":
125
  unittest.main()
wordpress_transport.py CHANGED
@@ -15,6 +15,13 @@ class WordPressUnavailableError(ConnectionError):
15
  """Raised when none of the WordPress origin addresses can be reached."""
16
 
17
 
 
 
 
 
 
 
 
18
  def request_wordpress_json(
19
  method: str,
20
  url: str,
@@ -24,7 +31,7 @@ def request_wordpress_json(
24
  timeout: int = 30,
25
  ) -> requests.Response:
26
  """Send a WordPress JSON request directly or through the Hostinger pull relay."""
27
- if os.getenv("WP_PULL_RELAY_ENABLED", "").strip().lower() in ("1", "true", "yes"):
28
  from upload_pull_queue import submit_request
29
 
30
  parsed = urlparse(url)
@@ -40,8 +47,13 @@ def request_wordpress_json(
40
  response.url = url
41
  return response
42
  except Exception as exc:
 
 
 
 
43
  print(
44
- f" [WARNING] Hostinger pull relay JSON request failed ({exc}); falling back to direct transport.",
 
45
  flush=True,
46
  )
47
 
@@ -95,15 +107,20 @@ def post_multipart_to_wordpress_origin(
95
  raise ValueError("WP_BRIDGE_URL must be an absolute HTTPS URL")
96
 
97
  pull_relay_error: Exception | None = None
98
- if os.getenv("WP_PULL_RELAY_ENABLED", "").strip().lower() in ("1", "true", "yes"):
99
  from upload_pull_queue import submit
100
 
101
  try:
102
  return submit(fields, timeout=read_timeout + 30)
103
  except Exception as exc:
 
 
 
 
104
  pull_relay_error = exc
105
  print(
106
- f" [WARNING] Hostinger pull relay upload failed ({exc}); falling back to direct transport.",
 
107
  flush=True,
108
  )
109
 
 
15
  """Raised when none of the WordPress origin addresses can be reached."""
16
 
17
 
18
+ def _env_enabled(name: str, default: bool = False) -> bool:
19
+ value = os.getenv(name)
20
+ if value is None:
21
+ return default
22
+ return value.strip().lower() in ("1", "true", "yes")
23
+
24
+
25
  def request_wordpress_json(
26
  method: str,
27
  url: str,
 
31
  timeout: int = 30,
32
  ) -> requests.Response:
33
  """Send a WordPress JSON request directly or through the Hostinger pull relay."""
34
+ if _env_enabled("WP_PULL_RELAY_ENABLED"):
35
  from upload_pull_queue import submit_request
36
 
37
  parsed = urlparse(url)
 
47
  response.url = url
48
  return response
49
  except Exception as exc:
50
+ if not _env_enabled("WP_PULL_DIRECT_FALLBACK"):
51
+ raise WordPressUnavailableError(
52
+ f"Hostinger pull relay JSON request failed: {exc}"
53
+ ) from exc
54
  print(
55
+ f" [WARNING] Hostinger pull relay JSON request failed ({exc}); "
56
+ "WP_PULL_DIRECT_FALLBACK is enabled, trying direct transport.",
57
  flush=True,
58
  )
59
 
 
107
  raise ValueError("WP_BRIDGE_URL must be an absolute HTTPS URL")
108
 
109
  pull_relay_error: Exception | None = None
110
+ if _env_enabled("WP_PULL_RELAY_ENABLED"):
111
  from upload_pull_queue import submit
112
 
113
  try:
114
  return submit(fields, timeout=read_timeout + 30)
115
  except Exception as exc:
116
+ if not _env_enabled("WP_PULL_DIRECT_FALLBACK"):
117
+ raise WordPressUnavailableError(
118
+ f"Hostinger pull relay failed: {exc}"
119
+ ) from exc
120
  pull_relay_error = exc
121
  print(
122
+ f" [WARNING] Hostinger pull relay upload failed ({exc}); "
123
+ "WP_PULL_DIRECT_FALLBACK is enabled, trying direct transport.",
124
  flush=True,
125
  )
126