Commit 3b608051 authored by Daniel Vrátil's avatar Daniel Vrátil 🤖

Introduce thread-safe Entity::retrieveByNameOrCreate()

During DB initialization when multiple resources try to synchronize
and insert first item, the mimetypes, parttypes and other similar
tables are empty and the entries need to be created. This presents
a race condition when multiple threads try to create the new entry
at the same time - one of them is succesfull, the others usually
abort, which leads to the resource failing the synchronization.

retrieveByNameOrCreate() will try to retrieve the item from cache
or DB first. If it does not get any result it tries to acquire a
lock. If the thread gets a lock it inserts the new entry into DB
and cache. Otherwise the thread just waits for the lock (i.e. until
the thread that acquired the lock inserts the new entity) and then
retrieves the entity from the cache.

This isn't really a common situation, which would happen during normal
usage of Akonadi. We however see it quite often during unit-test
initialization when all three Knut resources start pushing their
data into Akonadi at the same time and trigger this race condition.
parent 7c044c4b
......@@ -54,13 +54,9 @@ bool AkAppend::buildPimItem(const Protocol::CreateItemCommand &cmd, PimItem &ite
return failureResponse("Cannot append item into virtual collection");
}
MimeType mimeType = MimeType::retrieveByName(cmd.mimeType());
MimeType mimeType = MimeType::retrieveByNameOrCreate(cmd.mimeType());
if (!mimeType.isValid()) {
MimeType m(cmd.mimeType());
if (!m.insert()) {
return failureResponse(QStringLiteral("Unable to create mimetype '") % cmd.mimeType() % QStringLiteral("'."));
}
mimeType = m;
return failureResponse(QStringLiteral("Unable to create mimetype '") % cmd.mimeType() % QStringLiteral("'."));
}
item.setRev(0);
......
......@@ -523,16 +523,11 @@ bool List::parseStream()
}
}
Q_FOREACH (const QString &mtName, cmd.mimeTypes()) {
const MimeType mt = MimeType::retrieveByName(mtName);
if (mt.isValid()) {
mMimeTypes.append(mt.id());
} else {
MimeType mt(mtName);
if (!mt.insert()) {
return failureResponse("Failed to create mimetype record");
}
mMimeTypes.append(mt.id());
const MimeType mt = MimeType::retrieveByNameOrCreate(mtName);
if (!mt.isValid()) {
return failureResponse("Failed to create mimetype record");
}
mMimeTypes.append(mt.id());
}
mEnabledCollections = cmd.enabled();
......
......@@ -65,13 +65,9 @@ bool RelationStore::parseStream()
}
const QString typeName = QString::fromUtf8(cmd.type());
RelationType relationType = RelationType::retrieveByName(typeName);
const RelationType relationType = RelationType::retrieveByNameOrCreate(typeName);
if (!relationType.isValid()) {
RelationType t(typeName);
if (!t.insert()) {
return failureResponse(QStringLiteral("Unable to create relation type '") % typeName % QStringLiteral("'"));
}
relationType = t;
return failureResponse(QStringLiteral("Unable to create relation type '") % typeName % QStringLiteral("'"));
}
Relation existingRelation = fetchRelation(cmd.left(), cmd.right(), relationType.id());
......
......@@ -80,12 +80,9 @@ bool SearchPersistent::parseStream()
}
Q_FOREACH (const QString &mimeType, cmd.mimeTypes()) {
MimeType mt = MimeType::retrieveByName(mimeType);
const MimeType mt = MimeType::retrieveByNameOrCreate(mimeType);
if (!mt.isValid()) {
mt.setName(mimeType);
if (!mt.insert()) {
return failureResponse("Failed to create new mimetype");
}
return failureResponse("Failed to create new mimetype");
}
col.addMimeType(mt);
}
......
......@@ -42,13 +42,9 @@ bool TagAppend::parseStream()
TagType tagType;
if (!cmd.type().isEmpty()) {
const QString typeName = QString::fromUtf8(cmd.type());
tagType = TagType::retrieveByName(typeName);
tagType = TagType::retrieveByNameOrCreate(typeName);
if (!tagType.isValid()) {
TagType t(typeName);
if (!t.insert()) {
return failureResponse(QStringLiteral("Unable to create tagtype '") % typeName % QStringLiteral("'"));
}
tagType = t;
return failureResponse(QStringLiteral("Unable to create tagtype '") % typeName % QStringLiteral("'"));
}
}
......
......@@ -58,12 +58,9 @@ bool TagStore::parseStream()
TagType type = TagType::retrieveById(changedTag.typeId());
const QString newTypeName = QString::fromUtf8(cmd.type());
if (newTypeName != type.name()) {
TagType newType = TagType::retrieveByName(newTypeName);
const TagType newType = TagType::retrieveByNameOrCreate(newTypeName);
if (!newType.isValid()) {
newType.setName(newTypeName);
if (!newType.insert()) {
return failureResponse("Failed to create new tag type");
}
return failureResponse("Failed to create new tag type");
}
changedTag.setTagType(newType);
changes << AKONADI_PARAM_MIMETYPE;
......
......@@ -251,12 +251,9 @@ Flag::List HandlerHelper::resolveFlags(const QSet<QByteArray> &flagNames)
Flag::List flagList;
flagList.reserve(flagNames.size());
Q_FOREACH (const QByteArray &flagName, flagNames) {
Flag flag = Flag::retrieveByName(QString::fromUtf8(flagName));
const Flag flag = Flag::retrieveByNameOrCreate(QString::fromUtf8(flagName));
if (!flag.isValid()) {
flag = Flag(QString::fromUtf8(flagName));
if (!flag.insert()) {
throw HandlerException("Unable to create flag");
}
throw HandlerException("Unable to create flag");
}
flagList.append(flag);
}
......@@ -294,12 +291,9 @@ Tag::List HandlerHelper::resolveTagsByGID(const QStringList &tagsGIDs)
tag.setGid(tagGID);
tag.setParentId(0);
TagType type = TagType::retrieveByName(QStringLiteral("PLAIN"));
const TagType type = TagType::retrieveByNameOrCreate(QStringLiteral("PLAIN"));
if (!type.isValid()) {
type.setName(QStringLiteral("PLAIN"));
if (!type.insert()) {
throw HandlerException("Unable to create tag type");
}
throw HandlerException("Unable to create tag type");
}
tag.setTagType(type);
if (!tag.insert()) {
......
......@@ -136,11 +136,18 @@ class <xsl:value-of select="$className"/> : private Entity
<xsl:if test="column[@name = 'name'] and $className != 'PartType'">
/** Returns the record with name @p name. */
<xsl:text>static </xsl:text><xsl:value-of select="$className"/> retrieveByName( const <xsl:value-of select="column[@name = 'name']/@type"/> &amp;name );
/** Returns the record with name @p name. If such record does not exist,
it will be created. This method is thread-safe, so if multiple callers
call it on non-existent name, only one will create the new record, others
will wait and read it from the cache. */
<xsl:text>static </xsl:text><xsl:value-of select="$className"/> retrieveByNameOrCreate( const <xsl:value-of select="column[@name = 'name']/@type"/> &amp;name );
</xsl:if>
<xsl:if test="column[@name = 'name'] and $className = 'PartType'">
<!-- Special case for PartTypes, which are identified by "NS:NAME" -->
<xsl:text>static PartType retrieveByFQName( const QString &amp;ns, const QString &amp;name );</xsl:text>
<xsl:text>static PartType retrieveByFQNameOrCreate( const QString &amp;ns, const QString &amp;name );</xsl:text>
</xsl:if>
/** Retrieve all records from this table. */
......
......@@ -345,6 +345,33 @@ QVector&lt; <xsl:value-of select="$className"/> &gt; <xsl:value-of select="$clas
<xsl:with-param name="cache">nameCache</xsl:with-param>
</xsl:call-template>
}
<xsl:value-of select="$className"/><xsl:text> </xsl:text><xsl:value-of select="$className"/>::retrieveByNameOrCreate( const <xsl:value-of select="column[@name = 'name']/@type"/> &amp;name)
{
static QMutex lock;
auto rv = retrieveByName(name);
if (rv.isValid()) {
return rv;
}
if (lock.tryLock()) {
rv.setName(name);
if (!rv.insert()) {
lock.unlock();
return <xsl:value-of select="$className"/>();
}
if (Private::cacheEnabled) {
Private::addToCache(rv);
}
lock.unlock();
return rv;
}
lock.lock();
lock.unlock();
return retrieveByName(name);
}
</xsl:if>
<xsl:if test="column[@name = 'name'] and $className = 'PartType'">
......@@ -358,6 +385,34 @@ QVector&lt; <xsl:value-of select="$className"/> &gt; <xsl:value-of select="$clas
<xsl:with-param name="cache">nameCache</xsl:with-param>
</xsl:call-template>
}
<xsl:text>PartType PartType::retrieveByFQNameOrCreate( const QString &amp; ns, const QString &amp; name )</xsl:text>
{
static QMutex lock;
PartType rv = retrieveByFQName(ns, name);
if (rv.isValid()) {
return rv;
}
if (lock.tryLock()) {
rv.setNs(ns);
rv.setName(name);
if (!rv.insert()) {
lock.unlock();
return PartType();
}
if (Private::cacheEnabled) {
Private::addToCache(rv);
}
lock.unlock();
return rv;
}
lock.lock();
lock.unlock();
return retrieveByFQName(ns, name);
}
</xsl:if>
QVector&lt;<xsl:value-of select="$className"/>&gt; <xsl:value-of select="$className"/>::retrieveAll()
......
......@@ -48,13 +48,9 @@ PartType PartTypeHelper::fromFqName(const QByteArray &fqName)
PartType PartTypeHelper::fromFqName(const QString &ns, const QString &name)
{
PartType partType = PartType::retrieveByFQName(ns, name);
const PartType partType = PartType::retrieveByFQNameOrCreate(ns, name);
if (!partType.isValid()) {
PartType pt(name, ns);
if (!pt.insert()) {
throw PartTypeException("Failed to append part type");
}
partType = pt;
throw PartTypeException("Failed to append part type");
}
return partType;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment