com.vmware.dcp.common.http.netty.NettyHttpServiceClientTest.java Source code

Java tutorial

Introduction

Here is the source code for com.vmware.dcp.common.http.netty.NettyHttpServiceClientTest.java

Source

/*
 * Copyright (c) 2014-2015 VMware, Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License.  You may obtain a copy of
 * the License at http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed
 * under the License is distributed on an "AS IS" BASIS, without warranties or
 * conditions of any kind, EITHER EXPRESS OR IMPLIED.  See the License for the
 * specific language governing permissions and limitations under the License.
 */

package com.vmware.dcp.common.http.netty;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.net.URI;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;

import javax.net.ssl.SSLContext;

import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.handler.ssl.util.SelfSignedCertificate;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.vmware.dcp.common.CommandLineArgumentParser;
import com.vmware.dcp.common.Operation;
import com.vmware.dcp.common.Service;
import com.vmware.dcp.common.ServiceClient;
import com.vmware.dcp.common.ServiceDocument;
import com.vmware.dcp.common.StatefulService;
import com.vmware.dcp.common.StatelessService;
import com.vmware.dcp.common.UriUtils;
import com.vmware.dcp.common.Utils;
import com.vmware.dcp.common.test.MinimalTestServiceState;
import com.vmware.dcp.common.test.TestProperty;
import com.vmware.dcp.common.test.VerificationHost;
import com.vmware.dcp.services.common.ExampleFactoryService;
import com.vmware.dcp.services.common.ExampleService.ExampleServiceState;
import com.vmware.dcp.services.common.MinimalTestService;

public class NettyHttpServiceClientTest {

    private VerificationHost host;
    private NettyHttpServiceClient client;

    public String testURI;

    public int requestCount = 100;

    public int connectionCount = 32;

    @Before
    public void setUp() throws Exception {
        CommandLineArgumentParser.parseFromProperties(this);
        this.host = VerificationHost.create(0, null);
        CommandLineArgumentParser.parseFromProperties(this.host);

        this.host.setMaintenanceIntervalMicros(
                TimeUnit.MILLISECONDS.toMicros(VerificationHost.FAST_MAINT_INTERVAL_MILLIS));

        this.client = (NettyHttpServiceClient) NettyHttpServiceClient.create(getClass().getCanonicalName(),
                Executors.newFixedThreadPool(4), Executors.newScheduledThreadPool(1), this.host);

        SSLContext clientContext = SSLContext.getInstance(ServiceClient.TLS_PROTOCOL_NAME);
        clientContext.init(null, InsecureTrustManagerFactory.INSTANCE.getTrustManagers(), null);
        this.client.setSSLContext(clientContext);
        this.host.setClient(this.client);

        SelfSignedCertificate ssc = new SelfSignedCertificate();
        this.host.setCertificateFileReference(ssc.certificate().toURI());
        this.host.setPrivateKeyFileReference(ssc.privateKey().toURI());

        try {
            this.host.start();
        } catch (Throwable e) {
            throw new Exception(e);
        }

        this.host.setStressTest(this.host.isStressTest);
    }

    @After
    public void tearDown() {
        this.client.stop();
        this.host.tearDown();
    }

    @Test
    public void throughputGetRemote() throws Throwable {
        if (this.testURI == null) {
            return;
        }
        this.host.setOperationTimeOutMicros(TimeUnit.SECONDS.toMicros(120));
        this.host.setTimeoutSeconds(120);
        this.host.log("Starting HTTP GET stress test against %s, request count: %d, connection limit: %d",
                this.testURI, this.requestCount, this.connectionCount);
        this.client.setConnectionLimitPerHost(this.connectionCount);
        long start = Utils.getNowMicrosUtc();
        getThirdPartyServerResponse(this.testURI, this.requestCount);
        long end = Utils.getNowMicrosUtc();
        double thpt = this.requestCount / ((end - start) / 1000000.0);
        this.host.log("Connection limit: %d, Request count: %d, Requests per second:%f", this.connectionCount,
                this.requestCount, thpt);
    }

    @Test
    public void throughputPostRemote() throws Throwable {
        if (this.testURI == null) {
            return;
        }
        this.host.setOperationTimeOutMicros(TimeUnit.SECONDS.toMicros(120));
        this.host.setTimeoutSeconds(120);
        this.host.log("Starting HTTP POST stress test against %s, request count: %d, connection limit: %d",
                this.testURI, this.requestCount, this.connectionCount);
        this.client.setConnectionLimitPerHost(this.connectionCount);
        long start = Utils.getNowMicrosUtc();
        ExampleServiceState body = new ExampleServiceState();
        body.name = UUID.randomUUID().toString();
        this.host.sendHttpRequest(this.client, this.testURI, Utils.toJson(body), this.requestCount);
        long end = Utils.getNowMicrosUtc();
        double thpt = this.requestCount / ((end - start) / 1000000.0);
        this.host.log("Connection limit: %d, Request count: %d, Requests per second:%f", this.connectionCount,
                this.requestCount, thpt);
    }

    @Test
    public void httpsGetAndPut() throws Throwable {
        List<Service> services = this.host.doThroughputServiceStart(10, MinimalTestService.class,
                this.host.buildMinimalTestState(), null, null);

        List<URI> uris = new ArrayList<>();
        for (Service s : services) {
            URI u = UriUtils.extendUri(this.host.getSecureUri(), s.getSelfLink());
            uris.add(u);
        }

        this.host.getServiceState(null, MinimalTestServiceState.class, uris);

        this.host.testStart(uris.size());
        for (URI u : uris) {
            MinimalTestServiceState body = (MinimalTestServiceState) this.host.buildMinimalTestState();
            Operation put = Operation.createPut(u).setBody(body).setCompletion(this.host.getCompletion());
            this.host.send(put);
        }
        this.host.testWait();
    }

    @Test
    public void httpsFailure() throws Throwable {
        List<Service> services = this.host.doThroughputServiceStart(10, MinimalTestService.class,
                this.host.buildMinimalTestState(), null, null);

        List<URI> uris = new ArrayList<>();
        for (Service s : services) {
            URI u = UriUtils.extendUri(this.host.getSecureUri(), s.getSelfLink());
            uris.add(u);
        }

        this.host.getServiceState(null, MinimalTestServiceState.class, uris);

        this.host.testStart(uris.size());
        for (URI u : uris) {
            MinimalTestServiceState body = (MinimalTestServiceState) this.host.buildMinimalTestState();
            // simulate exception to reproduce https connection pool blocking on failure
            body.id = MinimalTestService.STRING_MARKER_HAS_CONTEXT_ID;
            Operation put = Operation.createPatch(u).setBody(body).setCompletion((o, e) -> {
                this.host.completeIteration();
            });
            this.host.send(put);
        }
        this.host.testWait();
    }

    @Test
    public void getSingleNoQueueingNotFound() throws Throwable {
        this.host.testStart(1);
        Operation get = Operation.createGet(UriUtils.buildUri(this.host, UUID.randomUUID().toString()))
                .addPragmaDirective(Operation.PRAGMA_DIRECTIVE_NO_QUEUING).setCompletion((op, ex) -> {
                    if (op.getStatusCode() == Operation.STATUS_CODE_NOT_FOUND) {
                        this.host.completeIteration();
                        return;
                    }

                    this.host.failIteration(new Throwable("Expected Operation.STATUS_CODE_NOT_FOUND"));
                });

        this.host.send(get);
        this.host.testWait();
    }

    @Test
    public void getQueueServiceAvailability() throws Throwable {
        String targetPath = UUID.randomUUID().toString();
        Operation startOp = Operation.createPost(UriUtils.buildUri(this.host, targetPath))
                .setCompletion(this.host.getCompletion());
        StatelessService testStatelessService = new StatelessService() {
            @Override
            public void handleRequest(Operation update) {
                update.complete();
            }
        };
        this.host.testStart(2);
        Operation get = Operation.createGet(UriUtils.buildUri(this.host, targetPath))
                .addPragmaDirective(Operation.PRAGMA_DIRECTIVE_QUEUE_FOR_SERVICE_AVAILABILITY)
                .setCompletion((op, ex) -> {
                    if (op.getStatusCode() == Operation.STATUS_CODE_OK) {
                        this.host.completeIteration();
                        return;
                    }

                    this.host.failIteration(new Throwable("Expected Operation.STATUS_CODE_OK"));
                });

        this.host.send(get);
        this.host.startService(startOp, testStatelessService);
        this.host.testWait();
    }

    @Test
    public void remotePatchTimeout() throws Throwable {
        doRemotePatchWithTimeout(false);
    }

    @Test
    public void remotePatchWithCallbackTimeout() throws Throwable {
        doRemotePatchWithTimeout(true);
    }

    private void doRemotePatchWithTimeout(boolean useCallback) throws Throwable {
        List<Service> services = this.host.doThroughputServiceStart(1, MinimalTestService.class,
                this.host.buildMinimalTestState(), EnumSet.noneOf(Service.ServiceOption.class), null);
        this.host.toggleNegativeTestMode(true);
        this.host.setOperationTimeOutMicros(TimeUnit.MILLISECONDS.toMicros(250));

        // send a request to the MinimalTestService, with a body that makes it NOT complete it
        MinimalTestServiceState body = new MinimalTestServiceState();
        body.id = MinimalTestService.STRING_MARKER_TIMEOUT_REQUEST;

        int count = NettyHttpServiceClient.DEFAULT_CONNECTIONS_PER_HOST * 2;
        // timeout tracking currently works only for remote requests ...
        this.host.testStart(count);
        for (int i = 0; i < count; i++) {
            Operation request = Operation.createPatch(services.get(0).getUri()).forceRemote().setBody(body)
                    .setCompletion((o, e) -> {
                        if (e != null) {
                            // timeout occurred, good
                            this.host.completeIteration();
                            return;
                        }
                        this.host.failIteration(new IllegalStateException("Request should have timed out"));
                    });
            if (useCallback) {
                this.host.sendRequestWithCallback(request.setReferer(this.host.getReferer()));
            } else {
                this.host.send(request);
            }
        }
        this.host.testWait();
        this.host.toggleNegativeTestMode(false);
    }

    @Test
    public void putSingle() throws Throwable {
        long serviceCount = 1;
        List<Service> services = this.host.doThroughputServiceStart(serviceCount, MinimalTestService.class,
                this.host.buildMinimalTestState(), null, null);
        this.host.doPutPerService(EnumSet.of(TestProperty.SINGLE_ITERATION), services);
        this.host.doPutPerService(EnumSet.of(TestProperty.FORCE_REMOTE, TestProperty.SINGLE_ITERATION), services);
        this.host.doPutPerService(EnumSet.of(TestProperty.CALLBACK_SEND, TestProperty.SINGLE_ITERATION), services);
        this.host.doPutPerService(
                EnumSet.of(TestProperty.FORCE_REMOTE, TestProperty.CALLBACK_SEND, TestProperty.SINGLE_ITERATION),
                services);

        // check that content type is set and preserved
        URI u = services.get(0).getUri();
        this.host.testStart(1);
        MinimalTestServiceState body = new MinimalTestServiceState();
        body.id = MinimalTestService.STRING_MARKER_USE_DIFFERENT_CONTENT_TYPE;
        Operation patch = Operation.createPatch(u).setBody(body).setCompletion((o, e) -> {
            if (e != null) {
                this.host.failIteration(e);
                return;
            }

            if (!Operation.MEDIA_TYPE_APPLICATION_X_WWW_FORM_ENCODED.equals(o.getContentType())) {
                this.host.failIteration(
                        new IllegalArgumentException("unexpected content type: " + o.getContentType()));
                return;
            }

            this.host.completeIteration();
        });
        this.host.send(patch);
        this.host.testWait();

        // verify content de-serializes with slightly different content type
        String contentType = "application/json; charset=UTF-8";
        this.host.testStart(1);
        MinimalTestServiceState body1 = new MinimalTestServiceState();
        body1.id = UUID.randomUUID().toString();
        body1.stringValue = UUID.randomUUID().toString();
        patch = Operation.createPatch(u).setBody(body1).setContentType(contentType).forceRemote();

        Operation p = patch;
        p.setCompletion((o, e) -> {
            if (e != null) {
                this.host.failIteration(e);
                return;
            }
            try {
                MinimalTestServiceState rsp = o.getBody(MinimalTestServiceState.class);
                assertEquals(body1.stringValue, rsp.stringValue);
                assertEquals(o.getContentType(), p.getContentType());
                this.host.completeIteration();
            } catch (Throwable ex) {
                this.host.failIteration(ex);
            }
        });
        this.host.send(p);
        this.host.testWait();
    }

    @Test
    public void putSingleNoQueueing() throws Throwable {
        long s = Utils.getNowMicrosUtc();
        this.host.waitForServiceAvailable(ExampleFactoryService.SELF_LINK);

        URI uriToMissingService = UriUtils.buildUri(this.host,
                ExampleFactoryService.SELF_LINK + "/" + UUID.randomUUID().toString());

        this.host.testStart(1);
        // Use a URI that belongs to a replicated factory, like Examples, which would normally
        // cause the this.host to queue the request until the child became available
        Operation put = Operation.createPut(uriToMissingService).setBody(this.host.buildMinimalTestState())
                .addRequestHeader(Operation.PRAGMA_HEADER, Operation.PRAGMA_DIRECTIVE_NO_QUEUING)
                .setCompletion(this.host.getExpectedFailureCompletion());

        this.host.send(put);
        this.host.testWait();

        uriToMissingService = UriUtils.buildUri(this.host,
                ExampleFactoryService.SELF_LINK + "/" + UUID.randomUUID().toString());

        this.host.testStart(1);
        put = Operation.createPut(uriToMissingService).setBody(this.host.buildMinimalTestState()).forceRemote()
                .addRequestHeader(Operation.PRAGMA_HEADER, Operation.PRAGMA_DIRECTIVE_NO_QUEUING)
                .setCompletion(this.host.getExpectedFailureCompletion());

        this.host.send(put);
        this.host.testWait();
        long e = Utils.getNowMicrosUtc();

        if (e - s > this.host.getOperationTimeoutMicros() / 2) {
            throw new TimeoutException("Request got queued, it should have bypassed queuing");
        }

        uriToMissingService = UriUtils.buildUri(this.host,
                ExampleFactoryService.SELF_LINK + "/" + UUID.randomUUID().toString());

        ServiceClient nonDcpLookingClient = null;
        try {
            nonDcpLookingClient = NettyHttpServiceClient.create(UUID.randomUUID().toString(),
                    Executors.newFixedThreadPool(1), Executors.newScheduledThreadPool(1));
            nonDcpLookingClient.start();
            s = Utils.getNowMicrosUtc();
            // try a JAVA HTTP client and verify we do not queue.
            this.host.sendWithJavaClient(uriToMissingService, Operation.MEDIA_TYPE_APPLICATION_JSON,
                    Utils.toJson(new ExampleServiceState()));

            // try a DCP client but with user agent saying its NOT DCP. Notice that there is no pragma directive
            // so unless the service this.host detects the user agent, it will try to queue and wait for service
            this.host.testStart(1);
            put = Operation.createPut(uriToMissingService).setBody(this.host.buildMinimalTestState())
                    .setExpiration(Utils.getNowMicrosUtc() + TimeUnit.SECONDS.toMicros(1000))
                    .setReferer(this.host.getReferer()).forceRemote()
                    .setCompletion(this.host.getExpectedFailureCompletion());
            nonDcpLookingClient.send(put);
            this.host.testWait();

            e = Utils.getNowMicrosUtc();
            if (e - s > this.host.getOperationTimeoutMicros() / 2) {
                throw new TimeoutException("Request got queued, it should have bypassed queuing");
            }
        } finally {
            if (nonDcpLookingClient != null) {
                nonDcpLookingClient.stop();
            }
        }
    }

    @Test
    public void putRemoteLargeAndBinaryBody() throws Throwable {
        List<Service> services = this.host.doThroughputServiceStart(1, MinimalTestService.class,
                this.host.buildMinimalTestState(), null, null);
        // large, binary body
        this.host.doPutPerService(EnumSet.of(TestProperty.FORCE_REMOTE, TestProperty.SINGLE_ITERATION,
                TestProperty.LARGE_PAYLOAD, TestProperty.BINARY_PAYLOAD), services);
        // try local (do not force remote)
        this.host.doPutPerService(
                EnumSet.of(TestProperty.SINGLE_ITERATION, TestProperty.LARGE_PAYLOAD, TestProperty.BINARY_PAYLOAD),
                services);

        // large, string (JSON) body
        this.host.doPutPerService(
                EnumSet.of(TestProperty.FORCE_REMOTE, TestProperty.SINGLE_ITERATION, TestProperty.LARGE_PAYLOAD),
                services);
    }

    @Test
    public void putSingleWithFailure() throws Throwable {
        long serviceCount = 1;
        List<Service> services = this.host.doThroughputServiceStart(serviceCount, MinimalTestService.class,
                this.host.buildMinimalTestState(), null, null);
        this.host.doPutPerService(EnumSet.of(TestProperty.FORCE_FAILURE, TestProperty.SINGLE_ITERATION), services);

        this.host.doPutPerService(
                EnumSet.of(TestProperty.FORCE_REMOTE, TestProperty.FORCE_FAILURE, TestProperty.SINGLE_ITERATION),
                services);

        // send some garbage that the service will not even be able to parse
        this.host.testStart(1);
        this.host.send(Operation.createPut(services.get(0).getUri()).setBody("this is not JSON")
                .setCompletion(this.host.getExpectedFailureCompletion()));
        this.host.testWait();

        // create an operation with no body and verify completion gets called with
        // failure
        this.host.testStart(1);
        this.host.send(Operation.createPatch(services.get(0).getUri())
                .setCompletion(this.host.getExpectedFailureCompletion()));
        this.host.testWait();
    }

    @Test
    public void throughputPutRemote() throws Throwable {
        long serviceCount = 32;
        List<Service> services = this.host.doThroughputServiceStart(serviceCount, MinimalTestService.class,
                this.host.buildMinimalTestState(), null, null);

        for (int i = 0; i < 2; i++) {
            this.host.doPutPerService(this.requestCount, EnumSet.of(TestProperty.FORCE_REMOTE), services);
            this.host.doPutPerService(this.requestCount,
                    EnumSet.of(TestProperty.FORCE_REMOTE, TestProperty.CALLBACK_SEND), services);
        }

        this.host.log("Single connection run");
        ((NettyHttpServiceClient) this.host.getClient()).setConnectionLimitPerHost(1);
        this.host.doPutPerService(this.requestCount, EnumSet.of(TestProperty.FORCE_REMOTE), services);
    }

    @Test
    public void throughputNonPersistedServiceGetSingleConnection() throws Throwable {
        long serviceCount = 256;
        ((NettyHttpServiceClient) this.host.getClient()).setConnectionLimitPerHost(1);
        MinimalTestServiceState body = (MinimalTestServiceState) this.host.buildMinimalTestState();

        EnumSet<TestProperty> props = EnumSet.of(TestProperty.FORCE_REMOTE);
        long c = this.host.computeIterationsFromMemory(props, (int) serviceCount);
        List<Service> services = this.host.doThroughputServiceStart(serviceCount, MinimalTestService.class, body,
                EnumSet.noneOf(Service.ServiceOption.class), null);

        doGetThroughputTest(props, body, c, services);
    }

    @Test
    public void throughputNonPersistedServiceGet() throws Throwable {
        int serviceCount = 1;
        MinimalTestServiceState body = (MinimalTestServiceState) this.host.buildMinimalTestState();
        // produce a JSON PODO that serialized is about 2048 bytes
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 53; i++) {
            sb.append(UUID.randomUUID().toString());
        }
        body.stringValue = sb.toString();

        long c = this.requestCount;
        List<Service> services = this.host.doThroughputServiceStart(serviceCount, MinimalTestService.class, body,
                EnumSet.noneOf(Service.ServiceOption.class), null);

        // in memory test, just cloning and serialization, avoid sockets
        for (int i = 0; i < 3; i++) {
            doGetThroughputTest(EnumSet.noneOf(TestProperty.class), body, c, services);
        }

        // using loop back, sockets
        for (int i = 0; i < 3; i++) {
            doGetThroughputTest(EnumSet.of(TestProperty.FORCE_REMOTE), body, c, services);
        }

        // again but skip serialization, ask service to return string for response
        for (int i = 0; i < 3; i++) {
            doGetThroughputTest(EnumSet.of(TestProperty.FORCE_REMOTE, TestProperty.TEXT_RESPONSE), body, c,
                    services);
        }
    }

    public void doGetThroughputTest(EnumSet<TestProperty> props, MinimalTestServiceState body, long c,
            List<Service> services) throws Throwable {

        long concurrencyFactor = c / 10;
        this.host.log("Properties: %s, count: %d, bytes per rsp: %d", props, c,
                Utils.toJson(body).getBytes().length);
        URI u = services.get(0).getUri();

        AtomicInteger inFlight = new AtomicInteger();
        this.host.testStart(c);

        Operation get = Operation.createGet(u).setCompletion((o, e) -> {
            inFlight.decrementAndGet();
            if (e != null) {
                this.host.failIteration(e);
                return;
            }

            MinimalTestServiceState st = o.getBody(MinimalTestServiceState.class);
            try {
                assertTrue(st.id != null);
                assertTrue(st.documentSelfLink != null);
                assertTrue(st.documentUpdateTimeMicros > 0);
            } catch (Throwable ex) {
                this.host.failIteration(ex);
            }
            this.host.completeIteration();
        });
        if (props.contains(TestProperty.FORCE_REMOTE)) {
            get.forceRemote();
        }

        if (props.contains(TestProperty.TEXT_RESPONSE)) {
            get.addRequestHeader("Accept", Operation.MEDIA_TYPE_TEXT_PLAIN);
        }

        for (int i = 0; i < c; i++) {
            inFlight.incrementAndGet();
            this.host.send(get);
            if (inFlight.get() < concurrencyFactor) {
                continue;
            }
            while (inFlight.get() > concurrencyFactor) {
                Thread.sleep(10);
            }
        }
        this.host.testWait();
        this.host.logThroughput();
    }

    private String getThirdPartyServerResponse(String uri, int count) throws Throwable {
        return this.host.sendHttpRequest(this.client, uri, null, count);
    }

    final String cookieServicePath = "/test/cookies";

    public void singleCookieTest(boolean forceRemote) throws Throwable {
        this.host.startServiceAndWait(CookieService.class, this.cookieServicePath);

        // Ask cookie service to set a cookie
        CookieServiceState setState = new CookieServiceState();
        setState.action = CookieAction.SET;
        setState.cookies = new HashMap<>();
        setState.cookies.put("key", "value");

        Operation setOp = Operation.createPatch(UriUtils.buildUri(this.host, this.cookieServicePath))
                .setCompletion(this.host.getCompletion()).setBody(setState);
        if (forceRemote) {
            setOp.forceRemote();
        }
        this.host.testStart(1);
        this.host.send(setOp);
        this.host.testWait();

        // Retrieve set cookies
        List<Map<String, String>> actualCookies = new ArrayList<>();
        Operation getOp = Operation.createGet(UriUtils.buildUri(this.host, this.cookieServicePath))
                .setCompletion((o, e) -> {
                    if (e != null) {
                        this.host.failIteration(e);
                        return;
                    }

                    CookieServiceState getState = o.getBody(CookieServiceState.class);
                    actualCookies.add(getState.cookies);
                    this.host.completeIteration();
                });
        if (forceRemote) {
            getOp.forceRemote();
        }
        this.host.testStart(1);
        this.host.send(getOp);
        this.host.testWait();

        assertNotNull("expect cookies to be set", actualCookies.get(0));
        assertEquals(1, actualCookies.get(0).size());
        assertEquals("value", actualCookies.get(0).get("key"));
    }

    @Test
    public void singleCookieLocal() throws Throwable {
        singleCookieTest(false);
    }

    @Test
    public void singleCookieRemote() throws Throwable {
        singleCookieTest(true);
    }

    public enum CookieAction {
        SET, DELETE,
    }

    public static class CookieServiceState extends ServiceDocument {
        public CookieAction action;
        public Map<String, String> cookies;
    }

    public static class CookieService extends StatefulService {
        public CookieService() {
            super(CookieServiceState.class);
        }

        @Override
        public void handleGet(Operation op) {
            CookieServiceState state = new CookieServiceState();
            state.cookies = op.getCookies();
            op.setBody(state).complete();
        }

        @Override
        public void handlePatch(Operation op) {
            CookieServiceState state = op.getBody(CookieServiceState.class);
            if (state == null) {
                op.fail(new IllegalArgumentException("body required"));
                return;
            }

            switch (state.action) {
            case SET:
                for (Entry<String, String> e : state.cookies.entrySet()) {
                    op.addResponseCookie(e.getKey(), e.getValue());
                }
                break;
            case DELETE:
                break;
            default:
                op.fail(new IllegalArgumentException("invalid action"));
                return;
            }

            op.complete();
        }
    }
}