Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
import org.apache.hadoop.hbase.master.cleaner.SnapshotCleanerChore;
import org.apache.hadoop.hbase.master.hbck.HbckChore;
import org.apache.hadoop.hbase.master.http.MasterDumpServlet;
import org.apache.hadoop.hbase.master.http.MasterHbckServlet;
import org.apache.hadoop.hbase.master.http.MasterRedirectServlet;
import org.apache.hadoop.hbase.master.http.MasterStatusServlet;
import org.apache.hadoop.hbase.master.http.api_v1.ResourceConfigFactory;
Expand Down Expand Up @@ -724,6 +725,7 @@ protected MasterRpcServices createRpcServices() throws IOException {
protected void configureInfoServer(InfoServer infoServer) {
infoServer.addUnprivilegedServlet("master-status", "/master-status", MasterStatusServlet.class);
infoServer.addUnprivilegedServlet("api_v1", "/api/v1/*", buildApiV1Servlet());
infoServer.addUnprivilegedServlet("hbck", "/hbck", MasterHbckServlet.class);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why add a new endpoint instead of having the existing /hbck.jsp honor an "application/json" header and render its response there? Probably a bunch of that jsp's POJOs can be reused...

Copy link
Contributor Author

@apurtell apurtell Jun 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not and still do not know how to make a JSP page honor different content-encodings. JSP is not the tool I would like to implement this with. If this is the preferred implementation that's fine but I would not want to pursue it.


infoServer.setAttribute(MASTER, this);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* 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.hadoop.hbase.master.http;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.ServerName;
import org.apache.hadoop.hbase.client.RegionInfo;
import org.apache.hadoop.hbase.master.HMaster;
import org.apache.hadoop.hbase.master.HbckChore;
import org.apache.hadoop.hbase.master.http.gson.GsonFactory;
import org.apache.hadoop.hbase.master.janitor.CatalogJanitor;
import org.apache.hadoop.hbase.master.janitor.Report;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.yetus.audience.InterfaceAudience;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.hbase.thirdparty.com.google.gson.Gson;

@InterfaceAudience.Private
public class MasterHbckServlet extends HttpServlet {

public static final String START_TIMESTAMP = "start_timestamp";
public static final String END_TIMESTAMP = "end_timestamp";
public static final String INCONSISTENT_REGIONS = "inconsistent_regions";
public static final String ORPHAN_REGIONS_ON_RS = "orphan_regions_on_rs";
public static final String ORPHAN_REGIONS_ON_FS = "orphan_regions_on_fs";
public static final String HOLES = "holes";
public static final String OVERLAPS = "overlaps";
public static final String UNKNOWN_SERVERS = "unknown_servers";
public static final String EMPTY_REGIONINFO = "empty_regioninfo";

private static final long serialVersionUID = 1L;
private static final Logger LOG = LoggerFactory.getLogger(MasterHbckServlet.class);
private static final Gson GSON = GsonFactory.buildGson();

@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use the raw Servlet API when we have Jersey? Instead of building a response by hand, you could instantiate a POJO and return it, be done.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other servlets are implemented using the Servlet API and I used them as template.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree to Nick’s comment on the REST Endpoint returning JSON backed my Model which is a standard way of implementing REST APIs.

throws ServletException, IOException {
final HMaster master = (HMaster) getServletContext().getAttribute(HMaster.MASTER);
if (!master.isInitialized()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!isInitialized() could mean that this master instance is a backup master, in which case the response should be a redirect to this endpoint on the active master.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this is not something I thought about. Seems reasonable to handle it with a redirect.

LOG.warn("Master is not initialized yet");
sendError(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE,
"master is not initialized yet");
return;
}
final HbckChore hbckChore = master.getHbckChore();
if (hbckChore == null || hbckChore.isDisabled()) {
LOG.warn("Hbck chore is disabled");
sendError(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Hbck chore is disabled");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really a 503? The server is fine. I'm no HTTP status code expert, but I'd say this is more like a 204 SC_NO_CONTENT or a 404 SC_NOT_FOUND meaning "hbck resource is not available".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the HBCK chore is not available the service is not available, was my thinking. However I can see 204 being quite appropriate.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

404 is usually for resource not found right - if hbck is the resource which is disabled, it means the requested resource is not found.
204 would mean that the resource is found but didn't have content to respond with.
May 404 is right? Given the message contains details that chore is disabled?

return;
}
if (!Boolean.parseBoolean(request.getParameter("cache"))) {
try {
master.getMasterRpcServices().runHbckChore(null, null);
} catch (org.apache.hbase.thirdparty.com.google.protobuf.ServiceException se) {
LOG.warn("Failed generating a new hbck chore report; using cache", se);
}
try {
master.getMasterRpcServices().runCatalogScan(null, null);
} catch (org.apache.hbase.thirdparty.com.google.protobuf.ServiceException se) {
LOG.warn("Failed generating a new catalogjanitor report; using cache", se);
}
}
Map<String, Object> result = new HashMap<>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you define a POJO for the response object, we can have some hope of tracing API compatibility in the future using our existing compatibility report.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, this is a good reason.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With POJO, we can achieve something like what OData does to even query for a sub document as mentioned here. I am not sure if there is a right equivalent support already available in HBase, else we can skip the OData part but the POJO adds value.

result.put(START_TIMESTAMP, hbckChore.getCheckingStartTimestamp());
result.put(END_TIMESTAMP, hbckChore.getCheckingEndTimestamp());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our HBCK chore should expose a report so that a reader can get a static view... or a read-write lock so that a reader can block a writer from clobbering its view.

final Map<String, Pair<ServerName, List<ServerName>>> inconsistentRegions =
hbckChore.getInconsistentRegions();
if (inconsistentRegions != null && !inconsistentRegions.isEmpty()) {
result.put(INCONSISTENT_REGIONS, inconsistentRegions);
}
final Map<String, ServerName> orphanRegionsOnRS = hbckChore.getOrphanRegionsOnRS();
if (orphanRegionsOnRS != null && !orphanRegionsOnRS.isEmpty()) {
result.put(ORPHAN_REGIONS_ON_RS, orphanRegionsOnRS);
}
final Map<String, Path> orphanRegionsOnFS = hbckChore.getOrphanRegionsOnFS();
if (orphanRegionsOnFS != null && !orphanRegionsOnFS.isEmpty()) {
result.put(ORPHAN_REGIONS_ON_FS, orphanRegionsOnFS);
}
final CatalogJanitor janitor = master.getCatalogJanitor();
if (janitor != null) {
final Report report = janitor.getLastReport();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CatalogueJanitor exposes the report concept. Brilliant!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and unfortunately the result is a mix of CatalogJanitor's nicely structured Report and HbckChore's other return types.

Unifying these things is out of scope, though. Or, we can make it a precondition, but I won't pursue it at this time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Down review I accepted your proposal to implement this with Jersey, so a clean unification of these two data sources can be done when working up the model (POJO) of the report encoding to return from the request handler.

if (report != null && !report.isEmpty()) {
List<Pair<RegionInfo, RegionInfo>> holes = report.getHoles();
if (holes != null && !holes.isEmpty()) {
result.put(HOLES, holes);
}
List<Pair<RegionInfo, RegionInfo>> overlaps = report.getOverlaps();
if (overlaps != null && !overlaps.isEmpty()) {
result.put(OVERLAPS, overlaps);
}
List<Pair<RegionInfo, ServerName>> unknownServers = report.getUnknownServers();
if (unknownServers != null && !unknownServers.isEmpty()) {
result.put(UNKNOWN_SERVERS, unknownServers);
}
List<byte[]> emptyRegionInfo = report.getEmptyRegionInfo();
if (!emptyRegionInfo.isEmpty()) {
result.put(EMPTY_REGIONINFO, emptyRegionInfo);
}
}
}
response.setContentType("application/json");
PrintWriter out = response.getWriter();
out.write(GSON.toJson(result));
out.write('\n');
}

private static void sendError(HttpServletResponse response, int code, String message)
throws IOException {
response.setContentType("application/json");
Map<String, Object> result = new HashMap<>();
result.put("error", message);
response.setStatus(code);
PrintWriter out = response.getWriter();
out.write(GSON.toJson(result));
out.write('\n');
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/*
* 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.hadoop.hbase.master.http;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;

import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.HBaseClassTestRule;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.ServerName;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.RegionInfo;
import org.apache.hadoop.hbase.client.RegionInfoBuilder;
import org.apache.hadoop.hbase.client.TableDescriptor;
import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
import org.apache.hadoop.hbase.master.HMaster;
import org.apache.hadoop.hbase.master.HbckChore;
import org.apache.hadoop.hbase.master.MasterRpcServices;
import org.apache.hadoop.hbase.master.http.gson.GsonFactory;
import org.apache.hadoop.hbase.master.janitor.CatalogJanitor;
import org.apache.hadoop.hbase.master.janitor.Report;
import org.apache.hadoop.hbase.testclassification.MasterTests;
import org.apache.hadoop.hbase.testclassification.MediumTests;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.Pair;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import org.apache.hbase.thirdparty.com.google.gson.Gson;
import org.apache.hbase.thirdparty.com.google.gson.reflect.TypeToken;

/**
* Tests for the master hbck servlet.
*/
@Category({ MasterTests.class, MediumTests.class })
public class TestMasterHbckServlet {

@ClassRule
public static final HBaseClassTestRule CLASS_RULE =
HBaseClassTestRule.forClass(TestMasterHbckServlet.class);

static final ServerName FAKE_HOST = ServerName.valueOf("fakehost", 12345, 1234567890);
static final ServerName FAKE_HOST_2 = ServerName.valueOf("fakehost2", 12345, 1234567890);
static final TableDescriptor FAKE_TABLE =
TableDescriptorBuilder.newBuilder(TableName.valueOf("mytable")).build();
static final RegionInfo FAKE_HRI = RegionInfoBuilder.newBuilder(FAKE_TABLE.getTableName())
.setStartKey(Bytes.toBytes("a")).setEndKey(Bytes.toBytes("b")).build();
static final RegionInfo FAKE_HRI_2 = RegionInfoBuilder.newBuilder(FAKE_TABLE.getTableName())
.setStartKey(Bytes.toBytes("a")).setEndKey(Bytes.toBytes("c")).build();
static final RegionInfo FAKE_HRI_3 = RegionInfoBuilder.newBuilder(FAKE_TABLE.getTableName())
.setStartKey(Bytes.toBytes("d")).setEndKey(Bytes.toBytes("e")).build();
static final Path FAKE_PATH = new Path(
"/hbase/data/default/" + FAKE_TABLE.getTableName() + "/" + FAKE_HRI_3.getEncodedName());
static final long FAKE_START_TIMESTAMP = System.currentTimeMillis();
static final long FAKE_END_TIMESTAMP = System.currentTimeMillis() + 1000;
static final Gson GSON = GsonFactory.buildGson();

private HMaster master;

@Before
public void setupMocks() {
// Fake inconsistentRegions
Map<String, Pair<ServerName, List<ServerName>>> inconsistentRegions = new HashMap<>();
inconsistentRegions.put(FAKE_HRI.getEncodedName(),
new Pair<>(FAKE_HOST, Arrays.asList(FAKE_HOST_2)));

// Fake orphanRegionsOnRS
Map<String, ServerName> orphanRegionsOnRS = new HashMap<>();

// Fake orphanRegionsOnFS
Map<String, Path> orphanRegionsOnFS = new HashMap<>();
orphanRegionsOnFS.put(FAKE_HRI_3.getEncodedName(), FAKE_PATH);

// Fake HbckChore
HbckChore hbckChore = mock(HbckChore.class);
doReturn(FAKE_START_TIMESTAMP).when(hbckChore).getCheckingStartTimestamp();
doReturn(FAKE_END_TIMESTAMP).when(hbckChore).getCheckingEndTimestamp();
doReturn(inconsistentRegions).when(hbckChore).getInconsistentRegions();
doReturn(orphanRegionsOnRS).when(hbckChore).getOrphanRegionsOnRS();
doReturn(orphanRegionsOnFS).when(hbckChore).getOrphanRegionsOnFS();

// Fake region holes
List<Pair<RegionInfo, RegionInfo>> holes = new ArrayList<>();

// Fake region overlaps
List<Pair<RegionInfo, RegionInfo>> overlaps = new ArrayList<>();
overlaps.add(new Pair<>(FAKE_HRI, FAKE_HRI_2));

// Fake unknown servers
List<Pair<RegionInfo, ServerName>> unknownServers = new ArrayList<>();
unknownServers.add(new Pair<>(FAKE_HRI, FAKE_HOST_2));

// Fake empty region info
List<String> emptyRegionInfo = new ArrayList<>();
emptyRegionInfo.add(FAKE_HRI_3.getEncodedName());

// Fake catalog janitor report
Report report = mock(Report.class);
doReturn(FAKE_START_TIMESTAMP).when(report).getCreateTime();
doReturn(holes).when(report).getHoles();
doReturn(overlaps).when(report).getOverlaps();
doReturn(unknownServers).when(report).getUnknownServers();
doReturn(emptyRegionInfo).when(report).getEmptyRegionInfo();

// Fake CatalogJanitor
CatalogJanitor janitor = mock(CatalogJanitor.class);
doReturn(report).when(janitor).getLastReport();

// Fake master

master = mock(HMaster.class);
doReturn(HBaseConfiguration.create()).when(master).getConfiguration();
doReturn(true).when(master).isInitialized();
doReturn(Optional.of(FAKE_HOST)).when(master).getActiveMaster();
doReturn(janitor).when(master).getCatalogJanitor();
doReturn(hbckChore).when(master).getHbckChore();
MasterRpcServices services = mock(MasterRpcServices.class);
doReturn(services).when(master).getRpcServices();
doReturn(services).when(master).getMasterRpcServices();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do all this mockery? At this point, it's it simpler to stand up a mini-cluster and manufacture the state you need? It looks like TestMetaFixer has some utility code for manufacturing faulty states.

Copy link
Contributor Author

@apurtell apurtell Jun 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because when I wrote this test this was the way I understood it could be implemented and I don't think too much is mocked here, just what is needed to produce a report that has exactly what I want to check for later.

}

@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void testHbckServletWithMocks() throws Exception {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No testing of error states?

// Set up request and response mocks
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintWriter writer = new PrintWriter(out);
doReturn(writer).when(response).getWriter();

// Set up servlet config
ServletContext context = mock(ServletContext.class);
Map<String, Object> configMap = new HashMap<>();
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
return configMap.get(invocation.getArgument(0));
}
}).when(context).getAttribute(anyString());
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
configMap.put(invocation.getArgument(0), invocation.getArgument(1));
return null;
}
}).when(context).setAttribute(anyString(), any());
ServletConfig config = mock(ServletConfig.class);
doReturn(context).when(config).getServletContext();

// Instantiate the servlet and call doGet
MasterHbckServlet servlet = new MasterHbckServlet();
servlet.init(config);
servlet.getServletContext().setAttribute(HMaster.MASTER, master);
servlet.doGet(request, response);
// Flush the writer
writer.flush();

// Response should be now be in 'out'
String responseString = new String(out.toByteArray(), StandardCharsets.UTF_8);
Map<String, Object> result =
GSON.fromJson(responseString, new TypeToken<Map<String, Object>>() {
}.getType());

// Check that the result is as expected
long startTimestamp = ((Double) result.get(MasterHbckServlet.START_TIMESTAMP)).longValue();
assertEquals(FAKE_START_TIMESTAMP, startTimestamp);
long endTimestamp = ((Double) result.get(MasterHbckServlet.END_TIMESTAMP)).longValue();
assertEquals(FAKE_END_TIMESTAMP, endTimestamp);
Map<String, Object> inconsistentRegions =
(Map<String, Object>) result.get(MasterHbckServlet.INCONSISTENT_REGIONS);
assertNotNull(inconsistentRegions);
assertEquals(1, inconsistentRegions.size());
assertNotNull(inconsistentRegions.get(FAKE_HRI.getEncodedName()));
assertNull(inconsistentRegions.get(FAKE_HRI_3.getEncodedName()));
Map<String, Object> orphanRegionsOnRS =
(Map<String, Object>) result.get(MasterHbckServlet.ORPHAN_REGIONS_ON_RS);
assertNull(orphanRegionsOnRS);
Map<String, String> orphanRegionsOnFS =
(Map<String, String>) result.get(MasterHbckServlet.ORPHAN_REGIONS_ON_FS);
assertNotNull(orphanRegionsOnFS);
assertEquals(1, orphanRegionsOnFS.size());
assertNull(orphanRegionsOnFS.get(FAKE_HRI.getEncodedName()));
assertNotNull(orphanRegionsOnFS.get(FAKE_HRI_3.getEncodedName()));
List holes = (List) result.get(MasterHbckServlet.HOLES);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that we are just comparing null vs non-null and size of the list, this raw use of List is fine.

assertNull(holes);
List overlaps = (List) result.get(MasterHbckServlet.OVERLAPS);
assertNotNull(overlaps);
assertEquals(1, overlaps.size());
List unknownServers = (List) result.get(MasterHbckServlet.UNKNOWN_SERVERS);
assertNotNull(unknownServers);
assertEquals(1, unknownServers.size());
List emptyRegionInfo = (List) result.get(MasterHbckServlet.EMPTY_REGIONINFO);
assertNotNull(emptyRegionInfo);
assertEquals(1, emptyRegionInfo.size());
}

}