From 32913b8a3c652dfa6a273272202bd6033f8ae287 Mon Sep 17 00:00:00 2001 From: koutst Date: Wed, 24 Apr 2024 22:13:26 +0000 Subject: [PATCH 1/4] Add caching on client id --- src/purchase/views/purchase_view.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/purchase/views/purchase_view.py b/src/purchase/views/purchase_view.py index 4643552ed..c79879f06 100644 --- a/src/purchase/views/purchase_view.py +++ b/src/purchase/views/purchase_view.py @@ -56,6 +56,7 @@ def create(self, request): data = request.data amount = data["amount"] + client_id = data["client_id"] purchase_method = data["purchase_method"] purchase_type = data["purchase_type"] content_type_str = data["content_type"] @@ -77,6 +78,17 @@ def create(self, request): with transaction.atomic(): user = User.objects.select_for_update().get(id=user.id) + cached_client_id = cache.get(f"purchase_client_id_{client_id}") + if cached_client_id: + return Response( + { + "detail": "This purchase has already been processed", + }, + status=400, + ) + + cache.set(f"purchase_client_id_{client_id}", True, timeout=60) + purchase_data = { "amount": amount, "user": user.id, From 0b6706bd18908c1a481e062cbd52292a31a115b5 Mon Sep 17 00:00:00 2001 From: koutst Date: Thu, 25 Apr 2024 14:51:48 +0000 Subject: [PATCH 2/4] Add test for purchase caching --- src/purchase/tests/test_send_rsc.py | 43 +++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/purchase/tests/test_send_rsc.py b/src/purchase/tests/test_send_rsc.py index f30ee6fc0..e9d73e280 100644 --- a/src/purchase/tests/test_send_rsc.py +++ b/src/purchase/tests/test_send_rsc.py @@ -45,7 +45,7 @@ def test_list_purchases(self): ) response = self._post_support_response( - purchaser, post.id, "researchhubpost", tip_amount + purchaser, post.id, "client_id_1", "researchhubpost", tip_amount ) self.assertContains(response, "id", status_code=201) @@ -68,7 +68,7 @@ def test_list_purchases_cannot_list_other_users_purchases(self): ) response = self._post_support_response( - purchaser, post.id, "researchhubpost", tip_amount + purchaser, post.id, "client_id_2", "researchhubpost", tip_amount ) self.assertContains(response, "id", status_code=201) @@ -125,7 +125,9 @@ def test_support_paper_distribution(self): amount="10000", user=user, content_type=DISTRIBUTION_CONTENT_TYPE ) - response = self._post_support_response(user, paper.id, "paper", amount) + response = self._post_support_response( + user, paper.id, "client_id_3", "paper", amount + ) self.assertContains(response, "id", status_code=201) self.assertTrue(Escrow.objects.filter(hold_type=Escrow.AUTHOR_RSC).count() == 1) author_pot = Escrow.objects.filter(hold_type=Escrow.AUTHOR_RSC).first() @@ -146,7 +148,7 @@ def test_support_post_distribution(self): ) response = self._post_support_response( - user, post.id, "researchhubpost", tip_amount + user, post.id, "client_id_4", "researchhubpost", tip_amount ) self.assertContains(response, "id", status_code=201) purchase_id = response.data["id"] @@ -193,7 +195,7 @@ def test_support_comment_distribution(self): ) response = self._post_support_response( - user, comment.id, "rhcommentmodel", tip_amount + user, comment.id, "client_id_5", "rhcommentmodel", tip_amount ) self.assertContains(response, "id", status_code=201) purchase_id = response.data["id"] @@ -224,7 +226,35 @@ def test_support_comment_distribution(self): ) self.assertEqual(poster_balance_amount, float(tip_amount)) - def _post_support_response(self, user, object_id, content_type, amount=10): + def test_repeat_request_support_comment_distribution(self): + user = create_random_authenticated_user("rep_user") + poster = create_random_authenticated_user("rep_user") + post = create_post(created_by=poster) + comment = create_rh_comment(created_by=poster, post=post) + client_id = "client_id_6" + + tip_amount = 100 + + # give the user 10,000 RSC + DISTRIBUTION_CONTENT_TYPE = ContentType.objects.get(model="distribution") + Balance.objects.create( + amount="10000", user=user, content_type=DISTRIBUTION_CONTENT_TYPE + ) + + response = self._post_support_response( + user, comment.id, client_id, "rhcommentmodel", tip_amount + ) + self.assertContains(response, "id", status_code=201) + + # make a second request with the same client_id which should fail. + response = self._post_support_response( + user, comment.id, client_id, "rhcommentmodel", tip_amount + ) + self.assertEqual(response.status_code, 400) + + def _post_support_response( + self, user, object_id, client_id, content_type, amount=10 + ): url = "/api/purchase/" return get_authenticated_post_response( user, @@ -235,5 +265,6 @@ def _post_support_response(self, user, object_id, content_type, amount=10): "object_id": object_id, "purchase_method": "OFF_CHAIN", "purchase_type": "BOOST", + "client_id": client_id, }, ) From 0112cfc64e9b8c55a65d79e5b12d7a9a43a899e1 Mon Sep 17 00:00:00 2001 From: koutst Date: Tue, 30 Apr 2024 14:43:43 +0000 Subject: [PATCH 3/4] Cache response, return success on repeat request --- src/purchase/tests/test_send_rsc.py | 5 +++-- src/purchase/views/purchase_view.py | 14 ++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/purchase/tests/test_send_rsc.py b/src/purchase/tests/test_send_rsc.py index e9d73e280..c510f7d82 100644 --- a/src/purchase/tests/test_send_rsc.py +++ b/src/purchase/tests/test_send_rsc.py @@ -247,10 +247,11 @@ def test_repeat_request_support_comment_distribution(self): self.assertContains(response, "id", status_code=201) # make a second request with the same client_id which should fail. - response = self._post_support_response( + response_repeat_call = self._post_support_response( user, comment.id, client_id, "rhcommentmodel", tip_amount ) - self.assertEqual(response.status_code, 400) + self.assertEqual(response.data, response_repeat_call.data) + self.assertEqual(response_repeat_call.status_code, 201) def _post_support_response( self, user, object_id, client_id, content_type, amount=10 diff --git a/src/purchase/views/purchase_view.py b/src/purchase/views/purchase_view.py index c79879f06..d5aee293d 100644 --- a/src/purchase/views/purchase_view.py +++ b/src/purchase/views/purchase_view.py @@ -78,17 +78,13 @@ def create(self, request): with transaction.atomic(): user = User.objects.select_for_update().get(id=user.id) - cached_client_id = cache.get(f"purchase_client_id_{client_id}") - if cached_client_id: + cached_serialized_data = cache.get(f"purchase_client_id_{client_id}") + if cached_serialized_data: return Response( - { - "detail": "This purchase has already been processed", - }, - status=400, + cached_serialized_data, + status=201, ) - cache.set(f"purchase_client_id_{client_id}", True, timeout=60) - purchase_data = { "amount": amount, "user": user.id, @@ -219,6 +215,8 @@ def create(self, request): serializer = self.serializer_class(purchase, context=context) serializer_data = serializer.data + cache.set(f"purchase_client_id_{client_id}", serializer_data, timeout=3600) + if recipient and user: self.send_purchase_notification( purchase, unified_doc, recipient, notification_type From b4301d4e0df61bbd8507f08b6f5086035145c007 Mon Sep 17 00:00:00 2001 From: koutst Date: Tue, 30 Apr 2024 14:53:35 +0000 Subject: [PATCH 4/4] Move caching into transaction --- src/purchase/views/purchase_view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/purchase/views/purchase_view.py b/src/purchase/views/purchase_view.py index d5aee293d..ecf60d007 100644 --- a/src/purchase/views/purchase_view.py +++ b/src/purchase/views/purchase_view.py @@ -212,10 +212,10 @@ def create(self, request): ) distributor.distribute() - serializer = self.serializer_class(purchase, context=context) - serializer_data = serializer.data + serializer = self.serializer_class(purchase, context=context) + serializer_data = serializer.data - cache.set(f"purchase_client_id_{client_id}", serializer_data, timeout=3600) + cache.set(f"purchase_client_id_{client_id}", serializer_data, timeout=3600) if recipient and user: self.send_purchase_notification(