4/AMQP9 - an AMQP/0.9.1 profile for RestMS

This document defines the AMQP9 profile for RestMS. The AMQP9 profile defines the behaviour of a set of feed, join, and pipe types that provide an AMQP/0.9.1-interoperable messaging model.

  • Name: www.restms.org/spec:4/AMQP9
  • Version: draft/4
  • Editor: Pieter Hintjens <moc.xitami|hp#moc.xitami|hp>
  • Contributors: Steve Vinoski <gro.eeei|iksoniv#gro.eeei|iksoniv>, Brad Clements <moc.skrowkrum|ckb#moc.skrowkrum|ckb>

License

Copyright (c) 2009 by the Editor and Contributors.

This Specification is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.

This Specification is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses>.

Change Process

This document is governed by the Digital Standard Organization's Consensus-Oriented Specification System.

Goals and structure of this document

This document defines the AMQP9 profile for RestMS. The AMQP9 profile defines the behaviour of a set of feed, join, and pipe types that provide an AMQP-interoperable messaging model.

We cover these aspects of the AMQP9 profile:

  • What the profile is designed for;
  • What the resources look like and how they work;
  • Guidelines for client applications;
  • Guidelines for server implementers.

Language

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119[2].

Purpose and scope

The AMQP9 profile aims to provide a messaging model that interoperates with networks running the AMQP/0.9.1 protocol[1] or earlier versions. It implements the AMQP exchange types as feed types, and adds workload-distribution feeds that implement AMQP shared queues. A RestMS server that implements the AMQP9 profile can operate as the client of an AMQP server, or as a node on a federated AMQP network.

Syntax and semantics

The Defaults profile is formally called "4/AMQP9" to distinguish it from future variations.

It defines these feed types:

  • The fanout feed type routes the message to all joins attached to the feed without filtering. The server MUST implement the fanout feed type.
  • The direct feed type routes the message to each join attached to the feed that has an address pattern that is identical to the message address. One message can be routed to multiple pipes. The server MUST implement the direct feed type.
  • The topic feed type routes the message to all joins attached to the feed using a topic matching algorithm. The topic matching algorithm works as follows: the message address is a string of topic names, separated by dots. The join address pattern is a string of topic names mixed with wild cards. "*" matches a single topic name and "#" matches zero or more topic names. The server SHOULD implement the topic feed type.
  • The headers feed type routes the message to all joins attached to the feed using a header matching algorithm. The header matching algorithm works as follows: the join specifies a set of header elements. The message matches if it has identical values for the header elements specified in the join. The server MAY implement the headers feed type.
  • The rotator feed type routes the message to at most one pipe, using a round-robin algorithm to choose the next join from those attached to the feed. Does not apply any filtering. If the feed has no attached joins, the server queues the message for an arbitrary period. The server SHOULD implement the rotator feed type.
  • The service feed type works as the rotator feed, with the additional property of self-deleting when the the number of joins attached to it drops from 1 to zero. This means that the presence or absence of the feed indicates the availability of the "service" it represents. The server SHOULD implement the service feed type.

A fanout, direct, topic, or headers feed maps to an AMQP exchange of the same type. In these feeds route by doing some kind of matching between the address attribute of a message (a literal string), and the address attribute of a join (a pattern). If the feed is private, a hashed name is used for the exchange so that it cannot be discovered by AMQP applications, otherwise the feed name and exchange name are identical. When a RestMS client posts a message to the feed, the RestMS server publishes this message to the corresponding exchange, using as AMQP routing-key the address property of the message.

A rotator or service feed maps to an AMQP shared queue. These feeds route by distributing messages to pipes on a round-robin basis. If the feed is private, a hashed name is used for the queue so that it cannot be discovered by AMQP applications, otherwise the feed name is used as the queue name. When a RestMS client posts a message to the feed, the server publishes this to the default exchange, using as routing-key the feed name. If there is an address specified in the message envelope, this is used as the message-id, unless a message-id is also specified, in which case the address is not passed to the AMQP system.

The AMQP9 profile defines these join types:

  • the default join type which the client uses by creating a join with no type property. The default join type specifies an address pattern and selects messages depending on the feed semantics.

