Skip to content
Open
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
37 changes: 26 additions & 11 deletions .github/workflows/cloud-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,18 @@ jobs:
ls -al image.tar*


- name: Prepare SSH Dir
- name: Prepare SSH dir
run: |
mkdir -pv ~/.ssh/

- name: Write Key
- name: Write key
env:
EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }}
run: |
echo "$EC2_SSH_KEY" > ~/.ssh/actions.key
chmod 600 ~/.ssh/actions.key

- name: Write SSH Config
- name: Write SSH config
env:
EC2_HOST: ${{ secrets.EC2_HOST }}
EC2_USERNAME: ${{ secrets.EC2_USERNAME }}
Expand All @@ -63,21 +63,36 @@ jobs:
END

- name: Copy Docker image to EC2 instance
env:
EC2_USERNAME: ${{ secrets.EC2_USERNAME }}
run: |
scp image.tar.gz ec2:/home/ubuntu
scp image.tar.gz ec2:/home/$EC2_USERNAME

- name: Stop Running Container
- name: Ensure Docker is installed and running
run: |
ssh ec2 'sudo snap install docker'
ssh ec2 'sudo systemctl start docker'

- name: Stop running container
run: |
ssh ec2 'docker ps -q | xargs --no-run-if-empty docker stop | xargs --no-run-if-empty docker rm'
ssh ec2 'sudo docker ps -q | xargs --no-run-if-empty docker stop | xargs --no-run-if-empty docker rm'

- name: Configure Docker & Load Image
- name: Configure Docker & load lmage
env:
EC2_USERNAME: ${{ secrets.EC2_USERNAME }}
run: |
ssh ec2 "docker load -i /home/ubuntu/image.tar.gz"
ssh ec2 "sudo docker load -i /home/$EC2_USERNAME/image.tar.gz"

- name: Delete Gzipped Image Tarball
- name: Delete gzipped image tarball
env:
EC2_USERNAME: ${{ secrets.EC2_USERNAME }}
run: |
ssh ec2 "rm -f /home/ubuntu/image.tar.gz"
ssh ec2 "rm -f /home/$EC2_USERNAME/image.tar.gz"

- name: Run Docker container on EC2 instance
run: |
ssh ec2 "docker run -p 80:80 -d ramanujan-machine-web-portal:latest"
ssh ec2 "sudo docker run -p 80:80 -d ramanujan-machine-web-portal:latest"

- name: Cleanup Docker detritus
run: |
ssh ec2 "sudo docker system prune -f"
24 changes: 14 additions & 10 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ RUN apt-get -y install curl gnupg
RUN apt-get -y install libpq-dev libgmp-dev libgmp3-dev libmpfr-dev libmpc-dev
RUN apt-get install -y npm
RUN npm i -g n && n lts && npm i -g npm@latest
RUN pip install --upgrade protobuf grpcio grpcio-tools --break-system-packages

# React frontend
COPY react-frontend ./react-frontend
Expand All @@ -30,36 +31,39 @@ RUN npm run build
# create virtual environment for FastAPI web server and install dependencies
WORKDIR /srv/ramanujan-machine-web-portal
ENV BKPATH=$PATH
COPY protos ./protos
COPY python-backend ./python-backend
RUN cp -rf react-frontend/build python-backend/build
WORKDIR /srv/ramanujan-machine-web-portal/python-backend
RUN --mount=type=secret,id=creds cat /run/secrets/creds >> .env
COPY .creds .creds
RUN --mount=type=secret,id=creds cat .creds >> .env
RUN rm -rf venv
RUN python3 -m venv venv
# . is equivalent to source in sh
RUN . venv/bin/activate
RUN ./venv/bin/python -m pip install -r /srv/ramanujan-machine-web-portal/python-backend/requirements.txt
RUN ./venv/bin/python -m pip install --upgrade protobuf grpcio grpcio-tools --break-system-packages
# generate gRPC code for web server
RUN ./venv/bin/python -m grpc_tools.protoc --proto_path=/srv/ramanujan-machine-web-portal/protos --python_out=/srv/ramanujan-machine-web-portal/python-backend/ --grpc_python_out=/srv/ramanujan-machine-web-portal/python-backend/ /srv/ramanujan-machine-web-portal/protos/lirec.proto
ENV PATH=$BKPATH
RUN unset VIRTUAL_ENV

