We’re sorry we missed you at re:Invent, but we can still meet!

Did someone say collection data types? Oh, we did!

Learn how to use lists, sets, and dictionary collection data types.

アレン・ヘルトン
著者

Share

Today feels like a good day for a launch. A launch that you all have been waiting for. How do I know? You told us yourselves!

How does a launch of a new cacheable data type sound? What about three new cacheable data types?!

If you’ve been thinking to yourself, “gee, I wish I could cache objects, ordered lists, and sets of unique elements,” good news! Starting today, you can!

We are releasing support for the following collection data types:

  • List  An array that allows duplicate elements and remembers the order they were inserted
  • Set – An unordered array that stores distinct elements
  • Dictionary – A field/value object store that allows you to “pick and choose” individual key values for gets and sets

Get specifics about the new data types.

How to use collection data types

Those sound super cool, I know. But if you’re wondering how you can use them in real life, we got you covered. 

Lists

A list persists the order you add elements to it. It will also allow duplicate elements. Let’s take a chat room for a fictitious game, Acorn Hunt. Players work together in a timed run to gather as many acorns as possible. During the game, players can chat to coordinate with each other. When the game is over, the chat history goes away.

Player chat needs to be listed chronologically. Momento lists solve this use case perfectly with the listPushBack function call. Consider this endpoint that adds a message to the chat.

//
// POST /games/{gameId}/messages
//
exports.handler = async (event) => {
  const { gameId } = event.pathParameters;
  const { userName } = event.authorizer.context;
  const input = JSON.parse(event.body);

  const message = { userName, message: input.message };

  const momento = await getMomentoClient();  
  await momento.listPushBack(CACHE_NAME, gameId, JSON.stringify(message));

  return { statusCode: 201 };
}

And just like that we have a functional chat room! When we call the GET endpoint to view our chat messages, we’d get a response like this:

//
// GET /games/{gameId}/messages
exports.handler = async (event) => {
  const { gameId } = event.pathParameters;
  const momento = await getMomentoClient();
  const chat = await momento.listFetch(CACHE_NAME, gameId);
  const messages = chat.valueListString().map(message => JSON.parse(message));

  return { 
    statusCode: 200,
    body: JSON.stringify(messages)
  };
}

And just like that we have a functional chat room! When we call the GET endpoint to view our chat messages, we’d get a response like this:

[
  {
    userName: 'mo-the-squirrel',
    message: 'Can someone get that acorn by the big oak tree?'
  },
  { 
    userName: 'honey-badger', 
    message: 'on it' 
  },
  { 
    userName: 'mo-the-squirrel', 
    message: 'thx!!' 
  }
]

Don’t be fooled by the simplicity. Lists are powerful structures and include some useful additional features like popping elements from the front or back of the list, adding multiple elements at once, and removing all elements with a specific value.

Learn more about what you can do with lists.

Sets

A set will provide a distinct collection of elements. If order isn’t important but de-duplication is, we’d use a set.

Back to our game example, we can use a set to keep track of the players in a game. A player can’t be in the same game twice and it doesn’t matter what order the players are in. In the event one of your players has an intermittent connection where they keep disconnecting and reconnecting to the game, a set is highly valuable because you guarantee you won’t include them twice.

If I am adding a player to a game, I can implement it using the setAddElement command.

//
// POST /games/{gameId}/players
//
exports.handler = async (event) => {
  const { gameId } = event.pathParameters;
  const { userName } = event.authorizer.context

  const momento = await getMomentoClient();
  await momento.setAddElement(CACHE_NAME, gameId, userName);

  return { statusCode: 201 }
}

No matter how many times this endpoint is retried or called upon, that one username will only be included in the set for this game one time.

To get all of the players, we can implement an endpoint using the fetchSet command.

//
// GET /games/{gameId}/players
//
exports.handler = async (event) => {
  const { gameId } = event.pathParameters;
  const { userName } = event.authorizer.context

  const momento = await getMomentoClient();  
  const playerSet = await momento.setFetch(CACHE_NAME, gameId);  
  const players = Array.from(playerSet.valueSetString());

  return { 
    statusCode: 200,
    body: JSON.stringify(players)
  };
}

When we call the GET for the list of players, we get an array of all player usernames.

[
  "mo-the-squirrel",
  "honey-badger",
  "nuts-about-nuts"
]

No duplicates, no nonsense. Just an accurate collection of elements with exactly the information we need.

Learn more about what you can do with sets.

Dictionaries

Before we released collection data types, users could store entire stringified items in Momento Cache. When you wanted to fetch a specific piece of data from that item, you had to pull the entire thing out of the cache, pare it down to the info you want, and send the details back. That’s what we call e-waste. There’s no need to fetch everything when you only need one or two details. It’s more environmentally sustainable and a little lighter on your wallet to boot!

In Acorn Hunt, imagine we store some metadata for all the users in our current game. In some instances in the game, we want to load all of a player’s metadata—like when we load their profile. In other instances, we only want a subset of data, like their level or which clan they belong to. We can build an endpoint that loads all or a subset of data about the player with a dictionary.

//
// GET /users/{userName}
//
const handler = async (event) => {
  const { userName } = event.pathParameters;
  const attributes = event.queryStringParameters?.attributes;

  let details;
  if (attributes) {
    const dictionaryFields = attributes.split(',').map(att => att.trim());
    const fields = await momento.dictionaryGetFields(CACHE_NAME, userName, dictionaryFields);
    details = fields.valueRecord();
  } else {
    const player = await momento.dictionaryFetch(CACHE_NAME, userName);
    details = player.valueRecord();    
  }
  
  return {
    statusCode: 200,
    body: JSON.stringify(details)
  };
}

When called directly, this endpoint would return all of the user metadata. However, when a query string parameter is provided, we only load specific fields from the metadata. Take a peek at how the responses would change using dictionaryFetch versus dictionaryGetFields to load different information.

// GET /users/mo-the-squirrel
{
  "userName": "honey-badger",
  "level": "49",
  "clan": "Flying Squirrelz",
  "winRatio": ".8",
  "name": "Mo"
}

// GET /users/mo-the-squirrel?attributes=level,clan
{
  "level": "49",
  "clan": "Flying Squirrelz"
}

Scoping data down to only the fields we need lowers not only our data transfer costs but our overall latency as well. Win-win! 

Updating elements in a dictionary is trivial. Simply call the dictionarySetField command and it will add or update the value with the given key.

await momento.dictionarySetField(CACHE_NAME, userName, 'level', '50');

Learn more about what you can do with dictionaries.

This feels familiar…

If you’re a Redis user, some of this might sound familiar to you. They do a great job with lists, sets, and hashes, but we found some ways to tackle them a little differently that led to some great results.

Auto-truncate

Imagine you are pushing data into a list of fixed size. You typically have one of two options: pop data then push, or push data freely and run a background job periodically to trim the list down to size. Sounds like a lot of unnecessary compute and network traffic to push data to a fixed length list, right? With lists, we allow you to do this in one fell swoop with the truncateFrontToSize と truncateBackToSize parameters. 

Imagine we wanted to limit the max number of visible messages in Acorn Hunt to 30. By updating our listPushBack call, we can do that with a single command!

const MAX_CHAT_SIZE = 30;
const options = { truncateFrontToSize: MAX_CHAT_SIZE };
await momento.listPushBack(CACHE_NAME, gameId, JSON.stringify(message), options);

Faster and simpler. Boom💥

Byte support

The Momento SDK natively supports both strings and bytes. This means you can directly store and retrieve your data as bytes without making an additional call to base64 encode your content. Base64 encoding increases your data size, which leads to unnecessary inefficiencies and more e-waste.

Let’s take another example. Imagine we wanted to store a thumbnail of the player’s avatar in their metadata. Using Amazon S3 as the file store, we can load the avatar and cache it in the user profile with the following code.

const avatarBytes = await getAvatarBytes(avatarKey);
await momento.dictionarySetField(CACHE_NAME, userName, 'avatar', avatarBytes);

const getAvatarBytes = async (objectKey) => {
  const response = await s3.send(new GetObjectCommand({
    Bucket: 'acorn-hunt-avatars',
    Key: objectKey
  }));
  const stream = response.Body;
  return new Promise((resolve, reject) => {
    const chunks = [];
    stream.on('data', (chunk) => chunks.push(chunk));
    stream.on('end', () => resolve(Buffer.concat(chunks)));
  })
};

Storing the bytes directly in the cache streamlines operations and gets the image rendering as quickly as possible.

Streamlined Time to Live (TTL)

All cached data expires. With Momento Cache, you might be familiar with the automatic expiration of data with standard cache items. We know that not all cache items are the same and some of them might need a longer life than others. This holds true not only with items, but with CDTs as well.

With our newly introduced collection data types, you can set individual TTL settings inline when you create or update any collection type. If we allow users in Acorn Hunt to configure the duration of the game, we could update the code to set the TTL of the player list.

const options = { ttl: CollectionTtl(gameSettings.duration) };
await momento.setAddElement(CACHE_NAME, gameId, userName, options);

Other caching solutions require multiple calls to set data and configure the TTL. But with our new collection data types, it’s done with a single call!

Learn more about what you can do with collection data type TTLs.

Spiky workload? We’ve got you covered

Bursts happen to all of us. Our application could be operating with a standard influx of requests when a sudden surge in traffic takes us from 2 requests per second (RPS) to 2,000 RPS. 

In serverless applications with traditional caching solutions, this becomes a problem. When Lambda scales up to meet demand, it initializes a new connection for each new execution environment. When your cache doesn’t scale with the same elasticity as the rest of your application, you run into a scaling bottleneck that causes serious downstream issues. 

Scaling bottlenecks lead to Too Many Request errors which negatively impact performance and could ultimately result in data loss. Nobody wants that.

With Momento Cache, your caching workloads scale alongside Lambda seamlessly. No dropping connections, no errors telling you to slow down, no data loss. 

With collection data types, we handle traffic spikes like a pro. Whether you’re caching a single item, pushing a handful of elements to a list, updating field values in a dictionary, or removing an entire set from your cache, we got you.

Ready to take it for a spin?

If you’re anything like us, you’re probably itching to go try these out for yourself! As of today, you can try them with the following SDKs:

If we don’t currently support your favorite language, keep in touch! We’re actively working on full support for Go, Rust, and Java.

Think these are cool? Just wait. Momento collection data types have more tasty treats in store—soon Acorn Hunt will be able to implement some terrific new features!

We are building Momento collection data types based on customer feedback and we want to keep the ball rolling! 試してみる and tell us what you like and don’t like. Do CDTs spark an idea for something super cool? Tell us! 

Feel free to reach out via our お問い合わせフォーム, our Discord, or directly on Twitter!

Share