Skip to content

Commit 729356b

Browse files
Fix for the SequenceId Ordering Problem (#221)
SequenceIds are currently ordered lexicographically, which is wrong as the SDC standards define no order on them. We hence order them by the timestamp of the first message that they were used in. # Checklist The following aspects have been respected by the author of this pull request, confirmed by both pull request assignee **and** reviewer: * Adherence to coding conventions * [x] Pull Request Assignee * [x] Reviewer (midtuna) * Adherence to javadoc conventions * [x] Pull Request Assignee * [x] Reviewer (midtuna) * Changelog update (necessity checked and entry added or not added respectively) * [x] Pull Request Assignee * [x] Reviewer (midtuna) * README update (necessity checked and entry added or not added respectively) * [x] Pull Request Assignee * [x] Reviewer (midtuna) * config update (necessity checked and entry added or not added respectively) * [x] Pull Request Assignee * [x] Reviewer (midtuna) * SDCcc executable ran against a test device (if necessary) * [x] Pull Request Assignee * [x] Reviewer (midtuna) --------- Co-authored-by: midttuna <[email protected]>
1 parent c7ff37e commit 729356b

File tree

4 files changed

+171
-42
lines changed

4 files changed

+171
-42
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424
- support multiple mds for test case for BICEPS.R5042
2525
- inconsistent messaging in SDCcc logs ("No problems were found" and "Test run was invalid" one after another.)
2626
- incorrect behavior of the configuration option SDCcc.SummarizeMessageEncodingErrors
27+
- SequenceIds are now ordered by the timestamp of the first message that used them
2728

2829
### Removed
2930

sdccc/src/main/java/com/draeger/medical/sdccc/messages/MessageStorage.java

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,12 @@
7777
import org.apache.commons.io.input.BOMInputStream;
7878
import org.apache.logging.log4j.LogManager;
7979
import org.apache.logging.log4j.Logger;
80+
import org.hibernate.ScrollMode;
8081
import org.hibernate.Session;
8182
import org.hibernate.SessionFactory;
8283
import org.hibernate.Transaction;
84+
import org.hibernate.query.spi.ScrollableResultsImplementor;
85+
import org.hibernate.query.spi.StreamDecorator;
8386
import org.somda.sdc.dpws.CommunicationLog;
8487
import org.somda.sdc.dpws.soap.ApplicationInfo;
8588
import org.somda.sdc.dpws.soap.CommunicationContext;
@@ -1079,6 +1082,7 @@ private void awaitFlushBarrier() {
10791082

10801083
/**
10811084
* Retrieves all SequenceId attribute values that have been seen.
1085+
* Orders them by the timestamp of the first message that used the respective SequenceId.
10821086
*
10831087
* @return stream of all SequenceId attribute values that have been seen
10841088
* @throws IOException if storage is closed
@@ -1090,17 +1094,20 @@ public Stream<String> getUniqueSequenceIds() throws IOException {
10901094
throw new IOException(GET_UNIQUE_SEQUENCE_IDS_CALLED_ON_CLOSED_STORAGE);
10911095
}
10921096

1093-
final CriteriaQuery<String> criteria;
1094-
1097+
final CriteriaQuery<String> messageContentQuery;
10951098
try (final Session session = sessionFactory.openSession()) {
1099+
session.beginTransaction();
1100+
10961101
final CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder();
1097-
criteria = criteriaBuilder.createQuery(String.class);
1098-
final Root<MdibVersionGroupEntity> mdibVersionGroupEntityRoot = criteria.from(MdibVersionGroupEntity.class);
1099-
criteria.select(mdibVersionGroupEntityRoot.get(MdibVersionGroupEntity_.sequenceId));
1100-
criteria.distinct(true);
1102+
messageContentQuery = criteriaBuilder.createQuery(String.class);
1103+
final Root<MessageContent> messageContentRoot = messageContentQuery.from(MessageContent.class);
1104+
messageContentQuery.select(
1105+
messageContentRoot.join(MessageContent_.mdibVersionGroups).get(MdibVersionGroupEntity_.sequenceId));
1106+
1107+
messageContentQuery.orderBy(criteriaBuilder.asc(messageContentRoot.get(MessageContent_.nanoTimestamp)));
11011108
}
11021109

1103-
return this.getQueryResult(criteria);
1110+
return this.getOrderedQueryResult(messageContentQuery).distinct();
11041111
}
11051112

11061113
/**
@@ -1693,11 +1700,11 @@ public GetterResult<MessageContent> getInboundMessagesByTimeIntervalAndBodyType(
16931700
}
16941701

16951702
final boolean present;
1696-
try (final Stream<MessageContent> countingStream = this.getQueryResult(messageContentQuery)) {
1703+
try (final Stream<MessageContent> countingStream = this.getOrderedQueryResult(messageContentQuery)) {
16971704
present = countingStream.findAny().isPresent();
16981705
}
16991706

1700-
return new GetterResult<>(this.getQueryResult(messageContentQuery), present);
1707+
return new GetterResult<>(this.getOrderedQueryResult(messageContentQuery), present);
17011708
}
17021709

17031710
/**
@@ -1773,11 +1780,11 @@ public GetterResult<MessageContent> getInboundMessagesByTimestampAndBodyType(
17731780
}
17741781

17751782
final boolean present;
1776-
try (final Stream<MessageContent> countingStream = this.getQueryResult(messageContentQuery)) {
1783+
try (final Stream<MessageContent> countingStream = this.getOrderedQueryResult(messageContentQuery)) {
17771784
present = countingStream.findAny().isPresent();
17781785
}
17791786

1780-
return new GetterResult<>(this.getQueryResult(messageContentQuery), present);
1787+
return new GetterResult<>(this.getOrderedQueryResult(messageContentQuery), present);
17811788
}
17821789

17831790
/**
@@ -1817,11 +1824,11 @@ public GetterResult<ManipulationData> getManipulationDataByManipulation(final St
18171824
}
18181825

18191826
final boolean present;
1820-
try (final Stream<ManipulationData> countingStream = this.getQueryResult(criteria)) {
1827+
try (final Stream<ManipulationData> countingStream = this.getOrderedQueryResult(criteria)) {
18211828
present = countingStream.findAny().isPresent();
18221829
}
18231830

1824-
return new GetterResult<>(this.getQueryResult(criteria), present);
1831+
return new GetterResult<>(this.getOrderedQueryResult(criteria), present);
18251832
}
18261833

18271834
/**
@@ -1886,10 +1893,10 @@ public GetterResult<ManipulationData> getManipulationDataByParametersAndManipula
18861893
criteriaBuilder.and(parameterExistPredicates.toArray(new Predicate[0]))));
18871894
}
18881895
final boolean present;
1889-
try (final Stream<ManipulationData> countingStream = this.getQueryResult(criteria)) {
1896+
try (final Stream<ManipulationData> countingStream = this.getOrderedQueryResult(criteria)) {
18901897
present = countingStream.findAny().isPresent();
18911898
}
1892-
return new GetterResult<>(this.getQueryResult(criteria), present);
1899+
return new GetterResult<>(this.getOrderedQueryResult(criteria), present);
18931900
}
18941901

18951902
private <T> Stream<T> getQueryResult(final CriteriaQuery<T> criteriaQuery) {
@@ -1902,6 +1909,16 @@ private <T> Stream<T> getQueryResult(final CriteriaQuery<T> criteriaQuery) {
19021909
.onClose(resultIterator::close);
19031910
}
19041911

1912+
private <T> Stream<T> getOrderedQueryResult(final CriteriaQuery<T> criteriaQuery) {
1913+
final Session session = sessionFactory.openSession();
1914+
final Stream<T> results = getOrderedStreamForQuery(session, criteriaQuery);
1915+
1916+
final ResultIterator<T> resultIterator = new ResultIterator<>(session, results);
1917+
1918+
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(resultIterator, Spliterator.ORDERED), false)
1919+
.onClose(resultIterator::close);
1920+
}
1921+
19051922
// be aware, that this does not use evict on cached objects
19061923
private <T> Stream<T> getStreamForQuery(final CriteriaQuery<T> criteriaQuery) {
19071924
final Session session = sessionFactory.openSession();
@@ -1926,6 +1943,23 @@ private <T> Stream<T> getStreamForQuery(final Session session, final CriteriaQue
19261943
.stream();
19271944
}
19281945

1946+
// be aware, that this does not use evict on cached objects
1947+
private <T> Stream<T> getOrderedStreamForQuery(final Session session, final CriteriaQuery<T> criteriaQuery) {
1948+
// The stream provided by Hibernate does not have the ORDERED characteristic.
1949+
// We hence build our own.
1950+
final ScrollableResultsImplementor scrollableResults =
1951+
(ScrollableResultsImplementor) session.createQuery(criteriaQuery)
1952+
.setReadOnly(true)
1953+
.setCacheable(false)
1954+
.setFetchSize(FETCH_SIZE)
1955+
.scroll(ScrollMode.FORWARD_ONLY);
1956+
final OrderedStreamIterator<T> iterator = new OrderedStreamIterator<>(scrollableResults);
1957+
final Spliterator<T> spliterator =
1958+
Spliterators.spliteratorUnknownSize(iterator, Spliterator.NONNULL | Spliterator.ORDERED);
1959+
1960+
return (Stream<T>) new StreamDecorator(StreamSupport.stream(spliterator, false), scrollableResults::close);
1961+
}
1962+
19291963
private void transmit(final List<DatabaseEntry> results) {
19301964
try (final Session session = sessionFactory.openSession()) {
19311965
final Transaction transaction = session.beginTransaction();
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the MIT License.
3+
* Copyright (c) 2023-2024 Draegerwerk AG & Co. KGaA.
4+
*
5+
* SPDX-License-Identifier: MIT
6+
*/
7+
8+
package com.draeger.medical.sdccc.messages
9+
10+
import org.hibernate.query.spi.CloseableIterator
11+
import org.hibernate.query.spi.ScrollableResultsImplementor
12+
13+
/**
14+
* Iterator to be used to create Streams with the ORDERED characteristic from ScrollableResults.
15+
*/
16+
class OrderedStreamIterator<T>(private val results: ScrollableResultsImplementor) : CloseableIterator<T> {
17+
18+
override fun close() {
19+
results.close()
20+
}
21+
22+
override fun remove() {
23+
throw UnsupportedOperationException(
24+
"this stream does not support the" +
25+
" remove operation"
26+
)
27+
}
28+
29+
override fun hasNext(): Boolean {
30+
if (results.isClosed) {
31+
return false
32+
}
33+
return results.next()
34+
}
35+
36+
override fun next(): T {
37+
val element = results.get()
38+
@Suppress("UNCHECKED_CAST")
39+
return if (element.size == 1) {
40+
element[0]
41+
} else {
42+
element
43+
} as T
44+
}
45+
}

sdccc/src/test/java/com/draeger/medical/sdccc/messages/TestMessageStorage.java

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import javax.xml.namespace.QName;
5757
import org.apache.commons.io.ByteOrderMark;
5858
import org.apache.commons.lang3.tuple.Pair;
59+
import org.jetbrains.annotations.NotNull;
5960
import org.junit.jupiter.api.BeforeEach;
6061
import org.junit.jupiter.api.Test;
6162
import org.junit.jupiter.api.io.TempDir;
@@ -155,15 +156,7 @@ public void testMdibVersionOverflow(@TempDir final File dir) throws IOException,
155156
1, false, true, mock(MessageFactory.class), new HibernateConfigImpl(dir), this.testRunObserver)) {
156157
final ListMultimap<String, String> multimap = ArrayListMultimap.create();
157158

158-
final String transactionId = "transactionId";
159-
final String requestUri = "requestUri";
160-
161-
final X509Certificate certificate = CertificateUtil.getDummyCert();
162-
final CommunicationContext headerContext = new CommunicationContext(
163-
new HttpApplicationInfo(multimap, transactionId, requestUri),
164-
new TransportInfo(
165-
Constants.HTTPS_SCHEME, null, null, null, null, Collections.singletonList(certificate)),
166-
null);
159+
final CommunicationContext headerContext = getCommunicationContext(multimap);
167160

168161
try (final Message message = new Message(
169162
CommunicationLog.Direction.INBOUND,
@@ -196,15 +189,7 @@ public void testMdibVersionCloseToOverflow(@TempDir final File dir) throws IOExc
196189
1, false, true, mock(MessageFactory.class), new HibernateConfigImpl(dir), this.testRunObserver)) {
197190
final ListMultimap<String, String> multimap = ArrayListMultimap.create();
198191

199-
final String transactionId = "transactionId";
200-
final String requestUri = "requestUri";
201-
202-
final X509Certificate certificate = CertificateUtil.getDummyCert();
203-
final CommunicationContext headerContext = new CommunicationContext(
204-
new HttpApplicationInfo(multimap, transactionId, requestUri),
205-
new TransportInfo(
206-
Constants.HTTPS_SCHEME, null, null, null, null, Collections.singletonList(certificate)),
207-
null);
192+
final CommunicationContext headerContext = getCommunicationContext(multimap);
208193

209194
try (final Message message = new Message(
210195
CommunicationLog.Direction.INBOUND,
@@ -248,15 +233,7 @@ public void testGetUniqueSequenceIds(@TempDir final File dir) throws IOException
248233
1, false, true, mock(MessageFactory.class), new HibernateConfigImpl(dir), this.testRunObserver)) {
249234
final ListMultimap<String, String> multimap = ArrayListMultimap.create();
250235

251-
final String transactionId = "transactionId";
252-
final String requestUri = "requestUri";
253-
254-
final X509Certificate certificate = CertificateUtil.getDummyCert();
255-
final CommunicationContext headerContext = new CommunicationContext(
256-
new HttpApplicationInfo(multimap, transactionId, requestUri),
257-
new TransportInfo(
258-
Constants.HTTPS_SCHEME, null, null, null, null, Collections.singletonList(certificate)),
259-
null);
236+
final CommunicationContext headerContext = getCommunicationContext(multimap);
260237

261238
try (final Message message = new Message(
262239
CommunicationLog.Direction.INBOUND,
@@ -302,6 +279,78 @@ public void testGetUniqueSequenceIds(@TempDir final File dir) throws IOException
302279
}
303280
}
304281

282+
/**
283+
* Tests whether SequenceId values are ordered by the timestamp of the first message they appear in.
284+
*
285+
* @param dir message storage directory
286+
* @throws IOException on io exceptions
287+
* @throws CertificateException on certificate exceptions
288+
*/
289+
@Test
290+
public void testGetUniqueSequenceIdsOrdering(@TempDir final File dir) throws IOException, CertificateException {
291+
try (final MessageStorage messageStorage = new MessageStorage(
292+
1, false, true, mock(MessageFactory.class), new HibernateConfigImpl(dir), this.testRunObserver)) {
293+
final ListMultimap<String, String> multimap = ArrayListMultimap.create();
294+
295+
final CommunicationContext headerContext = getCommunicationContext(multimap);
296+
297+
try (final Message message = new Message(
298+
CommunicationLog.Direction.INBOUND,
299+
CommunicationLog.MessageType.REQUEST,
300+
headerContext,
301+
messageStorage)) {
302+
message.write(String.format(
303+
BASE_MESSAGE_STRING, "action", String.format(SEQUENCE_ID_METRIC_BODY_STRING, "3", "3"))
304+
.getBytes(StandardCharsets.UTF_8));
305+
}
306+
messageStorage.flush();
307+
308+
try (final Message message = new Message(
309+
CommunicationLog.Direction.INBOUND,
310+
CommunicationLog.MessageType.REQUEST,
311+
headerContext,
312+
messageStorage)) {
313+
message.write(String.format(
314+
BASE_MESSAGE_STRING, "action", String.format(SEQUENCE_ID_METRIC_BODY_STRING, "3", "2"))
315+
.getBytes(StandardCharsets.UTF_8));
316+
}
317+
messageStorage.flush();
318+
319+
try (final Message message = new Message(
320+
CommunicationLog.Direction.INBOUND,
321+
CommunicationLog.MessageType.REQUEST,
322+
headerContext,
323+
messageStorage)) {
324+
message.write(String.format(
325+
BASE_MESSAGE_STRING, "action", String.format(SEQUENCE_ID_METRIC_BODY_STRING, "3", "1"))
326+
.getBytes(StandardCharsets.UTF_8));
327+
}
328+
messageStorage.flush();
329+
330+
try (final Stream<String> sequenceIdStream = messageStorage.getUniqueSequenceIds()) {
331+
assertEquals(List.of("urn:uuid:3", "urn:uuid:2", "urn:uuid:1"), sequenceIdStream.toList());
332+
}
333+
334+
try (final MessageStorage.GetterResult<MessageContent> inboundMessages =
335+
messageStorage.getInboundMessages()) {
336+
assertEquals(3, inboundMessages.getStream().count());
337+
}
338+
}
339+
}
340+
341+
private static @NotNull CommunicationContext getCommunicationContext(final ListMultimap<String, String> multimap)
342+
throws CertificateException, IOException {
343+
final String transactionId = "transactionId";
344+
final String requestUri = "requestUri";
345+
346+
final X509Certificate certificate = CertificateUtil.getDummyCert();
347+
return new CommunicationContext(
348+
new HttpApplicationInfo(multimap, transactionId, requestUri),
349+
new TransportInfo(
350+
Constants.HTTPS_SCHEME, null, null, null, null, Collections.singletonList(certificate)),
351+
null);
352+
}
353+
305354
/**
306355
* Tests whether headers and the transaction id are stored properly.
307356
*

0 commit comments

Comments
 (0)