Chapter 11. Serializing and deserializing the current value of an object

Index

Introduction
Requirements
stasher::create_manage_object<classptr>(): call a functor after deserializing an object
stasher::currentbaseObj<classptr>: virtual superclass for stasher::create_manage_object()'s functors
stasher::current<classptr>, stasher::currentptr<classptr>: a basic stasher::currentBaseObj subclass

Introduction

The manager methods described in Chapter 10, Asynchronous connection manager provide stable means for automatically retrieving objects from stasher when the objects get updated. The objects get retrieved as read-only file descriptors.

LIBCXX provides templates that serialize and deserialize class instances to file descriptors. This part introduces templates and methods that get combined with the manager interface into a set of templates and classes that deserialize the current value of an object in a stasher object repository.

The manager notifies the templates when the object in the object repository changes. The templates and methods generate code that retrieve and deserialize the raw file descriptor into some class instance.

Requirements

  • The class must be a reference-counted object that's virtually subclassed from x::obj. See LIBCXX's documentation for more information.

  • The class must implement two non-default constructors. The first constructor that takes two parameters, an x::uuid and a file descriptor. This constructor gets called when the object is retrieved from the repository. The constructor, presumably, gets to read the file descriptor and initialize the class instance from its contents. Although LIBCXX's serialization and deserialization templates are a convenient way to do so, their usage is not required. The constructor gets the raw file descriptor, and it's up to the constructor to read what it needs to read from it.

    If the constructor throws an exception, the presumed reason is that the object file in the object repository got corrupted or cannot be parsed (created by a previous version of the application, or something along the lines). It's preferrable for the first constructor to handle that, but if that gets handled by throwing an exception, the second non-default constructor gets called, which takes only a x::uuid parameter.

    The intention here is to initialize a default object instance that replaces the unparsable object in the repository, but to do that the existing object's uuid must be known. For that reason, the constructor gets called with the existing object's uuid, with the presumption that whenever the instantiated object gets serialized later, its valid, serialized contents replace the unparsable object in the repository.

  • These constructors get typically invoked from a client connection thread callback. As such, it has certain limitations, see the section called “What asynchronous C++ API methods can and cannot do” for more information.

  • In most cases the class also needs to implement a default constructor.

The examples in this chapter use the following class as an example:

#ifndef inventory_H
#define inventory_H

#include <x/obj.H>
#include <x/ref.H>
#include <x/ptr.H>
#include <x/uuid.H>
#include <x/fd.H>
#include <x/fditer.H>
#include <x/serialize.H>
#include <x/deserialize.H>
#include <x/exception.H>

#include <map>
#include <iostream>

// An inventory of stuff

class inventoryObj : virtual public x::obj {

public:

	// This is the inventory, key product name, int is the number in stock.

	std::map<std::string, int> stock;

	// uuid of this object in the repository

	x::uuid uuid;

	// Default constructor

	inventoryObj() {}

	// Destructor
	~inventoryObj() {}

	// Copy constructor
	inventoryObj(const inventoryObj &o) : stock(o.stock), uuid(o.uuid)
	{
	}

	// Serialization function

	template<typename iter_type>
	void serialize(iter_type &iter)
	{
		iter(stock);
	}

	// Construct from an object in the repository

	inventoryObj(const x::uuid &uuidArg, const x::fd &fd) : uuid(uuidArg)
	{
		x::fdinputiter b(fd), e;

		x::deserialize::iterator<x::fdinputiter> iter(b, e);

		serialize(iter);
	}

	// The above constructor threw an exception, so the manager calls this
	// one. We should report an error somehow, the inventory object must be
	// corrupted, but this is just an example.

	inventoryObj(const x::uuid &uuidArg) : uuid(uuidArg)
	{
	}
};

typedef x::ref<inventoryObj> inventory;

typedef x::ptr<inventoryObj> inventoryptr;
#endif

This is a fairly straightforward, simple class, with a serialization function, and a constructor that employs the serialization function to deserialize the existing object from a file descriptor. An instance of this class gets stored in an object in the stasher object repository. Use the following program to update this object:

#include <stasher/client.H>
#include "inventory.H"

#include <sstream>
#include <iterator>

std::string apply_updates(const inventory &i,
			  int argc, char **argv);

void setinventory(int argc, char **argv)
{
	if (argc < 2)
		return;

	auto client=stasher::client::base::connect();

	stasher::client::base::transaction tran=
		stasher::client::base::transaction::create();

	// Get the existing object, if it exists, first.

	stasher::client::base::getreq req
		=stasher::client::base::getreq::create();

	req->openobjects=true;
	req->objects.insert(argv[1]);

	stasher::contents contents=client->get(req)->objects;

	if (!contents->succeeded)
		throw EXCEPTION(contents->errmsg);

	auto existing=contents->find(argv[1]);

	if (existing == contents->end())
	{
		// Object did not exist, create a new one.

		std::string i=apply_updates(inventory::create(), argc, argv);

		if (i.empty())
			return; // New one is empty, nothing to do

		tran->newobj(argv[1], i);
	}
	else
	{
		// Update an existing object.
		auto i=apply_updates(inventory::create(existing->second.uuid,
						       existing->second.fd),
				     argc, argv);

		// If there's no inventory at all, delete it, else update it.
		if (i.empty())
			tran->delobj(argv[1], existing->second.uuid);
		else
			tran->updobj(argv[1],
				     existing->second.uuid, i);
	}

	// Note that this is a basic, simple example, that doesn't check for
	// req_rejected_stat, so if two updates occur at the same time, one
	// is going to get tossed out. This program is for demo purposes only.
	stasher::putresults results=client->put(tran);

	if (results->status != stasher::req_processed_stat)
		throw EXCEPTION(x::tostring(results->status));

	std::cout << "Updated" << std::endl;
}

// Apply updates to the inventory in an existing object.

std::string apply_updates(const inventory &i,
			  int argc, char **argv)
{
	for (int n=2; n+1<argc; n += 2)
	{
		int count=0;

		std::istringstream(argv[n+1]) >> count;

		std::string name(argv[n]);

		if (count == 0)
			i->stock.erase(name);
		else
			i->stock[name]=count;
	}

	// Now, serialize it into a string.

	std::string buf;

	if (i->stock.empty())
		return buf; // Empty inventory, return an empty string.

	typedef std::back_insert_iterator<std::string> ins_buf_iter_t;

	ins_buf_iter_t iter(buf);

	x::serialize::iterator<ins_buf_iter_t> serialize(iter);

	i->serialize(serialize);

	return buf;
}

int main(int argc, char **argv)
{
	try {
		setinventory(argc, argv);
	} catch (const x::exception &e)
	{
		std::cerr << e << std::endl;
		return 1;
	}
	return 0;
}

The first parameter to setinventory is the name of an object for this inventory class. The remaining parameters is a list of name and count parameters. setinventory adds them or replace them in the inventory object. For example:

$ ./setinventory instock apples 2 bananas 3

This puts a key of apples with the value of 2, and bananas with the value of 3 into inventoryObj's std::map (a 0 count removes that key from the std::map). If an object named instock does not exist, it gets created, otherwise its existing contents are read and updated.

Note

This simplistic example does not handle the stasher::req_rejected_stat error indicating that something else updated the same object at the same time. For these simple examples it's presumed that only setinventory updates them.