The AMQP9 profile defines these pipe types:

  • the default pipe type is implemented according to the 3/Defaults profile.
  • The ondemand pipe type works as the default pipe type, but contains at most one message at a time. The action of retrieving a message asynclet will fetch a message from the feed. The server SHOULD check that ondemand pipes are joined only to rotator or service feeds and respond with "403 Forbidden" if the client requests a join from an ondemand pipe to another feed type. The server MAY implement the ondemand pipe type.

The default pipe type is propagated to the AMQP network as bindings that use the server-assigned hash 'name' of the pipe. Pipes MAY be implemented as private queues on the AMQP server but this is not the only architecture.

The AMQP9 profile extends the message resource as follows:

<message address="{address literal}"
    [ delivery_mode = "{delivery-mode}" ]
    [ priority = "{priority}" ]
    [ correlation_id = "{correlation-id}" ]
    [ reply_to = "{reply-to}" ]
    [ expiration = "{expiration}" ]
    [ message_id = "{message-id}" ]
    [ timestamp = "{timestamp}" ]
    [ type = "{type}" ]
    [ user_id = "{user-id}" ]
    [ app_id = "{app-id}" ]
    [ sender_id = "{sender-id}" ]
    >
    [ <header name="{header name}" value="{header value}" /> ] ...
</message>
  • The priority is a value from 0 to 9 and may be used by the AMQP server to prioritise message delivery.
  • The timestamp is formatted according to the HTTP/1.1 specifications for date/time formats [3] and may be set by AMQP APIs to indicate the time of origin of a message.
  • The semantics of delivery_mode, correlation_id, expiration, type, user_id, app_id and sender_id are defined by AMQP implementations, APIs, and client applications.

AMQP currently does not implement multicontent messages. AMQP9 profile implementations SHOULD for the purposes of interoperability with AMQP restrict the number of contents per message to one.

The AMQP9 profile specifies the following additional server behaviour:

  • The server MUST implement the 3/Defaults profile.
  • When the client successfully POSTs a message to the a default type feed, the server MUST respond with a null (empty) document.

Guidelines for clients

To illustrate these example we use the Perl RestMS class library.

Publish-subscribe scenario

In this example we show how to implement a topic-based newsfeed service that distributes news articles using a hierarchical category structure. The newsfeed service works as follows:

  • A news publisher sends a stream of news covering various categories.
  • Clients subscribe to and receive messages from specific news categories.

We use the Parrot pattern with one publisher talking to an arbitrary number of clients. Here is the example news category tree:

rec
   |
   o- pets
   |  |
   |  o- cats
   |  |
   |  o- dogs
   |
   o- cars

News categories are written with periods separating each level, like this: "rec.pets.cats", according to the AMQP topic exchange specifications. Subscribers can request specific categories, or use wildcards: '*' specifies any value for one level, '#' specifies any value for any number of levels.

Our sample news stream contains these news items (we show the news category and the item title):

rec.pets.dogs   Montreal: Canine Championship series opens
rec.cars        The oil shock: does it affect you?
rec.pets.dogs   Steroids: the ugly truth from Montreal
rec.pets.cats   Cat vs. dog: facts or fictions?
rec.pets.dogs   Montreal in chaos: winner is a cat!
rec.cars        Red, white, or blue: what it says about you
rec.cars        Parking - who, when, where, why: a new survey
rec.pets.cats   Superiority: it comes naturally

Both applications start with standard code to work with the default domain:

use RestMS ();
my $hostname = (shift or "live.zyre.com");
my $domain = RestMS::Domain->new (hostname => $hostname, verbose => 0);

The subscriber creates a pipe and creates a join with the address "rec.pets.*":

#   Grab reference to topic feed called 'news'
my $newsfeed = $domain->feed (name => "news", type => "topic");
my $pipe = $domain->pipe ();
my $join = $pipe->join (feed => $newsfeed, address => "rec.pets.*");

#   Receive and print messages
while (1) {
    my $message = $pipe->recv;
    $message->carp ($message->address.": ".$message->content);
}

The publisher sends a series of messages to the feed:

#   Create a topic feed called 'news'
my $newsfeed = $domain->feed (name => "news", type => "topic");
my $message = RestMS::Message->new;

