Skip to content
Draft
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
113 changes: 113 additions & 0 deletions .github/workflows/pr_chronon_integration_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
name: pr-chronon-integration-tests

on:
pull_request:
types:
- opened
- synchronize
- labeled
paths:
- "sdk/python/feast/infra/online_stores/chronon_online_store/**"
- "sdk/python/feast/infra/offline_stores/contrib/chronon_offline_store/**"
- "sdk/python/feast/infra/chronon_provider.py"
- "sdk/python/tests/unit/infra/online_stores/chronon_online_store/**"
- "sdk/python/tests/unit/infra/offline_stores/contrib/chronon_offline_store/**"
- "sdk/python/tests/integration/online_store/test_chronon_online_store.py"
- "sdk/python/tests/integration/online_store/test_chronon_online_store_real_service.py"
- "sdk/python/tests/integration/offline_store/test_chronon_offline_store.py"
- "infra/scripts/chronon/**"
- "chronon/**"
- ".github/workflows/pr_chronon_integration_tests.yml"

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
chronon-python-tests:
if:
((github.event.action == 'labeled' && (github.event.label.name == 'approved' || github.event.label.name == 'lgtm' || github.event.label.name == 'ok-to-test')) ||
(github.event.action != 'labeled' && (contains(github.event.pull_request.labels.*.name, 'ok-to-test') || contains(github.event.pull_request.labels.*.name, 'approved') || contains(github.event.pull_request.labels.*.name, 'lgtm')))) &&
github.event.pull_request.base.repo.full_name == 'feast-dev/feast'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
repository: ${{ github.event.repository.full_name }}
ref: ${{ github.ref }}
token: ${{ secrets.GITHUB_TOKEN }}
submodules: recursive
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
architecture: x64
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "8"
- name: Install dependencies
run: make install-python-dependencies-ci
- name: Install Chronon build dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
apt-transport-https \
autoconf \
automake \
bison \
build-essential \
ca-certificates \
curl \
flex \
g++ \
gnupg \
libtool \
pkg-config \
python3-pip \
python3-venv
echo "deb https://repo.scala-sbt.org/scalasbt/debian all main" | sudo tee /etc/apt/sources.list.d/sbt.list
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x99E82A75642AC823" | sudo apt-key add -
sudo apt-get update
sudo apt-get install -y sbt
curl -sSL "http://archive.apache.org/dist/thrift/0.13.0/thrift-0.13.0.tar.gz" -o /tmp/thrift.tar.gz
sudo rm -rf /usr/src/thrift
sudo mkdir -p /usr/src/thrift
sudo tar zxf /tmp/thrift.tar.gz -C /usr/src/thrift --strip-components=1
cd /usr/src/thrift
sudo ./configure --without-python --without-cpp
sudo make -j2
sudo make install
python3 -m pip install --break-system-packages build
thrift -version
- name: Build Chronon quickstart and service jars
run: |
cd chronon/quickstart/mongo-online-impl
sbt assembly
cd "${GITHUB_WORKSPACE}/chronon"
sbt "project service" assembly
- name: Run Chronon unit and stub integration tests
run: |
uv run pytest -c sdk/python/pytest.ini \
sdk/python/tests/unit/infra/online_stores/chronon_online_store \
sdk/python/tests/unit/infra/offline_stores/contrib/chronon_offline_store \
sdk/python/tests/integration/online_store/test_chronon_online_store.py \
sdk/python/tests/integration/offline_store/test_chronon_offline_store.py \
--integration
- name: Start live Chronon service
run: infra/scripts/chronon/start-local-chronon-service.sh
- name: Run Chronon live-service integration test
env:
CHRONON_SERVICE_URL: http://127.0.0.1:9000
run: |
uv run pytest -c sdk/python/pytest.ini \
sdk/python/tests/integration/online_store/test_chronon_online_store_real_service.py \
--integration
- name: Stop live Chronon service
if: always()
run: infra/scripts/chronon/stop-local-chronon-service.sh
144 changes: 144 additions & 0 deletions infra/scripts/chronon/start-local-chronon-service.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env bash

set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
CHRONON_DIR="${ROOT_DIR}/chronon"

