You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 4 Next »

This page describes the requirements and initial design of the new Qpid JMS Client that supports AMQP 1.0 (simply referred to as the "Qpid JMS Client" below).

Requirements

Functional requirements

  • AMQP 1.0
  • JMS 2
    • JMS 1.1 support will be added in the future if demand requires.
  • ONGOING: Contribute to the JMS mapping work within the OASIS AMQP Binding & Mappings TC

The following sub-sections allocate the functional requirements to several sequential project phases.

Phase 0

This is the initial set-up of the basics.

  • Open and close a connection. Includes writing automated tests.

This will implicitly require work on:

  • Project folder structure
  • Build tool configuration
  • CI server job
  • Automated HTML documentation generation.
Phase 1
  • Synchronous, Auto-Ack style 'simple client'
  • Main operations on JMS Connections, Sessions, Consumers, Producers, Queues, Message (Text?).
    • Include closing/stopping/deleting where applicable.
  • Configuration
    • Ability to configure settings such as pre-fetch etc
      • TODO per connection or per consumer or both? If the latter then not sure how to pass this setting in (address options?), given that JMS API doesn't offer an obvious way to do it.
    • TODO define how JNDI configuration will work
  • SASL
  • Correct handling of non-happy path scenarios, namely:
    • Unexpected errors
    • Failures that are nevertheless AMQP-compliant, e.g. the Broker spontaneously sends a Close or End frame.
    • Timeouts due to connectivity failure etc. Necessary because of the asynchronous nature of AMQP.
    • Interruption of application-owned threads.
Phase 2
  • SSL
  • Transactions
  • Client Ack
  • Selectors
  • JNDI
  • MessageListener.onMessage()
  • Durable Subscriptions
  • JMS 2.0 features:
    • Asynchronous send
    • Shared Topic Subscriptions

Non-functional requirements

  • Logging
    • Should play nicely with application logging, e.g. allow the application to plug in its choice of third party logging framework (which would also be passed through to Proton if possible).
    • Should allow control of logging thresholds for categories including:
      • A specific object, e.g. a Connection
      • A logical function, e.g. network I/O
  • Initially will only run on Java 1.7 (because some JMS 2 features require Java 1.7).

Out of scope

  • AMQP 0-x
  • ADDR and BURL addressing formats
  • Failover. Instead, this will be implemented in the future either entirely below or entirely above the client code.
  • Accepting legacy system properties (unelss they happen to match what we would choose now).

Design

Layers

+================================+
|
| JMS
|
| Implementations of javax.jms
|
+================================+
               |
               |
              \|/
+================================+
|
| AMQP
|
| Wrappers around Proton's engine
| classes, primarily to add locking
|
+================================+
            |                 |
            |                 |
           \|/                |
