Repository: liberapay/liberapay.com

Issue:
PoolError exceptions aren't handled
If we set `DATABASE_MAXCONN` too low, we could get exceptions like this:

```
Traceback (most recent call last):
  ...
  File "env/lib/python3.6/site-packages/postgres/__init__.py", line 451, in get_cursor
    return CursorContextManager(self.pool, **kw)
  File "env/lib/python3.6/site-packages/postgres/context_managers.py", line 35, in __init__
    conn = self.pool.getconn()
  File "env/lib/python3.6/site-packages/psycopg2_pool/__init__.py", line 236, in getconn
    return super(ThreadSafeConnectionPool, self).getconn()
  File "env/lib/python3.6/site-packages/psycopg2_pool/__init__.py", line 120, in getconn
    raise PoolError("connection pool exhausted")
psycopg2_pool.PoolError: connection pool exhausted
```



Discussion:

Resolving patch:
diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py
--- a/liberapay/models/participant.py
+++ b/liberapay/models/participant.py
@@ -2596,6 +2596,7 @@ def find_partial_match(new_sp, current_schedule_map):
                    AND ( tippee_p.goal IS NULL OR tippee_p.goal >= 0 )
                    AND tippee_p.is_suspended IS NOT TRUE
                    AND tippee_p.payment_providers > 0
+              ORDER BY t.tippee
             """, (self.id,))
 
             # Get the existing schedule
@@ -2638,7 +2639,7 @@ def find_partial_match(new_sp, current_schedule_map):
                   ORDER BY coalesce(pt.team, pt.recipient)
                          , pt.ctime DESC
                 """, dict(payer=self.id, tippees=tippees)))
-                for tip in list(renewable_tips):
+                for tip in renewable_tips:
                     if tip.renewal_mode == 2 and tip.tippee_p.payment_providers & 1 == 0:
                         # Automatic payments are only possible through Stripe.
                         tip.renewal_mode = 1
diff --git a/liberapay/payin/stripe.py b/liberapay/payin/stripe.py
--- a/liberapay/payin/stripe.py
+++ b/liberapay/payin/stripe.py
@@ -326,7 +326,7 @@ def settle_charge_and_transfers(db, payin, charge, intent_id=None):
                 execute_transfer(db, pt, pt.destination_id, charge.id)
             elif refunded_amount and pt.remote_id:
                 sync_transfer(db, pt)
-    elif charge.status == 'failed':
+    elif charge.status in ('failed', 'pending'):
         for pt in payin_transfers:
             update_payin_transfer(db, pt.id, None, charge.status, error)
 
diff --git a/liberapay/wireup.py b/liberapay/wireup.py
--- a/liberapay/wireup.py
+++ b/liberapay/wireup.py
@@ -18,6 +18,7 @@
 from postgres.cursors import SimpleRowCursor
 import psycopg2
 from psycopg2.extensions import adapt, AsIs, new_type, register_adapter, register_type
+from psycopg2_pool import PoolError
 import raven
 import requests
 import sass
@@ -463,6 +464,10 @@ def tell_sentry(exception, state, allow_reraise=True):
             # Don't flood Sentry when DB is down
             return
 
+        if isinstance(exception, PoolError):
+            # If this happens, then the `DATABASE_MAXCONN` value is too low.
+            state['exception'] = NeedDatabase()
+
         if isinstance(exception, psycopg2.Error):
             from liberapay.website import website
             if getattr(website, 'db', None):


Test patch:
diff --git a/tests/py/test_payins.py b/tests/py/test_payins.py
--- a/tests/py/test_payins.py
+++ b/tests/py/test_payins.py
@@ -794,10 +794,10 @@ def test_03_payin_stripe_sdd_one_to_many(self):
         payin_transfers = self.db.all("SELECT * FROM payin_transfers ORDER BY id")
         assert len(payin_transfers) == 2
         pt1, pt2 = payin_transfers
-        assert pt1.status == 'pre'
+        assert pt1.status == 'pending'
         assert pt1.amount == EUR('50.00')
         assert pt1.remote_id is None
-        assert pt2.status == 'pre'
+        assert pt2.status == 'pending'
         assert pt2.amount == EUR('50.00')
         assert pt2.remote_id is None