#   Create a message with embedded plain content
$message->content_type ("text/plain");
$message->encoding ("plain");

#   Now send our articles to the news feed
$message->content ("Montreal: Canine Championship series opens");
$message->send ($newsfeed, address => "rec.pets.dogs");
$message->content ("The oil shock: does it affect you?");
$message->send ($newsfeed, address => "rec.cars");
$message->content ("Steroids: the ugly truth from Montreal");
$message->send ($newsfeed, address => "rec.pets.dogs");
$message->content ("Cat vs. dog: facts or fictions?");
$message->send ($newsfeed, address => "rec.pets.cats");
$message->content ("Montreal in chaos: winner is a cat!");
$message->send ($newsfeed, address => "rec.pets.dogs");
$message->content ("Red, white, or blue: what it says about you");
$message->send ($newsfeed, address => "rec.cars");
$message->content ("Parking - who, where, why: a new survey");
$message->send ($newsfeed, address => "rec.cars");
$message->content ("Superiority: it comes naturally");
$message->send ($newsfeed, address => "rec.pets.cats");

We start the subscriber, and then run the publisher. The subscriber shows this output:

rec.pets.dogs: Montreal: Canine Championship series opens
rec.pets.dogs: Steroids: the ugly truth from Montreal
rec.pets.cats: Cat vs. dog: facts or fictions?
rec.pets.dogs: Montreal in chaos: winner is a cat!
rec.pets.cats: Superiority: it comes naturally

Request-response service scenario

In this example we show how to implement a service that returns a "fortune cookie". The fortune cookie service works as follows:

  • A client sends a request message to a fortune service (Wolfpack pattern).
  • The fortune service responds with a fortune message (Housecat pattern).

We have two applications, one being the 'service' and one being the 'client'. The service waits for incoming requests and responds to each request that it gets, in a loop. The client sends a request and waits for a response, a single time.

Both applications start with standard code to work with the default domain:

use RestMS ();
my $hostname = (shift or "live.zyre.com");
my $domain = RestMS::Domain->new (hostname => $hostname, verbose => 0);

The fortune service then creates the wiring: a service feed for clients to send requests to, a pipe to hold incoming requests, and a join to bind the two together. It then loops to receive incoming requests, and for each one it formats and returns a fortune response:

#   Create a feed called 'fortune' for clients to send requests to
my $fortune = $domain->feed (name => "fortune", type => "service");

#   Create an unnamed pipe and bind it to the feed
my $pipe = $domain->pipe (cached => 1);
my $join = $pipe->join (feed => $fortune);

#   Grab a reference to the default feed, to send replies to
my $default = $domain->feed (name => "default");

$default->carp ("Fortune cookie service initialized OK");

#   Now loop forever, processing requests
while (1) {
    #   See how we don't poll - this will wait until a message arrives
    my $request = $pipe->recv;

    #   Create a new response message
    my $response = RestMS::Message->new;

    #   Grab a fortune via the shell (hopefully fortune is on path)
    $response->content (`fortune`);
    $response->content_type ("text/plain");

    #   Send the response back via the direct feed
    #   We use the reply-to address provided in the request
    $response->send ($default, address => $request->reply_to);
}

The fortune client is a bit simpler. It creates a pipe and joins this to the default feed. It posts a request message to the fortune feed, then waits for a response:

#   Grab a reference to the 'default' feed, so we can get our replies
my $default = $domain->feed (name => "default");

#   Create a pipe for our replies
my $pipe = $domain->pipe ();

#   Grab a reference to the 'fortune' feed
my $fortune = $domain->feed (name => "fortune", type => "service");

#   Send a request to the fortune feed
my $request = RestMS::Message->new;
$request->send ($fortune, reply_to => $pipe->name);

#   Wait for the response, and print it
my $response = $pipe->recv;
print $response->content;

#   Free up the pipe, which we no longer need
$pipe->delete;

To see the low-level HTTP requests and responses these applications generate, set the verbose argument to '1' when you create the domain.

Checking profile availability

Clients SHOULD check that the server properly announces its support for the AMQP9 profile before using it. The server indicates that it supports the AMQP9 profile by listing it as a child of the domain:

Client:
-------------------------------------------------
GET /restms/domain/default

