Skip to content

Commit fe237f7

Browse files
lampajrjohnaohara
authored andcommitted
Don't use lock for labelValues computation
As part of this the following changes have been added: - Manage concurrent runs transformation - Fix test recalculation modal Signed-off-by: Andrea Lamparelli <[email protected]>
1 parent 23f4c78 commit fe237f7

File tree

6 files changed

+69
-68
lines changed

6 files changed

+69
-68
lines changed

horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/DatasetServiceImpl.java

+8-28
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import java.time.Instant;
44
import java.util.*;
55
import java.util.concurrent.atomic.AtomicReference;
6-
import java.util.concurrent.locks.ReentrantLock;
76
import java.util.stream.Collectors;
87
import java.util.stream.Stream;
98

@@ -53,7 +52,7 @@ public class DatasetServiceImpl implements DatasetService {
5352
private static final Logger log = Logger.getLogger(DatasetServiceImpl.class);
5453

5554
//@formatter:off
56-
private static final String LABEL_QUERY = """
55+
private static final String LABEL_QUERY = """
5756
WITH
5857
used_labels AS (
5958
SELECT label.id AS label_id, label.name, ds.schema_id, count(le) AS count
@@ -86,7 +85,7 @@ lvalues AS (
8685
JOIN used_labels ul ON label.id = ul.label_id
8786
GROUP BY lvalues.label_id, ul.name, function, ul.count
8887
""";
89-
protected static final String LABEL_PREVIEW = """
88+
protected static final String LABEL_PREVIEW = """
9089
WITH
9190
le AS (
9291
SELECT * FROM jsonb_populate_recordset(NULL::extractor, (?1)::jsonb)
@@ -110,7 +109,7 @@ WHEN jsonb_array_length((?1)::jsonb) = 1 THEN jsonb_agg(lvalues.value) -> 0
110109
FROM lvalues
111110
""";
112111

113-
private static final String SCHEMAS_SELECT = """
112+
private static final String SCHEMAS_SELECT = """
114113
SELECT dataset_id,
115114
jsonb_agg(
116115
jsonb_build_object('id', schema.id, 'uri', ds.uri, 'name', schema.name, 'source', 0, 'type', 2, 'key', ds.index::text, 'hasJsonSchema', schema.schema IS NOT NULL)
@@ -119,13 +118,13 @@ WHEN jsonb_array_length((?1)::jsonb) = 1 THEN jsonb_agg(lvalues.value) -> 0
119118
JOIN dataset ON dataset.id = ds.dataset_id
120119
JOIN schema ON schema.id = ds.schema_id
121120
""";
122-
private static final String VALIDATION_SELECT = """
121+
private static final String VALIDATION_SELECT = """
123122
validation AS (
124123
SELECT dataset_id, jsonb_agg(jsonb_build_object('schemaId', schema_id, 'error', error)) AS errors
125124
FROM dataset_validationerrors GROUP BY dataset_id
126125
)
127126
""";
128-
private static final String DATASET_SUMMARY_SELECT = """
127+
private static final String DATASET_SUMMARY_SELECT = """
129128
SELECT ds.id, ds.runid AS runId,
130129
ds.ordinal, ds.testid AS testId,
131130
test.name AS testname, ds.description,
@@ -140,7 +139,7 @@ SELECT dataset_id, jsonb_agg(jsonb_build_object('schemaId', schema_id, 'error',
140139
LEFT JOIN validation ON validation.dataset_id = ds.id
141140
LEFT JOIN dataset_view dv ON dv.dataset_id = ds.id AND dv.view_id =
142141
""";
143-
private static final String LIST_SCHEMA_DATASETS = """
142+
private static final String LIST_SCHEMA_DATASETS = """
144143
WITH ids AS (
145144
SELECT dataset_id AS id FROM dataset_schemas WHERE uri = ?1
146145
),
@@ -162,15 +161,15 @@ WHERE dataset_id IN (SELECT id FROM ids) GROUP BY dataset_id
162161
LEFT JOIN dataset_view dv ON dv.dataset_id = ds.id
163162
WHERE ds.id IN (SELECT id FROM ids)
164163
""";
165-
private static final String ALL_LABELS_SELECT = """
164+
private static final String ALL_LABELS_SELECT = """
166165
SELECT dataset.id as dataset_id,
167166
COALESCE(jsonb_object_agg(label.name, lv.value) FILTER (WHERE label.name IS NOT NULL), '{}'::jsonb) AS values
168167
FROM dataset
169168
LEFT JOIN label_values lv ON dataset.id = lv.dataset_id
170169
LEFT JOIN label ON label.id = label_id
171170
""";
172171

173-
//@formatter:on
172+
//@formatter:on
174173
@Inject
175174
EntityManager em;
176175

@@ -183,12 +182,6 @@ WHERE ds.id IN (SELECT id FROM ids)
183182
@Inject
184183
TransactionManager tm;
185184

186-
// This is a nasty hack that will serialize all run -> dataset transformations and label calculations
187-
// The problem is that PostgreSQL's SSI will for some (unknown) reason rollback some transactions,
188-
// probably due to false sharing of locks. For some reason even using advisory locks in DB does not
189-
// solve the issue so we have to serialize this even outside the problematic transactions.
190-
private final ReentrantLock recalculationLock = new ReentrantLock();
191-
192185
@PermitAll
193186
@WithRoles
194187
@Override
@@ -562,20 +555,7 @@ void updateFingerprints(int testId) {
562555
}
563556
}
564557

565-
void withRecalculationLock(Runnable runnable) {
566-
recalculationLock.lock();
567-
try {
568-
runnable.run();
569-
} finally {
570-
recalculationLock.unlock();
571-
}
572-
}
573-
574558
public void onNewDataset(Dataset.EventNew event) {
575-
withRecalculationLock(() -> calculateLabelValues(event.testId, event.datasetId, event.labelId, event.isRecalculation));
576-
}
577-
578-
public void onNewDatasetNoLock(Dataset.EventNew event) {
579559
calculateLabelValues(event.testId, event.datasetId, event.labelId, event.isRecalculation);
580560
}
581561

horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/RunServiceImpl.java

+39-9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.Objects;
1515
import java.util.Optional;
1616
import java.util.TreeMap;
17+
import java.util.concurrent.ConcurrentHashMap;
1718
import java.util.function.BiConsumer;
1819
import java.util.stream.Collectors;
1920
import java.util.stream.StreamSupport;
@@ -64,6 +65,7 @@
6465
import io.hyperfoil.tools.horreum.api.data.ValidationError;
6566
import io.hyperfoil.tools.horreum.api.services.RunService;
6667
import io.hyperfoil.tools.horreum.api.services.SchemaService;
68+
import io.hyperfoil.tools.horreum.api.services.TestService;
6769
import io.hyperfoil.tools.horreum.bus.AsyncEventChannels;
6870
import io.hyperfoil.tools.horreum.datastore.BackendResolver;
6971
import io.hyperfoil.tools.horreum.datastore.Datastore;
@@ -152,6 +154,8 @@ WHEN jsonb_typeof(data) = 'array' THEN ?1 IN (SELECT jsonb_array_elements(data)-
152154
@Inject
153155
Session session;
154156

157+
private final ConcurrentHashMap<Integer, TestService.RecalculationStatus> transformations = new ConcurrentHashMap<>();
158+
155159
@Transactional
156160
@WithRoles(extras = Roles.HORREUM_SYSTEM)
157161
void onTestDeleted(int testId) {
@@ -1118,14 +1122,38 @@ public void recalculateAll(String fromStr, String toStr) {
11181122
}
11191123
}
11201124

1125+
/**
1126+
* Transforms the data for a given run by applying applicable schemas and transformers.
1127+
* It ensures any existing datasets for the run are removed before creating new ones,
1128+
* handles timeouts for ongoing transformations, and creates datasets with the transformed data.
1129+
*
1130+
* @param runId the ID of the run to transform
1131+
* @param isRecalculation flag indicating if this is a recalculation
1132+
* @return the number of datasets created, or 0 if the run is invalid or not found or already ongoing
1133+
*/
11211134
@WithRoles(extras = Roles.HORREUM_SYSTEM)
11221135
@Transactional
11231136
int transform(int runId, boolean isRecalculation) {
11241137
if (runId < 1) {
11251138
log.errorf("Transformation parameters error: run %s", runId);
11261139
return 0;
11271140
}
1141+
11281142
log.debugf("Transforming run ID %d, recalculation? %s", runId, Boolean.toString(isRecalculation));
1143+
int numDatasets = 0;
1144+
1145+
// check whether there is an ongoing transformation on the same runId
1146+
TestService.RecalculationStatus status = new TestService.RecalculationStatus(1);
1147+
TestService.RecalculationStatus prev = transformations.putIfAbsent(runId, status);
1148+
// ensure the transformation is removed, with this approach we should be sure
1149+
// it gets removed even if transaction-level exception occurs, e.g., timeout
1150+
Util.registerTxSynchronization(tm, txStatus -> transformations.remove(runId, status));
1151+
if (prev != null) {
1152+
// there is an ongoing transformation that has recently been initiated
1153+
log.warnf("Transformation for run %d already in progress", runId);
1154+
return numDatasets;
1155+
}
1156+
11291157
// We need to make sure all old datasets are gone before creating new; otherwise we could
11301158
// break the runid,ordinal uniqueness constraint
11311159
for (DatasetDAO old : DatasetDAO.<DatasetDAO> list("run.id", runId)) {
@@ -1138,9 +1166,8 @@ int transform(int runId, boolean isRecalculation) {
11381166
RunDAO run = RunDAO.findById(runId);
11391167
if (run == null) {
11401168
log.errorf("Cannot load run ID %d for transformation", runId);
1141-
return 0;
1169+
return numDatasets; // this is 0
11421170
}
1143-
int ordinal = 0;
11441171
Map<Integer, JsonNode> transformerResults = new TreeMap<>();
11451172
// naked nodes (those produced by implicit identity transformers) are all added to each dataset
11461173
List<JsonNode> nakedNodes = new ArrayList<>();
@@ -1247,7 +1274,8 @@ int transform(int runId, boolean isRecalculation) {
12471274
}
12481275
} else if (!result.isContainerNode() || (result.isObject() && !result.has("$schema")) ||
12491276
(result.isArray()
1250-
&& StreamSupport.stream(result.spliterator(), false).anyMatch(item -> !item.has("$schema")))) {
1277+
&& StreamSupport.stream(result.spliterator(), false)
1278+
.anyMatch(item -> !item.has("$schema")))) {
12511279
logMessage(run, PersistentLogDAO.WARN, "Dataset will contain element without a schema.");
12521280
}
12531281
JsonNode existing = transformerResults.get(transformerId);
@@ -1285,12 +1313,14 @@ int transform(int runId, boolean isRecalculation) {
12851313
}
12861314
nakedNodes.add(node);
12871315
logMessage(run, PersistentLogDAO.DEBUG,
1288-
"This test (%d) does not use any transformer for schema %s (key %s), passing as-is.", run.testid, uri,
1316+
"This test (%d) does not use any transformer for schema %s (key %s), passing as-is.", run.testid,
1317+
uri,
12891318
key);
12901319
}
12911320
}
12921321
if (schemasAndTransformers > 0) {
1293-
int max = transformerResults.values().stream().filter(JsonNode::isArray).mapToInt(JsonNode::size).max().orElse(1);
1322+
int max = transformerResults.values().stream().filter(JsonNode::isArray).mapToInt(JsonNode::size).max()
1323+
.orElse(1);
12941324

12951325
for (int position = 0; position < max; position += 1) {
12961326
ArrayNode all = instance.arrayNode(max + nakedNodes.size());
@@ -1305,7 +1335,7 @@ int transform(int runId, boolean isRecalculation) {
13051335
String message = String.format(
13061336
"Transformer %d produced an array of %d elements but other transformer " +
13071337
"produced %d elements; dataset %d/%d might be missing some data.",
1308-
entry.getKey(), node.size(), max, run.id, ordinal);
1338+
entry.getKey(), node.size(), max, run.id, numDatasets);
13091339
logMessage(run, PersistentLogDAO.WARN, "%s", message);
13101340
log.warnf(message);
13111341
}
@@ -1316,18 +1346,18 @@ int transform(int runId, boolean isRecalculation) {
13161346
}
13171347
}
13181348
nakedNodes.forEach(all::add);
1319-
createDataset(new DatasetDAO(run, ordinal++, run.description, all), isRecalculation);
1349+
createDataset(new DatasetDAO(run, numDatasets++, run.description, all), isRecalculation);
13201350
}
13211351
mediator.validateRun(run.id);
1322-
return ordinal;
13231352
} else {
1353+
numDatasets = 1;
13241354
logMessage(run, PersistentLogDAO.INFO, "No applicable schema, dataset will be empty.");
13251355
createDataset(new DatasetDAO(
13261356
run, 0, "Empty Dataset for run data without any schema.",
13271357
instance.arrayNode()), isRecalculation);
13281358
mediator.validateRun(run.id);
1329-
return 1;
13301359
}
1360+
return numDatasets;
13311361
}
13321362

13331363
private String limitLength(String str) {

horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/ServiceMediator.java

+11-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.util.concurrent.BlockingQueue;
77
import java.util.concurrent.ConcurrentHashMap;
88
import java.util.concurrent.LinkedBlockingQueue;
9+
import java.util.concurrent.locks.ReentrantLock;
910

1011
import jakarta.enterprise.context.ApplicationScoped;
1112
import jakarta.enterprise.context.control.ActivateRequestContext;
@@ -102,6 +103,8 @@ public class ServiceMediator {
102103

103104
private Map<AsyncEventChannels, Map<Integer, BlockingQueue<Object>>> events = new ConcurrentHashMap<>();
104105

106+
private final ReentrantLock lock = new ReentrantLock();
107+
105108
public ServiceMediator() {
106109
}
107110

@@ -151,7 +154,6 @@ void updateLabels(Dataset.LabelsUpdatedEvent event) {
151154
}
152155

153156
void newDataset(Dataset.EventNew eventNew) {
154-
//Note: should we call onNewDataset which will enable a lock?
155157
datasetService.onNewDataset(eventNew);
156158
}
157159

@@ -166,7 +168,7 @@ void newChange(Change.Event event) {
166168
@ActivateRequestContext
167169
@WithRoles(extras = Roles.HORREUM_SYSTEM)
168170
public void processDatasetEvents(Dataset.EventNew newEvent) {
169-
datasetService.onNewDatasetNoLock(newEvent);
171+
newDataset(newEvent);
170172
validateDataset(newEvent.datasetId);
171173
}
172174

@@ -232,8 +234,13 @@ int transform(int runId, boolean isRecalculation) {
232234
return runService.transform(runId, isRecalculation);
233235
}
234236

235-
void withRecalculationLock(Runnable run) {
236-
datasetService.withRecalculationLock(run);
237+
void withSharedLock(Runnable runnable) {
238+
lock.lock();
239+
try {
240+
runnable.run();
241+
} finally {
242+
lock.unlock();
243+
}
237244
}
238245

239246
void newExperimentResult(ExperimentService.ExperimentResult result) {

horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/TestServiceImpl.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,7 @@ public void recalculateDatasets(int testId) {
720720
// transform will add proper roles anyway
721721
// messageBus.executeForTest(testId, () -> datasetService.withRecalculationLock(() -> {
722722
// mediator.executeBlocking(() -> mediator.transform(runId, true));
723-
mediator.executeBlocking(() -> mediator.withRecalculationLock(() -> {
723+
mediator.executeBlocking(() -> mediator.withSharedLock(() -> {
724724
int newDatasets = 0;
725725
try {
726726
newDatasets = mediator.transform(runId, true);

horreum-web/src/api.tsx

-8
Original file line numberDiff line numberDiff line change
@@ -342,14 +342,6 @@ export function updateChangeDetection(
342342

343343
}
344344

345-
export function updateRunsAndDatasetsAction(
346-
testId: number,
347-
runs: number,
348-
datasets: number
349-
): any {
350-
return {type: "Tests/UPDATE_RUNS_AND_DATASETS", testId, runs, datasets}
351-
}
352-
353345
///Runs
354346
export function fetchRunSummary(id: number, token: string | undefined, alerting: AlertContextType): Promise<RunSummary> {
355347
return apiCall(runApi.getRunSummary(id, token), alerting, "FETCH_RUN_SUMMARY", "Failed to fetch data for run " + id + ", try uploading a Run and use new Run id instead.");

horreum-web/src/domain/tests/RecalculateDatasetsModal.tsx

+10-18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {useCallback, useEffect, useState, useRef, useContext} from "react"
1+
import { useCallback, useEffect, useState, useRef, useContext } from "react"
22
import { Bullseye, Button, Modal, Progress, Spinner } from "@patternfly/react-core"
3-
import {fetchTest, RecalculationStatus, testApi, TestStorage, updateRunsAndDatasetsAction} from "../../api"
3+
import { fetchTest, RecalculationStatus, testApi, TestStorage } from "../../api"
44
import {AppContext} from "../../context/appContext";
55
import {AppContextType} from "../../context/@types/appContextTypes";
66

@@ -13,7 +13,7 @@ type RecalculateDatasetsModalProps = {
1313

1414
export default function RecalculateDatasetsModal(props: RecalculateDatasetsModalProps) {
1515
const { alerting } = useContext(AppContext) as AppContextType;
16-
const [test, setTest] = useState<TestStorage | undefined>( undefined)
16+
const [test, setTest] = useState<TestStorage | undefined>(undefined)
1717
const [progress, setProgress] = useState(-1)
1818
const [status, setStatus] = useState<RecalculationStatus>()
1919
const timerId = useRef<number>()
@@ -32,19 +32,16 @@ export default function RecalculateDatasetsModal(props: RecalculateDatasetsModal
3232
if (!props.isOpen) {
3333
return
3434
}
35+
36+
// fetch the current test
3537
fetchTest(props.testId, alerting).then(setTest)
36-
}, [props.testId]);
3738

38-
useEffect(() => {
39-
if (!props.isOpen) {
40-
return
41-
}
39+
// fetch the latest recalculation status
4240
if (test?.runs === undefined) {
43-
testApi.getRecalculationStatus(props.testId).then(status => {
44-
updateRunsAndDatasetsAction(props.testId, status.totalRuns, status.datasets)
45-
})
41+
testApi.getRecalculationStatus(props.testId).then(setStatus)
4642
}
47-
}, [test, props.isOpen])
43+
}, [props.isOpen]);
44+
4845
return (
4946
<Modal
5047
title={`Re-transform datasets for test ${test?.name || "<unknown test>"}`}
@@ -66,11 +63,6 @@ export default function RecalculateDatasetsModal(props: RecalculateDatasetsModal
6663
.then(status => {
6764
setStatus(status)
6865
setProgress(status.finished)
69-
updateRunsAndDatasetsAction(
70-
props.testId,
71-
status.totalRuns,
72-
status.datasets
73-
)
7466
if (status.finished === status.totalRuns) {
7567
onClose()
7668
}
@@ -112,7 +104,7 @@ export default function RecalculateDatasetsModal(props: RecalculateDatasetsModal
112104
)}
113105
{progress < 0 && (
114106
<div style={{ marginBottom: "16px" }}>
115-
This test has {test?.runs || "<unknown number>"} of runs; do you want to recalculate all datasets?
107+
This test has {status?.totalRuns || "<unknown number>"} of runs; do you want to recalculate all datasets?
116108
</div>
117109
)}
118110
{progress >= 0 && (

0 commit comments

Comments
 (0)