One of many exciting features that the upcoming Zato 3.0 release will bring is API caching - this post provides an overiew of functionality that is already available if you install Zato directly from source code.

What is new

  • In-RAM caches accessible to Python-based services and external clients (including REST and other format or protocols)
  • A convenient set of practical cache commands based on real-world needs
  • GUI and cache contents search
  • HTTP channel caching
  • Integration with memcached

Built-in caches

In-RAM caches are perfect for rarely changing yet frequently read business data. For instance:

  • A customer's products, stored in a remote SQL database, will change infrequently but may be read very often
  • A user's permissions may be read from multiple databases and the process may take several seconds but once read, they will not change much and fast access is of primary importance.

Each Zato cluster has a default cache and there can be multiple independent caches for different purposes each with its own configuration.

In v3.0, there is no persistent storage for caches and synchronization between servers in a Zato cluster is performed in background. Both aspects will be extended in future releases.

Real-world commands

Built-in caches offer all the standard CRUD commands operating on cache entries but they come in several versions, each of which is meant to facilitate operations in real-world applications.

All of commands are available to Python services and external API clients through dedicated REST endpoints.

More commands will be added in the future, based on feedback from users employing caches in projects.

Command Description
get Returns an entry matching input key
get-by-prefix Returns all entries whose keys start with a given prefix
get-by-suffix Returns all entries whose keys end with a given suffix
get-by-regex Returns all entries whose keys match a regular expression
get-contains Returns entries with keys that contain a single input substring
get-contains-all Returns entries with keys that contain all of input substrings
get-contains-any Returns entries with keys that contain at least one of of input substrings
get-not-contains Returns entries with keys that do not contain the input substring
set Sets input value for a given key
set-by-prefix Sets input value for all keys starting with a given prefix
set-by-suffix Sets input value for all keys ending with a given suffix
set-by-regex Sets input value for all keys matching a regular expression
set-contains Sets input value for all keys that contain a single input substring
set-contains-all Sets input value for keys that contain all of input substrings
set-contains-any Sets input value for keys that contain at least one of input substrings
set-not-contains Sets input value for keys that do not contain the input substring
delete Deletes a given key
delete-by-prefix Deletes all keys starting with a given prefix
delete-by-suffix Deletes all keys ending with a given suffix
delete-by-regex Deletes all keys matching a regular expression
delete-contains Deletes all keys that contain a single input substring
delete-contains-all Deletes keys that contain all of input substrings
delete-contains-any Deletes keys that contain at least one of input substrings
delete-not-contains Deletes keys that do not contain the input substring
expire Sets expiration for a given key
expire-by-prefix Sets expiration for all keys starting with a given prefix
expire-by-suffix Sets expiration for all keys ending with a given suffix
expire-by-regex Sets expiration for all keys matching a regular expression
expire-contains Sets expiration for all keys that contain a single input substring
expire-contains-all Sets expiration for keys that contain all of input substrings
expire-contains-any Sets expiration for keys that contain at least one of input substrings
expire-not-contains Sets expiration for keys that do not contain the input substring
keys Returns all keys in cache
values Returns all values in cache
items Returns all items (entries) in cache
clear Clears out the cache completely

Additionally, Python code can access dict-like methods .iterkeys, .itervalues and .iteritems that return iterators over keys or values without building result lists upfront.

Cache configuration

Each cache can have its own policy that is best suited to a given purpose:

  • Cache size upon reaching of which the least commonly used entries will be evicted
  • Max item size - values that are too big will be rejected
  • Whether a given item's expiration time should be extended on each get or set operation

Screenshots

Screenshots

GUI

As with other Zato features, a GUI in web-admin lets one easily manage cache definitions, add new or update existing entries from the browser, and look up items by keys, values or both.

Screenshots

Screenshots

Auto-caching in HTTP channels

Caching of responses, either REST or SOAP, is such a common activity that it is available under one click for all HTTP channels

When creating a new channel or updating an existing one, pick a definition of cache that should hold responses for that channel and servers will automatically cache them on first request.

On subsequent invocations, all responses from that channel will be served from cache until underlying keys expire or are deleted.

Screenshots

Usage from Python

The self.cache object is an entry point to all the caches from a service's perspective. For instance, to add new entries:

# -*- coding: utf-8 -*-

from __future__ import absolute_import, division, print_function, unicode_literals

# stdlib
from time import sleep

# Zato
from zato.server.service import Service

class CacheDemo(Service):
    """ A demo service that uses built-in caches.
    """
    def get_data(self):
        """ Returns data from external systems.
        """
        # Assume this takes a long time to return, e.g. several remote calls
        sleep(2)

        return 'MyData'

    def handle(self):

        # Get data from external resources
        data = self.get_data()

        # Store it in default cache
        self.cache.default.set('data', data)

        # Or, store it in an explicitly named cache
        cache = self.cache.get_cache('builtin', 'Customer products')
        cache.set('cust-123-data', data)

Both entries can be now found in web-admin:

Screenshots

Screenshots

REST API

All cache commands are available through REST API calls. Here, set and get are used for illustrative purposes.

Note how input can be sent in both body and query string and that the last get command obtains metadata in addition to actual business value.

$ curl -XPOST localhost:11223/zato/cache -d '{"key":"my-key", "value":"my-value"}'
$
$ curl -XGET localhost:11223/zato/cache?key=my-key
{"value": "my-value"}
$
$ curl -XGET localhost:11223/zato/cache -d '{"key":"my-key"}'
{"value": "my-value"}
$ 
$ curl -XGET localhost:11223/zato/cache -d '{"key":"my-key", "details":true}'
{"hits": 4, "key": "my-key", "last_read": "2017-12-18T11:57:52.202461",
"value": "my-value", "expiry": null, "prev_write": "2017-12-18T11:56:57.714813",
"prev_read": "2017-12-18T11:57:47.225827", "expires_at": null,
"last_write": "2017-12-18T11:57:43.200757", "position": 0}
$ 

Summary

API caching in Zato 3.0 is a new feature, based on practical needs, that without doubt will come in handy in many integration scenarios that require efficient and convenient access to data cached in RAM.

Watch this space for future posts describing in detail commands, configuration options, response hooks or runtime usage statistics API.

This blog post discusses an integration scenario that showcases a new feature in Zato 3.0 - SMS texting with Twilio.

Use-case

Suppose you'd like to send text messages that originate from multiple sources, from multiple systems communicating natively over different protocols, such as the most commonly used ones:

  • REST
  • AMQP
  • WebSockets
  • FTP
  • WebSphere MQ

Naturally, the list could grow but the main points are that:

  • Ubiquitous as it is, HTTP is far from being the only protocol used in more complex environments
  • You don't want to distribute credentials to Twilio to each of backend or frontend systems that wants to text

The solution is to route all the messages through a dedicated Zato service that will:

  • Offer to each system communication in their own native protocol
  • Be the only place where credentials are kept

Let's say that the system that we are building will send text messages informing customers about the availability of their order. For simplicity, only REST and AMQP will be shown below but the same principle will hold for other protocols that Zato supports.

Screenshots

Code

# -*- coding: utf-8 -*-

from __future__ import absolute_import, division, print_function, unicode_literals

# Zato
from zato.server.service import Service

class SMSAdapter(Service):
    """ Sends template-based text messages to users given on input.
    """
    name = 'sms.adapter'

    class SimpleIO:
        input_required = ('user_name', 'order_no')

    def get_phone_number(self, user_name):
        """ Returns a phone number by user_name.+1234567890
        In practice, this would be read from a database or cache.
        """
        users = {
            'mary.major': '+15550101',
            'john.doe': '+15550102',
        }

        return users[user_name]

    def handle(self):

        # In a real system there would be more templates,
        # perhaps in multiple natural languages, and they would be stored in files
        # on disk instead of directly in the body of a service.
        template = "Hello, we are happy to let you know that" \
            "your order #{order_no} is ready for pickup."

        # Get phone number from DB
        phone_number = self.get_phone_number(self.request.input.user_name)

        # Convert the template to an actual message
        msg = template.format(order_no=self.request.input.order_no)

        # Get connection to Twilio
        sms = self.out.sms.twilio.get('My SMS')

        # Send messages
        sms.conn.send(msg, to=phone_number)

In reality, the code would contain more logic, for instance to look up users in an SQL or Cassandra database or to send messages based on different templates but to illustrate the point, the service above will suffice.

Note that the service uses SimpleIO which means it can be used with both JSON and XML even if only the former is used in the example.

This is the only piece of code needed and the rest is simply configuration in web-admin described below.

Channel configuration

The service needs to be mounted on channels - in this scenario it will be HTTP/REST and AMQP ones but could be any other as required in a given integration project.

In all cases, however, no changes to the code are needed in order to support additional protocols - assigning a service to a channel is merely a matter of additional configuration without any coding.

Screenshots

Screenshots

Screenshots

Screenshots

Twilio configuration

Fill out the form in Connections -> SMS -> Twilio to get a new connection to Twilio SMS messaging facilities. Account SID and token are the same values that you are given by Twilio for your account.

Default from is useful if you typically send messages from the same number or nickname. On the other hand, default to is handy if the recipient is usually the same for all messages sent. Both of these values can be always overridden on a per call basis.

Screenshots

Screenshots

Invocation samples

We can now invoke the service from both curl and RabbitMQ's GUI:

Screenshots

Screenshots

Result

In either case, the result is a text message delivered to the intended recipient :-)

Screenshots

There is more!

It is frequently very convenient to test connections without actually having to develop any code - this is why SMS Twilio connections offer a form to do exactly that. Just click on 'Send a message', fill in your message, click Submit and you're done!

Screenshots

Screenshots

Summary

Authoring API services, including ones that send text messages with Twilio is an easy matter with Zato.

Multiple input protocols are supported out of the box and you can rest assured that API keys and other credentials never sprawl all throughout the infrastructure, everything is contained in a single place.

