From eb1b54c2582a728102df4d08e83c2d8d887e04a0 Mon Sep 17 00:00:00 2001 From: pavlos Date: Fri, 29 May 2026 16:49:20 +0300 Subject: [PATCH] fix(smpp): route UDH multipart SMS Signed-off-by: pavlos --- .../InMemorySmppServerMessageStore.java | 1 + .../smpp/server/SmppServerBindHandler.java | 34 ++- .../core/smpp/server/SmppServerWorker.java | 10 +- .../core/smpp/server/tasks/OutTask.java | 5 +- .../core/worker/InMemoryMessageTracker.java | 3 + .../sendium/core/worker/MessageState.java | 11 + .../InMemorySmppServerMessageStoreTest.java | 17 ++ .../SmppServerWorkerReassemblyTest.java | 222 ++++++++++++++++++ .../worker/InMemoryMessageTrackerTest.java | 15 ++ .../src/test/java/utils/NativeE2eSmoke.java | 31 ++- 10 files changed, 342 insertions(+), 7 deletions(-) create mode 100644 sendium-core/src/test/java/gr/cytech/sendium/core/smpp/server/SmppServerWorkerReassemblyTest.java diff --git a/sendium-core/src/main/java/gr/cytech/sendium/core/smpp/server/InMemorySmppServerMessageStore.java b/sendium-core/src/main/java/gr/cytech/sendium/core/smpp/server/InMemorySmppServerMessageStore.java index e0e4e02..3d768e7 100644 --- a/sendium-core/src/main/java/gr/cytech/sendium/core/smpp/server/InMemorySmppServerMessageStore.java +++ b/sendium-core/src/main/java/gr/cytech/sendium/core/smpp/server/InMemorySmppServerMessageStore.java @@ -46,6 +46,7 @@ public Future persistMessages(List> eventsQueu String destAddr = msg.to; MessageState state = new MessageState(gatewayMsgId, accountId, systemId, sourceAddr, destAddr, null); + state.setReassembledParts(msg.reassembledParts); worker.getWorkerResources().getDlrService().saveInitialState(state); } } catch (Exception e) { diff --git a/sendium-core/src/main/java/gr/cytech/sendium/core/smpp/server/SmppServerBindHandler.java b/sendium-core/src/main/java/gr/cytech/sendium/core/smpp/server/SmppServerBindHandler.java index 29f0821..6269f80 100644 --- a/sendium-core/src/main/java/gr/cytech/sendium/core/smpp/server/SmppServerBindHandler.java +++ b/sendium-core/src/main/java/gr/cytech/sendium/core/smpp/server/SmppServerBindHandler.java @@ -14,9 +14,12 @@ import org.slf4j.LoggerFactory; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; public class SmppServerBindHandler implements SmppServerHandler { private static final Logger logger = LoggerFactory.getLogger(SmppServerBindHandler.class); + private static final long UNPUSHED_DLR_REPLAY_BIND_WAIT_MILLIS = 5_000; + private static final long UNPUSHED_DLR_REPLAY_BIND_POLL_MILLIS = 50; public final ServerConnections connections; private final SmppServerWorker worker; private final ConcurrentHashMap pendingSessionContexts; @@ -84,15 +87,42 @@ public void sessionCreated( logger.info("Session created for account ID: {}", accountId); session.serverReady(handler); + scheduleUnpushedDlrReplay(handler); + } + + private void scheduleUnpushedDlrReplay(SmppServerSessionHandler handler) { + if (worker.getMessageStore() == null) { + return; + } + Thread.ofVirtual() + .name("SmppServer-UnpushedDlrReplay-" + handler.getSessionId()) + .start(() -> replayUnpushedDlrsWhenBound(handler)); + } + + private void replayUnpushedDlrsWhenBound(SmppServerSessionHandler handler) { try { - if (worker.getMessageStore() != null) { - worker.getMessageStore().onClientConnected(handler.getSystemId()); + if (!waitForBoundSession(handler)) { + logger.warn("Skipping unpushed DLR replay because session did not become bound for systemId:{}", + handler.getSystemId()); + return; } + worker.getMessageStore().onClientConnected(handler.getSystemId()); } catch (Exception e) { logger.warn("Failed to process unpushed DLRs for systemId:{}", handler.getSystemId(), e); } } + private boolean waitForBoundSession(SmppServerSessionHandler handler) throws InterruptedException { + long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(UNPUSHED_DLR_REPLAY_BIND_WAIT_MILLIS); + while (System.nanoTime() < deadline) { + if (handler.getSession() != null && handler.getSession().isBound()) { + return true; + } + TimeUnit.MILLISECONDS.sleep(UNPUSHED_DLR_REPLAY_BIND_POLL_MILLIS); + } + return handler.getSession() != null && handler.getSession().isBound(); + } + public void sessionDestroyed(Long sessionId, SmppServerSession session) { logger.info("Session destroyed: id:{} - name:{}", sessionId, session.getConfiguration().getName()); if (session.hasCounters()) { diff --git a/sendium-core/src/main/java/gr/cytech/sendium/core/smpp/server/SmppServerWorker.java b/sendium-core/src/main/java/gr/cytech/sendium/core/smpp/server/SmppServerWorker.java index 5e41100..187dc43 100644 --- a/sendium-core/src/main/java/gr/cytech/sendium/core/smpp/server/SmppServerWorker.java +++ b/sendium-core/src/main/java/gr/cytech/sendium/core/smpp/server/SmppServerWorker.java @@ -626,7 +626,12 @@ public M doMessage(int pThreadIndex, M pMsg) throws IOException { } for (DeliverSm deliverSm : requests) { - deliverSm.setReferenceObject(new Object[]{handler, pMsg}); + Object deliverMsgId = deliverSm.getReferenceObject(); + if (deliverMsgId instanceof String msgId) { + deliverSm.setReferenceObject(new Object[]{handler, pMsg, msgId}); + } else { + deliverSm.setReferenceObject(new Object[]{handler, pMsg}); + } enqueueOut(deliverSm); if (MessageTrace.shouldLog(configurationProvider, MessageTrace.EVENT_DELIVER_ENQUEUED)) { logger.info("message.deliver.enqueued worker={} {}", getFullName(), MessageTrace.identifiers(pMsg)); @@ -689,6 +694,7 @@ protected DeliverSm getDeliverSm(M pMsg, String messageId, int errorCode, Addres } deliverSm.setEsmClass(SmppConstants.ESM_CLASS_MT_SMSC_DELIVERY_RECEIPT); + deliverSm.setReferenceObject(messageId); return deliverSm; } @@ -861,7 +867,7 @@ protected boolean checkReassembling(M msg) { } public void reEnqueueIn(List> inEvents) { - inEvents.forEach(event -> enqueueNoExceptions(event.pMsg)); + inEvents.forEach(event -> enqueueToRouterNoExceptions(event.pMsg)); inEventQueue.addAll(inEvents); } diff --git a/sendium-core/src/main/java/gr/cytech/sendium/core/smpp/server/tasks/OutTask.java b/sendium-core/src/main/java/gr/cytech/sendium/core/smpp/server/tasks/OutTask.java index b865d79..4724667 100644 --- a/sendium-core/src/main/java/gr/cytech/sendium/core/smpp/server/tasks/OutTask.java +++ b/sendium-core/src/main/java/gr/cytech/sendium/core/smpp/server/tasks/OutTask.java @@ -15,6 +15,7 @@ public class OutTask implements Runnable { private final SmppServerWorker worker; private final Pdu pdu; private M msg; + private String deliverMsgId; public OutTask(SmppServerWorker worker, Pdu pdu) { this.worker = worker; @@ -42,6 +43,7 @@ public void run() { Object[] arr = (Object[]) pdu.getReferenceObject(); SmppServerSessionHandler handler = (SmppServerSessionHandler) arr[0]; msg = (M) arr[1]; + deliverMsgId = arr.length > 2 && arr[2] instanceof String id ? id : null; success = handler.sendPduRequest((PduRequest) pdu); } } catch (Exception e) { @@ -53,7 +55,8 @@ public void run() { worker.outTaskFailed(pdu, msg); } else if (!pdu.isResponse() && msg != null) { if (MessageTrace.shouldLog(worker.getConfigurationProvider(), MessageTrace.EVENT_DELIVER_SENT)) { - logger.info("message.deliver.sent worker={} {}", worker.getFullName(), MessageTrace.identifiers(msg)); + logger.info("message.deliver.sent worker={} deliverMsgId={} {}", worker.getFullName(), + MessageTrace.value(deliverMsgId), MessageTrace.identifiers(msg)); } } } diff --git a/sendium-core/src/main/java/gr/cytech/sendium/core/worker/InMemoryMessageTracker.java b/sendium-core/src/main/java/gr/cytech/sendium/core/worker/InMemoryMessageTracker.java index 056e3ed..18d5eee 100644 --- a/sendium-core/src/main/java/gr/cytech/sendium/core/worker/InMemoryMessageTracker.java +++ b/sendium-core/src/main/java/gr/cytech/sendium/core/worker/InMemoryMessageTracker.java @@ -8,6 +8,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.HashMap; import java.util.Optional; import java.util.concurrent.ConcurrentLinkedQueue; @@ -86,6 +87,8 @@ public void createAndEnqueueDLR(int mqid, String smscid, String smsid, String fr dlrMsg.errcode = errorCode != null ? errorCode : ""; dlrMsg.systemId = msgState.getSystemId(); dlrMsg.owner_id = msgState.getAccountId(); + var reassembledParts = msgState.getReassembledParts(); + dlrMsg.reassembledParts = reassembledParts == null ? null : new ArrayList<>(reassembledParts); dlrMsg.type = StandardMessage.MSG_DLR; try { outWorker.enqueueToRouter(dlrMsg); diff --git a/sendium-core/src/main/java/gr/cytech/sendium/core/worker/MessageState.java b/sendium-core/src/main/java/gr/cytech/sendium/core/worker/MessageState.java index d3091ce..25dcecd 100644 --- a/sendium-core/src/main/java/gr/cytech/sendium/core/worker/MessageState.java +++ b/sendium-core/src/main/java/gr/cytech/sendium/core/worker/MessageState.java @@ -3,6 +3,8 @@ import io.quarkus.runtime.annotations.RegisterForReflection; import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; @RegisterForReflection public class MessageState implements Serializable { @@ -15,6 +17,7 @@ public class MessageState implements Serializable { private String destAddr; private String operatorMsgId; private String forwardDlrUrl; + private List reassembledParts; private MessageStatus status; private long timestamp; @@ -69,6 +72,10 @@ public MessageStatus getStatus() { return status; } + public List getReassembledParts() { + return reassembledParts == null ? null : new ArrayList<>(reassembledParts); + } + public long getTimestamp() { return timestamp; } @@ -81,6 +88,10 @@ public void setStatus(MessageStatus status) { this.status = status; } + public void setReassembledParts(List reassembledParts) { + this.reassembledParts = reassembledParts == null ? null : new ArrayList<>(reassembledParts); + } + public void setTimestamp(long timestamp) { this.timestamp = timestamp; } diff --git a/sendium-core/src/test/java/gr/cytech/sendium/core/smpp/server/InMemorySmppServerMessageStoreTest.java b/sendium-core/src/test/java/gr/cytech/sendium/core/smpp/server/InMemorySmppServerMessageStoreTest.java index 529c377..e63447c 100644 --- a/sendium-core/src/test/java/gr/cytech/sendium/core/smpp/server/InMemorySmppServerMessageStoreTest.java +++ b/sendium-core/src/test/java/gr/cytech/sendium/core/smpp/server/InMemorySmppServerMessageStoreTest.java @@ -78,6 +78,23 @@ void persistMessages_SavesStateForEachMessage() { assertEquals("sys2", captor.getAllValues().get(1).getSystemId()); } + @Test + void persistMessages_SavesReassembledPartIds() { + StandardMessage msg = new StandardMessage(); + msg.serial = "gw-1"; + msg.owner_id = "account1"; + msg.systemId = "sys1"; + msg.from = "from1"; + msg.to = "to1"; + msg.reassembledParts = new ArrayList<>(List.of("part-1", "part-2")); + + messageStore.persistMessages(List.of(new InEvent<>(msg, null, 1, new Timestamp(System.currentTimeMillis())))); + + ArgumentCaptor captor = ArgumentCaptor.forClass(MessageState.class); + verify(dlrService).saveInitialState(captor.capture()); + assertEquals(List.of("part-1", "part-2"), captor.getValue().getReassembledParts()); + } + @Test void persistMessages_WithNullMessage_Skips() { List> events = new ArrayList<>(); diff --git a/sendium-core/src/test/java/gr/cytech/sendium/core/smpp/server/SmppServerWorkerReassemblyTest.java b/sendium-core/src/test/java/gr/cytech/sendium/core/smpp/server/SmppServerWorkerReassemblyTest.java new file mode 100644 index 0000000..a2227f6 --- /dev/null +++ b/sendium-core/src/test/java/gr/cytech/sendium/core/smpp/server/SmppServerWorkerReassemblyTest.java @@ -0,0 +1,222 @@ +package gr.cytech.sendium.core.smpp.server; + +import com.cloudhopper.commons.charset.CharsetUtil; +import com.cloudhopper.smpp.SmppConstants; +import com.cloudhopper.smpp.pdu.DeliverSm; +import gr.cytech.sendium.conf.PropertyChangeListener; +import gr.cytech.sendium.conf.SendiumConfigurationProvider; +import gr.cytech.sendium.core.message.StandardMessage; +import gr.cytech.sendium.core.queue.Queue; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +class SmppServerWorkerReassemblyTest { + + @Test + void completeUdhPartsAreReassembledAndRoutedToRouterQueue() throws Exception { + Queue routerQueue = new Queue<>(); + TestSmppServerWorker worker = new TestSmppServerWorker(new TestConfigurationProvider(), routerQueue); + ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1); + MessagePartsHandler handler = new MessagePartsHandler<>( + worker.new CcatMessagePartsEventsListener(), TimeUnit.SECONDS.toMillis(30), executor); + + try { + handler.addMessagePart(messagePart("0500037F0202", "World", "part-2")); + handler.addMessagePart(messagePart("0500037F0201", "Hello ", "part-1")); + + StandardMessage routed = routerQueue.dequeue(1_000); + + assertThat(routed).isNotNull(); + assertThat(routed.body).isEqualTo("Hello World"); + assertThat(routed.binheader).isNull(); + assertThat(routed.reassembledParts).containsExactly("part-1", "part-2"); + assertThat(worker.workerQueueMessages).isEmpty(); + + InEvent persisted = worker.getInEventQueue().poll(1_000, TimeUnit.MILLISECONDS); + assertThat(persisted).isNotNull(); + assertThat(persisted.pMsg).isSameAs(routed); + } finally { + executor.shutdownNow(); + } + } + + @Test + void delayedUdhPartsAreRoutedToRouterQueueWithoutBecomingDeliverSm() throws Exception { + Queue routerQueue = new Queue<>(); + TestSmppServerWorker worker = new TestSmppServerWorker(new TestConfigurationProvider(), routerQueue); + ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1); + MessagePartsHandler handler = new MessagePartsHandler<>( + worker.new CcatMessagePartsEventsListener(), 10, executor); + StandardMessage part = messagePart("0500037F0201", "Hello ", "part-1"); + + try { + handler.addMessagePart(part); + + StandardMessage routed = routerQueue.dequeue(1_000); + + assertThat(routed).isSameAs(part); + assertThat(routed.body).isEqualTo("Hello "); + assertThat(routed.binheader).isEqualTo("0500037F0201"); + assertThat(routed.reassembledParts).isNull(); + assertThat(worker.workerQueueMessages).isEmpty(); + + InEvent persisted = worker.getInEventQueue().poll(1_000, TimeUnit.MILLISECONDS); + assertThat(persisted).isNotNull(); + assertThat(persisted.pMsg).isSameAs(part); + } finally { + executor.shutdownNow(); + } + } + + @Test + void reassembledDlrGeneratesDeliverSmPerOriginalPartIdWithSameStatus() throws Exception { + TestSmppServerWorker worker = new TestSmppServerWorker(new TestConfigurationProvider(), new Queue<>()); + StandardMessage dlr = new StandardMessage(); + dlr.from = "306900000001"; + dlr.to = "sender"; + dlr.type = StandardMessage.MSG_DLR; + dlr.state = StandardMessage.DLR_STAT_DELIVRD; + dlr.errcode = "0"; + dlr.acked = true; + dlr.reassembledParts = new ArrayList<>(List.of("part-1", "part-2", "part-3")); + + List deliverSms = worker.generateDeliverSmForDLR(dlr); + + assertThat(deliverSms).hasSize(3); + assertThat(deliverSms.stream().map(DeliverSm::getReferenceObject).collect(Collectors.toList())) + .containsExactly("part-1", "part-2", "part-3"); + assertThat(deliverSms).allSatisfy(deliverSm -> + assertThat(deliverSm.getEsmClass()).isEqualTo(SmppConstants.ESM_CLASS_MT_SMSC_DELIVERY_RECEIPT)); + List bodies = deliverSms.stream() + .map(deliverSm -> CharsetUtil.decode(deliverSm.getShortMessage(), worker.getCharsetGsm())) + .collect(Collectors.toList()); + assertThat(bodies).allSatisfy(body -> assertThat(body).contains("stat:DELIVRD")); + assertThat(bodies).anySatisfy(body -> assertThat(body).contains("id:part-1")); + assertThat(bodies).anySatisfy(body -> assertThat(body).contains("id:part-2")); + assertThat(bodies).anySatisfy(body -> assertThat(body).contains("id:part-3")); + } + + private static StandardMessage messagePart(String udh, String body, String serial) { + StandardMessage message = new StandardMessage(); + message.owner_id = "account-a"; + message.systemId = "system-a"; + message.from = "sender"; + message.to = "306900000001"; + message.type = StandardMessage.MSG_TEXT; + message.binheader = udh; + message.body = body; + message.serial = serial; + message.ctstamp = System.currentTimeMillis(); + return message; + } + + private static class TestSmppServerWorker extends SmppServerWorker { + private final List workerQueueMessages = new ArrayList<>(); + + TestSmppServerWorker(SendiumConfigurationProvider configurationProvider, Queue routerQueue) { + super(configurationProvider, "smpp", routerQueue); + } + + @Override + public void enqueue(StandardMessage pMsg) { + workerQueueMessages.add(pMsg); + } + } + + private static class TestConfigurationProvider implements SendiumConfigurationProvider { + private final Map props = new HashMap<>(); + + @Override + public long getLongPrpt(String[] props) { + return Long.parseLong(getPrpt(props)); + } + + @Override + public long getLongPrpt(String prop, long def) { + return Long.parseLong(this.props.getOrDefault(prop, Long.toString(def))); + } + + @Override + public String getPrpt(String[] props) { + return this.props.getOrDefault(props[0], props[1]); + } + + @Override + public String getPrpt(String prop) { + return props.get(prop); + } + + @Override + public String getPrpt(String property, String defaultValue) { + return props.getOrDefault(property, defaultValue); + } + + @Override + public int getIntPrpt(String[] props) { + return Integer.parseInt(getPrpt(props)); + } + + @Override + public int getIntPrpt(String s, int intPrpt) { + return Integer.parseInt(props.getOrDefault(s, Integer.toString(intPrpt))); + } + + @Override + public boolean getBlnPrpt(String[] props) { + return Boolean.parseBoolean(getPrpt(props)); + } + + @Override + public boolean getBlnPrpt(String s, boolean defaultValue) { + return Boolean.parseBoolean(props.getOrDefault(s, Boolean.toString(defaultValue))); + } + + @Override + public void loadDefaultParams(String[][] prms) { + for (String[] prm : prms) { + props.putIfAbsent(prm[0], prm[1]); + } + } + + @Override + public void loadDefaultParams(String prefix, String[][] prms) { + for (String[] prm : prms) { + props.putIfAbsent(prefix + "." + prm[0], prm[1]); + } + } + + @Override + public boolean storeProperties(Map props) { + this.props.putAll(props); + return true; + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener propertyChanged) { + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener propertyChangeListener) { + } + + @Override + public Set getAllKeysReadOnly() { + return Set.copyOf(props.keySet()); + } + + @Override + public String setProperty(String s, String value) { + return props.put(s, value); + } + } +} diff --git a/sendium-core/src/test/java/gr/cytech/sendium/core/worker/InMemoryMessageTrackerTest.java b/sendium-core/src/test/java/gr/cytech/sendium/core/worker/InMemoryMessageTrackerTest.java index 98da60d..71a211d 100644 --- a/sendium-core/src/test/java/gr/cytech/sendium/core/worker/InMemoryMessageTrackerTest.java +++ b/sendium-core/src/test/java/gr/cytech/sendium/core/worker/InMemoryMessageTrackerTest.java @@ -12,7 +12,9 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -106,6 +108,19 @@ void createAndEnqueueDLR_KnownMessage_ResolvesFromDlrService() throws Interrupte assertEquals("systemId", captor.getValue().systemId); } + @Test + void createAndEnqueueDLR_KnownReassembledMessage_RestoresPartIds() throws InterruptedException { + MessageState state = new MessageState("gw-123", "accountId", "systemId", "from", "to", null); + state.setReassembledParts(new ArrayList<>(List.of("part-1", "part-2"))); + when(dlrService.resolveAndRemoveDlr("op-456", 1)).thenReturn(java.util.Optional.of(state)); + + tracker.createAndEnqueueDLR(1, "op-456", "gw-123", "from", "to", "test body", 1, "0", new HashMap<>()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(StandardMessage.class); + verify(outWorker).enqueueToRouter(captor.capture()); + assertEquals(List.of("part-1", "part-2"), captor.getValue().reassembledParts); + } + @Test void createAndEnqueueDLR_UnknownMessage_DoesNotEnqueue() { when(dlrService.resolveAndRemoveDlr("unknown", 0)).thenReturn(java.util.Optional.empty()); diff --git a/sendium-core/src/test/java/utils/NativeE2eSmoke.java b/sendium-core/src/test/java/utils/NativeE2eSmoke.java index 8a585f4..db9737c 100644 --- a/sendium-core/src/test/java/utils/NativeE2eSmoke.java +++ b/sendium-core/src/test/java/utils/NativeE2eSmoke.java @@ -503,11 +503,38 @@ private DownstreamSmppClient() { } private void start() throws Exception { + Exception lastFailure = null; + int attempt = 0; + long deadline = System.nanoTime() + TIMEOUT.toNanos(); + while (System.nanoTime() < deadline) { + attempt++; + try { + session = client.bind(createConfiguration(), createSessionHandler()); + return; + } catch (Exception e) { + lastFailure = e; + if (session != null) { + session.destroy(); + session = null; + } + System.err.println("Downstream SMPP bind attempt " + attempt + " failed: " + + e.getClass().getSimpleName() + ": " + e.getMessage()); + Thread.sleep(500); + } + } + throw new IllegalStateException("Downstream SMPP client could not bind after " + attempt + " attempts", lastFailure); + } + + private SmppSessionConfiguration createConfiguration() { SmppSessionConfiguration configuration = new SmppSessionConfiguration(SmppBindType.TRANSCEIVER, "smpp-user", "smpp-pass"); configuration.setHost("localhost"); configuration.setPort(SENDIUM_SMPP_PORT); configuration.setWindowSize(1000); - session = client.bind(configuration, new DefaultSmppSessionHandler() { + return configuration; + } + + private DefaultSmppSessionHandler createSessionHandler() { + return new DefaultSmppSessionHandler() { @Override public PduResponse firePduRequestReceived(PduRequest pduRequest) { if (pduRequest instanceof DeliverSm receivedDeliverSm) { @@ -516,7 +543,7 @@ public PduResponse firePduRequestReceived(PduRequest pduRequest) { } return pduRequest.createResponse(); } - }); + }; } private SubmitSmResp sendSms(String from, String to, String text) throws SmppInvalidArgumentException, InterruptedException,