ISSUE #2310: Adds the autorecovery status service.

Descriptions of the changes in this PR:



### Motivation

It would be convenient to support enable, disable, or check the status of autorecovery with REST API, just like what does in the toggle command.

### Changes

Adds an API to enable, disable, and get the current status of autorecovery at `/api/v1/autorecovery/status`.

Master Issue: #2310 



Reviewers: Sijie Guo <None>, Enrico Olivelli <eolivelli@gmail.com>, Matteo Minardi <minardi.matteo@hotmail.it>

This closes #2313 from fantapsody/autorecovery_status, closes #2310
diff --git a/bookkeeper-http/http-server/src/main/java/org/apache/bookkeeper/http/HttpRouter.java b/bookkeeper-http/http-server/src/main/java/org/apache/bookkeeper/http/HttpRouter.java
index 99d5be6..91dcb88 100644
--- a/bookkeeper-http/http-server/src/main/java/org/apache/bookkeeper/http/HttpRouter.java
+++ b/bookkeeper-http/http-server/src/main/java/org/apache/bookkeeper/http/HttpRouter.java
@@ -50,6 +50,7 @@
     public static final String BOOKIE_STATE                 = "/api/v1/bookie/state";
     public static final String BOOKIE_IS_READY              = "/api/v1/bookie/is_ready";
     // autorecovery
+    public static final String AUTORECOVERY_STATUS          = "/api/v1/autorecovery/status";
     public static final String RECOVERY_BOOKIE              = "/api/v1/autorecovery/bookie";
     public static final String LIST_UNDER_REPLICATED_LEDGER = "/api/v1/autorecovery/list_under_replicated_ledger";
     public static final String WHO_IS_AUDITOR               = "/api/v1/autorecovery/who_is_auditor";
@@ -83,6 +84,8 @@
         this.endpointHandlers.put(BOOKIE_IS_READY, handlerFactory.newHandler(HttpServer.ApiType.BOOKIE_IS_READY));
 
         // autorecovery
+        this.endpointHandlers.put(AUTORECOVERY_STATUS, handlerFactory
+                .newHandler(HttpServer.ApiType.AUTORECOVERY_STATUS));
         this.endpointHandlers.put(RECOVERY_BOOKIE, handlerFactory.newHandler(HttpServer.ApiType.RECOVERY_BOOKIE));
         this.endpointHandlers.put(LIST_UNDER_REPLICATED_LEDGER,
             handlerFactory.newHandler(HttpServer.ApiType.LIST_UNDER_REPLICATED_LEDGER));
