Fordelen med å skrive et kommandotolket (interpreted) språk som Python
er at man slipper å kompilere koden mellom hver endring. Dette gjør at man i praksis kan se endringene i applikasjonen man utvikler med en gang. De fleste rammeverk har som regel også støtte for denne typen hot-reload
fra før — hurra! 🎉
Likevel, det finnes unntak som Celery
hvor dette må håndteres selv. Og hva med egne applikasjoner, rammeverk og skript? Jo, her står vi på egne ben.
Heldigvis finnes det allerede løsninger for oppdagelse av filendringer og restart av applikasjon i Python
, nemlig watchfiles
. Denne pakken bruker i bunn rust
som gjør at vi slipper kollisjon av tråder i Python
. Pakken vedlikeholdes av @samuelcolvin, mannen bak godbiter som pydantic
og arq
.
Implementasjon 🔗
watchfiles
har et minimalt grensesnitt som tillater både lytting etter endringer i kode og CLI for kjøring og restart av kommandoer/skript. Ettersom formålet er å kunne restarte en applikasjon inni en docker
-container, vil CLI-varianten være egnet løsning her.
All kode er forresten tilgjengelig på repoet endrekrohn/hot-reload-python-app 😎
Klargjøring av docker-compose.yml
🔗
Vi tar utgangspunkt i denne docker-compose
-filen med en enkelt applikasjon:
services:
application:
restart: always
init: true
build:
context: ./application
args:
INSTALL_DEV: ${INSTALL_DEV-false}
Videre vet jeg at jeg kun ønsker hot-reloading
i utviklingsmiljøet av applikasjonen. Jeg oppretter derfor følgende docker-compose.override.yml
.
services:
application:
command: ./start-reload.sh
volumes:
- ./application/code:/application/code
build:
args:
INSTALL_DEV: ${INSTALL_DEV-true}
Her er det tre ting å legge merke til:
- Kommandoen er satt til et spesielt
start-reload.sh
-skript som jeg senere skal definere. - Mitt lokale volum er mappet til volum i container slik at endringer gjenspeiles.
- Flagget
INSTALL_DEV
er satt slik at utvikleravhengigheter innstalleres.
Installasjon av watchfiles
i container 🔗
I og med at jeg bruker poetry
som pakkesjef kan jeg innstallere watchfiles
som utvikleravhengighet med følgende kommando:
poetry add -D watchfiles
Med flagget INSTALL_DEV
satt til true
vil da min dockerfile
innstallere denne pakken.
# Allow installing dev dependencies to run tests
ARG INSTALL_DEV=false
RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry export -f requirements.txt --without-hashes -o requirements.txt --dev ; else poetry export -f requirements.txt --without-hashes -o requirements.txt ; fi"
Jeg anbefaler å ta en titt på hvordan denne
dockerfilen
er satt opp for å skille avhengigheter. Kanskje kunne det også vært aktuelt med et eget innlegg om denne filen 🤔
Opprette start-reload.sh
-skript 🔗
Deretter må jeg definere skriptet som kjøres i docker-compose.override.yml
. Dette skriptet bruker watchfiles
sitt CLI-grensesnitt til å definere inngangsskriptet, samt hvilke(n) mapper den skal lytte til.
#! /usr/bin/env bash
watchfiles 'python ./code/main.py' ./code
Demonstrasjon 🔗
Til slutt kan vi kjøre opp applikasjonen med docker-compose
, gjøre noen endringer og se resultatet med en gang.
Til slutt 🔗
Avslutningsvis vil jeg påpeke en obs å være klar over.
Dersom du skulle bruke multiprocessing
-pakken i pythons
standardbibliotek er det viktig å avslutte prosessene.
Ved prosesser som bruker uviss tid bør du derfor bruke en event
som håndterer eventuelle while
-loops du måtte ha.
from multiprocessing.synchronize import Event
def some_task(shutdown: Event) -> None:
while not shutdown.is_set():
do_work()
Om du ikke avslutter disse gracefully vil det etter en viss tid sendes et signal om å drepe hele prosessen 💀. Tiden det tar før watchfiles
sender signal kan justeres med flaggene: --sigint-timeout
og --sigkill-timeout
.
Dette gjelder for øvrig alle koblinger du måtte ha åpen, eksempelvis til en database eller fil.
Docker sender også
SIGINT
på samme måte somwatchfiles
. Bruk avhot-reloading
ogwatchfiles
kan dermed forebygge eventuelle smutthull du skulle ha i din docker-applikasjon.