diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61c774d..b788729 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,11 +1,34 @@ name: CI on: [push, pull_request] +permissions: + contents: read|write-all jobs: - test: + populate-cache: runs-on: ubuntu-latest + timeout-minutes: 60 + name: Update docker cache + steps: + - uses: actions/checkout@v4 + - name: Cache docker images + id: custom-cache + uses: actions/cache@v4 + with: + path: ./custom-cache/ + key: custom-cache + - if: ${{ steps.custom-cache.outputs.cache-hit != 'true' || github.event_name == 'schedule' }} + name: Update cache + run: | + mkdir -p ./custom-cache/ + docker compose --profile all build + docker pull valkey/valkey:latest + docker save django-valkey-cluster:latest valkey/valkey:latest -o ./custom-cache/all.tar + test: + runs-on: ubuntu-latest + needs: [populate-cache] + timeout-minutes: 60 strategy: fail-fast: false matrix: @@ -13,43 +36,13 @@ jobs: - '3.10' - '3.11' - '3.12' - # - '3.13' +# - '3.13' django-version: - '4.2' - '5.0' - '5.1' - valkey-version: - - 'latest' - - '7.2' - - services: - valkey: - image: valkey/valkey:${{ matrix.valkey-version }} - ports: - - 6379:6379 - options: >- - --health-cmd "valkey-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - --volume /tmp:/tmp - env: - VALKEY_EXTRA_FLAGS: '--save "" --unixsocket /tmp/valkey.sock --unixsocketperm 777' - - sentinel: - image: bitnami/valkey-sentinel:${{ matrix.valkey-version }} - ports: - - 26379:26379 - options: >- - --health-cmd "valkey-cli -p 26379 ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - --volume /tmp:/tmp - env: - VALKEY_SENTINEL_QUORUM: "1" - VALKEY_SENTINEL_AOF_ENABLED: "no" - VALKEY_PRIMARY_SET: "mymaster" + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: true steps: - uses: actions/checkout@v4 @@ -57,6 +50,15 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Cache docker images + id: custom-cache + uses: actions/cache@v4 + with: + path: ./custom-cache/ + fail-on-cache-miss: true + key: custom-cache + - name: Use Cache + run: docker image load -i ./custom-cache/all.tar # - name: Cache # id: cached-poetry @@ -84,13 +86,9 @@ jobs: - name: tests run: | - VALKEY_PRIMARY=$(tests/start_valkey.sh) - VALKEY_SENTINEL=$(tests/start_valkey.sh --sentinel) - CONTAINERS="$VALKEY_PRIMARY $VALKEY_SENTINEL" - trap "docker stop $CONTAINERS && docker rm $CONTAINERS" EXIT - tests/wait_for_valkey.sh $VALKEY_PRIMARY 6379 - tests/wait_for_valkey.sh $VALKEY_SENTINEL 26379 - + poetry run invoke devenv + chmod +x ./util/wait-for-it.sh + ./util/wait-for-it.sh localhost:6379 poetry run pytest tests/*.py --ds=tests.settings.sqlite poetry run pytest tests/*.py --ds=tests.settings.sqlite_herd @@ -110,4 +108,3 @@ jobs: env: DJANGO: ${{ matrix.django-version }} - VALKEY: ${{ matrix.valkey-version }} diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..0d0340e --- /dev/null +++ b/compose.yaml @@ -0,0 +1,49 @@ + + +--- + +services: + + valkey: + image: valkey/valkey:latest + container_name: valkey-standalone + ports: + - 6379:6379 + entrypoint: '/usr/local/bin/docker-entrypoint.sh --enable-debug-command yes --enable-module-command yes --save "" --unixsocket /tmp/valkey.sock --unixsocketperm 777' + profiles: + - standalone + - sentinel + - replica + - all + + cluster: + container_name: valkey-cluster + build: + context: . + dockerfile: dockers/Dockerfile.cluster + ports: + - 16379:16379 + - 16380:16380 + - 16381:16381 + - 16382:16382 + - 16383:16383 + - 16384:16384 + volumes: + - "./dockers/cluster.valkey.conf:/valkey.conf:ro" + profiles: + - cluster + - all + + sentinel: + image: valkey/valkey:latest + container_name: valkey-sentinel + depends_on: + - valkey + entrypoint: "/usr/local/bin/valkey-sentinel /valkey.conf --port 26379" + ports: + - 26379:26379 + volumes: + - "./dockers/sentinel.conf:/valkey.conf" + profiles: + - sentinel + - all diff --git a/dockers/Dockerfile.cluster b/dockers/Dockerfile.cluster new file mode 100644 index 0000000..414b1a1 --- /dev/null +++ b/dockers/Dockerfile.cluster @@ -0,0 +1,6 @@ +FROM valkey/valkey:latest as rss + +COPY dockers/create_cluster.sh /create_cluster.sh +RUN chmod a+x /create_cluster.sh + +ENTRYPOINT [ "/create_cluster.sh"] \ No newline at end of file diff --git a/dockers/cluster.valkey.conf b/dockers/cluster.valkey.conf new file mode 100644 index 0000000..06cf941 --- /dev/null +++ b/dockers/cluster.valkey.conf @@ -0,0 +1,2 @@ +protected-mode no +enable-debug-command yes \ No newline at end of file diff --git a/dockers/create_cluster.sh b/dockers/create_cluster.sh new file mode 100644 index 0000000..99aaf50 --- /dev/null +++ b/dockers/create_cluster.sh @@ -0,0 +1,47 @@ +#! /bin/bash + +mkdir -p /nodes +touch /nodes/nodemap +if [ -z ${START_PORT} ]; then + START_PORT=16379 +fi +if [ -z ${END_PORT} ]; then + END_PORT=16384 +fi +if [ ! -z "$3" ]; then + START_PORT=$2 + START_PORT=$3 +fi +echo "STARTING: ${START_PORT}" +echo "ENDING: ${END_PORT}" + +for PORT in `seq ${START_PORT} ${END_PORT}`; do + mkdir -p /nodes/$PORT + if [[ -e /valkey.conf ]]; then + cp /valkey.conf /nodes/$PORT/valkey.conf + else + touch /nodes/$PORT/valkey.conf + fi + cat << EOF >> /nodes/$PORT/valkey.conf +port ${PORT} +cluster-enabled yes +daemonize yes +logfile /valkey.log +dir /nodes/$PORT +EOF + + set -x + /usr/local/bin/valkey-server /nodes/$PORT/valkey.conf + sleep 1 + if [ $? -ne 0 ]; then + echo "Valkey failed to start, exiting." + continue + fi + echo 127.0.0.1:$PORT >> /nodes/nodemap +done +if [ -z "${VALKEY_PASSWORD}" ]; then + echo yes | /usr/local/bin/valkey-cli --cluster create `seq -f 127.0.0.1:%g ${START_PORT} ${END_PORT}` --cluster-replicas 1 +else + echo yes | /usr/local/bin/valkey-cli -a ${VALKEY_PASSWORD} --cluster create `seq -f 127.0.0.1:%g ${START_PORT} ${END_PORT}` --cluster-replicas 1 +fi +tail -f /valkey.log \ No newline at end of file diff --git a/dockers/sentinel.conf b/dockers/sentinel.conf new file mode 100644 index 0000000..c199a8a --- /dev/null +++ b/dockers/sentinel.conf @@ -0,0 +1,5 @@ +sentinel resolve-hostnames yes +sentinel monitor mymaster valkey 6379 1 +sentinel down-after-milliseconds mymaster 5000 +sentinel failover-timeout mymaster 60000 +sentinel parallel-syncs mymaster 1 \ No newline at end of file diff --git a/util/wait-for-it.sh b/util/wait-for-it.sh new file mode 100644 index 0000000..6bf4fff --- /dev/null +++ b/util/wait-for-it.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available +# +# Copyright (c) 2016 Giles Hall +# The MIT License (MIT) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +# of the Software, and to permit persons to whom the Software is furnished to do +# so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi