mercredi 23 décembre 2015

Storing and loading configuration for REST server avoding global state (i.e. singleton vs. context vs. dependency injection)

I am developing an architecture in Java using tomcat and I have come across a situation that I believe is very generic and yet, after reading several questions/answers in StackOverflow, I couldn't find a definitive answer. My architecture has a REST API (running on tomcat) that receives one or more files and their associated metadata and writes them to storage. The configuration of the storage layer has a 1-1 relationship with the REST API server, and for that reason the intuitive approach is to write a Singleton to hold that configuration.

Obviously I am aware that Singletons bring testability problems due to global state and the hardship of mocking Singletons. I also thought of using the Context pattern, but I am not convinced that the Context pattern applies in this case and I worry that I will end up coding using the "Context anti-pattern" instead.

Let me give you some more background on what I am writing. The architecture is comprised of the following components:

  • Clients that send requests to the REST API uploading or retrieving "preservation objects", or simply put, POs (files + metadata) in JSON or XML format.

  • The high level REST API that receives requests from clients and stores data in a storage layer.

  • A storage layer that may contain a combination of OpenStack Swift containers, tape libraries and file systems. Each of these "storage containers" (I'm calling file systems containers for simplicity) is called an endpoint in my architecture. The storage layer obviously does not reside on the same server where the REST API is.

The configuration of endpoints is done through the REST API (e.g. POST /configEndpoint), so that an administrative user can register new endpoints, edit or remove existing endpoints through HTTP calls. Whilst I have only implemented the architecture using an OpenStack Swift endpoint, I anticipate that the information for each endpoint contains at least an IP address, some form of authentication information and a driver name, e.g. "the Swift driver", "the LTFS driver", etc. (so that when new storage technologies arrive they can be easily integrated to my architecture as long as someone writes a driver for it).

My problem is: how do I store and load configuration in an testable, reusable and elegant way? I won't even consider passing a configuration object to all the various methods that implement the REST API calls.

A few examples of the REST API calls and where the configuration comes into play:

// Retrieve a preservation object metadata (PO)
@GET
@Path("container/{containername}/{po}")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public PreservationObjectInformation getPOMetadata(@PathParam("containername") String containerName, @PathParam("po") String poUUID) {

    // STEP 1 - LOAD THE CONFIGURATION
    // One of the following options:
    // StorageContext.loadContext(containerName);
    // Configuration.getInstance(containerName);
    // Pass a configuration object as an argument of the getPOMetadata() method?
    // Some sort of dependency injection

    // STEP 2 - RETRIEVE THE METADATA FROM THE STORAGE
    // Call the driver depending on the endpoint (JClouds if Swift, Java IO stream if file system, etc.)
    // Pass poUUID as parameter

    // STEP 3 - CONVERT JSON/XML TO OBJECT
    // Unmarshall the file in JSON format
    PreservationObjectInformation poi = unmarshall(data);

    return poi;
}


// Delete a PO
@DELETE
@Path("container/{containername}/{po}")
public Response deletePO(@PathParam("containername") String containerName, @PathParam("po") String poName) throws IOException, URISyntaxException {

    // STEP 1 - LOAD THE CONFIGURATION
    // One of the following options:
    // StorageContext.loadContext(containerName); // Context
    // Configuration.getInstance(containerName); // Singleton
    // Pass a configuration object as an argument of the getPOMetadata() method?
    // Some sort of dependency injection

    // STEP 2 - CONNECT TO THE STORAGE ENDPOINT
    // Call the driver depending on the endpoint (JClouds if Swift, Java IO stream if file system, etc.)

    // STEP 3 - DELETE THE FILE

    return Response.ok().build();
}


// Submit a PO and its metadata
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Path("container/{containername}/{po}")
public Response submitPO(@PathParam("containername") String container, @PathParam("po") String poName, @FormDataParam("objectName") String objectName,
        @FormDataParam("inputstream") InputStream inputStream) throws IOException, URISyntaxException {

    // STEP 1 - LOAD THE CONFIGURATION
    // One of the following options:
    // StorageContext.loadContext(containerName);
    // Configuration.getInstance(containerName);
    // Pass a configuration object as an argument of the getPOMetadata() method?
    // Some sort of dependency injection

    // STEP 2 - WRITE THE DATA AND METADATA TO STORAGE
    // Call the driver depending on the endpoint (JClouds if Swift, Java IO stream if file system, etc.)

    return Response.created(new URI("container/" + container + "/" + poName))
            .build();
}

Aucun commentaire:

Enregistrer un commentaire