Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions server/mergin/sync/public_api_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import uuid
from datetime import datetime

import gevent
import psycopg2
from blinker import signal
from connexion import NoContent, request
Expand Down Expand Up @@ -951,6 +952,9 @@ def project_push(namespace, project_name):
f"Failed to upload a new project version using transaction id: {upload.id}: {str(err)}"
)
abort(422, "Failed to upload a new project version. Please try later.")
except gevent.timeout.Timeout:
db.session.rollback()
raise
finally:
upload.clear()

Expand Down Expand Up @@ -1165,6 +1169,10 @@ def push_finish(transaction_id):
f"transaction id: {transaction_id}.: {str(err)}"
)
abort(422, "Failed to create new version: {}".format(str(err)))
# catch exception during pg transaction so we can rollback and prevent PendingRollbackError during upload clean up
except gevent.timeout.Timeout:
db.session.rollback()
raise
finally:
# remove artifacts
upload.clear()
Expand Down
42 changes: 41 additions & 1 deletion server/mergin/tests/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial

import gevent
import psycogreen.gevent
import pytest
import sqlalchemy

from ..app import create_simple_app, GeventTimeoutMiddleware
from ..app import create_simple_app, GeventTimeoutMiddleware, db
from ..config import Configuration


Expand All @@ -31,3 +33,41 @@ def ping():
if use_middleware
else 200
)


def test_catch_timeout():
"""Test proper handling of gevent timeout with db.session.rollback"""
psycogreen.gevent.patch_psycopg()
Configuration.GEVENT_WORKER = True
Configuration.GEVENT_REQUEST_TIMEOUT = 1
application = create_simple_app()

def unhandled():
try:
db.session.execute("SELECT pg_sleep(1.1);")
finally:
db.session.execute("SELECT 1;")
return ""

def timeout():
try:
db.session.execute("SELECT pg_sleep(1.1);")
except gevent.timeout.Timeout:
db.session.rollback()
raise
finally:
db.session.execute("SELECT 1;")
return ""

application.add_url_rule("/unhandled", "unhandled", unhandled)
application.add_url_rule("/timeout", "timeout", timeout)
app_context = application.app_context()
app_context.push()

assert application.test_client().get("/timeout").status_code == 504

# in case of missing rollback sqlalchemy would raise error
with pytest.raises(sqlalchemy.exc.PendingRollbackError):
application.test_client().get("/unhandled")

db.session.rollback()
Loading