Server:
-------------------------------------------------
HTTP/1.1 200 OK
Content-Type: application/restms+xml

<?xml version="1.0"?>
<restms xmlns = "http://www.restms.org/schema/restms">
    <domain name = "default" title = "Default domain">
        <profile name = "4/AMQP9"
            href = "http://www.restms.org/spec:4/AMQP9" />
    </domain>
</restms>

Guidelines for server implementers

Integration with AMQP networks

The AMQP9 profile is designed to be able to interoperate with AMQP. This works as follows:

  • A RestMS domain maps to an AMQP network (an AMQP server plus connection credentials) according to the RestMS server configuration.
  • A feed maps to an AMQP exchange or shared queue, depending on the feed type.
  • A message posted by a RestMS application to a mapped feed will be sent to the AMQP server where it can be accessed by AMQP applications via the AMQP mechanisms of bindings and queues.
  • A message posted by an AMQP application to a mapped feed will be sent to the RestMS server where it can be accessed by RestMS application via the RestMS mechanisms of joins and pipes.

RestMS-AMQP message routing

Sane use of AMQP demands that applications agree in advance on their routing architecture. This requires agreement on the names and types of exchanges used, and the allowed values for routing keys used in bindings and messages. To assure routing between RestMS and AMQP applications we need similar conventions.

We start by re-iterating the three fundamental messaging patterns:

  1. In which a request is sent to a remote "service", implemented by one or more service applications (the Wolfpack pattern).
  2. In which the response to a service request is sent back to the original requesting application (the Housecat pattern).
  3. In which a publisher distributes data to multiple subscribers (the Parrot pattern).

To create dependable RestMS-AMQP message routing, these guidelines apply:

  • A service MUST correspond to a shared queue in AMQP terms, or a service or rotator feed in terms of the AMQP9 profile. AMQP service applications MUST consume from the shared queue. RestMS service applications MUST create a pipe and create a join from the pipe to the feed. A service MAY be implemented transparently by any mix of AMQP and RestMS applications, with messages being served in a round-robin fashion to each implementing application.
  • A request message that needs a reply MUST provide a reply_to attribute. For AMQP clients this MUST be the name of a private response queue. For RestMS clients this MUST be the name (server-generated hash) of a pipe.
  • Services that wish to send replies MUST use the reply_to attribute of the request message. If they are AMQP service applications, they MUST publish the reply to the default exchange using the reply_to attribute as routing key. If they are RestMS service applications, they MUST post the reply to the default feed using the reply_to attribute as address.
  • For data distribution, a fanout, topic, or headers feed maps to an AMQP exchange, and subscribers can be either RestMS clients or AMQP clients. RestMS clients create pipes and join them to the feeds they want to consume from, specifying the address pattern in each case. AMQP clients create private queues and bind them to the exchanges they want to consume from, specifying the routing key in each case.

The RestMS extension class

In order to interoperate with a RestMS server, an AMQP server must be able to mirror resources. Thus, if a RestMS client creates a feed, a matching exchange or shared queue should be created on the AMQP server, so that AMQP applications have access to it. And vice-versa, if an AMQP client creates a shared queue or exchange, this should be mirrored to the RestMS server as a feed.

There are some significant differences between the RESTful model we implement in RestMS, and the AMQP model for resource management. It would be possible, in theory and in time, to modify the AMQP protocol to become compatible with RestMS. For example, commands like Queue.Delete would need to become idempotent so that attempting to delete a non-existent queue was safe. Today, AMQP treats such attempts as fatal errors.

Rather than attempt to modify existing AMQP semantics, we propose an AMQP "extension class", an extension mechanism that AMQP allows. The advantage of an extension class is that a RestMS server can detect immediately whether its target AMQP server supports it, or not. There is no ambiguity. This class uses index 61501, which falls into the space allotted for extension classes.

<?xml version="1.0"?>
<!--
    Copyright (c) 1996-2009 iMatix Corporation

    This code is licensed under both the GPLv3 and the IETF licence, in accordance
    with the terms of the http://www.restms.org Intellectual Property Policy.
 -->
<class
    name    = "restms"
    handler = "connection"
    index   = "61501"
  >
  RestMS resource management class.
