Skip to content

Commit

Permalink
[Microservices] Create service base class (mlrun#6655)
Browse files Browse the repository at this point in the history
  • Loading branch information
alonmr authored Nov 7, 2024
1 parent e4f5061 commit 6e56885
Show file tree
Hide file tree
Showing 216 changed files with 1,532 additions and 1,149 deletions.
6 changes: 3 additions & 3 deletions dockerfiles/mlrun-api/start_api.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ MLRUN_MEMRAY_EXTRA_FLAGS=$(echo "${MLRUN_MEMRAY_EXTRA_FLAGS# }" | sed 's/^/ /')
if [[ -n "$MLRUN_MEMRAY_LOWER" && ( "$MLRUN_MEMRAY_LOWER" == "1" || "$MLRUN_MEMRAY_LOWER" == "true" || "$MLRUN_MEMRAY_LOWER" == "yes" || "$MLRUN_MEMRAY_LOWER" == "on" )]]; then
if [[ -n "$MLRUN_MEMRAY_OUTPUT_FILE" ]]; then
echo "Starting API with memray profiling output file $MLRUN_MEMRAY_OUTPUT_FILE..."
exec python -m memray run${MLRUN_MEMRAY_EXTRA_FLAGS% } --output "$MLRUN_MEMRAY_OUTPUT_FILE" --force -m server.api.main
exec python -m memray run${MLRUN_MEMRAY_EXTRA_FLAGS% } --output "$MLRUN_MEMRAY_OUTPUT_FILE" --force -m services.api.main
else
echo "Starting API with memray profiling..."
exec python -m memray run${MLRUN_MEMRAY_EXTRA_FLAGS% } -m server.api.main
exec python -m memray run${MLRUN_MEMRAY_EXTRA_FLAGS% } -m services.api.main
fi
else
exec uvicorn services.api.main:app \
exec uvicorn services.api.daemon:app \
--proxy-headers \
--host 0.0.0.0 \
--log-config server/py/services/uvicorn_log_config.yaml
Expand Down
20 changes: 20 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ extend-select = [
]
explicit-preview-rules = true

[tool.ruff.lint.isort]
known-first-party = ["mlrun"]
known-local-folder = ["services", "framework"]

[tool.ruff.lint.pycodestyle]
max-line-length = 120

Expand Down Expand Up @@ -48,6 +52,7 @@ root_packages = [
"mlrun",
"tests",
"server.py.services",
"server.py.framework",
]
include_external_packages = true

Expand Down Expand Up @@ -93,7 +98,9 @@ type = "forbidden"
source_modules = [
"mlrun",
"services",
"framework",
"server.py.services",
"server.py.framework",
]
forbidden_modules = [
"kfp",
Expand All @@ -105,6 +112,7 @@ type = "layers"
layers = [
"server.py.services",
"services",
"framework",
"mlrun",
]

Expand All @@ -116,9 +124,21 @@ source_modules = [
]
forbidden_modules = [
"services",
"framework",
"server.py.services",
"server.py.framework",
]
# Ignore integration tests until we have service specific integration tests infrastructure
ignore_imports = [
"tests.integration.sdk_api.alerts.test_alerts -> services ",
]

[[tool.importlinter.contracts]]
name = "MLRun common server code should not import specific services"
type = "forbidden"
source_modules = [
"server.py.framework",
]
forbidden_modules = [
"services",
]
13 changes: 13 additions & 0 deletions server/py/framework/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2024 Iguazio
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
File renamed without changes.
194 changes: 194 additions & 0 deletions server/py/framework/service/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# Copyright 2024 Iguazio
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import concurrent.futures
import contextlib
import traceback
from abc import ABC, abstractmethod

import fastapi
import fastapi.concurrency
import fastapi.exception_handlers

import mlrun.common.schemas
import mlrun.errors
import mlrun.utils
import mlrun.utils.version
from mlrun import mlconf

import framework.middlewares
import framework.utils.periodic


class Service(ABC):
def __init__(self):
# TODO: make the prefixes and service name configurable
service_name = "api"
self.SERVICE_PREFIX = f"/{service_name}"
self.BASE_VERSIONED_SERVICE_PREFIX = f"{self.SERVICE_PREFIX}/v1"
self.V2_SERVICE_PREFIX = f"{self.SERVICE_PREFIX}/v2"
self.app: fastapi.FastAPI = None
self._logger = mlrun.utils.logger.get_child(service_name)

def initialize(self):
self._initialize_app()
self._register_routes()
self._add_middlewares()
self._add_exception_handlers()

@abstractmethod
async def move_service_to_online(self):
pass

@abstractmethod
def _register_routes(self):
pass

def _initialize_app(self):
# Initializes fastAPI app - each service register the routers they implement
# API gateway registers all routers, alerts service registers alert router
self.app = fastapi.FastAPI(
title="MLRun", # TODO: configure
description="Machine Learning automation and tracking", # TODO: configure
version=mlconf.version,
debug=mlconf.httpdb.debug,
openapi_url=f"{self.SERVICE_PREFIX}/openapi.json",
docs_url=f"{self.SERVICE_PREFIX}/docs",
redoc_url=f"{self.SERVICE_PREFIX}/redoc",
default_response_class=fastapi.responses.ORJSONResponse,
lifespan=self.lifespan,
)

# https://fastapi.tiangolo.com/advanced/events/

@contextlib.asynccontextmanager
async def lifespan(self, app_: fastapi.FastAPI):
await self._setup_service()

# Let the service run
yield

await self._teardown_service()

async def _setup_service(self):
self._logger.info(
"On startup event handler called",
config=mlconf.dump_yaml(),
version=mlrun.utils.version.Version().get(),
)
loop = asyncio.get_running_loop()
loop.set_default_executor(
concurrent.futures.ThreadPoolExecutor(
max_workers=int(mlconf.httpdb.max_workers)
)
)

await self._custom_setup_service()

if mlconf.httpdb.state == mlrun.common.schemas.APIStates.online:
await self.move_service_to_online()

async def _custom_setup_service(self):
pass

async def _teardown_service(self):
await self._custom_teardown_service()
framework.utils.periodic.cancel_all_periodic_functions()

async def _custom_teardown_service(self):
pass

def _add_middlewares(self):
# middlewares, order matter
self.app.add_middleware(
framework.middlewares.EnsureBackendVersionMiddleware,
backend_version=mlconf.version,
)
self.app.add_middleware(
framework.middlewares.UiClearCacheMiddleware, backend_version=mlconf.version
)
self.app.add_middleware(
framework.middlewares.RequestLoggerMiddleware, logger=self._logger
)

def _add_exception_handlers(self):
self.app.add_exception_handler(Exception, self._generic_error_handler)
self.app.add_exception_handler(
mlrun.errors.MLRunHTTPStatusError, self._http_status_error_handler
)

async def _generic_error_handler(self, request: fastapi.Request, exc: Exception):
error_message = repr(exc)
return await fastapi.exception_handlers.http_exception_handler(
# we have no specific knowledge on what was the exception and what status code fits so we simply use 500
# This handler is mainly to put the error message in the right place in the body so the client will be able
# to show it
request,
fastapi.HTTPException(status_code=500, detail=error_message),
)

async def _http_status_error_handler(
self, request: fastapi.Request, exc: mlrun.errors.MLRunHTTPStatusError
):
request_id = None

# request might not have request id when the error is raised before the request id is set on middleware
if hasattr(request.state, "request_id"):
request_id = request.state.request_id
status_code = exc.response.status_code
error_message = repr(exc)
log_message = "Request handling returned error status"

if isinstance(exc, mlrun.errors.EXPECTED_ERRORS):
self._logger.debug(
log_message,
error_message=error_message,
status_code=status_code,
request_id=request_id,
)
else:
self._logger.warning(
log_message,
error_message=error_message,
status_code=status_code,
traceback=traceback.format_exc(),
request_id=request_id,
)

return await fastapi.exception_handlers.http_exception_handler(
request,
fastapi.HTTPException(status_code=status_code, detail=error_message),
)

def _initialize_data(self):
pass

async def _start_periodic_functions(self):
pass


class Daemon(ABC):
def __init__(self, service_cls: Service.__class__):
self._service = service_cls()

def initialize(self):
self._service.initialize()

@property
def app(self):
return self._service.app

@property
def service(self) -> Service:
return self._service
13 changes: 13 additions & 0 deletions server/py/framework/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2024 Iguazio
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2023 Iguazio
# Copyright 2024 Iguazio
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -11,7 +11,6 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import asyncio
import traceback
import typing
Expand Down
6 changes: 0 additions & 6 deletions server/py/services/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,3 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#

# Minimum client version that supports model monitoring,
# Will be fixed when MM will be defined as BC supported feature
MINIMUM_CLIENT_VERSION_FOR_MM = (
"1.7.0-rc43" # can be changed to 1.7.0 before 1.7.0 release
)
1 change: 1 addition & 0 deletions server/py/services/api/api/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import mlrun
import mlrun.common.schemas

import services.api.db.session
import services.api.utils.auth.verifier

Expand Down
3 changes: 2 additions & 1 deletion server/py/services/api/api/endpoints/alert_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@
from sqlalchemy.orm import Session

import mlrun.common.schemas
from mlrun.utils import logger

import services.api.utils.auth.verifier
import services.api.utils.singletons.project_member
from mlrun.utils import logger
from services.api.api import deps

router = APIRouter(prefix="/alert-templates")
Expand Down
3 changes: 2 additions & 1 deletion server/py/services/api/api/endpoints/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@
from sqlalchemy.orm import Session

import mlrun.common.schemas
from mlrun.utils import logger

import services.api.crud
import services.api.utils.auth.verifier
import services.api.utils.clients.chief
import services.api.utils.singletons.project_member
from mlrun.utils import logger
from services.api.api import deps

router = APIRouter(prefix="/projects/{project}/alerts")
Expand Down
5 changes: 3 additions & 2 deletions server/py/services/api/api/endpoints/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@

import mlrun.common.formatters
import mlrun.common.schemas
from mlrun.config import config
from mlrun.utils import logger

import services.api.crud
import services.api.utils.auth.verifier
import services.api.utils.singletons.project_member
from mlrun.config import config
from mlrun.utils import logger
from services.api.api import deps
from services.api.api.utils import (
artifact_project_and_resource_name_extractor,
Expand Down
5 changes: 3 additions & 2 deletions server/py/services/api/api/endpoints/artifacts_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@

import mlrun.common.formatters
import mlrun.common.schemas
from mlrun.common.schemas.artifact import ArtifactsDeletionStrategies
from mlrun.utils import logger

import services.api.crud
import services.api.utils.auth.verifier
import services.api.utils.pagination
import services.api.utils.singletons.project_member
from mlrun.common.schemas.artifact import ArtifactsDeletionStrategies
from mlrun.utils import logger
from services.api.api import deps
from services.api.api.utils import artifact_project_and_resource_name_extractor

Expand Down
1 change: 1 addition & 0 deletions server/py/services/api/api/endpoints/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import fastapi

import mlrun.common.schemas

import services.api.api.deps
import services.api.utils.auth.verifier

Expand Down
Loading

0 comments on commit 6e56885

Please sign in to comment.