gerlowskija commented on code in PR #975:
URL: https://github.com/apache/solr/pull/975#discussion_r964814595


##########
solr/core/src/java/org/apache/solr/api/V2HttpCall.java:
##########
@@ -362,14 +363,15 @@ private void invokeJerseyRequest(CoreContainer cores, 
SolrCore core, Application
 
       // Set properties that may be used by Jersey filters downstream
       containerRequest.setProperty(SOLR_QUERY_REQUEST_KEY, solrReq);
-      
containerRequest.setProperty(SolrRequestAuthorizer.CORE_CONTAINER_PROP_NAME, 
cores);
-      
containerRequest.setProperty(SolrRequestAuthorizer.HTTP_SERVLET_REQ_PROP_NAME, 
req);
-      
containerRequest.setProperty(SolrRequestAuthorizer.REQUEST_TYPE_PROP_NAME, 
requestType);
-      
containerRequest.setProperty(SolrRequestAuthorizer.SOLR_PARAMS_PROP_NAME, 
queryParams);
-      
containerRequest.setProperty(SolrRequestAuthorizer.COLLECTION_LIST_PROP_NAME, 
collectionsList);
-      
containerRequest.setProperty(SolrRequestAuthorizer.HTTP_SERVLET_RSP_PROP_NAME, 
response);
+      containerRequest.setProperty(SOLR_QUERY_RESPONSE_KEY, rsp);

Review Comment:
   Ah, looks like I missed the point 😬 
   
   I think the fully-qualified identifiers are from an IDE refactor and the 
unqualified ones were added by hand.  I'll add qualification across the board 
here, to keep things consistent and as-per David's preference.



##########
solr/core/src/java/org/apache/solr/jersey/RequestEventLogger.java:
##########
@@ -0,0 +1,49 @@
+/*
+ * 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.solr.jersey;
+
+import java.lang.invoke.MethodHandles;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.server.monitoring.RequestEventListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Logs out request-specific information useful for troubleshooting Jersey 
development.
+ *
+ * @see ApplicationEventLogger
+ */
+public class RequestEventLogger implements RequestEventListener {
+  private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private volatile long requestCount;

Review Comment:
   Happy to oblige if we do keep this class, though, see comment 
[here](https://github.com/apache/solr/pull/975/files#r960826852)



##########
solr/core/src/java/org/apache/solr/jersey/ApplicationEventLogger.java:
##########
@@ -0,0 +1,51 @@
+/*
+ * 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.solr.jersey;
+
+import java.lang.invoke.MethodHandles;
+import org.glassfish.jersey.server.monitoring.ApplicationEvent;
+import org.glassfish.jersey.server.monitoring.ApplicationEventListener;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.server.monitoring.RequestEventListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Logs out application-level information useful for troubleshooting Jersey 
development.
+ *
+ * @see RequestEventLogger
+ */
+public class ApplicationEventLogger implements ApplicationEventListener {
+  private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private volatile long requestCount = 0;
+
+  @Override
+  public void onEvent(ApplicationEvent event) {
+    if (log.isInfoEnabled()) {
+      log.info("Received ApplicationEvent {}", event.getType());
+    }
+  }
+
+  @Override
+  public RequestEventListener onRequest(RequestEvent requestEvent) {
+    requestCount++;
+    log.info("Starting Jersey request {}", requestCount);

Review Comment:
   ditto, re: my reply to Kevin 
[here](https://github.com/apache/solr/pull/975/files#r960826852)
   
   I'm not sure how useful these classes will be once the framework is in 
place.  I can keep them if you guys would like though



##########
solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java:
##########
@@ -215,54 +216,58 @@ public void handleRequest(SolrQueryRequest req, 
SolrQueryResponse rsp) {
         }
       }
     } catch (Exception e) {
-      if (req.getCore() != null) {
-        boolean isTragic = 
req.getCoreContainer().checkTragicException(req.getCore());
-        if (isTragic) {
-          if (e instanceof SolrException) {
-            // Tragic exceptions should always throw a server error
-            assert ((SolrException) e).code() == 500;
-          } else {
-            // wrap it in a solr exception
-            e = new SolrException(SolrException.ErrorCode.SERVER_ERROR, 
e.getMessage(), e);
-          }
-        }
-      }
-      boolean incrementErrors = true;
-      boolean isServerError = true;
-      if (e instanceof SolrException) {
-        SolrException se = (SolrException) e;
-        if (se.code() == SolrException.ErrorCode.CONFLICT.code) {
-          incrementErrors = false;
-        } else if (se.code() >= 400 && se.code() < 500) {
-          isServerError = false;
-        }
-      } else {
-        if (e instanceof SyntaxError) {
-          isServerError = false;
-          e = new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
-        }
-      }
-
+      e = normalizeReceivedException(req, e);
+      processErrorMetricsOnException(e, metrics);
       rsp.setException(e);
+    } finally {
+      long elapsed = timer.stop();
+      metrics.totalTime.inc(elapsed);
+    }
+  }
 
-      if (incrementErrors) {
-        SolrException.log(log, e);
+  public static void processErrorMetricsOnException(Exception e, 
HandlerMetrics metrics) {
+    boolean isClientError = false;
+    if (e instanceof SolrException) {
+      final SolrException se = (SolrException) e;
+      if (se.code() == SolrException.ErrorCode.CONFLICT.code) {
+        return;
+      } else if (se.code() >= 400 && se.code() < 500) {
+        isClientError = true;
+      }
+    }
 
-        metrics.numErrors.mark();
-        if (isServerError) {
-          metrics.numServerErrors.mark();
+    SolrException.log(log, e);
+    metrics.numErrors.mark();
+    if (isClientError) {
+      metrics.numClientErrors.mark();
+    } else {
+      metrics.numServerErrors.mark();
+    }
+  }
+
+  public static Exception normalizeReceivedException(SolrQueryRequest req, 
Exception e) {
+    if (req.getCore() != null) {
+      boolean isTragic = 
req.getCoreContainer().checkTragicException(req.getCore());

Review Comment:
   🙍  Bleh, really dislike all the 'lift' noise from code that was shifted 
around but not really added in the PR.  I get that even understanding what 
lines are "new" is a hard problem, but bleh.



##########
solr/core/src/java/org/apache/solr/core/PluginBag.java:
##########
@@ -490,4 +532,8 @@ public Api v2lookup(String path, String method, Map<String, 
String> parts) {
   public ApiBag getApiBag() {
     return apiBag;
   }
+
+  public ResourceConfig getJerseyEndpoints() {

Review Comment:
   I really dislike the name "ResourceConfig" that comes out of Jersey.  So my 
thought process here was to abstract away the bad name as much as we can by 
giving this method a name that describes things more from a conceptual or 
functional perspective.  The main thing ResourceConfig does for us is wrap up 
all of the APIs/endpoints we want available on a given core, etc, so 
emphasizing that in the method name seemed like it'd be clearer than 
"getResourceConfig" or even "getJerseyResourceConfig".
   
   No rhyme or reason I didn't do the same thing to CoreContainer - I just 
missed updating it when I renamed the method here.  (In any case - I've deleted 
the method off of CC in an upcoming commit, so consistency there is a bit moot.)
   
   Anyway - I don't feel strongly about it.  Happy to change or keep if you 
have a preference.



##########
solr/core/src/java/org/apache/solr/api/V2HttpCall.java:
##########
@@ -388,33 +335,65 @@ public CompositeApi add(Api api) {
     }
   }
 
+  // We don't rely on any of Jersey's authc/z features so we pass in this 
empty context for
+  // all requests.
+  public static final SecurityContext DEFAULT_SECURITY_CONTEXT = new 
SecurityContext() {
+    public boolean isUserInRole(String role) { return false; }
+    public boolean isSecure() { return false; }
+    public Principal getUserPrincipal() { return null; }
+    public String getAuthenticationScheme() { return null; }
+  };
+
+  private void invokeJerseyRequest(CoreContainer cores, SolrCore core, 
ApplicationHandler jerseyHandler, SolrQueryResponse rsp) {
+    try {
+      final ContainerRequest containerRequest = 
ContainerRequestUtils.createContainerRequest(req, response, 
jerseyHandler.getConfiguration());
+
+      // Set properties that may be used by Jersey filters downstream
+      containerRequest.setProperty(SOLR_QUERY_REQUEST_KEY, solrReq);
+      containerRequest.setProperty(SOLR_QUERY_RESPONSE_KEY, rsp);
+      containerRequest.setProperty(RequestContextConstants.CORE_CONTAINER_KEY, 
cores);

Review Comment:
   It holds nothing but keys currently, but it's easy to imagine it holding 
defaults or some other type of value down the road.  I'd prefer to keep 'KEY' 
in these names for now, as defense against that potential confusion.
   
   I'll change if you feel strongly though.



##########
solr/core/src/java/org/apache/solr/api/V2HttpCall.java:
##########
@@ -273,7 +278,7 @@ public static Api getApiInfo(
     }
 
     if (api == null) {
-      return getSubPathApi(requestHandlers, path, fullPath, new 
CompositeApi(null));

Review Comment:
   It ended up turning into dead code as I went through the PR, so this PR 
removes it.  On `main` it handles some especially nested introspect APIs, but 
some of the other changes to V2HttpCall made these invocations superfluous.
   
   (As a larger question, I wonder whether we still see "introspect" APIs as 
being valuable, or whether they could be replaced or reimplemented with some of 
the tooling that an OpenAPI integration would offer us.  But that's a 
discussion for another day I think...)



##########
solr/core/src/java/org/apache/solr/jersey/ApplicationEventLogger.java:
##########
@@ -0,0 +1,51 @@
+/*
+ * 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.solr.jersey;
+
+import java.lang.invoke.MethodHandles;
+import org.glassfish.jersey.server.monitoring.ApplicationEvent;
+import org.glassfish.jersey.server.monitoring.ApplicationEventListener;
+import org.glassfish.jersey.server.monitoring.RequestEvent;
+import org.glassfish.jersey.server.monitoring.RequestEventListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Logs out application-level information useful for troubleshooting Jersey 
development.
+ *
+ * @see RequestEventLogger
+ */
+public class ApplicationEventLogger implements ApplicationEventListener {
+  private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  private volatile long requestCount = 0;

Review Comment:
   Yeah, that'd be a good idea.
   
   I actually only had this class (and its partner, RequestEventLogger) around 
for my own personal debugging.  I intended to delete them from the PR prior to 
merging.
   
   I'm happy to keep them around (and make the change you suggested above) if 
you think they'd be a helpful aid to devs though?



##########
solr/core/src/java/org/apache/solr/core/SolrCore.java:
##########
@@ -184,7 +185,7 @@
  * to make it work. When multi-core support was added to Solr way back in 
version 1.3, this class
  * was required so that the core functionality could be re-used multiple times.
  */
-public final class SolrCore implements SolrInfoBean, Closeable {
+public class SolrCore implements SolrInfoBean, Closeable {

Review Comment:
   To be mock-able in tests.
   
   See SchemaNameAPITest, as an example.  The simplicity of that API maybe 
doesn't show the value of being able to mock SolrCore's.  (Maybe I should've 
picked a more involved API to convert to Jersey as an example in this PR...)
   
   Anyway, simplicity aside, you can probably imagine how being able to mock 
SolrCores could be helpful for unit testing in a more involved API.
   
   



##########
solr/core/src/java/org/apache/solr/handler/admin/ConfigSetsHandler.java:
##########
@@ -192,6 +200,28 @@ public Category getCategory() {
     return Category.ADMIN;
   }
 
+  public Boolean registerV2() {
+    return true;
+  }
+
+  @Override
+  public Collection<Api> getApis() {

Review Comment:
   I'm going to skip `List.of` here - `AnnotatedApi.getApis` returns a 
`List<Api>` itself, so there's a flattening problem with using `List.of` that's 
more trouble than its worth.



##########
solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java:
##########
@@ -215,54 +215,59 @@ public void handleRequest(SolrQueryRequest req, 
SolrQueryResponse rsp) {
         }
       }
     } catch (Exception e) {
-      if (req.getCore() != null) {
-        boolean isTragic = 
req.getCoreContainer().checkTragicException(req.getCore());
-        if (isTragic) {
-          if (e instanceof SolrException) {
-            // Tragic exceptions should always throw a server error
-            assert ((SolrException) e).code() == 500;
-          } else {
-            // wrap it in a solr exception
-            e = new SolrException(SolrException.ErrorCode.SERVER_ERROR, 
e.getMessage(), e);
-          }
-        }
-      }
-      boolean incrementErrors = true;
-      boolean isServerError = true;
-      if (e instanceof SolrException) {
-        SolrException se = (SolrException) e;
-        if (se.code() == SolrException.ErrorCode.CONFLICT.code) {
-          incrementErrors = false;
-        } else if (se.code() >= 400 && se.code() < 500) {
-          isServerError = false;
-        }
-      } else {
-        if (e instanceof SyntaxError) {
-          isServerError = false;
-          e = new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
-        }
-      }
-
+      e = normalizeReceivedException(req, e);
+      processErrorMetricsOnException(e, metrics);
       rsp.setException(e);
+    } finally {
+      long elapsed = timer.stop();
+      metrics.totalTime.inc(elapsed);
+    }
+  }
 
-      if (incrementErrors) {
-        SolrException.log(log, e);
+  public static void processErrorMetricsOnException(Exception e, 
HandlerMetrics metrics) {
+    boolean isClientError = false;
+    if (e instanceof SolrException) {
+      final SolrException se = (SolrException) e;
+      if (se.code() == SolrException.ErrorCode.CONFLICT.code) {
+        return;
+      } else if (se.code() >= 400 && se.code() < 500) {
+        isClientError = true;
+      }
+    }
+
+    SolrException.log(log, e);
+    metrics.numErrors.mark();
+    if (isClientError) {
+      metrics.numClientErrors.mark();
+    } else {
+      metrics.numServerErrors.mark();
+    }
+  }
 
-        metrics.numErrors.mark();
-        if (isServerError) {
-          metrics.numServerErrors.mark();
+  public static Exception normalizeReceivedException(SolrQueryRequest req, 
Exception e) {
+    if (req.getCore() != null) {
+      assert req.getCoreContainer() != null;
+      boolean isTragic = 
req.getCoreContainer().checkTragicException(req.getCore());
+      if (isTragic) {
+        if (e instanceof SolrException) {
+          // Tragic exceptions should always throw a server error
+          assert ((SolrException) e).code() == 500;

Review Comment:
   I'm happy to add this, but this code pre-exists my PR and might change 
pre-existing behavior.  Figured it might not've been clear given the diff.



##########
solr/core/src/java/org/apache/solr/handler/admin/api/GetSchemaNameResponse.java:
##########
@@ -0,0 +1,26 @@
+/*
+ * 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.solr.handler.admin.api;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.solr.jersey.SolrJerseyResponse;
+
+public class GetSchemaNameResponse extends SolrJerseyResponse {
+  @JsonProperty("name")
+  public String name;

Review Comment:
   We could go that route if you think there are advantages, but there's at 
least one tradeoff.  See the javadocs on 
`JerseyResource.instantiateJerseyResponse` 
[here](https://github.com/apache/solr/pull/975/files#diff-dc8c81d35faf30ab32e71cf916a001181724ea496ae28e5928890894e938d890R35)
 for some context.
   
   To attempt to summarize: an immutable response object would prevent our v2 
APIs from returning the sort of "mixed" responses that Solr returns today on 
some errors.  I don't love this current behavior of Solr's - in fact I'd call 
it a bug - but breaking compatibility with it would set us afoul of some tests 
and bloat the diff here.



##########
solr/core/src/java/org/apache/solr/handler/configsets/ListConfigSetsAPI.java:
##########
@@ -16,34 +16,41 @@
  */
 package org.apache.solr.handler.configsets;
 
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
 import static 
org.apache.solr.security.PermissionNameProvider.Name.CONFIG_READ_PERM;
 
-import java.util.List;
-import org.apache.solr.api.EndPoint;
-import org.apache.solr.client.solrj.SolrResponse;
-import org.apache.solr.cloud.OverseerSolrResponse;
-import org.apache.solr.common.util.NamedList;
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import org.apache.solr.api.JerseyResource;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.request.SolrQueryRequest;
-import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.jersey.PermissionName;
 
 /**
  * V2 API for adding or updating a single file within a configset.
  *
  * <p>This API (GET /v2/cluster/configs) is analogous to the v1 
/admin/configs?action=LIST command.
  */
-public class ListConfigSetsAPI extends ConfigSetAPIBase {
+@Path("/cluster/configs")
+public class ListConfigSetsAPI extends JerseyResource {
+
+  @Context public HttpHeaders headers;
+
+  private final CoreContainer coreContainer;
+
+  @Inject
   public ListConfigSetsAPI(CoreContainer coreContainer) {
-    super(coreContainer);
+    this.coreContainer = coreContainer;
   }
 
-  @EndPoint(method = GET, path = "/cluster/configs", permission = 
CONFIG_READ_PERM)
-  public void listConfigSet(SolrQueryRequest req, SolrQueryResponse rsp) 
throws Exception {
-    final NamedList<Object> results = new NamedList<>();
-    List<String> configSetsList = configSetService.listConfigs();
-    results.add("configSets", configSetsList);
-    SolrResponse response = new OverseerSolrResponse(results);
-    rsp.getValues().addAll(response.getResponse());
+  @GET
+  @Produces({"application/json", "application/javabin"})
+  @PermissionName(CONFIG_READ_PERM)
+  public ListConfigsetsResponse listConfigSet() throws Exception {
+    final ListConfigsetsResponse response = new ListConfigsetsResponse();
+    response.configSets = coreContainer.getConfigSetService().listConfigs();
+    return response;

Review Comment:
   See my reply to a similar comment 
[here](https://github.com/apache/solr/pull/975/files#r963069506).



##########
solr/core/src/java/org/apache/solr/handler/configsets/ListConfigsetsResponse.java:
##########
@@ -0,0 +1,29 @@
+/*
+ * 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.solr.handler.configsets;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+import org.apache.solr.jersey.SolrJerseyResponse;
+
+/** Response body POJO for the {@link ListConfigSetsAPI} resource. */
+public class ListConfigsetsResponse extends SolrJerseyResponse {

Review Comment:
   Let's continue the discussion about immutability on other threads where you 
mentioned it, but happy to move the class, sure.



##########
solr/core/src/java/org/apache/solr/core/PluginBag.java:
##########
@@ -213,6 +237,24 @@ public PluginHolder<T> put(String name, PluginHolder<T> 
plugin) {
                 apiBag.register(api, nameSubstitutes);
               }
             }
+
+            // TODO Should we use <requestHandler name="/blah"> to override 
the path that each
+            //  resource registers under?
+            Collection<Class<? extends JerseyResource>> jerseyApis =
+                apiSupport.getJerseyResources();
+            if (!CollectionUtils.isEmpty(jerseyApis)) {
+              for (Class<? extends JerseyResource> jerseyClazz : jerseyApis) {
+                if (log.isDebugEnabled()) {
+                  log.debug("Registering jersey resource class: {}", 
jerseyClazz.getName());
+                }
+                jerseyResources.register(jerseyClazz);
+                // See MetricsBeanFactory javadocs for a better understanding 
of this resource->RH
+                // mapping
+                if (inst instanceof RequestHandlerBase) {

Review Comment:
   Yes to both your points: (1) that this is sketchy code, and (2) that this is 
related to the method-visibility change on RHB.  I'm all for a better approach 
here, but I struggled for some time to find one without any luck.  Let me give 
a little more context around what this code is trying to do, maybe you can see 
an approach that I missed...
   
   This code exists to serve our metrics integration.
   
   Historically, v2 APIs got metrics "for free" because most v2 APIs were 
"just" a translation layer around an existing requestHandler.  Since the v2 
APIs called `handler.handleRequest` at some point, the metrics code in 
`RequestHandlerBase` would be invoked and metrics would "just work".  Of 
course, this only works for v2 APIs that have a v1 equivalent.  Newer v2 APIs 
(e.g. `ClusterAPI.roles`) never solved the metrics question afaict.
   
   I don't like relying on RequestHandler for this functionality, but I thought 
it _did_ made sense to have Jersey v2 APIs keep as close as possible to the 
existing metrics setup.  We'd be serving our users poorly if v1 and v2 requests 
for the same functionality were tracked under different metrics.  Or if the 
metric names produced by v2 changed anytime we made an internal refactor to 
move a v2 API over to Jersey.  What a pain that'd be to make a dashboard for!
   
   The way I thought to implement this for Jersey APIs was to give the Jersey 
"resource" some pointer back to its relevant request handler, so it can 
initialize its metrics.  But this ended up being tricky, for two main reasons:
   
   - In a sense, the `instanceof` check here is only necessary because we're 
using the same container (`PluginBag`) for many different types of plugins.  If 
the type-param 'T' was always a RequestHandler, this code wouldn't have to do 
nearly as much type-juggling.  I thought about maybe creating a PluginBag 
subclass specific to requestHandlers, but that seems to cut against the pretty 
explicit intention here.  It also steps on the toes of the `RequestHandlers` 
class which I don't fully understand.  IMO this is still the best option, but 
it seems like a large refactor to drag into this (already large) PR.
   - Other simpler solutions are off the table. For example, it'd be nice if 
`ApiSupport.getJerseyResources` returned a collection of actual instances 
instead of references to JerseyResource classes.  That'd give each 
RequestHandler a nice, strongly-typed place to give each resource instance the 
metrics-context that they need.  But this doesn't work for lifecycle reasons: 
RequestHandlers generally live as long as the SolrCore or CoreContainer they 
belong to, but Jersey generally instantiates a new resource instance for each 
incoming request.  (Jersey _does_ allow you to change this behavior, but doing 
so would impose some restrictions that aren't worth the hassle IMO.)  So 
`getJerseyResources` has to return class references instead of nice 
already-bootstrapped instances.
   
   ----
   
   Anyway, hope this wasn't too long of a response.  But I share your 
irritation at this section, and I wanted to give you the context needed to 
suggest an alternative if you can think of one.  Really hoping your fresh eyes 
will catch an approach I missed.  I'm going to spend some time this afternoon 
revisiting the issue as well.



##########
solr/core/src/java/org/apache/solr/handler/api/ApiRegistrar.java:
##########
@@ -79,12 +73,4 @@ public static void registerShardApis(ApiBag apiBag, 
CollectionsHandler collectio
     // here for simplicity.
     apiBag.registerObject(new DeleteReplicaAPI(collectionsHandler));
   }
-
-  public static void registerConfigsetApis(ApiBag apiBag, CoreContainer 
container) {

Review Comment:
   It was merely moved, but it's tough to spot as it's in a totally different 
file now: see `ConfigSetsHandler.getApis`.
   
   In short: I created this method in ApiRegistrar during the apispec-framework 
migration because at the time I thought container-level APIs couldn't be 
registered via `SomeRequestHandler.getApis()` lookup, but I was wrong.
   
   I plan to remove ApiRegistrar altogether in a subsequent commit, but since 
this PR converts one of the configset APIs to JAX-RS, I decided to move those 
APIs here-and-now. 



##########
solr/core/src/java/org/apache/solr/api/JerseyResource.java:
##########
@@ -0,0 +1,92 @@
+/*
+ * 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.solr.api;
+
+import static 
org.apache.solr.jersey.RequestContextConstants.SOLR_JERSEY_RESPONSE_KEY;
+
+import java.util.function.Supplier;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.core.Context;
+import org.apache.solr.jersey.CatchAllExceptionMapper;
+import org.apache.solr.jersey.SolrJerseyResponse;
+import org.apache.solr.servlet.HttpSolrCall;
+
+/** A marker parent type for all Jersey resource classes */

Review Comment:
   Yes, it's a rough analog to v1's SolrRequestHandler (or v2's Api).
   
   I've added these comparisons to the Javadocs here.  Let me know if you think 
it should get into more detail.  It's definitely worth explaining, but at the 
same time it's hard to give a just-thorough-enough Jersey/JAX-RS overview in 
Javadocs.  So if I didn't find the right balance, let me know.



##########
solr/core/src/java/org/apache/solr/jersey/MetricBeanFactory.java:
##########
@@ -0,0 +1,55 @@
+/*
+ * 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.solr.jersey;
+
+import org.apache.solr.core.PluginBag;
+import org.glassfish.hk2.api.Factory;
+
+/**
+ * Factory to inject JerseyMetricsLookupRegistry instances into Jersey 
resources and filters.
+ *
+ * <p>Currently, Jersey resources that have a corresponding v1 API produce the 
same metrics as their
+ * v1 equivalent and rely on the v1 requestHandler instance to do so. Solr 
facilitates this by
+ * building a map of the Jersey resource to requestHandler mapping (a {@link
+ * org.apache.solr.core.PluginBag.JerseyMetricsLookupRegistry}), and injecting 
it into the pre- and
+ * post- Jersey filters that handle metrics.
+ *
+ * <p>This isn't ideal, as requestHandler's don't really "fit" conceptually 
here. But it's
+ * unavoidable while we want our v2 APIs to exactly match the metrics produced 
by v1 calls.

Review Comment:
   I'd need to do a bit more research on the metric side to better understand 
what'd make sense.  I don't have a ton of experience working with Solr's 
metrics, beyond setting up the prom exporter.  That's another reason I was 
reluctant to strike out and do something different with them in this PR.
   
   The biggest change I can imagine from a user perspective would be that Solr 
could provide metrics on a per-API granularity, instead of a per-RequestHandler 
level.
   
   e.g. Today, `CollectionsHandler` bundles together 15 or 20 different APIs, 
some of which are only loosely related to one another.  Since metrics are 
RH-based, the request rate, total time, etc. metrics associated with 
CollectionsHandler lumps together collection creation requests with 
clusterstatus calls with shard-splits, etc.  That needn't be the case if we 
split from v1, which could be really cool.
   
   In terms of the implementation, we'd still have the Pre- and Post- Jersey 
filters that this PR introduces.  But presumably the Post- filter would invoke 
some sort of `recordMetrics(MetricsContext)` method defined by each 
JerseyResource.



##########
solr/core/src/test/org/apache/solr/handler/configsets/ListConfigSetsAPITest.java:
##########
@@ -0,0 +1,139 @@
+/*
+ * 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.solr.handler.configsets;
+
+import static org.apache.solr.SolrTestCaseJ4.assumeWorkingMockito;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import javax.inject.Singleton;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+import org.apache.solr.client.solrj.response.ConfigSetAdminResponse;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.ConfigSetService;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.handler.api.V2ApiUtils;
+import org.apache.solr.jersey.CoreContainerFactory;
+import org.apache.solr.jersey.SolrJacksonMapper;
+import org.glassfish.hk2.utilities.binding.AbstractBinder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class ListConfigSetsAPITest extends JerseyTest {
+
+  private CoreContainer mockCoreContainer;
+
+  @BeforeClass
+  public static void ensureWorkingMockito() {
+    assumeWorkingMockito();
+  }
+
+  @Override
+  protected Application configure() {
+    resetMocks();
+    final ResourceConfig config = new ResourceConfig();
+    config.register(ListConfigSetsAPI.class);
+    config.register(SolrJacksonMapper.class);
+    config.register(
+        new AbstractBinder() {
+          @Override
+          protected void configure() {
+            bindFactory(new CoreContainerFactory(mockCoreContainer))
+                .to(CoreContainer.class)
+                .in(Singleton.class);
+          }
+        });
+
+    return config;
+  }
+
+  private void resetMocks() {
+    mockCoreContainer = mock(CoreContainer.class);
+  }
+
+  @Test
+  public void testSuccessfulListConfigsetsRaw() throws Exception {
+    final String expectedJson =
+        
"{\"responseHeader\":{\"status\":0,\"QTime\":0},\"configSets\":[\"cs1\",\"cs2\"]}";
+    final ConfigSetService configSetService = mock(ConfigSetService.class);
+    when(mockCoreContainer.getConfigSetService()).thenReturn(configSetService);
+    when(configSetService.listConfigs()).thenReturn(List.of("cs1", "cs2"));
+
+    final Response response = target("/cluster/configs").request().get();
+    final String jsonBody = response.readEntity(String.class);
+
+    assertEquals(200, response.getStatus());
+    assertEquals("application/json", 
response.getHeaders().getFirst("Content-type"));
+    assertEquals(1, 1);
+    assertEquals(
+        expectedJson,
+        
"{\"responseHeader\":{\"status\":0,\"QTime\":0},\"configSets\":[\"cs1\",\"cs2\"]}");
+  }
+
+  @Test
+  public void testSuccessfulListConfigsetsTyped() throws Exception {
+    final ConfigSetService configSetService = mock(ConfigSetService.class);
+    when(mockCoreContainer.getConfigSetService()).thenReturn(configSetService);
+    when(configSetService.listConfigs()).thenReturn(List.of("cs1", "cs2"));
+
+    final ListConfigsetsResponse response =
+        target("/cluster/configs").request().get(ListConfigsetsResponse.class);
+
+    assertNotNull(response.configSets);
+    assertNull(response.error);
+    assertEquals(2, response.configSets.size());
+    assertTrue(response.configSets.contains("cs1"));
+    assertTrue(response.configSets.contains("cs2"));
+  }
+
+  /**
+   * Test the v2 to v1 response mapping for /cluster/configs
+   *
+   * <p>{@link org.apache.solr.handler.admin.ConfigSetsHandler} uses {@link 
ListConfigSetsAPI} (and
+   * its response class {@link ListConfigsetsResponse}) internally to serve 
the v1 version of this
+   * functionality. So it's important to make sure that the v2 response stays 
compatible with SolrJ
+   * - both because that's important in its own right and because that ensures 
we haven't
+   * accidentally changed the v1 response format.
+   */
+  @Test
+  public void testListConfigsetsV1Compatibility() throws Exception {

Review Comment:
   Hah, the old integration vs unit test debate.
   
   Each has a place at the table IMO.  Integration tests do give you better 
fidelity for high level functionality.  I wouldn't rely on unit tests alone.  
And unit tests do often need to change with the code, which is a pain.
   
   But IMO unit tests have a lot of advantages: they're faster, more targeted, 
run less risk of unrelated environmental issues (such as cause many of Solr's 
flaky build issues), allow testing of otherwise hard-to-trigger codepaths, etc.
   
   To answer your specific question about this test, I thought a unit test 
would be a good addition for two reasons:
   
   First, Solr has lots of integration tests around configset-listing, and I 
trust those to flag issues with json/xml/javabin serialization if this were to 
break.  But none of them are very focused on serialization.  They each leave a 
pretty big gap between seeing the failure and understanding the root cause and 
fixing it.  This test addresses that by having something very narrowly focused 
on just the response conversions involved, hopefully making a failure much more 
actionable.
   
   Second, I wanted to create an example test case based on a Jersey 
test-application.  The relative simplicity of the list-configset API doesn't 
make the point very well, but more complex APIs will benefit a lot from 
Jersey-framework-aided unit testing.  And I wanted to include an example of 
that in this PR.



##########
solr/core/src/test/org/apache/solr/handler/configsets/package-info.java:
##########
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** Testing for the org.apache.solr.handler.configsets package. */
+package org.apache.solr.handler.configsets;

Review Comment:
   I only usually add these when `check` yells at me about it - so I fear it is 
mandatory?  I'll remove it and see though.



##########
solr/core/src/java/org/apache/solr/jersey/RequestContextConstants.java:
##########
@@ -0,0 +1,57 @@
+/*
+ * 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.solr.jersey;
+
+import com.codahale.metrics.Timer;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.container.ContainerRequestContext;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.handler.RequestHandlerBase;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.security.AuthorizationContext;
+
+/**
+ * Keys used to store and retrieve values from the Jersey request context.
+ *
+ * <p>Properties are generally set in V2HttpCall's 'invokeJerseyRequest' and 
retrieved in individual
+ * {@link javax.ws.rs.container.ContainerRequestFilter}s using {@link
+ * ContainerRequestContext#getProperty(String)}
+ */
+public class RequestContextConstants {

Review Comment:
   Neat, I'll try it out.  Does anything stop folks from 
extending/instantiating them the way the private-ctor does in a class?



##########
solr/core/src/java/org/apache/solr/jersey/SolrJerseyResponse.java:
##########
@@ -0,0 +1,46 @@
+/*
+ * 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.solr.jersey;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Base response-body POJO to be used by Jersey resources.
+ *
+ * <p>Contains fields common to all Solr API responses, particularly the 
'responseHeader' and
+ * 'error' fields.
+ */
+public class SolrJerseyResponse implements JacksonReflectMapWriter {
+
+  @JsonProperty("responseHeader")
+  public ResponseHeader responseHeader = new ResponseHeader();
+
+  @JsonProperty("error")
+  public ErrorInfo error;
+
+  public static class ResponseHeader implements JacksonReflectMapWriter {

Review Comment:
   Yep, there's definitely stuff in Solr responses that's not covered here, 
yet. `echoParams` being a good example.  But my thought was that those gaps 
could be filled as we move forward, in our standard progress-not-perfection 
fashion.
   
   As we move v2 APIs to JAX-RS, we're going to hit tests that exercise e.g. 
echoParams, and that'll serve as a forcing function to get coverage for those 
additional fields here.
   
   I can imagine some rigidity issues: like if we had two APIs that used the 
same key within a responseHeader to store different types.  But there are 
tools/patterns for handling those as they come up, so IMO a strongly-typed 
response was still worth trying.
   
   But maybe I'm missing the specifics of your concern?  Was it primarily 
around incompleteness, or rigidity, or something else?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscr...@solr.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscr...@solr.apache.org
For additional commands, e-mail: issues-h...@solr.apache.org

Reply via email to