<doc>
    Provides methods to work with server-side resources as defined by
    the RestMS specification.  All methods are request-only, without
    response.  Errors are logged at the server side and not reported
    to the client.  This model is designed to allow an AMQP client to
    push state to an AMQP server, or vice-versa, rapidly and without
    handshaking.
</doc>

<doc name = "grammar">
    restms              = C:PIPE-CREATE
                        / C:PIPE-DESTROY
                        / C:FEED-CREATE
                        / C:FEED-DESTROY
                        / C:JOIN-CREATE
                        / C:JOIN-DESTROY
</doc>

<chassis name = "server" implement = "MAY" />
<chassis name = "client" implement = "MAY" />

<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->

<method name = "pipe-create" index = "10">
  create a pipe
  <doc>
  Creates a pipe of the specified type.  The pipe may already exist,
  if it has the same type.  Pipe names are unique across all types.
  </doc>
  <chassis name = "server" implement = "MUST" />
  <field name = "pipe type" type = "shortstr" >
    pipe type
    <doc>
    Specifies the type of the pipe to create.  Valid values are: pipe.
    </doc>
  </field>
  <field name = "pipe name" type = "shortstr" >
    Name of pipe
    <doc>
    Specifies the name of the pipe to create.  Pipe names may not contain
    slashes, spaces, or at signs.
    </doc>
  </field>
</method>

<method name = "pipe-delete" index = "20">
  delete a pipe
  <doc>
  Deletes a specified pipe, if it exists.  Safe to invoke on non-existent
  or already-deleted pipes.
  </doc>
  <chassis name = "server" implement = "MUST" />
  <field name = "pipe name" type = "shortstr" >
    pipe name
    <doc>
    Specifies the name of the pipe to delete.
    </doc>
  </field>
</method>

<method name = "feed-create" index = "30">
  create a feed
  <doc>
  Creates a feed of the specified type.  The feed may already exist,
  if it has the same type.  Feed names are unique across all types.
  </doc>
  <chassis name = "server" implement = "MUST" />
  <field name = "feed type" type = "shortstr" >
    Feed type
    <doc>
    Specifies the type of the feed to create.  Valid values are: fanout,
    direct, topic, headers, system, rotator, and service.
    </doc>
  </field>
  <field name = "feed name" type = "shortstr" >
    Name of feed
    <doc>
    Specifies the name of the feed to create.  Feed names may not contain
    slashes, spaces, or at signs.
    </doc>
  </field>
</method>

<method name = "feed-delete" index = "40">
  delete a feed
  <doc>
  Deletes a specified feed, if it exists.  Safe to invoke on non-existent
  or already-deleted feeds.
  </doc>
  <chassis name = "server" implement = "MUST" />
  <field name = "feed name" type = "shortstr" >
    feed name
    <doc>
    Specifies the name of the feed to delete.
    </doc>
  </field>
</method>

<method name = "join-create" index = "50">
  create a join
  <doc>
  Creates a join on the specified pipe and feed.  The join may already
  exist, if it has the same properties.  A join will causes messages to
  be delivered on the connection.  The consumer-tag property allows
  messages to be routed into end-application pipes.  Joins on exchange
  feeds use the consumer tag "x:{pipe-name}" and joins on queue feeds
  use the consumer tag "q:{pipe-name}".  AMQP does not allow the same
  tag to be used on multiple queues.
  </doc>
  <chassis name = "server" implement = "MUST" />
  <field name = "pipe name" type = "shortstr" >
    Name of pipe
    <doc>
    Specifies the name of the pipe, which must exist.
    </doc>
  </field>
  <field name = "feed name" type = "shortstr" >
    Name of feed
    <doc>
    Specifies the name of the feed, which must exist.
    </doc>
  </field>
  <field name = "address" type = "shortstr" >
    Join address
    <doc>
    Specifies the address to join.  This is an address literal or
    pattern who's semantics depend on the feed type.  The address
    may not contain slashes, spaces, or at signs.
    </doc>
  </field>
</method>