MONGO_CONTAINER="${CHRONON_MONGO_CONTAINER:-chronon-mongo}"
MAIN_CONTAINER="${CHRONON_MAIN_CONTAINER:-chronon-main}"
NETWORK_NAME="${CHRONON_NETWORK:-chronon-net}"
SERVICE_PORT="${CHRONON_SERVICE_PORT:-9000}"
SERVICE_HOST="${CHRONON_SERVICE_HOST:-127.0.0.1}"
SERVICE_PID_FILE="${CHRONON_SERVICE_PID_FILE:-/tmp/chronon-service.pid}"
SERVICE_LOG_FILE="${CHRONON_SERVICE_LOG_FILE:-/tmp/chronon-service.log}"
JAVA_BIN="${JAVA_BIN:-java}"

if [[ ! -d "${CHRONON_DIR}" ]]; then
echo "Chronon repo not found at ${CHRONON_DIR}" >&2
exit 1
fi

if [[ ! -f "${CHRONON_DIR}/quickstart/mongo-online-impl/target/scala-2.12/mongo-online-impl-assembly-0.1.0-SNAPSHOT.jar" ]]; then
echo "Missing quickstart Mongo implementation jar. Build chronon/quickstart/mongo-online-impl first." >&2
exit 1
fi

if [[ ! -f "${CHRONON_DIR}/service/target/scala-2.12/service-0.0.111-SNAPSHOT.jar" ]]; then
echo "Missing Chronon service jar. Build chronon service first." >&2
exit 1
fi

cleanup_stale_service() {
if [[ -f "${SERVICE_PID_FILE}" ]]; then
local pid
pid="$(cat "${SERVICE_PID_FILE}")"
if kill -0 "${pid}" >/dev/null 2>&1; then
kill "${pid}" >/dev/null 2>&1 || true
wait "${pid}" 2>/dev/null || true
fi
rm -f "${SERVICE_PID_FILE}"
fi
}

wait_for_mongo() {
local attempts=60
until docker exec "${MONGO_CONTAINER}" mongosh --quiet --eval 'db.runCommand({ ping: 1 }).ok' >/dev/null 2>&1; do
attempts=$((attempts - 1))
if [[ "${attempts}" -le 0 ]]; then
echo "Mongo did not become ready in time." >&2
exit 1
fi
sleep 2
done
}

wait_for_data_load() {
local attempts=90
until docker logs "${MAIN_CONTAINER}" 2>&1 | grep -q "Spark session available as 'spark'"; do
attempts=$((attempts - 1))
if [[ "${attempts}" -le 0 ]]; then
echo "Chronon quickstart data loader did not initialize Spark in time." >&2
exit 1
fi
sleep 2
done
}

wait_for_http() {
local url="$1"
local attempts=60
until curl --fail --silent "${url}" >/dev/null; do
attempts=$((attempts - 1))
if [[ "${attempts}" -le 0 ]]; then
echo "Chronon service did not become ready at ${url}." >&2
exit 1
fi
sleep 2
done
}

run_quickstart_online_prep() {
docker exec "${MAIN_CONTAINER}" bash -lc '
set -euo pipefail
cd /srv/chronon
run.py --conf production/group_bys/quickstart/purchases.v1 --mode upload --ds 2023-12-01
run.py --conf production/group_bys/quickstart/returns.v1 --mode upload --ds 2023-12-01
/opt/spark/bin/spark-submit --class ai.chronon.quickstart.online.Spark2MongoLoader --master local[*] /srv/onlineImpl/target/scala-2.12/mongo-online-impl-assembly-0.1.0-SNAPSHOT.jar default.quickstart_purchases_v1_upload mongodb://admin:admin@'"${MONGO_CONTAINER}"':27017/?authSource=admin # pragma: allowlist secret
/opt/spark/bin/spark-submit --class ai.chronon.quickstart.online.Spark2MongoLoader --master local[*] /srv/onlineImpl/target/scala-2.12/mongo-online-impl-assembly-0.1.0-SNAPSHOT.jar default.quickstart_returns_v1_upload mongodb://admin:admin@'"${MONGO_CONTAINER}"':27017/?authSource=admin # pragma: allowlist secret
run.py --mode metadata-upload --conf production/joins/quickstart/training_set.v2 --ds 2023-12-01
run.py --mode fetch --type join --name quickstart/training_set.v2 -k "{\"user_id\":\"5\"}"
'
}

cleanup_stale_service
(docker network inspect "${NETWORK_NAME}" >/dev/null 2>&1) || docker network create "${NETWORK_NAME}" >/dev/null
(docker rm -f "${MONGO_CONTAINER}" >/dev/null 2>&1) || true
(docker rm -f "${MAIN_CONTAINER}" >/dev/null 2>&1) || true

