A simple C++ client: getting and modifying objects

#include <iostream>
#include <stasher/client.H>
#include <stasher/userinit.H>
#include <x/fmtsize.H>

#include <vector>
#include <string>
#include <sstream>

void simpleget(int argc, char **argv)
{
	stasher::client client=stasher::client::base::connect();

	stasher::userinit limits=client->getlimits();

	std::cerr << "Maximum "
		  << limits.maxobjects
		  << " objects, "
		  << x::fmtsize(limits.maxobjectsize)
		  << " aggregate object size, per transaction."
		  << std::endl
		  << "Maximum "
		  << limits.maxsubs
		  << " concurrent subscriptions." << std::endl;

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

	req->openobjects=true;
	req->objects.insert(argv+1, argv+argc);

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

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

	for (int i=1; i<argc; ++i)
	{
		stasher::contents::base::map_t::iterator p=
			contents->find(argv[i]);

		std::cout << argv[i];

		if (p == contents->end())
		{
			std::cout << ": does not exist" << std::endl;
			continue;
		}

		std::cout << " (uuid " << x::tostring(p->second.uuid)
			  << "): ";

		x::istream is=p->second.fd->getistream();

		std::string firstline;

		std::getline(*is, firstline);
		std::cout << firstline << std::endl;
	}
}

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

	return 0;
}

stasher::client is an x::ref handle that represents a connection to the object repository server. Following the normal naming conventions for LIBCXX's reference-counted object, a stasher::clientptr is an x::ptr type.

stasher::client::base::connect connects to the default object repository node on the server server and returns the connection handle.

As documented in the class reference, an overloaded connect() takes an explicit directory name for an object repository node, if it's not installed in the stasher library's default location. If there are more than one object repository nodes, use defaultnodes() to enumerate them, then pick one for the connect().

connect() throws an x::exception if the connection fails. Alternatively, connect_client() allocates a connection handle, and the connection attempt gets made the first time the handle gets used in some way.

No explicit error or exception gets reported in the event that the postponed connection attempt fails. Rather, the corresponding request fails with an appropriate error code or indication. When an existing, established, connection with the object repository server breaks, all pending/outstanding requests fail with an appropriate error code, and another connection attempt gets made when the next request is made.

The administrative connection functions that are described in the documentation are available only to root or processes running under the same userid as the object repository node's daemon, and they enable some additional methods that are described in the client handle object's reference documentation; ordinary applications do not need them.

Client connection threads

stasher::client starts a thread, which handles the connection details. The thread stops automatically upon disconnection from the server. The thread also stops if the connection to stasher server terminates for any reason (but, as described above, it gets restarted automatically, by the next request of any kind).

Connection limits

A single application can create more than one client handle, to different repositories (if multiple repositories exist), or to the same object repository; however object repository servers have a server-configured limit on the maximum number of connections they'll accept from the same application. There are also several per-connection limits. The above example invokes getlimits(), and shows what those limits are: a single transaction can update/replace a maximum number of objects in the same transaction, and the total size of all new or replaced objects, in aggregate (meaning the sum total of the number of bytes in the new values of all new or replaced objects) is also limited. There's also a limit on the maximum number of open subscriptions.

getlimits() only works when a connection to the server has been established. If the client handle is disconnected, stasher::userinit returns all the limits as zeroes. It's a roundabout way of checking the current status of the client handle.

stasher::client->get(): get objects from the repository

This method takes a stasher::client::base::getreq as a parameter. This is an x::ref to an object with two documented members: a bool openobjects (defaults to false); and a std::set <std::string> objects, which enumerates the names of objects to retrieve from the object repository.

objects lists the names of objects to retrieve, in one transaction. They cannot exceed the limit on the maximum number of objects in a transaction.

The objects are arranged in a hierarchy, with / as a hierarchy separator, like a filesystem but with two key differences: there is no concept of a current directory and all object names are absolute, without the leading /; and there is no explicit equivalent to creating or removing directories. Put an object named fruits/apple, and the fruits hierarchy appears, if it did not exist before. Remove the fruits/apple object, and the fruits hierarchy disappears unless some other object is still in there.

Although stasher does not explicitly interpret the objects in any way, aside from implementing the object hierarchy, the client convention is to use the UTF-8 codeset.

stasher::client->get() returns an x::ref to an object that's not particularly interesting except for its objects member, which is a stasher::contents. This is an x::ref to a subclass of a std::map, specifically a stasher::contents::base::map_t. But before getting there, check succeeded first. It's a bool, with true indicating a succesful request. If false, look at the std::string error message in errmsg.

If an object name that was placed in stasher::client::base::getreq's objects is not in stasher::contents, this means that the object does not exist. If it does exist, the map's second is a stasher::retrobj which has two members: an x::uuid uuid that gives the object's uuid, and an x::fdptr fd.

When an object gets added to the object repository, it acquires a x::uuid assigned to it by the server, and it gets returned here, by stasher::client->get(). fd is an open read-only file descriptor, if stasher::client::base::getreq's openobjects was true file descriptor. If it was false, only each object's x::uuid gets returned, and the x::fdptr is null.

Note

