Skip to content

Commit 61c3078

Browse files
authored
GH Issue 1736: Keep calculated fields when renaming a sample type (#8186)
## Rationale Renaming a sample type shortly after editing it could lose its calculated fields ([GH Issue 1736](LabKey/internal-issues#1736)). During the save, a background thread (e.g. search indexing) could reload the query metadata cache with the pre-rename row, and the calculated-field save then wrote that stale row back over the rename. ## Related Pull Requests - <!-- list of links to related pull requests (replace this comment) --> ## Changes - Saving query metadata with a new name always stores it under the new name, even if the cached copy is stale - The query metadata cache is cleared again after the transaction commits, so a reload during the save doesn't leave stale data behind - Server-side test that forces the concurrent cache reload during a rename
1 parent 0b859dd commit 61c3078

3 files changed

Lines changed: 85 additions & 4 deletions

File tree

‎query/src/org/labkey/query/MetadataTableJSON.java‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,14 @@ public static void saveMetadata(String schemaName, String queryName, @Nullable S
152152
TableInfo rawTableInfo = getRawTableInfo(schema, queryName, updatedQueryName);
153153
TablesDocument doc = null;
154154
TableType xmlTable = null;
155+
String savedQueryName = updatedQueryName != null ? updatedQueryName : queryName;
155156

156157
if (queryDef != null)
157158
{
159+
// GH Issue 1736: make sure to put the updated query name into the queryDef
160+
if (updatedQueryName != null)
161+
queryDef.setName(updatedQueryName);
162+
158163
try
159164
{
160165
doc = parseDocument(queryDef.getMetaData());
@@ -180,7 +185,7 @@ else if (updatedQueryName != null)
180185
queryDef = new QueryDef();
181186
queryDef.setSchema(schemaName);
182187
queryDef.setContainer(container.getId());
183-
queryDef.setName(queryName);
188+
queryDef.setName(savedQueryName);
184189
}
185190

186191
if (doc == null)
@@ -192,7 +197,7 @@ else if (updatedQueryName != null)
192197
{
193198
TablesType tables = doc.addNewTables();
194199
xmlTable = tables.addNewTable();
195-
xmlTable.setTableName(queryName);
200+
xmlTable.setTableName(savedQueryName);
196201
}
197202

198203
if (xmlTable.getColumns() == null)

‎query/src/org/labkey/query/QueryServiceImpl.java‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import org.labkey.api.data.ConvertHelper;
5454
import org.labkey.api.data.DbSchema;
5555
import org.labkey.api.data.DbSchemaType;
56+
import org.labkey.api.data.DbScope;
5657
import org.labkey.api.data.DisplayColumn;
5758
import org.labkey.api.data.Filter;
5859
import org.labkey.api.data.ForeignKey;
@@ -76,6 +77,7 @@
7677
import org.labkey.api.data.TableSelector;
7778
import org.labkey.api.data.VirtualTable;
7879
import org.labkey.api.data.dialect.SqlDialect;
80+
import org.labkey.api.exp.PropertyType;
7981
import org.labkey.api.exp.property.DomainKind;
8082
import org.labkey.api.exp.query.ExpTable;
8183
import org.labkey.api.gwt.client.AuditBehaviorType;
@@ -206,6 +208,7 @@
206208
import java.util.concurrent.ConcurrentHashMap;
207209
import java.util.concurrent.ConcurrentMap;
208210
import java.util.concurrent.CopyOnWriteArrayList;
211+
import java.util.concurrent.TimeUnit;
209212
import java.util.function.Consumer;
210213
import java.util.function.Supplier;
211214
import java.util.stream.Collectors;
@@ -3878,5 +3881,66 @@ public void testRightAndIsnumeric() throws SQLException
38783881
assertEquals("isnumeric(NULL) on " + dialect, 0, results.getInt("n4"));
38793882
}
38803883
}
3884+
3885+
// GH Issue 1736
3886+
@Test
3887+
public void testRenameWithConcurrentCacheReload() throws Exception
3888+
{
3889+
DbScope scope = QueryManager.get().getDbSchema().getScope();
3890+
Container c = JunitUtil.getTestContainer();
3891+
User user = TestContext.get().getUser();
3892+
String oldName = "Users";
3893+
String newName = "junit" + GUID.makeHash();
3894+
3895+
QueryDef def = new QueryDef();
3896+
def.setSchema("core");
3897+
def.setName(oldName);
3898+
def.setContainer(c.getId());
3899+
def.setMetaData("<tables xmlns=\"http://labkey.org/data/xml\"><table tableName=\"" + oldName + "\" tableDbType=\"NOT_IN_DB\"><columns>" +
3900+
"<column columnName=\"junitCalc\"><conceptURI>http://www.labkey.org/exp/xml#calculated</conceptURI><valueExpression>1</valueExpression></column>" +
3901+
"</columns></table></tables>");
3902+
int queryDefId = QueryManager.get().insert(user, def).getQueryDefId();
3903+
3904+
GWTPropertyDescriptor calcField = new GWTPropertyDescriptor("junitCalc", PropertyType.INTEGER.getTypeUri());
3905+
calcField.setValueExpression("1");
3906+
// reloads the cache from committed data, as search indexing can while the rename is uncommitted
3907+
Runnable concurrentReload = () -> QueryManager.get().getQueryDef(c, "core", oldName, false);
3908+
3909+
try
3910+
{
3911+
try (DbScope.Transaction tx = scope.ensureTransaction())
3912+
{
3913+
QueryManager.get().renameQuery(user, c, "core", oldName, newName);
3914+
runOnOtherThread(concurrentReload);
3915+
assertNull("Expected the cache to hold the pre-rename row", QueryManager.get().getQueryDef(c, "core", newName, false));
3916+
3917+
QueryService.get().saveCalculatedFieldsMetadata("core", oldName, newName, List.of(calcField), true, user, c);
3918+
runOnOtherThread(concurrentReload);
3919+
tx.commit();
3920+
}
3921+
3922+
QueryDef saved = new TableSelector(QueryManager.get().getTableInfoQueryDef()).getObject(queryDefId, QueryDef.class);
3923+
assertEquals(newName, saved.getName());
3924+
assertTrue(saved.getMetaData(), saved.getMetaData().contains("tableName=\"" + newName + "\""));
3925+
assertTrue(saved.getMetaData(), saved.getMetaData().contains("junitCalc"));
3926+
3927+
assertNotNull("Cache should reload after commit", QueryManager.get().getQueryDef(c, "core", newName, false));
3928+
assertNull(QueryManager.get().getQueryDef(c, "core", oldName, false));
3929+
}
3930+
finally
3931+
{
3932+
QueryDef toDelete = new TableSelector(QueryManager.get().getTableInfoQueryDef()).getObject(queryDefId, QueryDef.class);
3933+
if (toDelete != null)
3934+
QueryManager.get().delete(toDelete);
3935+
}
3936+
}
3937+
3938+
private void runOnOtherThread(Runnable r) throws InterruptedException
3939+
{
3940+
Thread t = new Thread(r);
3941+
t.start();
3942+
t.join(TimeUnit.SECONDS.toMillis(30));
3943+
assertFalse("Reader thread did not finish", t.isAlive());
3944+
}
38813945
}
38823946
}

