Skip to main content

Load Lua RADOS classes from local file system

·19 mins

Object classes in RADOS written in Lua have up until now been limited to scripts that are embedded into every client request. This post describes how we have extended RADOS to load Lua scripts from the local file system, supporting a new way to manage object interfaces written in Lua.

Introduction #

The RADOS object store that powers Ceph supports an active storage-like feature called object classes that allow custom object interfaces to be defined. Object classes define a set of methods, each of which can perform arbitrary mutations on objects within a transactional context. The native object class interface is C++, and deploying object classes that are not in the upstream Ceph release means compiling and managing binary libraries and ensuring version compatibility. Alternatively, developers can define object classes in the embedded language Lua allowing for object classes to be defined on-the-fly. However in the current design every client request must be augmented with the Lua script to be executed. In this post we will describe a new form of interface management that allows object classes written in Lua to be loaded from the file system, allowing Lua scripts to effectively be installed into the system.

The remainder of this post will examine how existing object class modules are loaded into a running OSD, how Lua object classes are defined by attaching a Lua script to each request, and finally how we implement Lua class loading from the file system.

Object Class Loader #

I’ll now briefly describe how object classes are currently managed and loaded by Ceph. We’ll adopt a similar model for Lua scripts. The following is a directory listing of the object classes that are in the current upstream Ceph tree. Each directory corresponds to a separate class:

[nwatkins@kyoto ceph]$ ls -l src/cls/
total 84
drwxrwxr-x. 4 nwatkins nwatkins 4096 Dec  5 12:05 cephfs
drwxrwxr-x. 4 nwatkins nwatkins 4096 Dec  6 09:21 journal
drwxrwxr-x. 4 nwatkins nwatkins 4096 Dec  3 12:09 lock
drwxrwxr-x. 4 nwatkins nwatkins 4096 Dec  3 12:07 log
-rw-rw-r--. 1 nwatkins nwatkins 4265 Dec  5 10:40
drwxrwxr-x. 4 nwatkins nwatkins 4096 Dec  3 17:17 numops

Each class is compiled into a shared library that can be loaded into the OSD process at runtime. The following is a directory listing of the installation path for each object class:

