Using Mantissa Interstore Messaging

Why?

When there is an interaction between two user stores, care must be taken or the primary feature provided by Mantissa’s division of storage by user

Prerequisites

Readers should familiarize themselves with the following concepts in order to understand all sections of this document:

  • Twisted AMP Commands
  • Axiom Items
  • Mantissa Sharing

Overview

As you may have guessed from the title of this document, interaction between user stores is done using a message based API. Instead of opening two user stores directly and performing the usual inspection and modification of them with Axiom’s APIs, applications create messages and send them from one store to another. Messages are defined as AMP Command classes. Applications also define methods to handle and possibly respond to these messages. Methods can also be defined to handle successful answers to message or errors generated by some part of the message dispatch process.

The message delivery system is transactional . A message which is sent in a transaction which is committed is guaranteed to be delivered to its recipient. Similarly, a method which is called to handle a received message is called in a transaction; if this transaction fails, a persistent record of this failure is created for an administrator to examine (in the future, this may also be handled with transparent retry or some another strategy to increase reliability). And again, each answer is passed to the method defined to handle it in a transaction with similar handling of transaction failures as for message handling methods.

In this document, we will consider a simple multi-user appointment tracking application which uses the interstore messaging API to establish appointments between different users.

Defining Messages

Before you can send or receive a message, you first need to define the arguments, results, and errors of that message. This is done with a twisted.protocols.amp.Command subclass. For example, here is the definition of MakeAppointment , the message the appointment tracking example sends when one user wants to make an appointment with another user:

class MakeAppointment(Command):
    arguments = [("whom", SenderArgument()), ("when", String())]
    response = [("appointmentID", Unicode())]
    errors = {"busy": Busy}

Receiving Messages

Addressing

The Mantissa sharing system is used to specify the target of an interstore message. Targets are specified as instances of xmantissa.sharing.Identifier . These instances identify an Item instance in a particular user’s store by its shareID . To receive a message, these items must be shared to the sender using the xmantissa.ixmantissa.IMessageReceiver interface.

Method Dispatch

The simplest way to handle interstore messages is to use the xmantissa.interstore.AMPReceiver mixin. Once a message gets to an item, if that item’s class mixes in AMPReceiver , the message will be dispatched to a method based on the command in the message. These methods are defined similarly to normal AMP command responders, but using the xmantissa.interstore.commandMethod , xmantissa.interstore.answerMethod , and xmantissa.interstore.errorMethod exposers.

In the example appointment tracking application, the Calendar class’s peerRequestedAppointment method shows how commandMethod can be used.

@commandMethod.expose(MakeAppointment)
def peerRequestedAppointment(self, whom, when):
    app = Appointment(
        store=self.store, when=Time.fromISO8601TimeAndDate(when),
        withWhomUsername=whom.localpart, withWhomDomain=whom.domain,
        withWhomShareID=whom.shareID, remoteID=whom.shareID)
    role = getPrimaryRole(self.store, u"%s@%s" % (whom.localpart, whom.domain), True)
    appointmentID = role.shareItem(app, interfaces=[IMessageReceiver]).shareID
    return {'appointmentID': appointmentID}

Sending Messages

As AMPReceiver provides a relatively high-level API for receiving messages, xmantissa.interstore.AMPMessenger provides a high-level API for sending messages. AMPMessenger instances are ephemeral, intended to be created as necessary to send messages. However, to create one, a xmantissa.interstore.MessageQueue is required. Aside from that, two Identifier instances, a sender an a target, are also needed. Once created, an AMPMessenger instance’s xmantissa.interstore.AMPMessenger.messageRemote method may be used to send a message. This method takes an AMP Command subclass, any keyword arguments allowed by that Command and optionally a consequence .

An example of this can be seen in the example’s Calendar.requestAppointmentWith method. This is the method invoked by other application code (in the case of the example, by the web user interface code) to try to create a new appointment.

def requestAppointmentWith(self, whom, when):
    appointment = Appointment(
        store=self.store, when=when, withWhomShareID=whom.shareID,
        withWhomUsername=whom.localpart, withWhomDomain=whom.domain)
    role = getPrimaryRole(self.store, u"%s@%s" % (whom.localpart, whom.domain), True)
    appointmentID = role.shareItem(appointment, interfaces=[IMessageReceiver]).shareID

    messenger = AMPMessenger(
        self.messageQueue,
        Identifier(appointmentID, *getAccountNames(self.store).next()),
        whom)
    messenger.messageRemote(
        MakeAppointment, appointment, when=when.asISO8601TimeAndDate())

Consequences

If a consequence item is supplied to a messageRemote call, that item will be used to look up a handler for the response to the message, or a handler for any error which occurs while delivering that message.

The appointment tracking example uses consequences to make the result of any appointment setup attempt go to an instance of Appointment which locally represents that potential appointment. This lets it adjust local state easily to reflect either successful appointment setup or any errors. This is implemented using the answerMethod and errorMethod exposers:

@answerMethod.expose(MakeAppointment)
def appointmentMade(self, appointmentID):
    self.remoteID = appointmentID
    print 'Appointment made'


@errorMethod.expose(MakeAppointment, Busy)
def appointmentFailed(self, failure):
    self.failed = u"Appointment not made, too busy."

Conclusion

What you’ve learned here, about AMPReceiver , AMPMessenger , and the command, answer, and error method exposers, should let you write applications with interactions between multiple users in a way which preserves Mantissa’s horizontal scalability features.

See the full appointment tracking example source here:

  • listings/interstore/cal.py- The application model, including all of the interstore messaging code.
  • listings/interstore/webcal.py- The web interface for the application.
  • listings/interstore/xmantissa/plugins/calendar_offering.py- The dropin which makes the appointment tracking application available for inclusion in a Mantissa product.

This example is fully runnable. Just add it to your PYTHONPATH and start Mantissa server. It provides a Calendar offering which includes one installable powerup.