‎query/src/org/labkey/query/persist/QueryDefCache.java‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.labkey.api.collections.CaseInsensitiveMapWrapper;
2424
import org.labkey.api.collections.IntHashMap;
2525
import org.labkey.api.data.Container;
26+
import org.labkey.api.data.DbScope;
2627
import org.labkey.api.data.SimpleFilter;
2728
import org.labkey.api.data.TableSelector;
2829
import org.labkey.query.QueryDefinitionImpl;
@@ -166,9 +167,20 @@ QueryDef getQueryDefById(Container container, int queryDefId)
166167
return QUERY_DEF_DB_CACHE.get(container)._queryDefIdMap.get(queryDefId);
167168
}
168169

170+
// GH Issue 1736: a thread outside the transaction can reload pre-commit rows after the immediate remove, so remove again after commit
169171
public static void uncache(Container c)
170172
{
171-
QUERY_DEF_DB_CACHE.remove(c);
172-
QueryDefinitionImpl.clearHasPkColumnCache();
173+
QueryManager.get().getDbSchema().getScope().addCommitTask(new UncacheTask(c), DbScope.CommitTaskOption.IMMEDIATE, DbScope.CommitTaskOption.POSTCOMMIT, DbScope.CommitTaskOption.POSTROLLBACK);
174+
}
175+
176+
// record equality lets the transaction dedupe repeated uncaches of the same container
177+
private record UncacheTask(Container c) implements Runnable
178+
{
179+
@Override
180+
public void run()
181+
{
182+
QUERY_DEF_DB_CACHE.remove(c);
183+
QueryDefinitionImpl.clearHasPkColumnCache();
184+
}
173185
}
174186
}

0 commit comments

Comments
 (0)