diff --git a/.gitignore b/.gitignore index 82f9275..51f7c52 100644 --- a/.gitignore +++ b/.gitignore @@ -123,6 +123,7 @@ celerybeat.pid # Environments .env +.env.shared .venv env/ venv/ diff --git a/.vscode/settings.json b/.vscode/settings.json index fdbc9ea..fee4936 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "conventionalCommits.scopes": ["bot", "db"] + "conventionalCommits.scopes": ["bot", "db"], + "yaml.schemas": { + "kubernetes://schema/apps/v1%40deployment": "file:///c%3A/Users/rafab/Documents/Repos/Payphone-1/k8s.yml" + } } diff --git a/Dockerfile b/Dockerfile index c6244bf..df17e8c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,4 +8,3 @@ RUN uv sync --no-dev RUN uv add uvloop ENTRYPOINT [ "/bin/bash" ] -CMD [ "run.sh" ] diff --git a/README.md b/README.md index 1d3e1df..d3b44be 100644 --- a/README.md +++ b/README.md @@ -1 +1,8 @@ -# Payphone \ No newline at end of file +# Payphone + +Payphone is deployed using Kubernetes. To set up a local kubernetes cluster, you can use `minikube`. + +```sh +# To create a local cluster of 3 nodes: +minikube start --network-plugin=cni --cni=calico --nodes=3 +``` \ No newline at end of file diff --git a/compose.yml b/compose.yml index 7d7e8f8..49b796d 100644 --- a/compose.yml +++ b/compose.yml @@ -10,6 +10,7 @@ services: depends_on: - database - redis + command: run.sh restart: always database: diff --git a/k8s/bot.yml b/k8s/bot.yml new file mode 100644 index 0000000..ca266ec --- /dev/null +++ b/k8s/bot.yml @@ -0,0 +1,40 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: discord-bot + labels: + app: discord-bot +spec: + replicas: 11 + selector: + matchLabels: + app: discord-bot + template: + metadata: + labels: + app: discord-bot + spec: + containers: + - name: bot + image: nekidev/payphone:latest + env: + - name: IS_DEBUG + value: "false" + - name: SHARD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: TOTAL_SHARDS + value: "11" + - name: REDIS_HOST + value: "redis" + - name: REDIS_PORT + value: "6379" + resources: + limits: + cpu: "500m" + memory: "512Mi" + requests: + cpu: "250m" + memory: "256Mi" + restartPolicy: Always diff --git a/k8s/citus-coordinator.yml b/k8s/citus-coordinator.yml new file mode 100644 index 0000000..e8a24c5 --- /dev/null +++ b/k8s/citus-coordinator.yml @@ -0,0 +1,57 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: citus-coordinator +spec: + replicas: 1 + selector: + matchLabels: + app: citus + role: coordinator + template: + metadata: + labels: + app: citus + role: coordinator + spec: + containers: + - name: coordinator + image: citusdata/citus:latest + env: + - name: POSTGRES_DB + value: "payphone" + - name: POSTGRES_USER + value: "payphone" + - name: POSTGRES_PASSWORD + value: "payphone" + volumeMounts: + - name: citus-coordinator-storage + mountPath: /var/lib/postgresql/data + ports: + - containerPort: 5432 + resources: + requests: + memory: "2Gi" + cpu: "1" + limits: + memory: "2Gi" + cpu: "2" + volumes: + - name: citus-coordinator-storage + persistentVolumeClaim: + claimName: citus-coordinator-pvc + +--- +apiVersion: v1 +kind: Service +metadata: + name: citus-coordinator +spec: + selector: + app: citus + role: coordinator + ports: + - protocol: TCP + port: 5432 + targetPort: 5432 + type: ClusterIP diff --git a/k8s/citus-pvc.yml b/k8s/citus-pvc.yml new file mode 100644 index 0000000..26f859c --- /dev/null +++ b/k8s/citus-pvc.yml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: citus-coordinator-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi # Change this size as needed + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: citus-worker-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi # Change this size as needed diff --git a/k8s/citus-worker.yml b/k8s/citus-worker.yml new file mode 100644 index 0000000..068973b --- /dev/null +++ b/k8s/citus-worker.yml @@ -0,0 +1,63 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: citus-worker +spec: + replicas: 2 # Increase as needed for more workers + selector: + matchLabels: + app: citus + role: worker + serviceName: citus-worker-headless + template: + metadata: + labels: + app: citus + role: worker + spec: + containers: + - name: worker + image: citusdata/citus:latest + env: + - name: POSTGRES_DB + value: "payphone" + - name: POSTGRES_USER + value: "payphone" + - name: POSTGRES_PASSWORD + value: "payphone" + ports: + - containerPort: 5432 + resources: + requests: + memory: "2Gi" + cpu: "1" + limits: + memory: "2Gi" + cpu: "2" + volumeMounts: + - name: citus-worker-storage + mountPath: /var/lib/postgresql/data + volumeClaimTemplates: + - metadata: + name: citus-worker-storage + spec: + accessModes: [ "ReadWriteOnce" ] + storageClassName: "standard" # Update based on your storage class + resources: + requests: + storage: 5Gi # Size per worker + +--- +apiVersion: v1 +kind: Service +metadata: + name: citus-worker-headless +spec: + selector: + app: citus + role: worker + ports: + - protocol: TCP + port: 5432 + targetPort: 5432 + clusterIP: None # Defines the service as headless, allowing direct pod DNS entries diff --git a/k8s/redis.yml b/k8s/redis.yml new file mode 100644 index 0000000..d052769 --- /dev/null +++ b/k8s/redis.yml @@ -0,0 +1,40 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:7.4.1-alpine + ports: + - containerPort: 6379 + resources: + requests: + memory: "1Gi" + cpu: "1" + limits: + memory: "2Gi" + cpu: "1" + +--- +apiVersion: v1 +kind: Service +metadata: + name: redis +spec: + selector: + app: redis + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 + type: ClusterIP diff --git a/node-ips.txt b/node-ips.txt new file mode 100644 index 0000000..5d99aa5 --- /dev/null +++ b/node-ips.txt @@ -0,0 +1,2 @@ +45.134.39.59 +2.56.246.33 diff --git a/payphone/bot.py b/payphone/bot.py index f93b215..ed0f261 100644 --- a/payphone/bot.py +++ b/payphone/bot.py @@ -28,9 +28,6 @@ from payphone.core import ( ) -logging.setup() - - class Payphone(commands.AutoShardedBot): shutdown: managers.ValueProxy[bool] """A value proxy that connects with all other shard processes. @@ -74,11 +71,34 @@ class Payphone(commands.AutoShardedBot): ) async def on_ready(self) -> None: - """Runs when the bot has finished initializing. - """ + """Runs when the bot has finished initializing.""" self.log.info("Ready! Logged in as %s.", self.user.display_name) + async def set_presence(self, shard_id: int) -> None: + """Sets the presence of the bot back to online. + + Args: + shard_id (int): The ID of the shard to set the status for. + """ + + await self.change_presence( + activity=discord.CustomActivity( + name=f"Listening to p.help | Shard {shard_id + 1}" + ), + status=discord.Status.online, + shard_id=shard_id, + ) + + async def on_shard_connect(self, shard_id: int) -> None: + """Runs when the shard connects. + + Args: + shard_id (int): The ID of the shard that connected. + """ + + self.log.info("Shard has connected.") + async def on_shard_ready(self, shard_id: int) -> None: """Sets the activity for the shard. @@ -86,15 +106,9 @@ class Payphone(commands.AutoShardedBot): shard_id (int): The ID of the initialized shard. """ - await self.change_presence( - activity=discord.CustomActivity( - name=f"Listening to p.help | Shard {shard_id + 1}" - ), - status=discord.Status.online, - shard_id=shard_id, - ) + self.log.info("Shard is ready.") - self.log.info("Shard %s is ready!", shard_id) + await self.set_presence(shard_id) async def on_shard_resumed(self, shard_id: int) -> None: """Sets the activity for the shard. @@ -103,15 +117,9 @@ class Payphone(commands.AutoShardedBot): shard_id (int): The ID of the resumed shard. """ - await self.change_presence( - activity=discord.CustomActivity( - name=f"Listening to p.help | Shard {shard_id + 1}" - ), - status=discord.Status.online, - shard_id=shard_id, - ) + self.log.info("Shard has resumed.") - self.log.info("Shard %s was resumed!", shard_id) + await self.set_presence(shard_id) async def get_or_fetch_channel( self, channel_id: int diff --git a/payphone/core/logging.py b/payphone/core/logging.py index c3785fc..ae117f7 100644 --- a/payphone/core/logging.py +++ b/payphone/core/logging.py @@ -3,17 +3,36 @@ import logging from rich import logging as rlogging -FORMAT = "%(message)s" - -handler = rlogging.RichHandler(level=logging.INFO, rich_tracebacks=True) - -formatter = logging.Formatter(FORMAT) -handler.setFormatter(formatter) - logger = logging.getLogger("payphone") -def setup(): - """Sets up pycord logging.""" +def _get_handler(process_name: str, module: str | None = None) -> None: + """Generates a handler for the given process name and module. - logging.basicConfig(level=logging.DEBUG, handlers=[handler]) + Args: + process_name (str): The process name. + module (str | None, optional): The module inside the process. Defaults to None. + """ + + fmt = f"[blue][{process_name}{f": [green]{module}[/green]" if module is not None else ""}][/blue] %(message)s" + formatter = logging.Formatter(fmt) + + handler = rlogging.RichHandler( + level=logging.INFO, markup=True, rich_tracebacks=True + ) + handler.setFormatter(formatter) + + return handler + + +def setup(process_name: str): + """Sets up pycord logging. + + Args: + process_name (str): The name of the process. + """ + + logging.basicConfig(level=logging.DEBUG, handlers=[_get_handler(process_name)]) + + logging.getLogger("asyncio").addHandler(_get_handler(process_name, "asyncio")) + logging.getLogger("discord").addHandler(_get_handler(process_name, "discord")) diff --git a/procs/flush_messages.py b/procs/flush_messages.py index e067605..26b8a7e 100644 --- a/procs/flush_messages.py +++ b/procs/flush_messages.py @@ -117,9 +117,12 @@ async def loop() -> None: import asyncio from payphone.db import setup + from payphone.core import logging await setup.init() + logging.setup("Flushing") + try: while True: await asyncio.sleep(30) diff --git a/pyproject.toml b/pyproject.toml index 38dc137..2b7da20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,6 @@ requires-python = ">=3.12" dependencies = [ "aerich==0.7.1", "aiohttp>=3.10.10", - "click>=8.1.7", "colorthief>=0.2.1", "httpx>=0.27.2", "orjson>=3.10.7", diff --git a/run.py b/run.py index 628773f..b4ed4aa 100644 --- a/run.py +++ b/run.py @@ -5,8 +5,6 @@ from datetime import datetime, timezone from payphone.core import logging -logging.setup() - log = logging.logger @@ -28,13 +26,15 @@ def shard( from payphone.db import setup from payphone.bot import bot - from payphone.core import config + from payphone.core import config, logging bot.shard_ids = [shard_id] bot.shard_count = shard_count bot.shutdown = shutdown bot.scheduled_shutdown = scheduled_shutdown + logging.setup(f"Shard {shard_id}") + log.info("Running shard %s with default loop.", shard_id) bot.loop.set_debug(True) @@ -84,6 +84,8 @@ if __name__ == "__main__": from payphone.db import setup, models from payphone.core import config + logging.setup("Payphone") + redis = aredis.Redis(host="localhost" if config.IS_DEBUG else "redis") async def init(): diff --git a/uv.lock b/uv.lock index a614cef..ca4ab84 100644 --- a/uv.lock +++ b/uv.lock @@ -392,7 +392,6 @@ source = { virtual = "." } dependencies = [ { name = "aerich" }, { name = "aiohttp" }, - { name = "click" }, { name = "colorthief" }, { name = "httpx" }, { name = "orjson" }, @@ -407,7 +406,6 @@ dependencies = [ requires-dist = [ { name = "aerich", specifier = "==0.7.1" }, { name = "aiohttp", specifier = ">=3.10.10" }, - { name = "click", specifier = ">=8.1.7" }, { name = "colorthief", specifier = ">=0.2.1" }, { name = "httpx", specifier = ">=0.27.2" }, { name = "orjson", specifier = ">=3.10.7" },