Skip to content

Commit 6de74ae

Browse files
authored
Prevent automatic shutdown in Docker/Linux (#119)
* Manage a single Scheduler instance and inject it as a dependency * Switch the docker image over to the official gunicorn/uvicorn image * Fix linter errors
1 parent 3be772a commit 6de74ae

File tree

10 files changed

+79
-33
lines changed

10 files changed

+79
-33
lines changed

Dockerfile

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
FROM python:3.8 AS base
2-
ENV host 0.0.0.0
3-
ENV port 8000
1+
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8 AS base
2+
ENV PORT 8000
43
RUN apt update && apt-get install -y \
54
libgmp-dev \
65
libmpfr-dev \
@@ -10,14 +9,12 @@ COPY ./pyproject.toml /tmp/
109
COPY ./poetry.lock /tmp/
1110
RUN cd /tmp && poetry export -f requirements.txt > requirements.txt
1211
RUN pip install -r /tmp/requirements.txt
12+
EXPOSE $PORT
1313

1414
FROM base AS dev
15-
VOLUME [ "/app" ]
16-
EXPOSE $port
17-
CMD uvicorn app.main:app --reload --host "$host" --port "$port"
15+
VOLUME [ "/app/app" ]
16+
CMD /start-reload.sh
1817

1918
FROM base AS prod
20-
COPY ./app /app
21-
EXPOSE $port
22-
# TODO: We should not have to use the --reload flag here! See issue #80
23-
CMD uvicorn app.main:app --reload --host "$host" --port "$port"
19+
COPY ./app /app/app
20+
# The base image will start gunicorn

app/api/v1/guardian/ballot.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
from electionguard.election import CiphertextElectionContext
55
from electionguard.scheduler import Scheduler
66
from electionguard.serializable import write_json_object
7-
from fastapi import APIRouter, Body
7+
from fastapi import APIRouter, Body, Depends
88

9+
from app.core.scheduler import get_scheduler
910
from ..models import (
1011
convert_guardian,
1112
DecryptBallotSharesRequest,
@@ -17,7 +18,10 @@
1718

1819

1920
@router.post("/decrypt-shares", tags=[TALLY])
20-
def decrypt_ballot_shares(request: DecryptBallotSharesRequest = Body(...)) -> Any:
21+
def decrypt_ballot_shares(
22+
request: DecryptBallotSharesRequest = Body(...),
23+
scheduler: Scheduler = Depends(get_scheduler),
24+
) -> Any:
2125
"""
2226
Decrypt this guardian's share of one or more ballots
2327
"""
@@ -28,7 +32,6 @@ def decrypt_ballot_shares(request: DecryptBallotSharesRequest = Body(...)) -> An
2832
context = CiphertextElectionContext.from_json_object(request.context)
2933
guardian = convert_guardian(request.guardian)
3034

31-
scheduler = Scheduler()
3235
shares = [
3336
compute_decryption_share_for_ballot(guardian, ballot, context, scheduler)
3437
for ballot in ballots

app/api/v1/guardian/tally.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
ElectionDescription,
66
InternalElectionDescription,
77
)
8+
from electionguard.scheduler import Scheduler
89
from electionguard.serializable import write_json_object
9-
from fastapi import APIRouter, Body
10-
10+
from fastapi import APIRouter, Body, Depends
1111

12+
from app.core.scheduler import get_scheduler
1213
from ..models import (
1314
convert_guardian,
1415
convert_tally,
@@ -20,7 +21,10 @@
2021

2122

2223
@router.post("/decrypt-share", tags=[TALLY])
23-
def decrypt_share(request: DecryptTallyShareRequest = Body(...)) -> Any:
24+
def decrypt_share(
25+
request: DecryptTallyShareRequest = Body(...),
26+
scheduler: Scheduler = Depends(get_scheduler),
27+
) -> Any:
2428
"""
2529
Decrypt a single guardian's share of a tally
2630
"""
@@ -31,6 +35,6 @@ def decrypt_share(request: DecryptTallyShareRequest = Body(...)) -> Any:
3135
guardian = convert_guardian(request.guardian)
3236
tally = convert_tally(request.encrypted_tally, description, context)
3337

34-
share = compute_decryption_share(guardian, tally, context)
38+
share = compute_decryption_share(guardian, tally, context, scheduler)
3539

3640
return write_json_object(share)

app/api/v1/mediator/tally.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@
77
ElectionDescription,
88
InternalElectionDescription,
99
)
10+
from electionguard.scheduler import Scheduler
1011
from electionguard.serializable import read_json_object
1112
from electionguard.tally import (
1213
publish_ciphertext_tally,
1314
publish_plaintext_tally,
1415
CiphertextTally,
1516
)
16-
from fastapi import APIRouter, Body, HTTPException
17-
17+
from fastapi import APIRouter, Body, Depends, HTTPException
1818

19+
from app.core.scheduler import get_scheduler
1920
from ..models import (
2021
convert_tally,
2122
AppendTallyRequest,
@@ -29,27 +30,33 @@
2930

3031

3132
@router.post("", tags=[TALLY])
32-
def start_tally(request: StartTallyRequest = Body(...)) -> Any:
33+
def start_tally(
34+
request: StartTallyRequest = Body(...),
35+
scheduler: Scheduler = Depends(get_scheduler),
36+
) -> Any:
3337
"""
3438
Start a new tally of a collection of ballots
3539
"""
3640

3741
ballots, description, context = _parse_tally_request(request)
3842
tally = CiphertextTally("election-results", description, context)
3943

40-
return _tally_ballots(tally, ballots)
44+
return _tally_ballots(tally, ballots, scheduler)
4145

4246

4347
@router.post("/append", tags=[TALLY])
44-
def append_to_tally(request: AppendTallyRequest = Body(...)) -> Any:
48+
def append_to_tally(
49+
request: AppendTallyRequest = Body(...),
50+
scheduler: Scheduler = Depends(get_scheduler),
51+
) -> Any:
4552
"""
4653
Append ballots into an existing tally
4754
"""
4855

4956
ballots, description, context = _parse_tally_request(request)
5057
tally = convert_tally(request.encrypted_tally, description, context)
5158

52-
return _tally_ballots(tally, ballots)
59+
return _tally_ballots(tally, ballots, scheduler)
5360

5461

5562
@router.post("/decrypt", tags=[TALLY])
@@ -100,12 +107,14 @@ def _parse_tally_request(
100107

101108

102109
def _tally_ballots(
103-
tally: CiphertextTally, ballots: List[CiphertextAcceptedBallot]
110+
tally: CiphertextTally,
111+
ballots: List[CiphertextAcceptedBallot],
112+
scheduler: Scheduler,
104113
) -> Any:
105114
"""
106115
Append a series of ballots to a new or existing tally
107116
"""
108-
tally_succeeded = tally.batch_append(ballots)
117+
tally_succeeded = tally.batch_append(ballots, scheduler)
109118

110119
if tally_succeeded:
111120
published_tally = publish_ciphertext_tally(tally)

app/core/scheduler.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from functools import lru_cache
2+
from electionguard.scheduler import Scheduler
3+
4+
__all__ = ["get_scheduler"]
5+
6+
7+
@lru_cache
8+
def get_scheduler() -> Scheduler:
9+
return Scheduler()

app/main.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from app.api.v1.routes import get_routes
77
from app.core.config import Settings
8+
from app.core.scheduler import get_scheduler
89

910
logger = getLogger(__name__)
1011

@@ -37,6 +38,14 @@ def get_app(settings: Optional[Settings] = None) -> FastAPI:
3738

3839
app = get_app()
3940

41+
42+
@app.on_event("shutdown")
43+
def on_shutdown() -> None:
44+
# Ensure a clean shutdown of the singleton Scheduler
45+
scheduler = get_scheduler()
46+
scheduler.close()
47+
48+
4049
if __name__ == "__main__":
4150
# IMPORTANT: This should only be used to debug the application.
4251
# For normal execution, run `make start`.

docker-compose.dev.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,23 @@ services:
99
context: .
1010
target: dev
1111
volumes:
12-
- "./app:/app"
12+
- "./app:/app/app"
1313
ports:
1414
- 8000:8000
1515
environment:
1616
API_MODE: "mediator"
1717
PROJECT_NAME: "ElectionGuard Mediator API"
18-
port: 8000
18+
PORT: 8000
1919

2020
guardian:
2121
build:
2222
context: .
2323
target: dev
2424
volumes:
25-
- "./app:/app"
25+
- "./app:/app/app"
2626
ports:
2727
- 8001:8001
2828
environment:
2929
API_MODE: "guardian"
3030
PROJECT_NAME: "ElectionGuard Guardian API"
31-
port: 8001
31+
PORT: 8001

docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ services:
99
environment:
1010
API_MODE: "mediator"
1111
PROJECT_NAME: "ElectionGuard Mediator API"
12-
port: 8000
12+
PORT: 8000
1313

1414
guardian:
1515
build:
@@ -20,4 +20,4 @@ services:
2020
environment:
2121
API_MODE: "guardian"
2222
PROJECT_NAME: "ElectionGuard Guardian API"
23-
port: 8001
23+
PORT: 8001

tests/integration/conftest.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from typing import Generator
2+
import pytest
3+
from app.core.scheduler import get_scheduler
4+
5+
6+
@pytest.yield_fixture(scope="session", autouse=True)
7+
def scheduler_lifespan() -> Generator[None, None, None]:
8+
"""
9+
Ensure that the global scheduler singleton is
10+
torn down when tests finish. Otherwise, the test runner will hang
11+
waiting for the scheduler to complete.
12+
"""
13+
yield None
14+
scheduler = get_scheduler()
15+
scheduler.close()

tests/postman/docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ services:
1818
environment:
1919
API_MODE: "mediator"
2020
PROJECT_NAME: "ElectionGuard Mediator API"
21-
port: 80
21+
PORT: 80
2222

2323
guardian:
2424
build:
@@ -29,7 +29,7 @@ services:
2929
environment:
3030
API_MODE: "guardian"
3131
PROJECT_NAME: "ElectionGuard Guardian API"
32-
port: 80
32+
PORT: 80
3333

3434
test-runner:
3535
build:

0 commit comments

Comments
 (0)