docker run -d \
--name "${MONGO_CONTAINER}" \
--network "${NETWORK_NAME}" \
-p 27017:27017 \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD=admin \
mongo:latest >/dev/null

wait_for_mongo

docker run -d \
--name "${MAIN_CONTAINER}" \
--network "${NETWORK_NAME}" \
-p 4040:4040 \
-e USER=root \
-e SPARK_SUBMIT_PATH=/opt/spark/bin/spark-submit \
-e PYTHONPATH=/srv/chronon \
-e SPARK_VERSION=3.1.1 \
-e JOB_MODE='local[*]' \
-e PARALLELISM=2 \
-e EXECUTOR_MEMORY=2G \
-e EXECUTOR_CORES=4 \
-e DRIVER_MEMORY=1G \
-e CHRONON_LOG_TABLE=default.chronon_log_table \
-e CHRONON_ONLINE_CLASS=ai.chronon.quickstart.online.ChrononMongoOnlineImpl \
-e "CHRONON_ONLINE_ARGS=-Zuser=admin -Zpassword=admin -Zhost=${MONGO_CONTAINER} -Zport=27017 -Zdatabase=admin" \
-v "${CHRONON_DIR}/quickstart/mongo-online-impl:/srv/onlineImpl" \
ezvz/chronon \
bash -lc '/opt/spark/bin/spark-shell -i scripts/data-loader.scala && tail -f /dev/null' >/dev/null

wait_for_data_load
run_quickstart_online_prep

: > "${SERVICE_LOG_FILE}"
(
cd "${CHRONON_DIR}"
exec "${JAVA_BIN}" -jar service/target/scala-2.12/service-0.0.111-SNAPSHOT.jar \
run ai.chronon.service.WebServiceVerticle \
-Dserver.port="${SERVICE_PORT}" \
-conf service/src/main/resources/example_config.json
) >"${SERVICE_LOG_FILE}" 2>&1 &

echo "$!" > "${SERVICE_PID_FILE}"
wait_for_http "http://${SERVICE_HOST}:${SERVICE_PORT}/ping"

echo "CHRONON_SERVICE_URL=http://${SERVICE_HOST}:${SERVICE_PORT}"
21 changes: 21 additions & 0 deletions infra/scripts/chronon/stop-local-chronon-service.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash

set -euo pipefail

MONGO_CONTAINER="${CHRONON_MONGO_CONTAINER:-chronon-mongo}"
MAIN_CONTAINER="${CHRONON_MAIN_CONTAINER:-chronon-main}"
NETWORK_NAME="${CHRONON_NETWORK:-chronon-net}"
SERVICE_PID_FILE="${CHRONON_SERVICE_PID_FILE:-/tmp/chronon-service.pid}"

if [[ -f "${SERVICE_PID_FILE}" ]]; then
pid="$(cat "${SERVICE_PID_FILE}")"
if kill -0 "${pid}" >/dev/null 2>&1; then
kill "${pid}" >/dev/null 2>&1 || true
wait "${pid}" 2>/dev/null || true
fi
rm -f "${SERVICE_PID_FILE}"
fi

(docker rm -f "${MAIN_CONTAINER}" >/dev/null 2>&1) || true
(docker rm -f "${MONGO_CONTAINER}" >/dev/null 2>&1) || true
(docker network rm "${NETWORK_NAME}" >/dev/null 2>&1) || true
4 changes: 4 additions & 0 deletions sdk/python/feast/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
from feast.infra.offline_stores.contrib.athena_offline_store.athena_source import (
AthenaSource,
)
from feast.infra.offline_stores.contrib.chronon_offline_store.chronon_source import (
ChrononSource,
)
from feast.infra.offline_stores.contrib.oracle_offline_store.oracle_source import (
OracleSource,
)
Expand Down Expand Up @@ -63,6 +66,7 @@
"RequestSource",
"AthenaSource",
"OracleSource",
"ChrononSource",
"Project",
"FeastVectorStore",
"DocEmbedder",
Expand Down
5 changes: 5 additions & 0 deletions sdk/python/feast/infra/chronon_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from feast.infra.passthrough_provider import PassthroughProvider


class ChrononProvider(PassthroughProvider):
"""Optional provider wrapper for Chronon-backed Feast configurations."""
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__all__ = ["ChrononOfflineStore", "ChrononOfflineStoreConfig", "ChrononSource"]
Loading
Loading