diff --git a/bookkeeper-http/http-server/src/main/java/org/apache/bookkeeper/http/HttpServer.java b/bookkeeper-http/http-server/src/main/java/org/apache/bookkeeper/http/HttpServer.java
index c694a07..82c9c33 100644
--- a/bookkeeper-http/http-server/src/main/java/org/apache/bookkeeper/http/HttpServer.java
+++ b/bookkeeper-http/http-server/src/main/java/org/apache/bookkeeper/http/HttpServer.java
@@ -33,6 +33,7 @@
     enum StatusCode {
         OK(200),
         REDIRECT(302),
+        BAD_REQUEST(400),
         FORBIDDEN(403),
         NOT_FOUND(404),
         INTERNAL_ERROR(500),
@@ -84,6 +85,7 @@
         BOOKIE_IS_READY,
 
         // autorecovery
+        AUTORECOVERY_STATUS,
         RECOVERY_BOOKIE,
         LIST_UNDER_REPLICATED_LEDGER,
         WHO_IS_AUDITOR,
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/server/http/BKHttpServiceProvider.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/server/http/BKHttpServiceProvider.java
index 24795e5..aeb4d83 100644
--- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/server/http/BKHttpServiceProvider.java
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/server/http/BKHttpServiceProvider.java
@@ -40,6 +40,7 @@
 import org.apache.bookkeeper.proto.BookieServer;
 import org.apache.bookkeeper.replication.Auditor;
 import org.apache.bookkeeper.replication.AutoRecoveryMain;
+import org.apache.bookkeeper.server.http.service.AutoRecoveryStatusService;
 import org.apache.bookkeeper.server.http.service.BookieIsReadyService;
 import org.apache.bookkeeper.server.http.service.BookieStateService;
 import org.apache.bookkeeper.server.http.service.ConfigurationService;
@@ -225,6 +226,8 @@
                 return new BookieIsReadyService(bookieServer.getBookie());
 
             // autorecovery
+            case AUTORECOVERY_STATUS:
+                return new AutoRecoveryStatusService(configuration);
             case RECOVERY_BOOKIE:
                 return new RecoveryBookieService(configuration, bka, executor);
             case LIST_UNDER_REPLICATED_LEDGER:
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/server/http/service/AutoRecoveryStatusService.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/server/http/service/AutoRecoveryStatusService.java
new file mode 100644
index 0000000..f9ab650
--- /dev/null
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/server/http/service/AutoRecoveryStatusService.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.bookkeeper.server.http.service;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.UncheckedExecutionException;
+import java.util.Collections;
+import java.util.Map;
+import org.apache.bookkeeper.common.util.JsonUtil;
+import org.apache.bookkeeper.conf.ServerConfiguration;
+import org.apache.bookkeeper.http.HttpServer;
+import org.apache.bookkeeper.http.service.HttpEndpointService;
+import org.apache.bookkeeper.http.service.HttpServiceRequest;
+import org.apache.bookkeeper.http.service.HttpServiceResponse;
+import org.apache.bookkeeper.meta.LedgerUnderreplicationManager;
+import org.apache.bookkeeper.meta.MetadataDrivers;
+import org.apache.commons.lang3.ObjectUtils;
+
+/**
+ * HttpEndpointService that handles Autorecovery status related http requests.
+ *
+ * <p></p>The GET method returns the current status of Autorecovery. The output would be like {"enabled" : true}.
+ *
+ * <p>The PUT method requires a parameter 'enabled', and enables Autorecovery if its value is 'true',
+ * and disables Autorecovery otherwise. The behaviour is idempotent if Autorecovery status is already
+ * the same as desired. The output would be the current status after the action.
+ *
+ */
+public class AutoRecoveryStatusService implements HttpEndpointService {
+    protected final ServerConfiguration conf;
+
+    public AutoRecoveryStatusService(ServerConfiguration conf) {
+        this.conf = conf;
+    }
+
+    @Override
+    public HttpServiceResponse handle(HttpServiceRequest request) throws Exception {
+        return MetadataDrivers.runFunctionWithLedgerManagerFactory(conf,
+                ledgerManagerFactory -> {
+                    try (LedgerUnderreplicationManager ledgerUnderreplicationManager = ledgerManagerFactory
+                            .newLedgerUnderreplicationManager()) {
+                        switch (request.getMethod()) {
+                            case GET:
+                                return handleGetStatus(ledgerUnderreplicationManager);
+                            case PUT:
+                                return handlePutStatus(request, ledgerUnderreplicationManager);
+                            default:
+                                return new HttpServiceResponse("Not found method. Should be GET or PUT method",
+                                        HttpServer.StatusCode.NOT_FOUND);
+                        }
+                    } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
+                        throw new UncheckedExecutionException(e);
+                    } catch (Exception e) {
+                        throw new UncheckedExecutionException(e);
+                    }
+                });
+    }
+
+    private HttpServiceResponse handleGetStatus(LedgerUnderreplicationManager ledgerUnderreplicationManager)
+            throws Exception {
+        String body = JsonUtil.toJson(ImmutableMap.of("enabled",
+                ledgerUnderreplicationManager.isLedgerReplicationEnabled()));
+        return new HttpServiceResponse(body, HttpServer.StatusCode.OK);
+    }
+
+    private HttpServiceResponse handlePutStatus(HttpServiceRequest request,
+                                                LedgerUnderreplicationManager ledgerUnderreplicationManager)
+            throws Exception {
+        Map<String, String> params = ObjectUtils.defaultIfNull(request.getParams(), Collections.emptyMap());
+        String enabled = params.get("enabled");
+        if (enabled == null) {
+            return new HttpServiceResponse("Param 'enabled' not found in " + params,
+                    HttpServer.StatusCode.BAD_REQUEST);
+        }
+        if (Boolean.parseBoolean(enabled)) {
+            if (!ledgerUnderreplicationManager.isLedgerReplicationEnabled()) {
+                ledgerUnderreplicationManager.enableLedgerReplication();
+            }
+        } else {
+            if (ledgerUnderreplicationManager.isLedgerReplicationEnabled()) {
+                ledgerUnderreplicationManager.disableLedgerReplication();
+            }
+        }
+
+        // use the current status as the response
+        String body = JsonUtil.toJson(ImmutableMap.of("enabled",
+                ledgerUnderreplicationManager.isLedgerReplicationEnabled()));
+        return new HttpServiceResponse(body, HttpServer.StatusCode.OK);
+    }
+}
diff --git a/bookkeeper-server/src/test/java/org/apache/bookkeeper/server/http/service/AutoRecoveryStatusServiceTest.java b/bookkeeper-server/src/test/java/org/apache/bookkeeper/server/http/service/AutoRecoveryStatusServiceTest.java
new file mode 100644
index 0000000..69e851f
--- /dev/null
+++ b/bookkeeper-server/src/test/java/org/apache/bookkeeper/server/http/service/AutoRecoveryStatusServiceTest.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.bookkeeper.server.http.service;
+
+import static org.junit.Assert.assertEquals;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.apache.bookkeeper.http.HttpServer;
+import org.apache.bookkeeper.http.service.HttpServiceRequest;
+import org.apache.bookkeeper.http.service.HttpServiceResponse;
+import org.apache.bookkeeper.test.BookKeeperClusterTestCase;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link AutoRecoveryStatusService}.
+ */
+public class AutoRecoveryStatusServiceTest extends BookKeeperClusterTestCase {
+    private final ObjectMapper mapper = new ObjectMapper();
+    private AutoRecoveryStatusService autoRecoveryStatusService;
+    public AutoRecoveryStatusServiceTest() {
+        super(1);
+    }
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        autoRecoveryStatusService = new AutoRecoveryStatusService(baseConf);
+    }
+
+    @Test
+    public void testGetStatus() throws Exception {
+        HttpServiceRequest request = new HttpServiceRequest(null, HttpServer.Method.GET, null);
+        HttpServiceResponse response = autoRecoveryStatusService.handle(request);
+        assertEquals(HttpServer.StatusCode.OK.getValue(), response.getStatusCode());
+        JsonNode json = mapper.readTree(response.getBody());
+        assertEquals(Boolean.TRUE, json.get("enabled").asBoolean());
+    }
+
+    @Test
+    public void testEnableStatus() throws Exception {
+        Map<String, String> params = ImmutableMap.of("enabled", "true");
+        HttpServiceRequest request = new HttpServiceRequest(null, HttpServer.Method.PUT, params);
+        HttpServiceResponse response = autoRecoveryStatusService.handle(request);
+        assertEquals(HttpServer.StatusCode.OK.getValue(), response.getStatusCode());
+        JsonNode json = mapper.readTree(response.getBody());
+        assertEquals(Boolean.TRUE, json.get("enabled").asBoolean());
+
+        request = new HttpServiceRequest(null, HttpServer.Method.GET, params);
+        response = autoRecoveryStatusService.handle(request);
+        assertEquals(HttpServer.StatusCode.OK.getValue(), response.getStatusCode());
+        json = mapper.readTree(response.getBody());
+        assertEquals(Boolean.TRUE, json.get("enabled").asBoolean());
+    }
+
+    @Test
+    public void testDisableStatus() throws Exception {
+        Map<String, String> params = ImmutableMap.of("enabled", "false");
+        HttpServiceRequest request = new HttpServiceRequest(null, HttpServer.Method.PUT, params);
+        HttpServiceResponse response = autoRecoveryStatusService.handle(request);
+        assertEquals(HttpServer.StatusCode.OK.getValue(), response.getStatusCode());
+        JsonNode json = mapper.readTree(response.getBody());
+        assertEquals(Boolean.FALSE, json.get("enabled").asBoolean());
+
+        request = new HttpServiceRequest(null, HttpServer.Method.GET, params);
+        response = autoRecoveryStatusService.handle(request);
+        assertEquals(HttpServer.StatusCode.OK.getValue(), response.getStatusCode());
+        json = mapper.readTree(response.getBody());
+        assertEquals(Boolean.FALSE, json.get("enabled").asBoolean());
+    }
+
+    @Test
+    public void testInvalidParams() throws Exception {
+        Map<String, String> params = ImmutableMap.of("enable", "false");
+        HttpServiceRequest request = new HttpServiceRequest(null, HttpServer.Method.PUT, params);
+        HttpServiceResponse response = autoRecoveryStatusService.handle(request);
+        assertEquals(HttpServer.StatusCode.BAD_REQUEST.getValue(), response.getStatusCode());
+    }
+
+    @Test
+    public void testInvalidMethod() throws Exception {
+        HttpServiceRequest request = new HttpServiceRequest(null, HttpServer.Method.POST, null);
+        HttpServiceResponse response = autoRecoveryStatusService.handle(request);
+        assertEquals(HttpServer.StatusCode.NOT_FOUND.getValue(), response.getStatusCode());
+    }
+}