Skip to content

Commit f39fffa

Browse files
committed
[#962] Flag an admin action when a bounded storage write gives up on a configuration change
Storage.write() no longer retries until it succeeds: JDBCStorage.write() has long been bounded by a number of attempts and a time window, and PDBStorage.write() is bounded too since #937. Once the bound is spent, write() throws instead of replaying. Four configuration change paths of the pluggable backends change in-memory state around such a write, and none of them told the operator when the memory and the storage stopped agreeing: each set the server error result code and handed back a bare single line stack trace, with no message id to search the error log for and nothing said about what to do next. - AttributeIndexCfgManager.applyConfigurationDelete takes the index out of attrIndexMap and attrCryptoMap before the write, so a write which gives up leaves the trees in the storage while the configuration entry naming them is already gone. - VLVIndexCfgManager.applyConfigurationDelete does the same for a VLV index, whose data is held by two trees, the index and the counter which goes with it. - AttributeIndex.applyConfigurationChange applies the change with three writes: giving up on the second leaves the trees of the removed indexes behind, and giving up on the third leaves the entry limit the new configuration declares unapplied to the indexes which stay. - EntryContainer.applyConfigurationChange hands the entries and every index of the backend the parameters to encode with from now on, and giving up between the two leaves them encoded under settings which no longer agree. All four now report a message of their own - ERR_CONFIG_INDEX_DELETE_FAILED, ERR_CONFIG_VLV_INDEX_DELETE_FAILED, ERR_CONFIG_INDEX_CHANGE_FAILED and ERR_CONFIG_BACKEND_DATA_CHANGE_FAILED - which names what diverged and what has to be done about it, keeps the stack trace as its trailing cause, sets adminActionRequired, and is logged as well as returned: the divergence outlives the session which asked for the change. The order of the two deletions is deliberately left alone: deleting inside the write would let a replayed attempt find nothing to delete and commit an empty transaction, reporting success for work it did not do. AttributeIndex.applyConfigurationChange now publishes each part of the change after the write which applied that part, rather than all of it between the first write and the second: the map of indexes and the indexing options once the deletion has committed, still inside the entry container lock, and the configuration once the update has committed. EntryContainer.applyConfigurationChange no longer opens a transaction. Its body has performed no transactional work since what it wrapped was removed - 7713117 added the wrapper around the opening of the subordinate indexes and their entry limits, and 9f0904f (OPENDJ-1725) took that work out of it, leaving id2entry.setDataConfig and the assignment of config behind - so what was left was a transaction a bounded storage could give up on, in the middle of a sequence no rollback can undo. The single step which can fail, newDataConfig(cfg), now runs before the first mutation. The entry container lock stays: publishing to the other threads was always its doing and never the transaction's. ConfigChangeGivesUpTest covers the four paths through a backend whose storage gives up on a chosen write, and pins what the messages claim, including the adoption of the trees left behind that ERR_CONFIG_INDEX_DELETE_FAILED describes (#990).
1 parent 1af0a12 commit f39fffa

5 files changed

Lines changed: 805 additions & 20 deletions

File tree

‎opendj-server-legacy/src/main/java/org/opends/server/backends/pdb/PDBStorage.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -712,8 +712,8 @@ public void write(WriteOperation operation) throws Exception
712712
if (capSpent || (attempt > 1 && now - giveUpAt >= 0))
713713
{
714714
final long elapsedMs = TimeUnit.NANOSECONDS.toMillis(now - startedAt);
715-
//which of the two bounds was spent, so that the config change paths - which report this as a bare stack
716-
//trace with no message id - say whether raising the attempts or the window is what would have helped
715+
//which of the two bounds was spent, so that the config change paths - which report this as the trailing
716+
//cause of a message of their own - say whether raising the attempts or the window is what would have helped
717717
final String boundSpent = capSpent ? "attempt cap" : "retry window";
718718
final StorageRuntimeException spent = new StorageRuntimeException(
719719
"pdb: backend '" + config.getBackendId() + "' did not apply the transaction after " + attempt

‎opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/AttributeIndex.java‎

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -973,10 +973,6 @@ public void run(WriteableTransaction txn) throws Exception
973973
ccr.addMessage(NOTE_INDEX_ADD_REQUIRES_REBUILD.get(addedIndex));
974974
}
975975

976-
config = newConfiguration;
977-
indexingOptions = newIndexingOptions;
978-
indexIdToIndexes = Collections.unmodifiableMap(newIndexIdToIndexes);
979-
980976
// We get exclusive lock to ensure that no query is actually using the indexes that will be deleted.
981977
entryContainer.lock();
982978
try
@@ -992,6 +988,17 @@ public void run(WriteableTransaction txn) throws Exception
992988
}
993989
}
994990
});
991+
// Published once the deletion has committed, not before it: a write the storage gives up on
992+
// leaves the trees of the removed indexes behind, and a map which no longer names them is a
993+
// map through which nothing maintains them and nothing deletes them. The lock has drained
994+
// every operation which enters through shared access, so the window in which the map names
995+
// trees the write has just deleted lies inside it and no search sees it; published after the
996+
// write rather than from within it, so that an operation the storage replays publishes once,
997+
// from the attempt which committed. The added indexes are named only at the end of that
998+
// window rather than before the deletion, which costs nothing: the write above has just
999+
// asked for them to be rebuilt, so nothing may read them until it has been.
1000+
indexingOptions = newIndexingOptions;
1001+
indexIdToIndexes = Collections.unmodifiableMap(newIndexIdToIndexes);
9951002
}
9961003
finally
9971004
{
@@ -1020,11 +1027,22 @@ public void run(WriteableTransaction txn) throws Exception
10201027
{
10211028
updatedIndex.setIndexEntryLimit(newConfiguration.getIndexEntryLimit());
10221029
}
1030+
// Published last: the entry limit this configuration declares reaches the indexes which stay
1031+
// only once the write which untrusts them has committed, so a configuration published before
1032+
// that would declare a limit those indexes do not hold yet.
1033+
config = newConfiguration;
10231034
}
10241035
catch (Exception e)
10251036
{
1037+
// Logged as well as reported, for the reason given in the index delete listener of
1038+
// EntryContainer: what this index holds and what its configuration declares may no longer
1039+
// agree long after the session which asked for the change has ended.
1040+
final LocalizableMessage message = ERR_CONFIG_INDEX_CHANGE_FAILED.get(getAttributeType().getNameOrOID(),
1041+
entryContainer.getBaseDN(), StaticUtils.stackTraceToSingleLineString(e));
1042+
logger.error(message);
10261043
ccr.setResultCode(DirectoryServer.getCoreConfigManager().getServerErrorResultCode());
1027-
ccr.addMessage(LocalizableMessage.raw(StaticUtils.stackTraceToSingleLineString(e)));
1044+
ccr.setAdminActionRequired(true);
1045+
ccr.addMessage(message);
10281046
}
10291047