# create virtual environment for gRPC server and install dependencies
WORKDIR /srv/ramanujan-machine-web-portal
COPY protos ./protos
COPY lirec-grpc-server ./lirec-grpc-server
WORKDIR /srv/ramanujan-machine-web-portal/lirec-grpc-server
RUN rm -rf venv
RUN python3 -m venv venv
# . is equivalent to source in sh
RUN . venv/bin/activate
RUN venv/bin/python -m pip install -r requirements.txt
RUN ./venv/bin/python -m pip install -r /srv/ramanujan-machine-web-portal/lirec-grpc-server/requirements.txt
RUN ./venv/bin/python -m pip install --upgrade protobuf grpcio grpcio-tools --break-system-packages
# generate gRPC code for gRPC server directories
RUN ./venv/bin/python -m grpc_tools.protoc --proto_path=/srv/ramanujan-machine-web-portal/protos --python_out=/srv/ramanujan-machine-web-portal/lirec-grpc-server/ --grpc_python_out=/srv/ramanujan-machine-web-portal/lirec-grpc-server/ /srv/ramanujan-machine-web-portal/protos/lirec.proto
ENV PATH=$BKPATH
RUN unset VIRTUAL_ENV

# generate gRPC code in both client (web server) and gRPC server directories
WORKDIR /srv/ramanujan-machine-web-portal
RUN pip install grpcio grpcio-tools --break-system-packages
WORKDIR /srv/ramanujan-machine-web-portal/python-backend
RUN python3 -m grpc_tools.protoc --proto_path=/srv/ramanujan-machine-web-portal/protos --python_out=/srv/ramanujan-machine-web-portal/python-backend/ --grpc_python_out=/srv/ramanujan-machine-web-portal/python-backend/ /srv/ramanujan-machine-web-portal/protos/lirec.proto
WORKDIR /srv/ramanujan-machine-web-portal/lirec-grpc-server
RUN python3 -m grpc_tools.protoc --proto_path=/srv/ramanujan-machine-web-portal/protos --python_out=/srv/ramanujan-machine-web-portal/lirec-grpc-server/ --grpc_python_out=/srv/ramanujan-machine-web-portal/lirec-grpc-server/ /srv/ramanujan-machine-web-portal/protos/lirec.proto


# expose web port
EXPOSE 80
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ You will need to replace `XXXXXX-XXXXXXXXXX` with a Wolfram App Id, which you ca
Note that the first port is the port you can access via your web browser, e.g. `http://localhost:80` is where you would be able to interact with the app given the above configuration. You can change `80` to whatever port you wish, and the frontend of the web application runs on port `80` inside the container.

## Run Frontend and Backend Locally without Docker
Comprehensive developer documentation is available in a separate [README](./docs/DEVELOPER.md). Short versions of the instructions for each part of the web app can be found in READMEs in each subdirectory:
- Refer to [React Frontend README](./react-frontend/README.md) to run the web interface locally.
- Refer to [Python Backend README](./python-backend/README.md) to run the API locally.
- Refer to [Python gRPC Server README](./lirec-grpc-server/README.md) to run the gRPC server locally.
4 changes: 2 additions & 2 deletions docker_start.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh

# Start gRPC server
cd lirec-grpc-server
Expand All @@ -14,7 +14,7 @@ uvicorn main:app --host "0.0.0.0" --port 80 &
deactivate

# Wait for any process to exit
wait -n
wait