The returned file descriptor is an open read-only file descriptor to the actual object file in the repository. The application should not sit on it, but read it, and get rid of it, as in this example (since the x::fd is a reference-counted object, this gets taken care of when the reference goes out of scope completely and gets destroyed). Even if the object gets deleted from the repository, this file descriptor will remain open, and the object's space on disk remains used, until the file descriptor gets closed.

stasher::client->put(): update objects in the repository

#include <iostream>
#include <stasher/client.H>

void simpleput(int argc, char **argv)
{
	if (argc < 2)
		throw EXCEPTION("Usage: simpleput {object} {value}");

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

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

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

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

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

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

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

	const char *what;

	if (existing != contents->end())
	{
		if (argc > 2)
		{
			what="updated";
			tran->updobj(argv[1], existing->second.uuid, argv[2]);
		}
		else
		{
			what="deleted";
			tran->delobj(argv[1], existing->second.uuid);
		}
	}
	else
	{
		if (argc > 2)
			tran->newobj(argv[1], argv[2]);
		else
			throw EXCEPTION("Object doesn't exist anyway");
		what="created";
	}

	stasher::putresults results=client->put(tran);

	if (results->status == stasher::req_processed_stat)
	{
		std::cout << "Object "
			  << what
			  << ", new uuid: "
			  << x::tostring(results->newuuid)
			  << std::endl;
	}
	else
	{
		throw EXCEPTION(x::tostring(results->status));
	}
}

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

	return 0;
}

This method takes a stasher::client::base::transaction as a parameter. This is an x::ref to an object that defines which objects the transaction updates.

This example takes the name of an object and its new value as command line parameters. get() gets invoked to check if the object already exists. Note that in this case, stasher::client::base::getreq's openobjects flag does not get set to true. Here the existing object's contents are not needed, this is just a check if the object already exists, and what it's x::uuid is.

stasher::client::base::transaction is just a structure (it's an x::ref, actually) that holds a list of repository objects that gets updated by the transaction. The actual transaction gets executed by put(). Instantiate a stasher::client::base::transaction and invoke its methods, as follows:

newobj(name, value)

Add a new object to the object repository.

updobj(name, uuid, value)

Update/replace an existing object in the object repository. The new contents replace the object's existing contents.

delobj(name, uuid)

Delete an existing object from the object repository.

These methods can be invoked more than once (for different objects) to have a single transaction update multiple objects. The same transaction can add, replace, or delete different objects. There's a server-imposed limit on the maximum number of objects in one transaction.

name gives the name of the object that the transaction adds, replaces, or deleted. uuid specifies an existing object's x::uuid.

A value for a new or replaced object is a std::string or an opened x::fd whose contents specify the object's value. There's an limit on the total size of all new or replaced objects in the transaction.

Object names

Objects are identified by their name. Object names have a maximum size of 512 bytes, and they use / for a hierarchy separator, so group/files would represent an object named files in the group hierarchy. Object names are always absolute, but they do not begin with a / like an absolute pathname. There is no formal process to create or delete a hierarchy, that's analogous to creating or deleting a directory; create the object group/files and the group hierarchy gets created; remove it, and if there are no other objects in the group hierarchy, it gets removed. Although stasher itself does not impose any other standards on object names, other than their maximum size and / as a hierarchy separator, by convention object names should be coded in UTF-8. The stasher tool uses UTF-8 for object names, so application should use UTF-8 coding too, in order for stasher to be available for diagnostics.

stasher::client->put() returns a stasher::putresults which contains two fields: status, an enumerated status code, stasher::req_stat_t; and newuuid, the new x::uuid of objects added or replaced by the transaction, if it succeeded.

Object uuids

Each object in a stasher object repository has a universal unique identifier, uuid for short. The server assigns a uuid to each object added to the repository, and when an existing object's contents get replaced. A transaction status of stasher::req_processed_stat indicates that the transaction was processed, and stasher returns a newuuid, the x::uuid of new or updated objects. The same x::uuid applies to all objects added or updated by the transaction.

An existing object's uuid must be specified to update or delete it. A transaction status of stasher::req_rejected_stat indicates that the transaction was rejected because one of the existing objects in the transaction did not match it's given x::uuid. This includes a newobj() of an object that already exists, and an updobj() or a delobj() of a nonexistent object.

The transaction gets rejected if any of its objects' uuids do not match. None of the objects in the transaction get updated even if some of the uuids matched.

The above example calls get() first, to check if an object exists. If so, the transaction replaces it with updobj(), specifying its uuid. If the object does not exist, the transaction creates it with newobj(). The object gets deleted with delobj() by omitting the second parameter to the example program.

It's possible that another application can change its object between the time this sample program gets its uuid, and sends the transaction request. Since the object's uuid no longer matches, the update transaction will get rejected, with stasher::req_rejected_stat.

Note

This simplistic example prints the newuuid after deleting the object. Of course, in this case, the object got deleted, and the newuuid is meaningless. However, if the same trasnaction created or updated other objects, the given newuuid spplies to them.

Transaction status

Other stasher C++ API requests also report their success or failure by a status code. Generally, stasher::req_processed_stat indicates that the request was processed succesfully. Besides stasher::req_rejected_stat indicating a transaction uuid mismatch, the status code may also report other error conditions.