commit 3390f10fe27cd11766c2225b73c8ad12fd706289
parent 9f091a0583f2c493696605438b165bd11f40dcae
Author: Christian Grothoff <grothoff@gnunet.org>
Date: Mon, 26 Jan 2026 03:34:58 +0100
improve long-polling support for GET /private/orders/, for #9955
Diffstat:
5 files changed, 278 insertions(+), 110 deletions(-)
diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-claim.c b/src/backend/taler-merchant-httpd_post-orders-ID-claim.c
@@ -26,6 +26,7 @@
#include "platform.h"
#include <jansson.h>
#include <taler/taler_signatures.h>
+#include <taler/taler_dbevents.h>
#include <taler/taler_json_lib.h>
#include "taler-merchant-httpd_private-get-orders.h"
#include "taler-merchant-httpd_post-orders-ID-claim.h"
@@ -40,7 +41,7 @@
/**
* Run transaction to claim @a order_id for @a nonce.
*
- * @param instance_id instance to claim order at
+ * @param hc handler context with information about instance to claim order at
* @param order_id order to claim
* @param nonce nonce to use for the claim
* @param claim_token the token that should be used to verify the claim
@@ -53,12 +54,13 @@
* #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT if the order was successfully claimed
*/
static enum GNUNET_DB_QueryStatus
-claim_order (const char *instance_id,
+claim_order (struct TMH_HandlerContext *hc,
const char *order_id,
const char *nonce,
const struct TALER_ClaimTokenP *claim_token,
json_t **contract_terms)
{
+ const char *instance_id = hc->instance->settings.id;
struct TALER_ClaimTokenP order_ct;
enum GNUNET_DB_QueryStatus qs;
uint64_t order_serial;
@@ -179,10 +181,29 @@ claim_order (const char *instance_id,
*contract_terms = NULL;
return qs;
}
+ // FIXME: unify notifications? or do we need both?
TMH_notify_order_change (TMH_lookup_instance (instance_id),
TMH_OSF_CLAIMED,
timestamp,
order_serial);
+ {
+ struct TMH_OrderPayEventP pay_eh = {
+ .header.size = htons (sizeof (pay_eh)),
+ .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED),
+ .merchant_pub = hc->instance->merchant_pub
+ };
+
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Notifying clients about status change of order %s\n",
+ order_id);
+ GNUNET_CRYPTO_hash (order_id,
+ strlen (order_id),
+ &pay_eh.h_order_id);
+ TMH_db->event_notify (TMH_db->cls,
+ &pay_eh.header,
+ NULL,
+ 0);
+ }
qs = TMH_db->commit (TMH_db->cls);
if (0 > qs)
return qs;
@@ -232,7 +253,7 @@ TMH_post_orders_ID_claim (const struct TMH_RequestHandler *rh,
for (unsigned int i = 0; i<MAX_RETRIES; i++)
{
TMH_db->preflight (TMH_db->cls);
- qs = claim_order (hc->instance->settings.id,
+ qs = claim_order (hc,
order_id,
nonce,
&claim_token,
diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c
@@ -1906,6 +1906,24 @@ phase_payment_notification (struct PayContext *pc)
NULL,
0);
}
+ {
+ struct TMH_OrderPayEventP pay_eh = {
+ .header.size = htons (sizeof (pay_eh)),
+ .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED),
+ .merchant_pub = pc->hc->instance->merchant_pub
+ };
+
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Notifying clients about status change of order %s\n",
+ pc->order_id);
+ GNUNET_CRYPTO_hash (pc->order_id,
+ strlen (pc->order_id),
+ &pay_eh.h_order_id);
+ TMH_db->event_notify (TMH_db->cls,
+ &pay_eh.header,
+ NULL,
+ 0);
+ }
if ( (NULL != pc->parse_pay.session_id) &&
(NULL != pc->check_contract.contract_terms->fulfillment_url) )
{
diff --git a/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c b/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c
@@ -443,6 +443,8 @@ resume_kyc_with_response (struct KycContext *kc)
}
return;
}
+ // FIXME: should check that client set if-not-modified header to
+ // our ETAG before going 304 here!
kc->response_code = not_modified
? MHD_HTTP_NOT_MODIFIED
: MHD_HTTP_OK;
diff --git a/src/backend/taler-merchant-httpd_private-get-orders-ID.c b/src/backend/taler-merchant-httpd_private-get-orders-ID.c
@@ -1,6 +1,6 @@
/*
This file is part of TALER
- (C) 2017-2024 Taler Systems SA
+ (C) 2017-2024, 2026 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -285,6 +285,16 @@ struct GetOrderRequestContext
struct TALER_PrivateContractHashP h_contract_terms;
/**
+ * Set to the Etag of a response already known to the
+ * client. We should only return from long-polling
+ * on timeout (with "Not Modified") or when the Etag
+ * of the response differs from what is given here.
+ * Only set if @a have_lp_not_etag is true.
+ * Set from "lp_etag" query parameter.
+ */
+ struct GNUNET_ShortHashCode lp_not_etag;
+
+ /**
* Total amount the exchange deposited into our bank account
* (confirmed or unconfirmed), excluding fees.
*/
@@ -363,6 +373,11 @@ struct GetOrderRequestContext
bool refunded;
/**
+ * True if @e lp_not_etag was given.
+ */
+ bool have_lp_not_etag;
+
+ /**
* True if the order was paid.
*/
bool paid;
@@ -543,7 +558,7 @@ phase_init (struct GetOrderRequestContext *gorc)
struct TMH_HandlerContext *hc = gorc->hc;
struct TMH_OrderPayEventP pay_eh = {
.header.size = htons (sizeof (pay_eh)),
- .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_PAID),
+ .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED),
.merchant_pub = hc->instance->merchant_pub
};
@@ -843,6 +858,88 @@ phase_check_paid (struct GetOrderRequestContext *gorc)
/**
+ * Check if the @a reply satisfies the long-poll not_etag
+ * constraint. If so, return it as a reponse for @a gorc,
+ * otherwise suspend and wait for a change.
+ *
+ * @param[in,out] gorc request to handle
+ * @param reply body for JSON response (#MHD_HTTP_OK)
+ */
+static void
+check_reply (struct GetOrderRequestContext *gorc,
+ const json_t *reply)
+{
+ struct GNUNET_ShortHashCode sh;
+ unsigned int http_response_code;
+ bool not_modified;
+ struct MHD_Response *response;
+
+ {
+ char *can;
+
+ can = TALER_JSON_canonicalize (reply);
+ GNUNET_assert (GNUNET_YES ==
+ GNUNET_CRYPTO_kdf (&sh,
+ sizeof (sh),
+ "GOR-SALT",
+ strlen ("GOR-SALT"),
+ can,
+ strlen (can),
+ NULL,
+ 0));
+ GNUNET_free (can);
+ }
+ not_modified = gorc->have_lp_not_etag &&
+ (0 == GNUNET_memcmp (&sh,
+ &gorc->lp_not_etag));
+
+ if (not_modified &&
+ (! GNUNET_TIME_absolute_is_past (gorc->sc.long_poll_timeout)) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Status unchanged, not returning response yet\n");
+ GNUNET_assert (GNUNET_NO == gorc->suspended);
+ /* note: not necessarily actually unpaid ... */
+ GNUNET_CONTAINER_DLL_insert (gorc_head,
+ gorc_tail,
+ gorc);
+ gorc->phase = GOP_SUSPENDED_ON_UNPAID;
+ gorc->suspended = GNUNET_YES;
+ MHD_suspend_connection (gorc->sc.con);
+ return;
+ }
+ // FIXME: should check that client set if-not-modified header to
+ // our ETAG before going 304 here!
+ http_response_code = not_modified
+ ? MHD_HTTP_NOT_MODIFIED
+ : MHD_HTTP_OK;
+ response = TALER_MHD_make_json (reply);
+ {
+ char *etag;
+
+ etag = GNUNET_STRINGS_data_to_string_alloc (&sh,
+ sizeof (sh));
+ GNUNET_break (MHD_YES ==
+ MHD_add_response_header (response,
+ MHD_HTTP_HEADER_ETAG,
+ etag));
+ GNUNET_free (etag);
+ }
+
+ {
+ MHD_RESULT ret;
+
+ ret = MHD_queue_response (gorc->sc.con,
+ http_response_code,
+ response);
+ MHD_destroy_response (response);
+ phase_end (gorc,
+ ret);
+ }
+}
+
+
+/**
* Check if re-purchase detection applies to the order.
*
* @param[in,out] gorc order context to update
@@ -855,7 +952,7 @@ phase_check_repurchase (struct GetOrderRequestContext *gorc)
enum GNUNET_DB_QueryStatus qs;
char *taler_pay_uri;
char *order_status_url;
- MHD_RESULT ret;
+ json_t *reply;
if ( (gorc->paid) ||
(NULL == gorc->contract_terms->fulfillment_url) ||
@@ -938,9 +1035,7 @@ phase_check_repurchase (struct GetOrderRequestContext *gorc)
"host"));
return;
}
- ret = TALER_MHD_REPLY_JSON_PACK (
- gorc->sc.con,
- MHD_HTTP_OK,
+ reply = GNUNET_JSON_PACK (
GNUNET_JSON_pack_string ("taler_pay_uri",
taler_pay_uri),
GNUNET_JSON_pack_string ("order_status_url",
@@ -963,11 +1058,13 @@ phase_check_repurchase (struct GetOrderRequestContext *gorc)
gorc->contract_terms->pay_deadline),
GNUNET_JSON_pack_timestamp ("creation_time",
gorc->contract_terms->timestamp));
+
GNUNET_free (order_status_url);
GNUNET_free (taler_pay_uri);
GNUNET_free (already_paid_order_id);
- phase_end (gorc,
- ret);
+ check_reply (gorc,
+ reply);
+ json_decref (reply);
}
@@ -980,9 +1077,7 @@ static void
phase_unpaid_finish (struct GetOrderRequestContext *gorc)
{
struct TMH_HandlerContext *hc = gorc->hc;
- char *taler_pay_uri;
char *order_status_url;
- MHD_RESULT ret;
if (gorc->paid)
{
@@ -1012,49 +1107,54 @@ phase_unpaid_finish (struct GetOrderRequestContext *gorc)
NULL);
if (! gorc->order_only)
{
+ json_t *reply;
+
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Order %s claimed but not paid yet\n",
hc->infix);
- phase_end (gorc,
- TALER_MHD_REPLY_JSON_PACK (
- gorc->sc.con,
- MHD_HTTP_OK,
- GNUNET_JSON_pack_string ("order_status_url",
- order_status_url),
- GNUNET_JSON_pack_object_incref ("contract_terms",
- gorc->contract_terms_json),
- GNUNET_JSON_pack_string ("order_status",
- "claimed")));
+ reply = GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_string ("order_status_url",
+ order_status_url),
+ GNUNET_JSON_pack_object_incref ("contract_terms",
+ gorc->contract_terms_json),
+ GNUNET_JSON_pack_string ("order_status",
+ "claimed"));
GNUNET_free (order_status_url);
+ check_reply (gorc,
+ reply);
+ json_decref (reply);
return;
}
- taler_pay_uri = TMH_make_taler_pay_uri (gorc->sc.con,
- hc->infix,
- gorc->session_id,
- hc->instance->settings.id,
- &gorc->claim_token);
- ret = TALER_MHD_REPLY_JSON_PACK (
- gorc->sc.con,
- MHD_HTTP_OK,
- GNUNET_JSON_pack_string ("taler_pay_uri",
- taler_pay_uri),
- GNUNET_JSON_pack_string ("order_status_url",
- order_status_url),
- GNUNET_JSON_pack_string ("order_status",
- "unpaid"),
- /* undefined for unpaid v1 contracts */
- GNUNET_JSON_pack_allow_null (
- TALER_JSON_pack_amount ("total_amount",
- &gorc->contract_amount)),
- GNUNET_JSON_pack_string ("summary",
- gorc->contract_terms->summary),
- GNUNET_JSON_pack_timestamp ("creation_time",
- gorc->contract_terms->timestamp));
- GNUNET_free (taler_pay_uri);
+ {
+ char *taler_pay_uri;
+
+ taler_pay_uri = TMH_make_taler_pay_uri (gorc->sc.con,
+ hc->infix,
+ gorc->session_id,
+ hc->instance->settings.id,
+ &gorc->claim_token);
+ json_t *reply;
+
+ reply = GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_string ("taler_pay_uri",
+ taler_pay_uri),
+ GNUNET_JSON_pack_string ("order_status_url",
+ order_status_url),
+ GNUNET_JSON_pack_string ("order_status",
+ "unpaid"),
+ /* undefined for unpaid v1 contracts */
+ GNUNET_JSON_pack_allow_null (
+ TALER_JSON_pack_amount ("total_amount",
+ &gorc->contract_amount)),
+ GNUNET_JSON_pack_string ("summary",
+ gorc->contract_terms->summary),
+ GNUNET_JSON_pack_timestamp ("creation_time",
+ gorc->contract_terms->timestamp));
+ check_reply (gorc,
+ reply);
+ GNUNET_free (taler_pay_uri);
+ }
GNUNET_free (order_status_url);
- phase_end (gorc,
- ret);
-
}
@@ -1493,54 +1593,57 @@ phase_reply_result (struct GetOrderRequestContext *gorc)
TALER_amount_is_zero (&gorc->contract_amount));
gorc->last_payment = gorc->contract_terms->timestamp;
}
- ret = TALER_MHD_REPLY_JSON_PACK (
- gorc->sc.con,
- MHD_HTTP_OK,
- // Deprecated in protocol v6.
- GNUNET_JSON_pack_array_steal ("wire_reports",
- json_array ()),
- GNUNET_JSON_pack_uint64 ("exchange_code",
- gorc->exchange_ec),
- GNUNET_JSON_pack_uint64 ("exchange_http_status",
- gorc->exchange_hc),
- /* legacy: */
- GNUNET_JSON_pack_uint64 ("exchange_ec",
- gorc->exchange_ec),
- /* legacy: */
- GNUNET_JSON_pack_uint64 ("exchange_hc",
- gorc->exchange_hc),
- TALER_JSON_pack_amount ("deposit_total",
- &gorc->deposits_total),
- GNUNET_JSON_pack_object_incref ("contract_terms",
- gorc->contract_terms_json),
- GNUNET_JSON_pack_string ("order_status",
- "paid"),
- GNUNET_JSON_pack_timestamp ("last_payment",
- gorc->last_payment),
- GNUNET_JSON_pack_bool ("refunded",
- gorc->refunded),
- GNUNET_JSON_pack_bool ("wired",
- gorc->wired),
- GNUNET_JSON_pack_bool ("refund_pending",
- gorc->refund_pending),
- GNUNET_JSON_pack_allow_null (
- TALER_JSON_pack_amount ("refund_amount",
- &gorc->refund_amount)),
- GNUNET_JSON_pack_array_steal ("wire_details",
- gorc->wire_details),
- GNUNET_JSON_pack_array_steal ("refund_details",
- gorc->refund_details),
- GNUNET_JSON_pack_string ("order_status_url",
- order_status_url),
- (gorc->choice_index >= 0)
+ {
+ json_t *reply;
+
+ reply = GNUNET_JSON_PACK (
+ // Deprecated in protocol v6!
+ GNUNET_JSON_pack_array_steal ("wire_reports",
+ json_array ()),
+ GNUNET_JSON_pack_uint64 ("exchange_code",
+ gorc->exchange_ec),
+ GNUNET_JSON_pack_uint64 ("exchange_http_status",
+ gorc->exchange_hc),
+ /* legacy: */
+ GNUNET_JSON_pack_uint64 ("exchange_ec",
+ gorc->exchange_ec),
+ /* legacy: */
+ GNUNET_JSON_pack_uint64 ("exchange_hc",
+ gorc->exchange_hc),
+ TALER_JSON_pack_amount ("deposit_total",
+ &gorc->deposits_total),
+ GNUNET_JSON_pack_object_incref ("contract_terms",
+ gorc->contract_terms_json),
+ GNUNET_JSON_pack_string ("order_status",
+ "paid"),
+ GNUNET_JSON_pack_timestamp ("last_payment",
+ gorc->last_payment),
+ GNUNET_JSON_pack_bool ("refunded",
+ gorc->refunded),
+ GNUNET_JSON_pack_bool ("wired",
+ gorc->wired),
+ GNUNET_JSON_pack_bool ("refund_pending",
+ gorc->refund_pending),
+ GNUNET_JSON_pack_allow_null (
+ TALER_JSON_pack_amount ("refund_amount",
+ &gorc->refund_amount)),
+ GNUNET_JSON_pack_array_steal ("wire_details",
+ gorc->wire_details),
+ GNUNET_JSON_pack_array_steal ("refund_details",
+ gorc->refund_details),
+ GNUNET_JSON_pack_string ("order_status_url",
+ order_status_url),
+ (gorc->choice_index >= 0)
? GNUNET_JSON_pack_int64 ("choice_index",
gorc->choice_index)
: GNUNET_JSON_pack_end_ ());
+ check_reply (gorc,
+ reply);
+ json_decref (reply);
+ }
GNUNET_free (order_status_url);
gorc->wire_details = NULL;
gorc->refund_details = NULL;
- phase_end (gorc,
- ret);
}
@@ -1595,6 +1698,10 @@ TMH_private_get_orders_ID (
"allow_refunded_for_repurchase");
TALER_MHD_parse_request_timeout (connection,
&gorc->sc.long_poll_timeout);
+ TALER_MHD_parse_request_arg_auto (connection,
+ "lp_not_etag",
+ &gorc->lp_not_etag,
+ gorc->have_lp_not_etag);
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Starting GET /private/orders/%s processing with timeout %s\n",
hc->infix,
diff --git a/src/backend/taler-merchant-httpd_private-post-orders-ID-refund.c b/src/backend/taler-merchant-httpd_private-post-orders-ID-refund.c
@@ -49,26 +49,46 @@ trigger_refund_notification (
struct TMH_HandlerContext *hc,
const struct TALER_Amount *amount)
{
- const char *as;
- struct TMH_OrderRefundEventP refund_eh = {
- .header.size = htons (sizeof (refund_eh)),
- .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_REFUND),
- .merchant_pub = hc->instance->merchant_pub
- };
+ {
+ const char *as;
+ struct TMH_OrderRefundEventP refund_eh = {
+ .header.size = htons (sizeof (refund_eh)),
+ .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_REFUND),
+ .merchant_pub = hc->instance->merchant_pub
+ };
+
+ /* Resume clients that may wait for this refund */
+ as = TALER_amount2s (amount);
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Awakening clients on %s waiting for refund of no more than %s\n",
+ hc->infix,
+ as);
+ GNUNET_CRYPTO_hash (hc->infix,
+ strlen (hc->infix),
+ &refund_eh.h_order_id);
+ TMH_db->event_notify (TMH_db->cls,
+ &refund_eh.header,
+ as,
+ strlen (as));
+ }
+ {
+ struct TMH_OrderPayEventP pay_eh = {
+ .header.size = htons (sizeof (pay_eh)),
+ .header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_STATUS_CHANGED),
+ .merchant_pub = hc->instance->merchant_pub
+ };
- /* Resume clients that may wait for this refund */
- as = TALER_amount2s (amount);
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Awakening clients on %s waiting for refund of no more than %s\n",
- hc->infix,
- as);
- GNUNET_CRYPTO_hash (hc->infix,
- strlen (hc->infix),
- &refund_eh.h_order_id);
- TMH_db->event_notify (TMH_db->cls,
- &refund_eh.header,
- as,
- strlen (as));
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Notifying clients about status change of order %s\n",
+ hc->infix);
+ GNUNET_CRYPTO_hash (hc->infix,
+ strlen (hc->infix),
+ &pay_eh.h_order_id);
+ TMH_db->event_notify (TMH_db->cls,
+ &pay_eh.header,
+ NULL,
+ 0);
+ }
}