+======================+      |
|       Proton         |      |
|                      |      |
| Message |  Engine    |      |
|         |            |      |
+=========+============+      |
                    /|\       |
                     |        |
                     |       \|/
                   +====================
                   |
                   | JMS Driver interface
                   |
                   +=====================
                            /|\
                             |
                        +--------------+
                        |              |
                        |              |
                        |              |
                  +===========+        +==============
                  |                    |
                  |  Socket            |
                  |  driver            | Another driver,
                  |  (might use        | e.g. in-VM
                  |  Proton's Driver)  |
                  |                    |
                  +===========+        +==============
                    |
                    | TCP/IP
                    |
                   \|/
+================================+
|
| Broker
|
+================================+

Public API

  • Goals:
    • Minimal public API
    • Possible to write JMS applications that have a compile-time dependency on the javax.jms interfaces and not on Qpid-specific classes.
    • Minor releases maintain backwards compatibility between minor releases.

The public API consists of:

  • The JMS interfaces (probably provided by a third party library such as Geronomio Spec).
  • The constructor(s) for Qpid's implementation of javax.jms.ConnectionFactory and its sub-interfaces.
  • Qpid's javax.naming.spi.InitialContextFactory implementation.

Note that the logging output and logging category names also constitute a public interface.

In the future, the public API may need to be extended or more precisely specified (e.g. to include details of the mapping between JMS and AMQP when using MapMessage and ObjectMessage etc).

Locking

  • Goals:
    • Minimise the number of locks
    • Define the correct locking order

Threads fall into two categories:

  1. Application threads using the JMS API. Only use the top half of the Proton API.
  2. One or more driver threads for I/O. Managed by the JMS client. Only use the bottom half of the Proton API.

For a given connection:

  1. State shared by multiple application threads (namely the state of the objects in the JMS layer) is guarded by the JMS ConnectionLock.
  2. State shared by the application and driver threads is guarded by the AmqpConnection lock. This shared state is:
    1. A small number of flags the application and driver threads use to indicate to each other that "something has changed".
    2. The Proton objects. This sharing occurs inside Proton but needs to be guarded by the JMS client because Proton itself is not thread-safe.

Synchronous operations must follow the locking scheme indicated by the following pseudo-code:

  • jmsObject.doStuff
    • obtain ConnectionLock
    • amqpObject.doAmqpStuff
      • obtain amqpConnection lock
      • protonObject.doProtonStuff
      • release amqpConnection lock
    • JmsConnection.stateChanged (event notification, e.g. to wake up the driver so that I/O occurs)
    • release ConnectionLock

Where operations need to wait for a "remote" operation to complete, they must follow the scheme indicated by the following pseudo-code:

  • jmsObject.doRemoteStuff
    • obtain ConnectionLock
    • ...
    • JmsConnection.waitUntil(predicateObject)
      • obtain amqConnection lock
      • amqConnection.wait() (the Driver thread calls amqConnection.notifyAll())
      • ...
      • evaluate predicate
      • ...
      • release amqConnection lock
    • release ConnectionLock

This ensures that when the predicate object is invoked the thread will already possess both the JmsConnection lock and the AmqpConnection lock.

I/O layer

  • Goals:
    • Encapsulate any 3rd party library as much as possible
    • Allow transport to be easily swapped out to allow, for example:
      • in-VM operation
      • WebSockets
      • SCTP
      • Accepting Connection. Useful if the client is behind a firewall that forbids outbound connections.

TODO - design I/O
Use Netty?

Logging

  • Goals:
    • Consistent use of logging levels
    • TODO more...

TODO

Testing strategy

  • Goals:
    • Running all the tests should be fast (it should be exceptional for a test to take more than five seconds).
    • Tests operate at the correct level. Specifically:
      • Simple logic bugs cause unit tests to fail.
      • Errors in collaboration logic cause integration tests to fail.
    • Most (if not all) of our integration tests should run in-process.
    • The official JMS 2 TCK tests will also be run against the client.
Integration tests

For each test, our main choice is which layer to designate as the boundary for our tests. Everything "below" this boundary will be replaced with a mock implementation ("mock" is defined in a broad sense - it does not assume a imply the use of a mocking framework).

Our options are listed below in increasing system boundary extent. As the extent increases, the usual trade-offs apply:

  • More layers of the real JMS client will be exercised, thereby increasing the confidence that our tests give us.
  • We get less control over the tests, both in terms of their inputs and the assertions we can test.
  • Tests become harder to debug.

Layer to mock

Comments

Proton

Simple, in that our mock layer can be implemented in terms of Proton model objects without worrying about streams of bytes. This should also make it easier to debug tests.

Will not catch Proton bugs.

Hard to ensure that our mock Proton behaves consistently with respect to the real thing.

Proton encoder and decoder.
For simplicity, the mock encoder and decoder would produce and consume FrameBody objects respectively, rather than bytes.

On a test-by-test basis, the mock encoder could be configured to apply assertions to allow us to check that a particular JMS operation caused the right frame(s) to be generated. Similarly, the mock decoder could generate FrameBodys to simulate the Broker's behaviour.  This would allow us to replace Proton's Transport layer with a no-op implementation.

Alternatively, the the assertions and mock behaviour (for output and input respectively) could be applied to the Transport rather than the encoder/decoder.

Gives us full control, i.e. we can check exactly which frames were produced.  Can also control which frames are sent to the client to a greater extent than if a Proton peer was generating them.  This doesn't just apply to simulating error scenarios - some valid sequences of happy-path frames are impossible to generate using Proton.

However, writing tests in terms of frames is less intuitive than expressing them in terms of Proton model objects.  The former is a sequence of state transitions whereas the latter represents the actual state of the system.

The broker (in-process).
We would modify Driver to produce output in-process rather than requiring a Socket.
The bytes would be exchanged with a mock Broker which would be a thin wrapper round Proton.  The mock Broker would use test-specific logic to:
- Assert the expected state of its local Proton model objects
- Simulate a real Broker's output

Tests using this option would be run against both proton-j and proton-jni.

This option should still run acceptably fast despite containing almost all the layers of the real JMS client.

Cannot simulate all scenarios we want to test, e.g. server redirect, or certain error cases.

The broker (out-of-process).
We would either write a simple broker or use the existing one.
The client test would more closely resemble a conventional application than a JUnit test.

Useful for testing multi-threaded behaviour

We expect all of the above mocking options to be used at some point. Initially we will favour mocking the broker in-process where possible. This is because:

  1. It is easy to set up, with minimal changes to Proton.
  2. This is very nearly an end-to-end test, which will give us useful reasssurance particularly in the early stages of the project.
  • No labels