blob: d5878fdad29a2afaf9b8973785f4371d644a280e [file] [log] [blame]
/*
* 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.update.processor;
import static org.apache.solr.common.SolrException.ErrorCode.SERVER_ERROR;
import java.lang.invoke.MethodHandles;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.core.SolrCore;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.util.plugin.SolrCoreAware;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* This Factory generates an UpdateProcessor that helps to enforce Version
* constraints on documents based on per-document version numbers using a configured
* <code>versionField</code>, a comma-delimited list of fields to check for version
* numbers. It should be configured on the "default"
* update processor somewhere before the DistributedUpdateProcessorFactory.
* As an example, see the solrconfig.xml that the tests use:
* solr/core/src/test-files/solr/collection1/conf/solrconfig-externalversionconstraint.xml
* </p>
* <p>
* When documents are added through this processor, if a document with the same
* unique key already exists in the collection, then the values within the fields
* as specified by the comma-delimited <code>versionField</code> property are checked,
* and if in the <i>existing</i> document the values for all fields are not less than the
* field values in the <i>new</i> document, then the new document is rejected with a
* 409 Version Conflict error.
* </p>
* <p>
* In addition to the mandatory <code>versionField</code> init param, two additional
* optional init params affect the behavior of this factory:
* </p>
* <ul>
* <li><code>deleteVersionParam</code> - This string parameter controls whether this
* processor will intercept and inspect Delete By Id commands in addition to adding
* documents. If specified, then the value will specify the name(s) of the request
* parameter(s) which becomes mandatory for all Delete By Id commands. Like
* <code>versionField</code>, <code>deleteVersionParam</code> is comma-delimited.
* For each of the params given, it specifies the document version associated with
* the delete, where the index matches <code>versionField</code>. For example, if
* <code>versionField</code> was set to 'a,b' and <code>deleteVersionParam</code>
* was set to 'p1,p2', p1 should give the version for field 'a' and p2 should give
* the version for field 'b'. If the versions specified using these params are not
* greater then the value in the <code>versionField</code> for any existing document,
* then the delete will fail with a 409 Version Conflict error. When using this
* param, Any Delete By Id command with a high enough document version number to
* succeed will be internally converted into an Add Document command that replaces
* the existing document with a new one which is empty except for the Unique Key
* and fields corresponding to the fields listed in <code>versionField</code>
* to keeping a record of the deleted version so future Add Document commands will
* fail if their "new" version is not high enough.</li>
*
* <li><code>ignoreOldUpdates</code> - This boolean parameter defaults to
* <code>false</code>, but if set to <code>true</code> causes any update with a
* document version that is not great enough to be silently ignored (and return
* a status 200 to the client) instead of generating a 409 Version Conflict error.
* </li>
*
* <li><code>supportMissingVersionOnOldDocs</code> - This boolean parameter defaults to
* <code>false</code>, but if set to <code>true</code> allows any documents written *before*
* this feature is enabled and which are missing the versionField to be overwritten.
* </li>
* <li><code>tombstoneConfig</code> - a list of field names to values to add to the
* created tombstone document. In general is not a good idea to populate tombsone documents
* with anything other than the minimum required fields so that it doean't match queries</li>
* </ul>
* @since 4.6.0
*/
public class DocBasedVersionConstraintsProcessorFactory extends UpdateRequestProcessorFactory implements SolrCoreAware, UpdateRequestProcessorFactory.RunAlways {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private boolean ignoreOldUpdates = false;
private List<String> versionFields = null;
private List<String> deleteVersionParamNames = Collections.emptyList();
private boolean useFieldCache;
private boolean supportMissingVersionOnOldDocs = false;
private NamedList<Object> tombstoneConfig;
@SuppressWarnings("unchecked")
@Override
public void init( @SuppressWarnings({"rawtypes"})NamedList args ) {
Object tmp = args.remove("versionField");
if (null == tmp) {
throw new SolrException(SERVER_ERROR,
"'versionField' must be configured");
}
if (! (tmp instanceof String) ) {
throw new SolrException(SERVER_ERROR,
"'versionField' must be configured as a <str>");
}
versionFields = StrUtils.splitSmart((String)tmp, ',');
// optional
tmp = args.remove("deleteVersionParam");
if (null != tmp) {
if (! (tmp instanceof String) ) {
throw new SolrException(SERVER_ERROR,
"'deleteVersionParam' must be configured as a <str>");
}
deleteVersionParamNames = StrUtils.splitSmart((String)tmp, ',');
}
if (deleteVersionParamNames.size() > 0 && deleteVersionParamNames.size() != versionFields.size()) {
throw new SolrException(SERVER_ERROR, "The number of 'deleteVersionParam' params " +
"must either be 0 or equal to the number of 'versionField' fields");
}
// optional - defaults to false
tmp = args.remove("ignoreOldUpdates");
if (null != tmp) {
if (! (tmp instanceof Boolean) ) {
throw new SolrException(SERVER_ERROR,
"'ignoreOldUpdates' must be configured as a <bool>");
}
ignoreOldUpdates = (Boolean) tmp;
}
// optional - defaults to false
tmp = args.remove("supportMissingVersionOnOldDocs");
if (null != tmp) {
if (! (tmp instanceof Boolean) ) {
throw new SolrException(SERVER_ERROR,
"'supportMissingVersionOnOldDocs' must be configured as a <bool>");
}
supportMissingVersionOnOldDocs = ((Boolean)tmp).booleanValue();
}
tmp = args.remove("tombstoneConfig");
if (null != tmp) {
if (! (tmp instanceof NamedList) ) {
throw new SolrException(SERVER_ERROR,
"'tombstoneConfig' must be configured as a <lst>.");
}
tombstoneConfig = (NamedList<Object>)tmp;
}
super.init(args);
}
@Override
public UpdateRequestProcessor getInstance(SolrQueryRequest req,
SolrQueryResponse rsp,
UpdateRequestProcessor next ) {
return new DocBasedVersionConstraintsProcessor(versionFields,
ignoreOldUpdates,
deleteVersionParamNames,
supportMissingVersionOnOldDocs,
useFieldCache,
tombstoneConfig,
req, next);
}
@Override
public void inform(SolrCore core) {
if (core.getUpdateHandler().getUpdateLog() == null) {
throw new SolrException(SERVER_ERROR,
"updateLog must be enabled.");
}
if (core.getLatestSchema().getUniqueKeyField() == null) {
throw new SolrException(SERVER_ERROR,
"schema must have uniqueKey defined.");
}
useFieldCache = true;
for (String versionField : versionFields) {
SchemaField userVersionField = core.getLatestSchema().getField(versionField);
if (userVersionField == null || !userVersionField.stored() || userVersionField.multiValued()) {
throw new SolrException(SERVER_ERROR,
"field " + versionField + " must be defined in schema, be stored, and be single valued.");
}
if (useFieldCache) {
try {
userVersionField.getType().getValueSource(userVersionField, null);
} catch (Exception e) {
useFieldCache = false;
log.warn("Can't use fieldcache/valuesource: ", e);
}
}
}
canCreateTombstoneDocument(core.getLatestSchema());
}
/**
* Validates that the schema would allow tombstones to be created by DocBasedVersionConstraintsProcessor by
* checking if the required fields are of known types
*/
protected boolean canCreateTombstoneDocument(IndexSchema schema) {
Set<String> requiredFieldNames = schema.getRequiredFields().stream()
.filter(field -> field.getDefaultValue() == null)
.map(field -> field.getName())
.collect(Collectors.toSet());
if (tombstoneConfig != null) {
tombstoneConfig.forEach((k,v) -> requiredFieldNames.remove(k));
}
requiredFieldNames.remove(schema.getUniqueKeyField().getName());
if (versionFields != null) {
versionFields.forEach(field -> requiredFieldNames.remove(field));
}
if (!requiredFieldNames.isEmpty()) {
log.warn("The schema '{}' has required fields that aren't added in the tombstone. This can cause deletes to fail if those aren't being added in some other way. Required Fields={}",
schema.getSchemaName(),
requiredFieldNames);
return false;
}
return true;
}
}