Usage

When running as a standalone process, Sidecar can be called by sending an HTTP request to localhost. For example, to access the feature flags, you can use cURL:

curl http://localhost:12000/feature

Sidecar can also be used as a library in Python 3 apps to provide Service Discovery and Fault tolerance when making requests to other services.

Service Discovery Example

Currently, Sidecar provides service discovery through configuration files, though additional mechanisms may be added in the future. Here, then, is an example of using service discovery in your app:

# /my/config/path/discovery.json
{
    "coral": [
        "localhost:8771"
    ]
}

# myapp.py
import aiohttp

from sidecar import services, service_discovery


async def get_user(self):
    services_instance = services.Services(path="/my/config/path/discovery.json")
    await services_instance.start(loop=loop)
    services_instance = service_discovery.ServiceDiscovery(services_instance)
    scheme, netloc = services_instance.service_load_balancer("coral")
    url = "{}://{}/v2/ops/user/test@example.com".format(scheme, netloc)

    with aiohttp.ClientSession() as session:
        async with session.get(url,
                               params={"expand": "group"},
                               headers=headers,
                               allow_redirects=True) as response:
        data = await response.json() or {}

Fault Tolerance Example

The sidecar.call.fault_tolerant() decorator is the public API for adding fault tolerance to a service request with Sidecar.

The decorator accepts some values as arguments for import-time configuration, but configuration that requires state, such as a Sidecar-provided load balancing function or a connected Statsd protocol instance, can be provided as instance attributes on the object that contains the decorated method. sidecar.decorator.find_property() will detect and use these attributes.

Example:

# myapp.py
import os

from aiohttp import web
from aiohttp.web_exceptions import HTTPNotFound, HTTPBadGateway, HTTPServiceUnavailable
from aiohttp.web_reqrep import json_response
import asyncio

from sidecar import services, service_discovery
from sidecar.call import fault_tolerant
from sidecar.statsd import StatsD


class UserApi:
    """Public API for viewing existing users."""
    def __init__(self, service_discovery: ServiceDiscovery, statsd: StatsD):
        self.load_balancer = service_discovery.service_load_balancer("coral")
        self.statsd = statsd

    async def get_user_with_group(self, request):
        id_or_email = request.match_info["id_or_email"]
        try:
            user = await self._fetch_user_from_coral(id_or_email=id_or_email)
        except (SidecarTimeoutError, SidecarCircuitOpenError, SidecarRateLimitedError) as exc:
            error_message = "Request to internal HipChat service failed: {}".format(exc)
            log.error(error_message)
            raise HTTPBadGateway(reason=error_message)
        except SidecarRetryExhaustedError as exc:
            log.error("Sidecar retries exhausted: %s", exc.message)
            if exc.code == 503:
                raise HTTPServiceUnavailable(reason=exc.message)
            raise HTTPBadGateway(reason=exc.message)
        return json_response(data=user)

    @fault_tolerant("ops-user", retry_on_timeout=True, expected_in=3)
    async def _fetch_user_from_coral(self, call: Call, id_or_email: str):
        url = "{}://{}/v2/ops/user/{}".format(call.scheme, call.netloc, id_or_email)
        headers = {"X-HIPCHAT-GROUP": "0"}
        async with call.http_session.get(url,
                                        params={"expand": "group"},
                                        headers=headers,
                                        allow_redirects=True) as response:
            response_text = await response.text() or ''

        if response.status == 404:
            raise HTTPNotFound(reason="User {} does not exist".format(id_or_email))
        if response.status == 503:
            error_message = "Received 503 Service Unavailable from internal API {} with message {}".format(
                url, response_text)
            log.error(error_message)
            raise SidecarRetryRequestedError(code=503, message=error_message)
        if response.status != 200:
            error_message = "Received error from internal API {} with message {}".format(url, response_text)
            log.error(error_message)
            raise SidecarRetryRequestedError(code=response.status, message=error_message)

        try:
            user = json.loads(response_text)
        except json.JSONDecodeError as exc:
            error_message = "Error decoding JSON from success response from internal API {}: {}".format(url, exc)
            log.exception(error_message)
            raise HTTPBadGateway(reason=error_message)
        return user


def init(loop):
    webapp = web.Application(loop=loop)

    services_instance = services.Services(path="/my/config/path/discovery.json")
    await services_instance.start(loop=loop)
    services_instance = service_discovery.ServiceDiscovery(services_instance)
    statsd_instance = StatsD(
        loop, hostname=os.environ.get('HOSTNAME', ''), host='localhost', port='333333', prefix='myapp')
    await statsd_instance.start()
    UserApi(services_instance, statsd_instance)
    webapp.router.add_route('GET', '/user/{id_or_email}/', name='get_user')
    return webapp


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    app = loop.run_until_complete(init(loop))