<method name = "join-delete" index = "60">
  delete a join
  <doc>
  Deletes a specified join, if it exists.  Safe to invoke on non-existent
  or already-deleted joins, and referring to non-existent pipes and/or
  feeds.
  </doc>
  <chassis name = "server" implement = "MUST" />
  <field name = "pipe name" type = "shortstr" >
    Name of pipe
    <doc>
    Specifies the name of the pipe, which does not need to exist.
    </doc>
  </field>
  <field name = "feed name" type = "shortstr" >
    Name of feed
    <doc>
    Specifies the name of the feed, which does not need to exist.
    </doc>
  </field>
  <field name = "address" type = "shortstr" >
    Join address
    <doc>
    Specifies the join address.
    </doc>
  </field>
</method>

</class>

Resource synchronisation

The RestMS extension class defined above provides a clean way for the RestMS server to synchronize all its resources with an AMQP server, and vice-versa. Since the commands are asynchronous and not confirmed, they can be executed very rapidly, so that one server can synchronize its resources on another (for example, after failover) at the rate of tens of thousands of resources per second.

To avoid "storms", a server SHOULD track the origin of resources, and synchronise only those which were created by its own local clients.

When a server receives resource specifications from another server, these resources SHOULD be treated as configured and clients SHOULD not be allowed to delete them except on the original server.

Server implementations MAY take any reasonable approach to resources that are "orphaned", i.e. where their original owning server has disconnected and/or gone off line.

Message routing

The AMQP9 profile does not impose a specific architecture for interconnection with an AMQP network and there are several possibilities which we explain for the benefit of implementers.

In practice, AMQP servers may be paired for high-availability, and/or may be federated themselves into larger AMQP networks. In this discussion we will assume that the RestMS server speaks to exactly one AMQP server.

The first model relies on the AMQP server to do all routing. In this case, the RestMS server will synchronise all feeds, pipes, and joins with the AMQP server. All RestMS pipes and joins are instantiated as private queues and bindings on the AMQP server. Pipes are implemented in the simplest fashion: each pipe has an exclusive queue with the same name, with a consumer with tag x:{pipe-name}. When the RestMS server creates joins on the pipe, these are implemented as bindings that bring messages into the private queue. This is for exchange-based feeds. For queue-based feeds, the pipe is implemented as a consumer on the shared queue, with tag q:{pipe-name}. These distinct tags are used by the RestMS server to route messages coming from the AMQP server into separate pipes for end-application delivery.

When a RestMS client posts a message to a feed, the RestMS server forwards that message to the AMQP server. When an AMQP client publishes a message, this also arrives on the AMQP server. The AMQP server then routes that to all matching bindings, and thus into private queues. Message are then delivered to the RestMS server, which uses the consumer tag to sort the messages into their pipes. RestMS clients can then retrieve their messages from their pipes.

A second model is to open multiple connections or channels, e.g. one per pipe, and to use these to segment messages per pipes.

The advantage of these two model is that they are easy to understand and implement. The main disadvantage is that messages will be sent redundantly, if they match multiple joins/bindings. This will waste LAN bandwidth.

The optimal, but most complex model, is to use federation-style normalization. In this model, the RestMS server maintains its own routing data structures, and forwards binding requests to the AMQP server. When messages arrive, they are routed not on consumer tag, but according to the message routing key and/or other properties. This model demands that the RestMS implementation has the same routing capabilities as the AMQP server, i.e. implements exchanges and shared queues in much the same way. The advantage of this model is that it allows for stand-alone RestMS operation, and is the optimal design for RestMS-to-RestMS interoperation (with no extra hops to and from the AMQP server).

Finally, a RestMS server does not need to work with an AMQP network, it can be totally self-standing. This is plausible in a federated design, where the RestMS server may run alone, or as a federated node, depending on its configuration.

Bibliography
1. "The Advanced Message Queueing Protocol" - amqp.org
2. "Key words for use in RFCs to Indicate Requirement Levels" - ietf.org
3. "HTTP/1.1 Protocol Parameters" - http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html w3.org]

Edit | Files | Tags | Source | Print | Talk

Use the Talk page to discuss this specification.

Use one of these tags to define the specification's state:

  • raw - new specification
  • draft - has at least one implementation.
  • stable - has been deployed to real users.
  • legacy - is being replaced by newer specifications.
  • retired - has been replaced and is no longer used.
  • deleted - abandoned before becoming stable.
raw