Zato 2.0.8 has just been released - check here for installation instructions under Ubuntu, RHEL/CentOS, Debian, Docker and more.

This is primarily a regular patch release that closes GitHub tickets opened since 2.0.7 was published.

But there is more - several performance improvements have been backported from the upcoming 3.0 release and now your API servers will be up to 30% faster merely by upgrading from 2.0.7 to 2.0.8.

Zato is an open-source Python-based integration platform for ESB, SOA, REST, APIs and Cloud Integrations. Zato focuses on APIs, middleware and backend systems exclusively.

If you're thinking of building distributed systems with Python and need to handle business scenarios or processes of any complexity, from simple to large, Zato is the platform to choose.

Here are some screenshots to whet your appetite - head over to the documentation index for more!

Screenshots

Screenshots

Screenshots

Screenshots

Screenshots

Screenshots

Screenshots

Screenshots

Screenshots

Screenshots

Screenshots

Zato 3.0 will sport a completely rewritten AMQP subsystem and the changes are nothing short of exciting. Put simply, everything is faster, leaner and even more robust.

Synchronous channels

AMQP channels are now synchronous which means that an AMQP broker will wait for a Zato service to confirm that the message was received or it was not. In earlier Zato versions, AMQP channels worked in an implicit always-acknowledge mode. Now each message taken off an AMQP queue can be explicitly acknowledged or rejected, as below:

from zato.server.service import Service

class MyService(Service):

    def handle(self):
        if 'foo' in self.request.payload:
            self.request.amqp.ack()
        else:
            self.request.amqp.reject()

Message details

Channels have access to all metadata regarding an incoming message, for instance:

from zato.server.service import Service

class MyService(Service):

    def handle(self):
        self.logger.info(self.request.amqp.msg)

Screenshots

Connection pools

Previously, a single connection pool was shared by all channels and outgoing connections to a single AMQP broker.

Now each channel or outgoing connection may have its own pool, managed independently, which lets Zato servers use multiple CPUs in AMQP connections to improve performance - the underlying framework for connection pools was redesigned to achieve drastically reduced RAM usage - from megabytes down to kilobytes for each pool.

Channels:

Screenshots

Outgoing connections:

Screenshots

Correlating connections and consumers

When using AMQP GUIs, it was already possible to easily find Zato connections but the idea has been extended and now each connection, channel and consumer carry accounting information on which Zato component is the source or consumer of a message, including remote hostname, process ID, thread/greenlet ID and consumer tag prefix:

Screenshots

Screenshots

Get it from GitHub

A preview version of Zato 3.0 can be already installed from source code - all the new AMQP features are in the default branch, called 'main'. And remember to always feel free to get in touch - leave feedback in the forum or contact support if you need anything!

This is a preview of an upcoming feature in Zato 3.0 that will let one generate and distribute specifications for API services.

Introduction

Let's consider the following two modules with three services related to customers and customer cases. The implementation is left out so as to focus on I/O only.

# api1.py

from zato.server.service import Service

namespace = 'my.api.customer'

class GetCustomer(Service):
    """ Returns basic information about a customer.
    """
    name = 'get-customer'

    class SimpleIO:
        input_required = ('cust_id',)
        input_optional = ('cust_type', 'segment',)
        output_required = ('name', 'is_active', 'region')
        output_optional = ('last_seen',)

    def handle(self):
        pass # Skip implementation

class UpdateCustomer(Service):
    """ Updates basic information about a customer.
    """
    name = 'update-customer'

    class SimpleIO:
        input_required = ('name', 'is_active', 'region')
        output_required = ('result',)

    def handle(self):
        pass # Skip implementation
# api2.py

from zato.server.service import Service

namespace = 'my.api.customer-cases'

class GetCustomerCase(Service):
    """ Returns basic information about an individual case opened by a customer.
    """
    name = 'get-customer-cases'

    class SimpleIO:
        input_required = ('case_id',)
        output_required = ('cust_id', 'is_open', 'status', 'last_contact_date',)
        output_optional = ('supervisor_id', 'region_id')

    def handle(self):
        pass # Skip implementation

Screenshots

In Zato 3.0, and already available if installed from source, a specification for such services will look like below:

Screenshot

Screenshot

Screenshot

Screenshot

Key points

  • Services can be grouped into namespaces
  • For each service its documentation, as extracted from docstrings, is provided
  • Each service can optionally export information what other services it invokes, i.e. depends on
  • Input/Output for each service is based on SimpleIO
  • All parameters are automatically converted to correct types, e.g. is_active is a boolean, cust_id an integer
  • Brand-specific information and CSS can be applied to customize the output - here 'My API spec' is only a generic name that can be replaced with anything else
  • The API specification is output using a service and REST channels thus it can be secured as required just like any other channel in Zato

Future plans

While for now the output is the documentation, the underlying backend is already capable to automatically generate data fpr other API specification formats such as Swagger/OpenAPI, RAML or WSDL - this is exactly what the next Zato version will do, it will generate specs in several formats for easy consumption by API clients.