10301048
return ccr;

‎opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java‎

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,16 @@ public void run(WriteableTransaction txn) throws Exception
268268
}
269269
catch (Exception de)
270270
{
271+
// The configuration entry naming those trees is gone and the storage still holds them, so
272+
// this outlives the session which asked for the deletion: logged as well as reported. The
273+
// framework logs the result too (ConfigurationHandler.handleConfigChangeResult), but as an
274+
// argument of a message of its own, so only this call puts this message id in the error log.
275+
final LocalizableMessage message = ERR_CONFIG_INDEX_DELETE_FAILED.get(
276+
cfg.getAttribute().getNameOrOID(), getBaseDN(), StaticUtils.stackTraceToSingleLineString(de));
277+
logger.error(message);
271278
ccr.setResultCode(getCoreConfigManager().getServerErrorResultCode());
272-
ccr.addMessage(LocalizableMessage.raw(StaticUtils.stackTraceToSingleLineString(de)));
279+
ccr.setAdminActionRequired(true);
280+
ccr.addMessage(message);
273281
}
274282
finally
275283
{
@@ -369,8 +377,13 @@ public void run(WriteableTransaction txn) throws Exception
369377
}
370378
catch (Exception e)
371379
{
380+
// Reported and logged for the reason given in the index delete listener above.
381+
final LocalizableMessage message = ERR_CONFIG_VLV_INDEX_DELETE_FAILED.get(
382+
cfg.getName(), getBaseDN(), StaticUtils.stackTraceToSingleLineString(e));
383+
logger.error(message);
372384
ccr.setResultCode(getCoreConfigManager().getServerErrorResultCode());
373-
ccr.addMessage(LocalizableMessage.raw(StaticUtils.stackTraceToSingleLineString(e)));
385+
ccr.setAdminActionRequired(true);
386+
ccr.addMessage(message);
374387
}
375388
finally
376389
{
@@ -2557,24 +2570,31 @@ public ConfigChangeResult applyConfigurationChange(final PluggableBackendCfg cfg
25572570
EntryContainer.this.lock();
25582571
try
25592572
{
2560-
storage.write(new WriteOperation()
2561-
{
2562-
@Override
2563-
public void run(WriteableTransaction txn) throws Exception
2564-
{
2565-
id2entry.setDataConfig(newDataConfig(cfg));
2566-
EntryContainer.this.config = cfg;
2567-
}
2568-
});
2573+
// None of this is transactional: the entries and the indexes are handed the parameters to
2574+
// encode with from now on, and neither of those is a record in a tree. The storage.write which
2575+
// used to wrap it had a transactional body, removed long before the wrapper was; left behind,
2576+
// it was a transaction a bounded storage could give up on, and giving up between the entries
2577+
// and the indexes leaves the two encoded under settings which no longer agree. What can fail
2578+
// is done first, before anything has been changed; publishing to the other threads is what the
2579+
// entry container lock is held for here, and never was the transaction's doing.
2580+
final String cipherTransformation = cfg.getCipherTransformation();
2581+
final int cipherKeyLength = cfg.getCipherKeyLength();
2582+
final DataConfig dataConfig = newDataConfig(cfg);
2583+
id2entry.setDataConfig(dataConfig);
25692584
for (CryptoSuite indexCrypto : attrCryptoMap.values())
25702585
{
2571-
indexCrypto.newParameters(cfg.getCipherTransformation(), cfg.getCipherKeyLength(), indexCrypto.isEncrypted());
2586+
indexCrypto.newParameters(cipherTransformation, cipherKeyLength, indexCrypto.isEncrypted());
25722587
}
2588+
EntryContainer.this.config = cfg;
25732589
}
25742590
catch (Exception e)
25752591
{
2592+
final LocalizableMessage message =
2593+
ERR_CONFIG_BACKEND_DATA_CHANGE_FAILED.get(getBaseDN(), stackTraceToSingleLineString(e));
2594+
logger.error(message);
25762595
ccr.setResultCode(DirectoryServer.getCoreConfigManager().getServerErrorResultCode());
2577-
ccr.addMessage(LocalizableMessage.raw(stackTraceToSingleLineString(e)));
2596+
ccr.setAdminActionRequired(true);
2597+
ccr.addMessage(message);
25782598
}
25792599
finally
25802600
{

‎opendj-server-legacy/src/messages/org/opends/messages/backend.properties‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,3 +1132,27 @@ ERR_BACKEND_BASEDN_NO_LONGER_HELD_623=The base DNs of backend %s could not be ch
11321132
longer one this backend holds, which is what closing its root container leaves behind - an LDIF import, \
11331133
an index rebuild, an LDIF export and the backend being disabled all do that. Nothing has been changed; \
11341134
submit the change again once that has finished
1135+
ERR_CONFIG_INDEX_DELETE_FAILED_624=The index for attribute '%s' of backend base DN '%s' was \
1136+
removed from the configuration, but the trees holding its data could not be deleted: %s. Those \
1137+
trees may still hold the data of that index, in whole or in part, and no configuration names them \
1138+
any more, so nothing will delete them and nothing will keep them up to date. An index created \
1139+
again for that attribute with the same settings adopts those trees and is considered trusted over \
1140+
their stale content, so it must be rebuilt before it is used
1141+
ERR_CONFIG_VLV_INDEX_DELETE_FAILED_625=The VLV index '%s' of backend base DN '%s' was removed from \
1142+
the configuration, but the trees holding its data could not be deleted: %s. Those trees may still \
1143+
hold the data of that index, in whole or in part, and no configuration names them any more, so \
1144+
nothing will delete them and nothing will keep them up to date. A VLV index created again under \
1145+
the same name adopts those trees and is considered trusted over their stale content, so it must \
1146+
be rebuilt before it is used
1147+
ERR_CONFIG_INDEX_CHANGE_FAILED_626=The configuration of the index for attribute '%s' of backend \
1148+
base DN '%s' could not be applied in full: %s. What this index holds and what its configuration \
1149+
declares may no longer agree, so this index must be rebuilt before it is used, after this backend \
1150+
has been disabled and enabled again so that every tree the stored configuration declares is named \
1151+
once more. The trees of the index types this change removed may still hold their data while no \
1152+
configuration names them any more, so nothing will delete them and nothing will keep them up to \
1153+
date; an index type declared again for that attribute adopts those trees and is considered trusted \
1154+
over their stale content, and must be rebuilt too
1155+
ERR_CONFIG_BACKEND_DATA_CHANGE_FAILED_627=The compression, encoding and encryption settings of \
1156+
backend base DN '%s' could not be applied in full: %s. Its entries and its indexes may no longer \
1157+
be encoded under the same settings; disabling and enabling this backend builds both from the \
1158+
stored configuration again

0 commit comments

Comments
 (0)