[nwatkins@kyoto ceph]$ ls -l /usr/lib/rados-classes/*.so | grep cls
-rwxr-xr-x. 1 root root   241360 Dec  5 10:45 /usr/lib/rados-classes/
-rwxr-xr-x. 1 root root   727840 Dec  5 10:45 /usr/lib/rados-classes/
-rwxr-xr-x. 1 root root  1176504 Dec  5 10:45 /usr/lib/rados-classes/
-rwxr-xr-x. 1 root root   765960 Dec  5 10:45 /usr/lib/rados-classes/
-rwxr-xr-x. 1 root root   482920 Dec  5 10:45 /usr/lib/rados-classes/

The naming convention of the libraries is important. The OSD assumes a naming convention in which an object class named foo can be loaded from the file Concretely, when a RADOS client runs the command:

IoCtx::exec("oid", "foo", "foo-method", ...)

The OSD will check if the foo class has already been loaded, and if not, it will attempt to locate a shared library named and use dlopen to dynamically insert the library into the running process. Once the object class module has been loaded methods exported by the module may be invoked on behalf of the client ( in the above example).

Loading Lua Classes #

The Lua object class feature is not a native OSD feature. Rather, it is implemented as a statically loaded object class written in C++ that is used as a runtime host for dynamically defined object interfaces. It exists alongside other object classes:

[nwatkins@kyoto ceph]$ ls -l src/cls/lua
total 1996
-rw-rw-r--. 1 nwatkins nwatkins   20472 Dec  4 15:08
-rw-rw-r--. 1 nwatkins nwatkins    1823 Dec  4 14:59
-rw-rw-r--. 1 nwatkins nwatkins     412 Dec  4 14:59 cls_lua_client.hpp
-rw-rw-r--. 1 nwatkins nwatkins     354 Dec  4 14:59 cls_lua.h
-rw-rw-r--. 1 nwatkins nwatkins    3739 Dec  4 14:59
-rw-rw-r--. 1 nwatkins nwatkins   22899 Dec  4 14:59 lua_cmsgpack.c

And is compiled and loaded like any other object class:

[nwatkins@kyoto ceph]$ ls -l /usr/lib/rados-classes/*.so | grep cls_lua
-rwxr-xr-x. 1 root root   241360 Dec  5 10:45 /usr/lib/rados-classes/

The lua object class is invoked like any other object class:

IoCtx::exec("oid", "lua", "eval_json", input, output);

What is happening here? The eval_json method is being called on the lua class which specifies that the input should be assumed to be a JSON encoded string (there are other encoding available such as bufferlist and `msgpack). The JSON string is simply the Lua script, actual method input, and the method in the Lua script to invoke. Here is an example of the input (using Python dictionary notation). The Lua script shown below treats the input as a string and returns the string with each character capitalized:

cmd = {
  "script": """
      function upper(input, output)
        input_str = input:str()
        upper_str = string.upper(input_str)
  "handler": "upper",
  "input": "this string was in lower case",

The encoded input can be provided to IoCtx::exec:

ret, data = ioctx.execute('oid', 'lua', 'eval_json', json.dumps(cmd))
print data[:ret]

And when run, we see the output that we expect:

[nwatkins@kyoto src]$ python

Rather than using the lua class directly, clients typically use a thin wrapper around IoCtx::exec that performs input encoding, providing the appearance that RADOS provides access to dynamically defined object classes. There are a couple drawbacks to this approach:

  1. Clients must send a script along with each request
  2. The vanilla exec interface cannot invoke dynamic classes directly

Both of these issues are limiting. Providing a script with each request can be expensive and may introduce a major hassle for applications that must manage a set of dynamic interfaces. The second issue limits the power of dynamic interfaces. Existing applications that are written assuming a given a object class would need to be recompiled to switch to Lua defined classes because they would need use the lua class. However, it would be nice to be able to simply swap out the implementation and allow the application to continue referencing the same object class methods.

We address both of these issues in the remainder of this post. To address the first issue we introduce the ability for Lua scripts to be loaded from a local file system in a manner analogous to how native C++ modules are managed. The second issue is effectively solved for free when object classes are allowed to be resolved to separate Lua scripts.

The Goal #

From a management perspective the goal of this project is to allow object classes written in Lua to be installed to and loaded from the file system with identical semantics to the existing infrastructure for loading native C++ object class modules. In the same way that object classes written in C++ can be compiled and installed manually, in this post we only consider Lua scripts managed in the Ceph tree (but out-of-tree management is much easier with Lua scripts as they do not require compilation).

We stash the Lua scripts in the same directory structure as the native extensions. Below are three object classes, one native and two written in Lua. The native object class hello is mirrored by the Lua object class named hellolua which will be used as a demonstration class. The compress class is in its own directory and is a Lua class that contains data compression methods.

[nwatkins@kyoto ceph]$ ls -l src/cls/hello/
total 480
-rw-rw-r--. 1 nwatkins nwatkins  10328 Dec  3 11:10
-rw-rw-r--. 1 nwatkins nwatkins    289 Dec  3 12:24 cls_hello.lo
-rw-rw-r--. 1 nwatkins nwatkins    230 Dec  4 13:57 cls_hellolua.lua
-rw-rw-r--. 1 nwatkins nwatkins 468864 Dec  3 12:24 cls_hello.o
[nwatkins@kyoto ceph]$ ls -l src/cls/compress/
total 0
-rw-rw-r--. 1 nwatkins nwatkins 0 Dec  6 11:46 cls_compress.lua

Lua classes are installed like their native counterparts, although Lua scripts are installed in /usr/share/ceph/rados-classes as opposed to /usr/lib/rados-classes used for architecture-dependent binaries. The naming convention for Lua scripts is used for resolving class names to files, and native classes with the same name take precedence. Like native classes, Lua scripts are only loaded once. Changes to any Lua scripts will be reflected after an OSD restart. Note that this restriction isn’t fundamental and could be easily removed. Shipping a Lua script with each request still retains its dynamic nature, too.

Implementation #

First we’ll cover basic changes needed to install the Lua scripts. First some automake magic that will install the Lua scripts into the datadir (typically /usr/share/):

diff --git a/src/ b/src/
index 3d8a252..0e265d8 100644
--- a/src/
+++ b/src/
@@ -20,6 +20,7 @@ lib_LTLIBRARIES =
 noinst_LTLIBRARIES = 
 noinst_LIBRARIES =
 radoslib_LTLIBRARIES =
+dist_radoslua_DATA =
 # like bin_PROGRAMS, but these targets are only built for debug builds
@@ -286,4 +287,5 @@ DENCODER_DEPS =
 radoslibdir = $(libdir)/rados-classes
+radosluadir = $(datadir)/ceph/rados-classes
diff --git a/src/cls/ b/src/cls/
index 2221d6c..5b95f84 100644
--- a/src/cls/
+++ b/src/cls/
@@ -92,4 +92,7 @@ libcls_lua_la_LDFLAGS = ${AM_LDFLAGS} -version-info 1:0:0 -export-symbols-regex
 radoslib_LTLIBRARIES +=
+dist_radoslua_DATA += \
+    cls/hello/cls_hellolua.lua
 endif # WITH_OSD

Similar changes need to be made for the CMake build system, Debian, and RPM packaging but these are omitted from this post. Next we need to be able to refer to the directory containing the Lua scripts so we can load them dynamically at runtime. For this we define a configuration option populated by the automake variable specifying the installation location:

diff --git a/src/ b/src/
index 0e265d8..6672d37 100644
--- a/src/
+++ b/src/
@@ -87,6 +87,7 @@ AM_COMMON_CPPFLAGS = \
        -D_GNU_SOURCE \
        -DCEPH_LIBDIR=\"${libdir}\" \
        -DCEPH_PKGLIBDIR=\"${pkglibdir}\" \
+       -DCEPH_DATADIR=\"${datadir}\" \
diff --git a/src/common/config_opts.h b/src/common/config_opts.h
index a19cc5d..86eb42f 100644
--- a/src/common/config_opts.h
+++ b/src/common/config_opts.h
@@ -695,6 +695,7 @@ OPTION(osd_deep_scrub_stride, OPT_INT, 524288)
 OPTION(osd_deep_scrub_update_digest_min_age, OPT_INT, 2*60*60)   // objects must be this old (seconds) before we update the whole-object digest on scrub
 OPTION(osd_scan_list_ping_tp_interval, OPT_U64, 100)
 OPTION(osd_class_dir, OPT_STR, CEPH_LIBDIR "/rados-classes") // where rados plugins are stored
+OPTION(osd_lua_class_dir, OPT_STR, CEPH_DATADIR "/ceph/rados-classes") // where rados lua plugins are stored
 OPTION(osd_open_classes_on_start, OPT_BOOL, true)
 OPTION(osd_check_for_log_corruption, OPT_BOOL, false)
 OPTION(osd_use_stale_snap, OPT_BOOL, false)

Loading Lua Scripts From File System #

We’ll describe the implementation by tracing the execution of a IoCtx::exec operation and describing the changes required along that execution path. The first important stop is when the OSD performs pre-processing on the operation which obtains a reference to the object class requested, and optionally loads the class if it hasn’t already been loaded:

ClassHandler::ClassData *cls;
int r = class_handler->open_class(cname, &cls);
if (r) {
  derr << "class " << cname << " open got " << cpp_strerror(r) << dendl;
  if (r == -ENOENT)
    r = -EOPNOTSUPP;
    r = -EIO;
  return r;

Class Setup #

The open_class method performs a lookup on the class name which will implicitly create a reference to a class with the name, but may not yet load it. Initially a class is not in the CLASS_OPEN state, so the call to open_class will attempt to load the class.

int ClassHandler::open_class(const string& cname, ClassData **pcls)
  Mutex::Locker lock(mutex);
  ClassData *cls = _get_class(cname);
  if (cls->status != ClassData::CLASS_OPEN) {
    int r = _load_class(cls);
    if (r)
      return r;
  *pcls = cls;
  return 0;

The first thing that happens in _load_class is to handle the case that the class has not yet loaded the library which happens the first time the class loaded (status == CLASS_UNKNOWN), or each time after that until any of its dependencies are missing (status == CLASS_MISSING).

int ClassHandler::_load_class(ClassData *cls)
  if (cls->status == ClassData::CLASS_UNKNOWN ||
      cls->status == ClassData::CLASS_MISSING) {

The path to the library is constructed based on a naming convention (libcls_<<class-name>>.so) and then the OSD attempts to load the library using dlopen():

    char fname[PATH_MAX];

    // build the path to the shared library based on the class name
    snprintf(fname, sizeof(fname), "%s/" CLS_PREFIX "%s" CLS_SUFFIX,

    cls->handle = dlopen(fname, RTLD_NOW);

The dlopen() function will return a non-NULL value on success meaning that a native object class was loaded which satisfies our goal of having native object classes take precedence. Here we consider the error case. First we stat the path as it may be the case that the file exists and cannot be loaded for a variety of reasons:

    if (!cls->handle) {
      struct stat st;
      int r = ::stat(fname, &st);

In the case that stat cannot find the file we look for a Lua script with the same object class name. All other errors are handled normally:

      if (r < 0) {
        r = -errno;
        dout(0) << __func__ << " could not stat class " << fname
                << ": " << cpp_strerror(r) << dendl;
        if (r == -ENOENT)
          r = _load_lua_class(cls);
      } else {
        dout(0) << "_load_class could not open class " << fname
                << " (dlopen failed): " << dlerror() << dendl;
        r = -EIO;

If there was an error resolving the class name to a native module or a Lua script, then we change the state of the class to CLASS_MISSING and return an error.

      // dlopen error may be cleared if Lua class found
      if (r) {
        cls->status = ClassData::CLASS_MISSING;
        return r;

Let’s now look at what happens in _load_lua_class before continuing to describe this method. The first thing that we do is use a naming convention to construct a path name for a target Lua script. Notice that here we use the osd_lua_class_dir configuration option that we described previously in this post. Normally it points at the directory /usr/share/ceph/rados-classes/.

int ClassHandler::_load_lua_class(ClassData *cls)
  char fname[PATH_MAX];

  snprintf(fname, sizeof(fname),

Next we see if the file exists using stat system call and bail if there is an error:

  struct stat st;
  int r = ::stat(fname, &st);
  if (r < 0) {
    r = -errno;
    dout(0) << __func__ << " could not stat class " << fname
      << ": " << cpp_strerror(r) << dendl;
    return r;

The file exists, but lets add a sanity check before moving on:

  if (st.st_size > CLS_LUA_MAX_SIZE) {
    dout(0) << __func__ << " lua script too big " << fname
      << ": " << st.st_size << " bytes" << dendl;
    return -EIO;

Finally we read the script into a string that is stored in the C++ object representing the class:

  std::ifstream ifs(fname);

To finish up we manually add a dependency on the lua class which is needed to run the script we just read, and mark a special flag on the object class that indicates that this class is defined by a Lua script:

   * Mark 'lua' as a class dependency.
  ClassData *cls_dep = _get_class("lua");
  if (cls_dep->status != ClassData::CLASS_OPEN)

  cls->lua_handler = true;

  return 0;

This brings up an important implementation note. We use the object class machinery in the OSD to explicitly represent object classes that are backed by a Lua script. However, the existing representation of object classes assumes the class is backed by a dynamically loaded native shared library. In contrast, classes defined by a Lua script act as a proxy, forwarding their methods onto the native lua class that can evaluate Lua scripts that define object classes.

Now back to the _load_class method. At this point we have a new object class and have either loaded it from native shared library or a Lua script. Before we can use the class we need to make sure any dependencies are met. For native libraries a symbol within the shared library is resolved and interpreted as array of strings defining the set of dependencies. However, for classes backed by a Lua script there is no shared library to interact with. Thus, we need to be careful to ensure that existing code does not assume that a class is backed by a shared library. To do this we simply check that cls->handle is non-NULL (we omit the rest of the process of saving the set of dependencies).

    if (cls->handle) {
      cls_deps_t *(*cls_deps)();
      cls_deps = (cls_deps_t *(*)())dlsym(cls->handle, "class_deps");
      if (cls_deps) {
        cls_deps_t *deps = cls_deps();
        while (deps) {

The next step is to actually resolve the dependencies. The immediately proceeding code snippet populates the set of dependencies for shared libraries, but as we saw in _load_lua_class the lua dependency is manually attached to classes backed by Lua scripts. Resolving dependencies is a recursive call to _load_class and is omitted here. It is the same for all types of classes.

Next we configure the class as a proxy. First we grab a reference to the lua object class. The assertion is OK because we should have already resolved the dependency:

  if (cls->lua_handler) {
    ClassHandler::ClassData *lua_cls = _get_class("lua");
    assert(lua_cls && lua_cls->name == "lua" &&
        lua_cls->status == ClassData::CLASS_OPEN);

Next we grab a reference to the eval_bufferlist method on the lua class. Above we demonstrated how to encode operations via the eval_json method, and this is similar but uses internal Ceph encoding protocol.

    ClassHandler::ClassMethod *lua_method;
    lua_method = lua_cls->_get_method("eval_bufferlist");

Finally we setup a fixed method that will forward invocations to the lua class. Note that the method flags are set to read + write. This is a conservative view necessary because Lua scripts don’t currently have a callback method for exposing these flags. Finally, note that the cxx_func of this proxy method actually points to the method in the lua class.

    cls->lua_method_proxy.cxx_func = lua_method->cxx_func;
    cls-> = "lua_method_proxy";
    cls->lua_method_proxy.flags = CLS_METHOD_RD | CLS_METHOD_WR;
    cls->lua_method_proxy.cls = cls;

At this point I’ll briefly explain this method structure. The main structure representing an object class is ClassHandler::ClassData which contains the structure map<string, ClassMethod> methods_map; that maps the name of a method to a ClassHandler::ClassMethod structure. The ClassHandler::ClassMethod structure stores the method flags and a corresponding function pointer.

When a class is created that is backed by a Lua script it is configured such that a fixed method exists (ClassMethod lua_method_proxy;), and we modify the method resolution routine to always return the proxy method for Lua classes:

diff --git a/src/osd/ b/src/osd/
index 5616307..e60783b 100644
--- a/src/osd/
+++ b/src/osd/
@@ -296,6 +296,8 @@ ClassHandler::ClassFilter *ClassHandler::ClassData::register_cxx_filter(
 ClassHandler::ClassMethod *ClassHandler::ClassData::_get_method(const char *mname)
+  if (lua_handler)
+    return &lua_method_proxy;
   map<string, ClassHandler::ClassMethod>::iterator iter = methods_map.find(mname);
   if (iter == methods_map.end())
     return NULL;

The final operations in _load_class are to perform initialization on the class and activate the class by setting its status to CLASS_OPEN. Initialization is skipped for Lua classes:

  // initialize (non-Lua classes)
  if (cls->handle) {
    void (*cls_init)() = (void (*)())dlsym(cls->handle, "__cls_init");
    if (cls_init) {
      cls->status = ClassData::CLASS_INITIALIZING;
  dout(10) << "_load_class " << cls->name << " success" << dendl;
  cls->status = ClassData::CLASS_OPEN;
  return 0;

Forward Method Calls #

Recall that the OSD performs a pre-processing step on IoCtx::exec operations that verify that the target object class exists. In addition to this step per-method flags are extracted that assist in the remaining processing of the operation. Once a reference to the target class has been obtained the OSD grabs the flags:

int flags = cls->get_method_flags(mname.c_str());
if (flags < 0) {
  if (flags == -ENOENT)
    r = -EOPNOTSUPP;
    r = flags;
  return r;
is_read = flags & CLS_METHOD_RD;
is_write = flags & CLS_METHOD_WR;

Taking into account the description of how Lua classes are handled, we know that the class isn’t backed by a shared library. This call thus eventually resolves to the static method we defined that has its flags hard-coded (the _get_method call was shown above in which a Lua class simply returns a fixed proxy method):

int ClassHandler::ClassData::get_method_flags(const char *mname)
  Mutex::Locker l(handler->mutex);
  ClassMethod *method = _get_method(mname);
  if (!method)
    return -ENOENT;
  return method->flags;

The next stop in the OSD that we are interested in is when the actual operation is invoked. This happens in ReplicatedPG::do_osd_ops. First we grab a reference to the target class by name:

CassHandler::ClassData *cls;
result = osd->class_handler->open_class(cname, &cls);
assert(result == 0);   // init_op_flags() already verified this works.

Then the same for the target method:

ClassHandler::ClassMethod *method = cls->get_method(mname.c_str());
if (!method) {
  dout(10) << "call method " << cname << "." << mname << " does not exist" << dendl;
  result = -EOPNOTSUPP;

Finally the method is invoked. Note that the method name (mname) is passed to the method. Prior to the changes described in this post that parameter was not necessary because the method structure contained a direct function pointer to execute. However, when invoking a method on a Lua class the method doesn’t actually exist. Instead, we pass the target method name to the proxy method which forwards the request to the lua class.

result = method->exec((cls_method_context_t)&ctx, indata, outdata, mname);

Patching Method Execution #

What’s left in processing the request is simply preparing the parameters and forwarding the call to the lua class. Recall that we are using eval_bufferlist which expects the parameter order to be (1) Lua script, (2) method name, and (3) the original input data:

int ClassHandler::ClassMethod::exec(cls_method_context_t ctx,
    bufferlist& indata, bufferlist& outdata, const string& mname)
  if (cls->lua_handler) {
    bufferlist tmp_indata;
    ::encode(cls->lua_script, tmp_indata);
    ::encode(mname, tmp_indata);
    ::encode(indata, tmp_indata);
    indata = tmp_indata;
    assert(cxx_func); // we don't setup a C version

The remainder of the exec is left unchanged because the proxy method was already setup with its cxx_func to point at the eval_bufferlist method of the lua class:

  int ret;
  if (cxx_func) {
    // C++ call version
    ret = cxx_func(ctx, &indata, &outdata);
  } else {
    // C version
    char *out = NULL;
    int olen = 0;
    ret = func(ctx, indata.c_str(), indata.length(), &out, &olen);
    if (out) {
      // assume *out was allocated via cls_alloc (which calls malloc!)
      buffer::ptr bp = buffer::claim_malloc(olen, out);
  return ret;

And those are the only changes needed. The entire patch set is fairly small, and includes other important bits like fixing some assumptions on classes being backed by shared libraries.

Testing #

We’ll start with a test to make sure everything works as expected. Here is the Lua class we’ll be using. The say_hello method returns the same result at the hello class that is written in C++, but we append (from Lua) to the output to distinguish between the two.

[nwatkins@kyoto src]$ cat /usr/share/ceph/rados-classes/cls_hellolua.lua 
function say_hello(input, output)
    output:append("Hello, ")
    if #input > 0 then
    output:append("! (from Lua)")


We’ll test with the following Python snippet:

ret, out = ioctx.execute('oid', 'hellolua', 'say_hello', "Ceph")
print out

When run we get the following output which is what we expected to get:

[nwatkins@kyoto src]$ python
Hello, Ceph! (from Lua)

Next we’ll test that the scripts are re-loaded after each restart by modifying the script to append -- Updated to the message returned to the client:

[nwatkins@kyoto src]$ python
Hello, Ceph! (from Lua) -- Updated

That worked just fine. Now we can test that native libraries take precedence. To do this we’ll rename cls_hellolua.lua to cls_hello.lua and then call the say_hello method on the hello class:

[nwatkins@kyoto src]$ python
Hello, Ceph!

And that is what we expected from the native hello class. Now we’ll test the dependency mechanism. We’ll rename to prevent it from being loaded. Then we’ll try to run a Lua script class which should fail, and finally restore the naming of the lua library file and try again for success:

mv /usr/lib/rados-classes/ /usr/lib/rados-classes/ 

[nwatkins@kyoto src]$ python
rados.Error: Ioctx.exec(rbd): failed to exec hellolua:say_hello on oid: errno ENOTSUP

[nwatkins@kyoto src]$ cat out/osd.0.log | grep lua
2015-12-06 20:55:34.996635 7f75c61a3700  0 _load_class could not stat class ./usr/lib/rados-classes/ (2) No such file or directory

Now for the success case:

mv /usr/lib/rados-classes/ /usr/lib/rados-classes/

[nwatkins@kyoto src]$ python
Hello, Ceph! (from Lua)

Great, so everything is working as expected. In future posts we’ll present more advanced techniques for managing dynamic storage interfaces.