Skip to content
Open
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
8 changes: 8 additions & 0 deletions changelog/unreleased/SOLR-18078.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
title: Support deleting implictly created RequestHandlers through ConfigAPI
type: added # added, changed, fixed, deprecated, removed, dependency_update, security, other
authors:
- name: Eric Pugh
links:
- name: SOLR-18078
url: https://issues.apache.org/jira/browse/SOLR-18078
54 changes: 52 additions & 2 deletions solr/core/src/java/org/apache/solr/core/ConfigOverlay.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.solr.common.MapSerializable;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CoreAdminParams;
Expand All @@ -38,6 +39,7 @@ public class ConfigOverlay implements MapSerializable {
private final Map<String, Object> data;
private Map<String, Object> props;
private Map<String, Object> userProps;
private final Set<String> deletedPlugins;

@SuppressWarnings({"unchecked"})
public ConfigOverlay(Map<String, Object> jsonObj, int version) {
Expand All @@ -48,6 +50,13 @@ public ConfigOverlay(Map<String, Object> jsonObj, int version) {
if (props == null) props = Collections.emptyMap();
userProps = (Map<String, Object>) data.get("userProps");
if (userProps == null) userProps = Collections.emptyMap();

List<String> deleted = (List<String>) data.get("deleted");
if (deleted == null) {
deletedPlugins = Collections.emptySet();
} else {
deletedPlugins = Set.copyOf(deleted);
}
}

public Object getXPathProperty(String xpath) {
Expand Down Expand Up @@ -262,12 +271,53 @@ public ConfigOverlay addNamedPlugin(Map<String, Object> info, String typ) {
@SuppressWarnings({"unchecked"})
public ConfigOverlay deleteNamedPlugin(String name, String typ) {
Map<String, Object> dataCopy = Utils.getDeepCopy(data, 4);

// Remove from overlay if present
Map<?, ?> reqHandler = (Map<?, ?>) dataCopy.get(typ);
if (reqHandler == null) return this;
reqHandler.remove(name);
if (reqHandler != null) {
reqHandler.remove(name);
}

// Add to deleted set (tombstone marker)
List<String> deleted = (List<String>) dataCopy.get("deleted");
if (deleted == null) {
deleted = new ArrayList<>();
dataCopy.put("deleted", deleted);
} else {
// Make a copy since the list might be immutable
deleted = new ArrayList<>(deleted);
dataCopy.put("deleted", deleted);
}

String deletionKey = typ + ":" + name;
if (!deleted.contains(deletionKey)) {
deleted.add(deletionKey);
}

return new ConfigOverlay(dataCopy, this.version);
}

/**
* Checks if a plugin has been marked as deleted in the overlay.
*
* @param typ the plugin type (e.g., "requestHandler")
* @param name the plugin name (e.g., "/update/json")
* @return true if the plugin is marked as deleted
*/
public boolean isPluginDeleted(String typ, String name) {
String deletionKey = typ + ":" + name;
return deletedPlugins.contains(deletionKey);
}

/**
* Gets the set of all deleted plugin keys in the format "type:name".
*
* @return an unmodifiable set of deleted plugin keys
*/
public Set<String> getDeletedPlugins() {
return deletedPlugins;
}

public static final String ZNODEVER = "znodeVersion";
public static final String NAME = "overlay";
}
22 changes: 20 additions & 2 deletions solr/core/src/java/org/apache/solr/core/PluginBag.java
Original file line number Diff line number Diff line change
Expand Up @@ -349,9 +349,27 @@ void init(Map<String, T> defaults, SolrCore solrCore, List<PluginInfo> infos) {
infos.stream().map(i -> i.name).collect(Collectors.toList()));
}
}

// Get the ConfigOverlay to check for deleted plugins
ConfigOverlay overlay = solrCore.getSolrConfig().getOverlay();

// Register default plugins, but skip those marked as deleted in the overlay
for (Map.Entry<String, T> e : defaults.entrySet()) {
if (!contains(e.getKey())) {
put(e.getKey(), new PluginHolder<>(null, e.getValue()));
String pluginName = e.getKey();

// Check if this default plugin has been marked as deleted
if (overlay.isPluginDeleted(meta.getCleanTag(), pluginName)) {
if (log.isDebugEnabled()) {
log.debug(
"Skipping default {} '{}' because it is marked as deleted in ConfigOverlay",
meta.getCleanTag(),
pluginName);
}
continue;
}

if (!contains(pluginName)) {
put(pluginName, new PluginHolder<>(null, e.getValue()));
}
}
}
Expand Down
11 changes: 9 additions & 2 deletions solr/core/src/java/org/apache/solr/core/RequestHandlers.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,17 @@ public PluginBag<SolrRequestHandler> getRequestHandlers() {
*/
void initHandlersFromConfig(SolrConfig config) {
List<PluginInfo> implicits = core.getImplicitHandlers();
ConfigOverlay overlay = config.getOverlay();

// use link map so we iterate in the same order
Map<String, PluginInfo> infoMap = new LinkedHashMap<>();
// deduping implicit and explicit requesthandlers
for (PluginInfo info : implicits) infoMap.put(info.name, info);
// deduping implicit and explicit requesthandlers, and filtering out deleted ones
for (PluginInfo info : implicits) {
// Skip implicit handlers that have been marked as deleted in the overlay
if (!overlay.isPluginDeleted(SolrRequestHandler.TYPE, info.name)) {
infoMap.put(info.name, info);
}
}
for (PluginInfo info : config.getPluginInfos(SolrRequestHandler.class.getName()))
infoMap.put(info.name, info);
ArrayList<PluginInfo> infos = new ArrayList<>(infoMap.values());
Expand Down
33 changes: 30 additions & 3 deletions solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -615,12 +615,34 @@ private ConfigOverlay deleteNamedComponent(
CommandOperation op, ConfigOverlay overlay, String typ) {
String name = op.getStr(CommandOperation.ROOT_OBJ);
if (op.hasError()) return overlay;

// Check if it exists in the overlay
if (overlay.getNamedPlugins(typ).containsKey(name)) {
return overlay.deleteNamedPlugin(name, typ);
} else {
op.addError(formatString("NO such {0} ''{1}'' ", typ, name));
return overlay;
}

// Check if it's an implicit handler (for requestHandler type)
if (SolrRequestHandler.TYPE.equals(typ)) {
List<PluginInfo> implicitHandlers = req.getCore().getImplicitHandlers();
for (PluginInfo pluginInfo : implicitHandlers) {
if (name.equals(pluginInfo.name)) {
// It's an implicit handler, so we can delete it by adding a tombstone marker
return overlay.deleteNamedPlugin(name, typ);
}
}
}

// Check if it's a default response writer (for queryResponseWriter type)
if ("queryResponseWriter".equals(typ)) {
if (SolrCore.DEFAULT_RESPONSE_WRITERS.containsKey(name)) {
// It's a default response writer, so we can delete it by adding a tombstone marker
return overlay.deleteNamedPlugin(name, typ);
}
}

// Not found anywhere
op.addError(formatString("NO such {0} ''{1}'' ", typ, name));
return overlay;
}

private ConfigOverlay updateNamedPlugin(
Expand Down Expand Up @@ -663,6 +685,11 @@ private ConfigOverlay updateNamedPlugin(

private boolean pluginExists(
SolrConfig.SolrPluginInfo info, ConfigOverlay overlay, String name) {
// Don't consider deleted plugins as existing
if (overlay.isPluginDeleted(info.getCleanTag(), name)) {
return false;
}

List<PluginInfo> l = req.getCore().getSolrConfig().getPluginInfos(info.clazz.getName());
for (PluginInfo pluginInfo : l) if (name.equals(pluginInfo.name)) return true;
return overlay.getNamedPlugins(info.getCleanTag()).containsKey(name);
Expand Down
129 changes: 129 additions & 0 deletions solr/core/src/test/org/apache/solr/core/TestConfigOverlay.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@
import static org.apache.solr.core.ConfigOverlay.isEditableProp;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.solr.SolrTestCase;
import org.apache.solr.common.params.CoreAdminParams;

public class TestConfigOverlay extends SolrTestCase {

Expand Down Expand Up @@ -64,4 +68,129 @@ public void testSetProperty() {
assertEquals(1, map.size());
assertEquals(100, map.get("initialSize"));
}

public void testDeletedPluginTombstone() {
ConfigOverlay overlay = new ConfigOverlay(Collections.emptyMap(), 0);

// Initially, no plugins should be deleted
assertFalse(overlay.isPluginDeleted("requestHandler", "/update/json"));
assertEquals(0, overlay.getDeletedPlugins().size());

// Delete a plugin
overlay = overlay.deleteNamedPlugin("/update/json", "requestHandler");

// Verify the plugin is marked as deleted
assertTrue(overlay.isPluginDeleted("requestHandler", "/update/json"));
Set<String> deleted = overlay.getDeletedPlugins();
assertEquals(1, deleted.size());
assertTrue(deleted.contains("requestHandler:/update/json"));

// Delete another plugin
overlay = overlay.deleteNamedPlugin("/sql", "requestHandler");
assertTrue(overlay.isPluginDeleted("requestHandler", "/sql"));
assertEquals(2, overlay.getDeletedPlugins().size());

// Verify the first one is still deleted
assertTrue(overlay.isPluginDeleted("requestHandler", "/update/json"));
}

public void testDeletedPluginFromOverlay() {
// Create an overlay with a custom request handler
Map<String, Object> data = new HashMap<>();
Map<String, Object> requestHandlers = new HashMap<>();
Map<String, Object> handlerConfig = new HashMap<>();
handlerConfig.put(CoreAdminParams.NAME, "/custom");
handlerConfig.put("class", "solr.DumpRequestHandler");
requestHandlers.put("/custom", handlerConfig);
data.put("requestHandler", requestHandlers);

ConfigOverlay overlay = new ConfigOverlay(data, 0);

// Verify the handler exists in overlay
assertEquals(1, overlay.getNamedPlugins("requestHandler").size());

// Delete it
overlay = overlay.deleteNamedPlugin("/custom", "requestHandler");

// Verify it's removed from overlay and added to deleted list
assertEquals(0, overlay.getNamedPlugins("requestHandler").size());
assertTrue(overlay.isPluginDeleted("requestHandler", "/custom"));
}

public void testDeletedPluginPersistence() {
// Create an overlay with deleted plugins in the data
Map<String, Object> data = new HashMap<>();
List<String> deleted = List.of("requestHandler:/update/json", "requestHandler:/sql");
data.put("deleted", deleted);

ConfigOverlay overlay = new ConfigOverlay(data, 0);

// Verify deleted plugins are loaded correctly
assertTrue(overlay.isPluginDeleted("requestHandler", "/update/json"));
assertTrue(overlay.isPluginDeleted("requestHandler", "/sql"));
assertEquals(2, overlay.getDeletedPlugins().size());
}

public void testDeleteSamePluginTwice() {
ConfigOverlay overlay = new ConfigOverlay(Collections.emptyMap(), 0);

// Delete a plugin
overlay = overlay.deleteNamedPlugin("/update/json", "requestHandler");
assertTrue(overlay.isPluginDeleted("requestHandler", "/update/json"));
assertEquals(1, overlay.getDeletedPlugins().size());

// Delete the same plugin again
overlay = overlay.deleteNamedPlugin("/update/json", "requestHandler");

// Should still only have one entry
assertTrue(overlay.isPluginDeleted("requestHandler", "/update/json"));
assertEquals(1, overlay.getDeletedPlugins().size());
}

public void testDeleteResponseWriter() {
ConfigOverlay overlay = new ConfigOverlay(Collections.emptyMap(), 0);

// Initially, no writers should be deleted
assertFalse(overlay.isPluginDeleted("queryResponseWriter", "xml"));
assertFalse(overlay.isPluginDeleted("queryResponseWriter", "json"));

// Delete a response writer
overlay = overlay.deleteNamedPlugin("xml", "queryResponseWriter");

// Verify the writer is marked as deleted
assertTrue(overlay.isPluginDeleted("queryResponseWriter", "xml"));
Set<String> deleted = overlay.getDeletedPlugins();
assertEquals(1, deleted.size());
assertTrue(deleted.contains("queryResponseWriter:xml"));

// Delete another writer
overlay = overlay.deleteNamedPlugin("csv", "queryResponseWriter");
assertTrue(overlay.isPluginDeleted("queryResponseWriter", "csv"));
assertEquals(2, overlay.getDeletedPlugins().size());

// Verify both are still deleted
assertTrue(overlay.isPluginDeleted("queryResponseWriter", "xml"));
assertTrue(overlay.isPluginDeleted("queryResponseWriter", "csv"));
}

public void testDeleteMixedPluginTypes() {
ConfigOverlay overlay = new ConfigOverlay(Collections.emptyMap(), 0);

// Delete different plugin types
overlay = overlay.deleteNamedPlugin("/update", "requestHandler");
overlay = overlay.deleteNamedPlugin("json", "queryResponseWriter");
overlay = overlay.deleteNamedPlugin("mycomponent", "searchComponent");

// Verify all are marked as deleted
assertTrue(overlay.isPluginDeleted("requestHandler", "/update"));
assertTrue(overlay.isPluginDeleted("queryResponseWriter", "json"));
assertTrue(overlay.isPluginDeleted("searchComponent", "mycomponent"));

// Verify the count
assertEquals(3, overlay.getDeletedPlugins().size());

// Verify they don't interfere with each other
assertFalse(overlay.isPluginDeleted("requestHandler", "json"));
assertFalse(overlay.isPluginDeleted("queryResponseWriter", "/update"));
}
}
Loading
Loading