# Exit with status of process that exited first
exit $?
2 changes: 2 additions & 0 deletions docs/DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ The web portal was developed by the [Virtual Institute for Scientific Software](

The repository comprises a complete web application, including web server code, React UI code and an automated deployment in GitHub Actions workflows that builds a Docker container and deploys it to a cloud server, currently AWS EC2.

![architecture.png](architecture.png)

### 1.2. Technologies Used

The web portal was written primarily in [Typescript](https://www.typescriptlang.org/docs/handbook/2/basic-types.html) and [Python](https://docs.python.org/3/tutorial/index.html).
Expand Down
Binary file added docs/architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 8 additions & 8 deletions lirec-grpc-server/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
numpy==1.26.4
setuptools==65.5.1
packaging==24.0
logger==1.4
## The following requirements were added by pip freeze:
gmpy2==2.2.0a1
grpcio>=1.64.0
grpcio-tools>=1.64.0
LIReC @ git+https://github.com/RamanujanMachine/LIReC.git@main
grpcio==1.66.1
grpcio-tools==1.66.1
LIReC @ git+https://github.com/RamanujanMachine/LIReC.git@ba7d9a9e6ec3c1126cacdcb628b4497a5a51be1c
logger==1.4
mpmath==1.3.0
numpy==1.26.4
ordered-set==4.1.0
packaging==24.0
protobuf==5.26.1
psycopg2==2.9.9
PyLaTeX==1.4.2
SQLAlchemy==2.0.30
sympy==1.12
typing_extensions==4.11.0
typing_extensions==4.11.0
12 changes: 7 additions & 5 deletions lirec-grpc-server/server.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from concurrent import futures

import grpc
from LIReC.db.access import db
from LIReC.db.access import db, PolyPSLQRelation

import constants
import lirec_pb2
Expand All @@ -14,10 +14,12 @@
class LIReCServicer(lirec_pb2_grpc.LIReCServicer):
def Identify(self, request: lirec_pb2.IdentifyRequest, context: object) -> lirec_pb2.IdentifyResponse:
logger.debug(f"Received request: <{type(request)}> {request}")
closed_forms = db.identify(values=[request.limit], wide_search=[1], min_prec=constants.DEFAULT_PRECISION)
logger.debug(f"Received response: <{type(closed_forms)}> {[str(item) for item in closed_forms]}")
return lirec_pb2.IdentifyResponse(closed_forms=[str(item) for item in closed_forms])

results = db.identify(values=[request.limit], wide_search=[1], min_prec=constants.DEFAULT_PRECISION, see_also=True)
logger.debug(f"Received response: <{type(results)}> {[str(item) for item in results]}")
if len(results) > 0 and type(results[0]) is list:
return lirec_pb2.IdentifyResponse(closed_forms=[str(item) for item in results[0]], see_also=[str(item) for item in results[1]])
else:
return lirec_pb2.IdentifyResponse(closed_forms=[str(item) for item in results])

def serve() -> None:
"""
Expand Down
2 changes: 1 addition & 1 deletion protos/lirec.proto
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ option objc_class_prefix = "HLW";
package lirec_rpc;

service LIReC {
// Sends a greeting
rpc Identify(IdentifyRequest) returns (IdentifyResponse) {}
}

Expand All @@ -17,4 +16,5 @@ message IdentifyRequest {

message IdentifyResponse {
repeated string closed_forms = 1;
repeated string see_also = 2;
}
41 changes: 23 additions & 18 deletions python-backend/call_wrapper.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,61 @@
import logging
import signal

import grpc
import lirec_pb2
import sympy.core.numbers
from constants import EXTERNAL_PROCESS_TIMEOUT
import lirec_pb2_grpc
from ramanujantools.pcf import PCF
import signal
from constants import EXTERNAL_PROCESS_TIMEOUT

logger = logging.getLogger('rm_web_app')


class TimeoutError(Exception):
class TimeoutException(Exception):
pass


def timeout_handler(signum: object, frame: object) -> None:
"""
Simple handler when execution of functions exceeds time limit specified as EXTERNAL_PROCESS_TIMEOUT
"""
raise TimeoutError("Function execution timed out")

signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(EXTERNAL_PROCESS_TIMEOUT)
def timeout_handler(signum, frame):
raise TimeoutException()


def pcf_limit(a, b, n) -> str:
"""
Invokes ResearchTools limit computation
"""
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(EXTERNAL_PROCESS_TIMEOUT)

try:
pcf = PCF(a, b)
lim = pcf.limit(n)
return lim.as_rounded_number()

except TimeoutError:
logger.error("Function execution timed out after {} seconds".format(EXTERNAL_PROCESS_TIMEOUT))
except TimeoutException:
print(f"PCF limit query exceeded timeout.")
return None
except Exception as e:
logger.error(f"Exception: {e}")
finally:
signal.alarm(0)


def lirec_identify(limit) -> list[sympy.core.numbers.Number]:
def lirec_identify(limit) -> [list[sympy.core.numbers.Number], list[sympy.core.numbers.Number]]:
"""
Invokes LIReC pslq algorithm
"""
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(EXTERNAL_PROCESS_TIMEOUT)

try:
with grpc.insecure_channel('localhost:50051') as channel:
stub = lirec_pb2_grpc.LIReCStub(channel)
request = lirec_pb2.IdentifyRequest(limit=limit)
response = stub.Identify(request)
return response.closed_forms
except TimeoutError:
logger.error("Function execution timed out after {} seconds".format(EXTERNAL_PROCESS_TIMEOUT))
return [response.closed_forms, response.see_also]
except TimeoutException:
print(f"PCF limit query exceeded timeout.")
return None
except Exception as e:
logger.error(f"Exception: {e}")
finally:
signal.alarm(0)
2 changes: 1 addition & 1 deletion python-backend/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
# evalf allows for verbose output via param universally set to this:
VERBOSE_EVAL = False
# when calling ResearchTools, only way this many seconds before throwing a timeout exception
EXTERNAL_PROCESS_TIMEOUT = 10
EXTERNAL_PROCESS_TIMEOUT = 30
# the free Wolfram API limits queries to 200 characters
WOLFRAM_CHAR_LIMIT = 200
Loading