├── .github ├── CODEOWNERS ├── release-notes.yml ├── ISSUE_TEMPLATE │ ├── 2_enhancement_request.md │ ├── 1_feature_request.md │ └── 3_bug_report.md ├── dependabot.yml ├── ISSUE_TEMPLATE.md └── workflows │ ├── release-notes.yml │ ├── pullrequest.yml │ └── main.yml ├── .gitignore ├── src ├── main │ ├── resources │ │ └── axonserver_download.txt │ └── java │ │ └── io │ │ └── axoniq │ │ └── axonserver │ │ └── connector │ │ ├── event │ │ ├── PersistentStream.java │ │ ├── transformation │ │ │ ├── impl │ │ │ │ ├── grpc │ │ │ │ │ ├── NonTransientException.java │ │ │ │ │ └── GrpcEventTransformation.java │ │ │ │ ├── TransformationStreamCompletedException.java │ │ │ │ └── DefaultActiveTransformation.java │ │ │ ├── event │ │ │ │ ├── EventTransformer.java │ │ │ │ ├── EventTransformationExecutor.java │ │ │ │ ├── TransformableEventStream.java │ │ │ │ ├── EventSources.java │ │ │ │ ├── impl │ │ │ │ │ ├── IterableEventTransformationExecutor.java │ │ │ │ │ └── TransformableEventIterable.java │ │ │ │ └── stream │ │ │ │ │ └── TokenRangeEvents.java │ │ │ ├── Transformer.java │ │ │ ├── Appender.java │ │ │ ├── ActiveTransformation.java │ │ │ ├── EventTransformation.java │ │ │ └── EventTransformationChannel.java │ │ ├── AppendEventsTransaction.java │ │ ├── EventStream.java │ │ ├── PersistentStreamSegment.java │ │ ├── impl │ │ │ ├── AppendEventsTransactionImpl.java │ │ │ └── BufferedAggregateEventStream.java │ │ ├── SnapshotChannel.java │ │ ├── PersistentStreamCallbacks.java │ │ └── AggregateEventStream.java │ │ ├── impl │ │ ├── StreamClosedException.java │ │ ├── DisposableReadonlyBuffer.java │ │ ├── NoopFlowControl.java │ │ ├── HeartbeatSender.java │ │ ├── AssertUtils.java │ │ ├── StreamTimeoutException.java │ │ ├── Headers.java │ │ ├── StreamUnexpectedlyCompletedException.java │ │ ├── SyncRegistration.java │ │ ├── CloseableBuffer.java │ │ ├── AxonConnectorThreadFactory.java │ │ ├── FutureListStreamObserver.java │ │ ├── MessageFactory.java │ │ ├── HeaderAttachingInterceptor.java │ │ ├── CloseableReadonlyBuffer.java │ │ ├── BufferingReplyChannel.java │ │ ├── AsyncRegistration.java │ │ ├── GrpcBufferingInterceptor.java │ │ ├── FutureStreamObserver.java │ │ ├── ServerAddress.java │ │ ├── ReconnectConfiguration.java │ │ ├── buffer │ │ │ ├── FlowControlledDisposableReadonlyBuffer.java │ │ │ └── BlockingCloseableBuffer.java │ │ ├── SynchronizedRequestStream.java │ │ └── CloseAwareReplyChannel.java │ │ ├── FlowControl.java │ │ ├── InstructionHandler.java │ │ ├── command │ │ └── CommandChannel.java │ │ ├── query │ │ ├── QueryDefinition.java │ │ ├── SubscriptionQueryResult.java │ │ └── QueryHandler.java │ │ ├── ErrorCategory.java │ │ ├── Registration.java │ │ └── control │ │ └── ProcessorInstructionHandler.java └── test │ ├── docker │ └── toxi-axonserver.yml │ ├── resources │ └── log4j2.properties │ └── java │ └── io │ └── axoniq │ └── axonserver │ └── connector │ ├── impl │ ├── ObjectUtilsTest.java │ ├── SyncRegistrationTest.java │ ├── BufferingReplyChannelTest.java │ ├── SynchronizedRequestStreamTest.java │ ├── AsyncRegistrationTest.java │ ├── buffer │ │ └── FlowControlledDisposableReadonlyBufferTest.java │ └── AbstractIncomingInstructionStreamTest.java │ ├── FailedResultStream.java │ ├── StubResultStream.java │ ├── testutils │ └── MessageFactory.java │ ├── ResultStreamPublisherTest.java │ ├── command │ └── CommandChannelTest.java │ ├── control │ └── FakeProcessorInstructionHandler.java │ ├── event │ └── impl │ │ └── BufferedAggregateEventStreamTest.java │ └── AxonServerConnectionFactoryTest.java ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── copyright-template.xml ├── CONTRIBUTING.md └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @AxonIQ/axon-server -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Maven build artifacts can be ignored 2 | target/ 3 | # ...but keep the wrapper jar 4 | !.mvn/wrapper/maven-wrapper.jar 5 | 6 | axon.ipr 7 | axon.iws 8 | 9 | # IntelliJ project files 10 | *.iml 11 | .idea/ 12 | 13 | # Eclipse project files 14 | .classpath 15 | .project 16 | .settings/ 17 | .factorypath 18 | 19 | # MacOS makes this one 20 | .DS_Store 21 | -------------------------------------------------------------------------------- /.github/release-notes.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | sections: 3 | - title: ":star: Features" 4 | labels: [ "Type: Feature" ] 5 | - title: ":chart_with_upwards_trend: Enhancements" 6 | labels: [ "Type: Enhancement" ] 7 | - title: ":beetle: Bug Fixes" 8 | labels: [ "Type: Bug" ] 9 | - title: ":hammer_and_wrench: Dependency Upgrade" 10 | labels: [ "Type: Dependency Upgrade" ] 11 | issues: 12 | exclude: 13 | labels: [ "Type: Incorrect Repository", "Type: Question" ] 14 | contributors: 15 | exclude: 16 | names: [ "dependabot", "dependabot[bot]" ] 17 | -------------------------------------------------------------------------------- /src/main/resources/axonserver_download.txt: -------------------------------------------------------------------------------- 1 | ********************************************** 2 | * * 3 | * !!! UNABLE TO CONNECT TO AXON SERVER !!! * 4 | * * 5 | * Are you sure it's running? * 6 | * Don't have Axon Server yet? * 7 | * Go to: https://axoniq.io/go-axon * 8 | * * 9 | ********************************************** 10 | 11 | To suppress this message, you can 12 | - explicitly configure an AxonServer location, 13 | - start with -Daxon.axonserver.suppressDownloadMessage=true 14 | -------------------------------------------------------------------------------- /src/test/docker/toxi-axonserver.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | axonserver: 5 | image: docker.io/axoniq/axonserver 6 | hostname: axonserver 7 | environment: 8 | - AXONIQ_AXONSERVER_NAME=axonserver 9 | - AXONIQ_AXONSERVER_HOSTNAME=localhost 10 | - AXONIQ_AXONSERVER_DEVMODE_ENABLED=true 11 | ports: 12 | - '8024:8024' 13 | volumes: 14 | - axonserver-data:/data 15 | - axonserver-eventstore:/eventdata 16 | 17 | toxiproxy: 18 | image: docker.io/shopify/toxiproxy 19 | hostname: toxiproxy 20 | ports: 21 | - '8474:8474' 22 | - '8124:8124' 23 | 24 | volumes: 25 | axonserver-data: 26 | axonserver-eventstore: 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2_enhancement_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Enhancement request' 3 | about: 'Suggest an enhancement/change to an existing feature for Axon Server Connector Java' 4 | title: 5 | labels: 'Type: Enhancement' 6 | --- 7 | 8 | 9 | 10 | ### Enhancement Description 11 | 12 | 13 | 14 | ### Current Behaviour 15 | 16 | 17 | 18 | ### Wanted Behaviour 19 | 20 | 21 | 22 | ### Possible Workarounds 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1_feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Feature request' 3 | about: 'Suggest a feature for Axon Server Connector Java' 4 | title: 5 | labels: 'Type: Feature' 6 | --- 7 | 8 | 9 | 10 | ### Feature Description 11 | 12 | 16 | 17 | ### Current Behaviour 18 | 19 | 20 | 21 | ### Wanted Behaviour 22 | 23 | 24 | 25 | ### Possible Workarounds 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: "/" 6 | schedule: 7 | interval: weekly 8 | day: "sunday" 9 | open-pull-requests-limit: 5 10 | labels: 11 | - "Type: Dependency Upgrade" 12 | - "Priority 1: Must" 13 | milestone: 41 14 | groups: 15 | github-dependencies: 16 | update-types: 17 | - "patch" 18 | - "minor" 19 | - "major" 20 | 21 | - package-ecosystem: maven 22 | directory: "/" 23 | schedule: 24 | interval: weekly 25 | day: "sunday" 26 | open-pull-requests-limit: 5 27 | labels: 28 | - "Type: Dependency Upgrade" 29 | - "Priority 1: Must" 30 | milestone: 41 31 | groups: 32 | maven-dependencies: 33 | update-types: 34 | - "patch" 35 | - "minor" 36 | - "major" 37 | ignore: 38 | - dependency-name: "org.junit:junit-bom" 39 | versions: [ "[6.0.0,)" ] -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3_bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Bug report' 3 | about: 'Report a bug in Axon Server Connector Java' 4 | title: 5 | labels: 'Type: Bug' 6 | --- 7 | 8 | 9 | 10 | ### Basic information 11 | 12 | * Axon Server Connector Java version: 13 | * JDK version: 14 | * Complete executable reproducer if available (e.g. GitHub Repo): 15 | 16 | ### Steps to reproduce 17 | 18 | 22 | 23 | ### Expected behaviour 24 | 25 | 26 | 27 | ### Actual behaviour 28 | 29 | 33 | -------------------------------------------------------------------------------- /src/test/resources/log4j2.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2021. AxonIQ 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | rootLogger.level=info 18 | rootLogger.appenderRef.stdout.ref=STDOUT 19 | appender.console.type=Console 20 | appender.console.name=STDOUT 21 | appender.console.layout.type=PatternLayout 22 | appender.console.layout.pattern=%style{%d{ISO8601}}{bright,black} %highlight{%-5level }[%style{%t}{bright,blue}] %style{%C{1.}}{bright,yellow}: %msg%n%throwable -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/PersistentStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2024. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.axoniq.axonserver.connector.event; 17 | 18 | /** 19 | * A connection to a persistent stream. Axon Server can assign zero or more segments to this connection. 20 | * 21 | * @author Marc Gathier 22 | * @since 2024.0.0 23 | */ 24 | public interface PersistentStream { 25 | 26 | /** 27 | * Closes the persistent stream. 28 | */ 29 | void close(); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/transformation/impl/grpc/NonTransientException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.transformation.impl.grpc; 18 | 19 | /** 20 | * @author Sara Pellegrini 21 | * @since 2023.1.0 22 | */ 23 | public class NonTransientException extends RuntimeException { 24 | 25 | public NonTransientException() { 26 | } 27 | 28 | public NonTransientException(String message) { 29 | super(message); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/transformation/impl/TransformationStreamCompletedException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.transformation.impl; 18 | 19 | /** 20 | * @author Sara Pellegrini 21 | * @since 2023.1.0 22 | */ 23 | public class TransformationStreamCompletedException extends RuntimeException { 24 | 25 | public TransformationStreamCompletedException() { 26 | } 27 | 28 | public TransformationStreamCompletedException(String message) { 29 | super(message); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/StreamClosedException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | /** 20 | * A {@link RuntimeException} to throw if a stream is closed. 21 | */ 22 | public class StreamClosedException extends RuntimeException { 23 | 24 | /** 25 | * Constructs a new {@link StreamClosedException} using the given {@code cause}. 26 | * 27 | * @param cause the {@link Throwable} further defining this {@link StreamClosedException} 28 | */ 29 | public StreamClosedException(Throwable cause) { 30 | super(cause); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/transformation/event/EventTransformer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.transformation.event; 18 | 19 | import io.axoniq.axonserver.connector.event.transformation.Appender; 20 | import io.axoniq.axonserver.grpc.event.EventWithToken; 21 | 22 | import java.util.function.BiConsumer; 23 | 24 | /** 25 | * Provides a way to transform a given event by invoking operations on the {@link Appender}. 26 | * 27 | * @author Sara Pellegrini 28 | * @since 2023.1.0 29 | */ 30 | public interface EventTransformer extends BiConsumer { 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/DisposableReadonlyBuffer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2022. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | /** 20 | * A {@link CloseableReadonlyBuffer buffer} that can be disposed of from the subscriber's side. 21 | * 22 | * @param the type of messages in this buffer 23 | * @author Milan Savic 24 | * @author Stefan Dragisic 25 | * @author Allard Buijze 26 | * @since 4.6.0 27 | */ 28 | public interface DisposableReadonlyBuffer extends CloseableReadonlyBuffer { 29 | 30 | /** 31 | * Disposes this buffer from the subscriber's side. 32 | */ 33 | void dispose(); 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/NoopFlowControl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2022. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.axoniq.axonserver.connector.FlowControl; 20 | 21 | /** 22 | * NOOP implementation of {@link FlowControl}. 23 | * 24 | * @author Milan Savic 25 | * @author Stefan Dragisic 26 | * @author Allard Buijze 27 | * @since 4.6.0 28 | */ 29 | public enum NoopFlowControl implements FlowControl { 30 | 31 | /** 32 | * Singleton instance of {@link NoopFlowControl}. 33 | */ 34 | INSTANCE; 35 | 36 | @Override 37 | public void request(long requested) { 38 | // noop 39 | } 40 | 41 | @Override 42 | public void cancel() { 43 | // noop 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/HeartbeatSender.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.axoniq.axonserver.grpc.InstructionAck; 20 | 21 | import java.util.concurrent.CompletableFuture; 22 | 23 | /** 24 | * Functional interface defining a method to send heartbeats with. 25 | */ 26 | @FunctionalInterface 27 | public interface HeartbeatSender { 28 | 29 | /** 30 | * Send a heartbeat, receiving a {@link CompletableFuture} containing the {@link InstructionAck} once the heartbeat 31 | * has been received. 32 | * 33 | * @return a {@link CompletableFuture} containing the {@link InstructionAck} once the heartbeat has been received 34 | */ 35 | CompletableFuture sendHeartbeat(); 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/transformation/Transformer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.transformation; 18 | 19 | /** 20 | * Functional interface that needs to be implemented to register transformation actions into the Transformation. 21 | * 22 | * @author Sara Pellegrini 23 | * @since 2023.1.0 24 | */ 25 | @FunctionalInterface 26 | public interface Transformer { 27 | 28 | /** 29 | * Registers the requested event changes to the event store. 30 | * 31 | * @param appender the appender used to register the transformation actions into the Transformation, since it 32 | * provides the methods to delete and replace events 33 | */ 34 | void transform(Appender appender); 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/FlowControl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2022. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector; 18 | 19 | /** 20 | * Controls the flow of the messages via communication channel. Implementations should send messages only when 21 | * requested. Once the cancellation is invoked, implementations should stop sending messages. 22 | * 23 | * @author Milan Savic 24 | * @author Stefan Dragisic 25 | * @author Allard Buijze 26 | * @since 4.6.0 27 | */ 28 | public interface FlowControl { 29 | 30 | /** 31 | * Requests the {@code requested} amount of responses to be sent. 32 | * 33 | * @param requested number of responses to be sent 34 | */ 35 | void request(long requested); 36 | 37 | /** 38 | * Cancels response sending. 39 | */ 40 | void cancel(); 41 | } 42 | -------------------------------------------------------------------------------- /copyright-template.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/AssertUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | /** 20 | * Utility class to perform assertion on method parameters. 21 | * 22 | * @author Allard Buijze 23 | * @since 4.5 24 | */ 25 | public class AssertUtils { 26 | 27 | private AssertUtils() { 28 | } 29 | 30 | /** 31 | * Assert that the given {@code condition} is true, or otherwise throws an {@link IllegalArgumentException} with 32 | * given {@code message}. 33 | * 34 | * @param condition The condition to validate 35 | * @param message The message in the exception 36 | */ 37 | public static void assertParameter(boolean condition, String message) { 38 | if (!condition) { 39 | throw new IllegalArgumentException(message); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/release-notes.yml: -------------------------------------------------------------------------------- 1 | # Trigger the workflow on milestone events 2 | on: 3 | milestone: 4 | types: [closed] 5 | name: Milestone Closure 6 | jobs: 7 | create-release-notes: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v6 12 | - name: Create Release Notes Markdown 13 | uses: docker://decathlon/release-notes-generator-action:3.1.5 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 16 | OUTPUT_FOLDER: temp_release_notes 17 | USE_MILESTONE_TITLE: "true" 18 | - name: Get the name of the created Release Notes file and extract Version 19 | run: | 20 | RELEASE_NOTES_FILE=$(ls temp_release_notes/*.md | head -n 1) 21 | echo "RELEASE_NOTES_FILE=$RELEASE_NOTES_FILE" >> $GITHUB_ENV 22 | VERSION=$(echo ${{ github.event.milestone.title }} | cut -d' ' -f6) 23 | echo "VERSION=$VERSION" >> $GITHUB_ENV 24 | - name: Create a Draft Release Notes on GitHub 25 | id: create_release 26 | uses: actions/create-release@v1 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 29 | with: 30 | tag_name: ${{ env.VERSION }} 31 | release_name: Axon Server Connector for Java version ${{ env.VERSION }} 32 | body_path: ${{ env.RELEASE_NOTES_FILE }} 33 | draft: true -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/StreamTimeoutException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | /** 20 | * A {@link RuntimeException} to throw if reading from a stream results in a timeout. 21 | * 22 | * @author Mitchell Herrijgers 23 | * @since 4.5.6 24 | */ 25 | public class StreamTimeoutException extends RuntimeException { 26 | 27 | /** 28 | * Constructs a new instance of {@link StreamTimeoutException}. 29 | */ 30 | public StreamTimeoutException() { 31 | } 32 | 33 | /** 34 | * Constructs a new instance of {@link StreamTimeoutException} with a detailed description of the cause of the 35 | * exception 36 | * 37 | * @param message the message describing the cause of the exception. 38 | */ 39 | public StreamTimeoutException(String message) { 40 | super(message); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/Headers.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.grpc.Metadata; 20 | 21 | /** 22 | * Utility class containing header definitions. 23 | */ 24 | public abstract class Headers { 25 | 26 | /** 27 | * A {@link Metadata.Key} defining the context from within which a message will be send. 28 | */ 29 | public static final Metadata.Key CONTEXT = 30 | Metadata.Key.of("AxonIQ-Context", Metadata.ASCII_STRING_MARSHALLER); 31 | 32 | /** 33 | * A {@link Metadata.Key} defining the access token from the application sending this message. 34 | */ 35 | public static final Metadata.Key ACCESS_TOKEN = 36 | Metadata.Key.of("AxonIQ-Access-Token", Metadata.ASCII_STRING_MARSHALLER); 37 | 38 | private Headers() { 39 | // Utility class 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/InstructionHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector; 18 | 19 | /** 20 | * Interface describing a component that is capable of handling instructions coming from AxonServer. 21 | * 22 | * @param the type of the instruction handled by this handler 23 | * @param the type of response used by {@link #handle(Object, ReplyChannel)}'s {@link ReplyChannel} 24 | */ 25 | @FunctionalInterface 26 | public interface InstructionHandler { 27 | 28 | /** 29 | * Handle the given {@code instruction}, using {@code replyChannel} to acknowledge the instruction and reply to 30 | * AxonServer. 31 | * 32 | * @param instruction the instruction to handle 33 | * @param replyChannel the channel onto which replies and acknowledgements to send 34 | */ 35 | void handle(I instruction, ReplyChannel replyChannel); 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/StreamUnexpectedlyCompletedException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | /** 20 | * A {@link RuntimeException} to throw if a stream is completed unexpectedly. 21 | * 22 | * @author Sara Pellegrini 23 | * @since 4.4.3 24 | */ 25 | public class StreamUnexpectedlyCompletedException extends RuntimeException { 26 | 27 | /** 28 | * Constructs a new instance of {@link StreamUnexpectedlyCompletedException}. 29 | */ 30 | public StreamUnexpectedlyCompletedException() { 31 | } 32 | 33 | /** 34 | * Constructs a new instance of {@link StreamUnexpectedlyCompletedException} with a detailed description of the 35 | * cause of the exception 36 | * 37 | * @param message the message describing the cause of the exception. 38 | */ 39 | public StreamUnexpectedlyCompletedException(String message) { 40 | super(message); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/SyncRegistration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.axoniq.axonserver.connector.Registration; 20 | 21 | import java.util.concurrent.CompletableFuture; 22 | 23 | /** 24 | * Synchronous implementation of the {@link Registration}. 25 | */ 26 | public class SyncRegistration implements Registration { 27 | 28 | private static final CompletableFuture COMPLETED = CompletableFuture.completedFuture(null); 29 | private final Runnable cancelAction; 30 | 31 | /** 32 | * Construct a {@link SyncRegistration}, using the given {@code cancelAction} when {@link #cancel()} is invoked. 33 | * 34 | * @param cancelAction the operation to perform when {@link #cancel()} is invoked 35 | */ 36 | public SyncRegistration(Runnable cancelAction) { 37 | this.cancelAction = cancelAction; 38 | } 39 | 40 | @Override 41 | public CompletableFuture cancel() { 42 | cancelAction.run(); 43 | return COMPLETED; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/transformation/event/EventTransformationExecutor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.transformation.event; 18 | 19 | import io.axoniq.axonserver.connector.event.transformation.EventTransformation; 20 | import io.axoniq.axonserver.connector.event.transformation.EventTransformationChannel; 21 | 22 | import java.util.concurrent.CompletableFuture; 23 | import java.util.function.Supplier; 24 | 25 | /** 26 | * Executes the Transformation by invoking APPLY operation. 27 | * 28 | * @author Sara Pellegrini 29 | * @since 2023.1.0 30 | */ 31 | public interface EventTransformationExecutor { 32 | 33 | /** 34 | * Executes the Transformation by invoking APPLY operation. 35 | * 36 | * @param channelSupplier supplies the {@link EventTransformationChannel} 37 | * @return a {@link CompletableFuture} to indicate when the APPLY process has been accepted 38 | */ 39 | CompletableFuture execute(Supplier channelSupplier); 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/pullrequest.yml: -------------------------------------------------------------------------------- 1 | name: PR - Axon Server Connector Java 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | name: Test and Build on JDK ${{ matrix.java-version }} 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | include: 13 | - java-version: 11 14 | sonar-enabled: false 15 | - java-version: 17 16 | sonar-enabled: true 17 | - java-version: 21 18 | sonar-enabled: true 19 | fail-fast: false # run both to the end 20 | 21 | steps: 22 | - name: Checkout Code 23 | uses: actions/checkout@v6 24 | 25 | - name: Set up JDK ${{ matrix.java-version }} 26 | uses: actions/setup-java@v5.1.0 27 | with: 28 | distribution: 'zulu' 29 | java-version: ${{ matrix.java-version }} 30 | cache: "maven" 31 | server-id: sonatype 32 | server-username: MAVEN_USERNAME 33 | server-password: MAVEN_PASSWORD 34 | 35 | - name: Build and Test with Sonar Analysis 36 | if: matrix.sonar-enabled 37 | run: | 38 | ./mvnw -B -U -Dstyle.color=always -Pcoverage clean verify \ 39 | org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \ 40 | -Dsonar.projectKey=AxonIQ_axonserver-connector-java 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | SONAR_TOKEN: ${{ secrets.CONNECTOR_SONAR_TOKEN }} 44 | 45 | - name: Build and Test without Sonar Analysis 46 | if: matrix.sonar-enabled != true 47 | run: | 48 | mvn -B -U -Dstyle.color=always clean verify 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/CloseableBuffer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2022. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.axoniq.axonserver.grpc.ErrorMessage; 20 | 21 | /** 22 | * A {@link CloseableReadonlyBuffer buffer} that can be closed by the producing side. 23 | * 24 | * @param the type of messages in this buffer 25 | * @author Milan Savic 26 | * @author Stefan Dragisic 27 | * @author Allard Buijze 28 | * @since 4.6.0 29 | */ 30 | public interface CloseableBuffer extends CloseableReadonlyBuffer { 31 | 32 | /** 33 | * Puts a message in this buffer. 34 | * 35 | * @param message the message to be put in this buffer 36 | */ 37 | void put(T message); 38 | 39 | /** 40 | * Closes this buffer from the producing side. 41 | */ 42 | void close(); 43 | 44 | /** 45 | * Closes this buffer exceptionally from the producing side. 46 | * 47 | * @param errorMessage an error indicating why this buffer is closed 48 | */ 49 | void closeExceptionally(ErrorMessage errorMessage); 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/AppendEventsTransaction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event; 18 | 19 | import io.axoniq.axonserver.grpc.event.Confirmation; 20 | import io.axoniq.axonserver.grpc.event.Event; 21 | 22 | import java.util.concurrent.CompletableFuture; 23 | 24 | /** 25 | * Interface providing operations to interact with a Transaction to append events onto the Event Store. 26 | */ 27 | public interface AppendEventsTransaction { 28 | 29 | /** 30 | * Append the given event to be committed as part of this transaction. 31 | * 32 | * @param event the event to append 33 | * @return this instance for fluent interfacing 34 | */ 35 | AppendEventsTransaction appendEvent(Event event); 36 | 37 | /** 38 | * Commit this transaction, appending all registered events into the Event Store. 39 | * 40 | * @return a CompletableFuture resolving the confirmation of the successful processing of the transaction 41 | */ 42 | CompletableFuture commit(); 43 | 44 | /** 45 | * Rolls back the transaction. 46 | */ 47 | void rollback(); 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/transformation/Appender.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.transformation; 18 | 19 | import io.axoniq.axonserver.grpc.event.Event; 20 | 21 | import java.util.concurrent.CompletableFuture; 22 | 23 | /** 24 | * Appends the transformation actions. 25 | * 26 | * @author Sara Pellegrini 27 | * @since 2023.1.0 28 | */ 29 | public interface Appender { 30 | 31 | /** 32 | * Appends to the transformation the request to delete the event with the given token. 33 | * 34 | * @param token the token of the event to be deleted 35 | * @return a {@link CompletableFuture} of the Appender, used for composition 36 | */ 37 | CompletableFuture deleteEvent(long token); 38 | 39 | /** 40 | * Appends to the transformation the request to replace the event with the given token. 41 | * 42 | * @param token the token of the event to be replaced 43 | * @param replacement the new event used to replace the original one 44 | * @return a {@link CompletableFuture} of the Appender, used for composition 45 | */ 46 | CompletableFuture replaceEvent(long token, Event replacement); 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/transformation/event/TransformableEventStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.transformation.event; 18 | 19 | import io.axoniq.axonserver.grpc.event.EventWithToken; 20 | 21 | import java.util.function.Predicate; 22 | 23 | /** 24 | * Stream of events that can be filtered and transformed. 25 | * 26 | * @author Sara Pellegrini 27 | * @since 2023.1.0 28 | */ 29 | public interface TransformableEventStream { 30 | 31 | /** 32 | * Filters events based on the provided {@code predicate}. 33 | * 34 | * @param predicate the predicate used for filtering events 35 | * @return filtered stream 36 | */ 37 | TransformableEventStream filter(Predicate predicate); 38 | 39 | /** 40 | * Transforms events by providing transformation description and the transformer. 41 | * 42 | * @param transformationDescription the description of the transformation 43 | * @param eventTransformer the transformer 44 | * @return the executor used to transform events 45 | */ 46 | EventTransformationExecutor transform(String transformationDescription, 47 | EventTransformer eventTransformer); 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/EventStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event; 18 | 19 | import io.axoniq.axonserver.connector.ResultStream; 20 | import io.axoniq.axonserver.grpc.event.EventWithToken; 21 | 22 | /** 23 | * Represents a stream of Events. It can be consumed in a blocking and non-blocking approach. Each event is accompanied 24 | * with a token, which can be used to reopen a new stream at the same position. 25 | */ 26 | public interface EventStream extends ResultStream { 27 | 28 | /** 29 | * Instructs AxonServer to not send messages with the given {@code payloadType} and {@code revision}. This 30 | * method is in no way a guarantee that these message will no longer be sent at all. Some messages may already have 31 | * been en-route, and AxonServer may decide to send messages every once in a while as a "beacon" to relay progress 32 | * of the tracking token. It may choose, however, to omit the actual payload, in that case. 33 | * 34 | * @param payloadType the type of payload to exclude from the stream 35 | * @param revision the revision of the payload to exclude from the stream 36 | */ 37 | void excludePayloadType(String payloadType, String revision); 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/io/axoniq/axonserver/connector/impl/ObjectUtilsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import org.junit.jupiter.api.Test; 20 | 21 | import static org.junit.jupiter.api.Assertions.assertEquals; 22 | import static org.junit.jupiter.api.Assertions.assertTrue; 23 | 24 | class ObjectUtilsTest { 25 | 26 | @Test 27 | void testHexGenerationUsesAllChars() { 28 | String actual = ObjectUtils.randomHex(5000); 29 | 30 | String chars = "0123456789abcdef"; 31 | for (int i = 0; i < chars.length(); i++) { 32 | String ch = chars.substring(i, i + 1); 33 | assertTrue(actual.contains(ch), "Random hex didn't pick a " + ch); 34 | } 35 | assertTrue(actual.matches("^[0-9a-f]+$")); 36 | } 37 | 38 | @Test 39 | void testRandomHexGeneration() { 40 | String random1 = ObjectUtils.randomHex(10); 41 | String random2 = ObjectUtils.randomHex(5); 42 | String random3 = ObjectUtils.randomHex(7); 43 | 44 | assertEquals(10, random1.length()); 45 | assertEquals(5, random2.length()); 46 | assertEquals(7, random3.length()); 47 | 48 | assertTrue(random1.matches("^[0-9a-f]+$")); 49 | assertTrue(random2.matches("^[0-9a-f]+$")); 50 | assertTrue(random3.matches("^[0-9a-f]+$")); 51 | } 52 | } -------------------------------------------------------------------------------- /src/test/java/io/axoniq/axonserver/connector/FailedResultStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector; 18 | 19 | import java.util.Optional; 20 | import java.util.concurrent.TimeUnit; 21 | 22 | /** 23 | * Implementation of {@link ResultStream} that throws {@link RuntimeException}s. 24 | * For unit test purpose. 25 | * 26 | * @author Sara Pellegrini 27 | */ 28 | public class FailedResultStream implements ResultStream { 29 | 30 | @Override 31 | public Long peek() { 32 | throw new RuntimeException(); 33 | } 34 | 35 | @Override 36 | public Long nextIfAvailable() { 37 | throw new RuntimeException(); 38 | } 39 | 40 | @Override 41 | public Long nextIfAvailable(long timeout, TimeUnit unit) { 42 | throw new RuntimeException(); 43 | } 44 | 45 | @Override 46 | public Long next() { 47 | throw new RuntimeException(); 48 | } 49 | 50 | @Override 51 | public void onAvailable(Runnable callback) { 52 | callback.run(); 53 | } 54 | 55 | @Override 56 | public void close() { 57 | throw new RuntimeException(); 58 | } 59 | 60 | @Override 61 | public boolean isClosed() { 62 | return false; 63 | } 64 | 65 | @Override 66 | public Optional getError() { 67 | return Optional.of(new RuntimeException()); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/AxonConnectorThreadFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import java.util.concurrent.ThreadFactory; 20 | import java.util.concurrent.atomic.AtomicInteger; 21 | 22 | /** 23 | * A {@link ThreadFactory} implementation for AxonServer Connector threads. 24 | */ 25 | public class AxonConnectorThreadFactory implements ThreadFactory { 26 | 27 | private final ThreadGroup threadGroup; 28 | private final AtomicInteger threadNumber = new AtomicInteger(); 29 | 30 | private AxonConnectorThreadFactory(String instanceId) { 31 | threadGroup = new ThreadGroup("axon-connector-" + instanceId); 32 | } 33 | 34 | /** 35 | * Constructs an {@link AxonConnectorThreadFactory} using the given {@code instanceId} as part of the thread group. 36 | * 37 | * @param instanceId the instance identifier to become part of the thread group 38 | * @return an {@link AxonConnectorThreadFactory} used to create threads 39 | */ 40 | public static AxonConnectorThreadFactory forInstanceId(String instanceId) { 41 | return new AxonConnectorThreadFactory(instanceId); 42 | } 43 | 44 | @Override 45 | @SuppressWarnings("NullableProblems") 46 | public Thread newThread(Runnable runnable) { 47 | return new Thread(threadGroup, runnable, threadGroup.getName() + "-" + nextThreadNumber()); 48 | } 49 | 50 | private int nextThreadNumber() { 51 | return threadNumber.getAndIncrement(); 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/FutureListStreamObserver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.grpc.stub.StreamObserver; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | import java.util.concurrent.CompletableFuture; 24 | import java.util.concurrent.ConcurrentLinkedQueue; 25 | 26 | /** 27 | * An implementation of both a {@link CompletableFuture} and {@link StreamObserver}. This future will complete 28 | * successfully on {@link #onNext(Object)} or on {@link #onCompleted()} (granted that the given {@code 29 | * valueWhenNoResult} is not of type {@link Throwable}). On {@link #onError(Throwable)}, this future will {@link 30 | * #completeExceptionally(Throwable)}. 31 | * 32 | * @param the type of result this {@link CompletableFuture} and {@link StreamObserver} resolves 33 | * @author Stefan Dragisic 34 | * @since 4.6.0 35 | */ 36 | public class FutureListStreamObserver extends CompletableFuture> implements StreamObserver { 37 | 38 | private final ConcurrentLinkedQueue results = new ConcurrentLinkedQueue(); 39 | 40 | @Override 41 | public void onNext(T value) { 42 | results.add(value); 43 | } 44 | 45 | @Override 46 | public void onError(Throwable t) { 47 | if (!isDone()) { 48 | completeExceptionally(t); 49 | } 50 | } 51 | 52 | @Override 53 | public void onCompleted() { 54 | if (!isDone()) { 55 | complete(new ArrayList<>(results)); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/io/axoniq/axonserver/connector/impl/SyncRegistrationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.junit.jupiter.api.Test; 21 | import org.junit.jupiter.api.Timeout; 22 | 23 | import java.util.concurrent.TimeUnit; 24 | import java.util.concurrent.TimeoutException; 25 | import java.util.concurrent.atomic.AtomicBoolean; 26 | 27 | import static org.junit.jupiter.api.Assertions.assertSame; 28 | import static org.junit.jupiter.api.Assertions.assertTrue; 29 | 30 | class SyncRegistrationTest { 31 | 32 | private final AtomicBoolean cancelled = new AtomicBoolean(); 33 | private SyncRegistration testSubject; 34 | 35 | @BeforeEach 36 | void setUp() { 37 | testSubject = new SyncRegistration(() -> cancelled.set(true)); 38 | } 39 | 40 | @Timeout(1) 41 | @Test 42 | void testAwaitDoesNotWait() throws TimeoutException, InterruptedException { 43 | assertSame(testSubject, testSubject.awaitAck(60, TimeUnit.SECONDS)); 44 | } 45 | 46 | @Test 47 | void testOnAckExecutesImmediately() { 48 | AtomicBoolean result = new AtomicBoolean(); 49 | testSubject.onAck(() -> result.set(true)); 50 | 51 | assertTrue(result.get()); 52 | } 53 | 54 | @Test 55 | void cancelReturnsCompletedFuture() { 56 | testSubject.cancel(); 57 | assertTrue(cancelled.get()); 58 | 59 | assertSame(testSubject.cancel(), testSubject.cancel()); 60 | assertTrue(testSubject.cancel().isDone()); 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /src/test/java/io/axoniq/axonserver/connector/StubResultStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector; 18 | 19 | import java.util.Optional; 20 | import java.util.concurrent.TimeUnit; 21 | import java.util.concurrent.atomic.AtomicLong; 22 | 23 | /** 24 | * Fake implementation of {@link ResultStream} that returns a certain number of {@link Long} elements. 25 | * For unit test purpose. 26 | * 27 | * @author Sara Pellegrini 28 | */ 29 | public class StubResultStream implements ResultStream { 30 | 31 | private final AtomicLong e; 32 | 33 | public StubResultStream(long elements) { 34 | this.e = new AtomicLong(elements); 35 | } 36 | 37 | @Override 38 | public Long peek() { 39 | return e.get() == 0L ? null : e.get(); 40 | } 41 | 42 | @Override 43 | public Long nextIfAvailable() { 44 | return e.get() == 0L ? null : e.getAndDecrement(); 45 | } 46 | 47 | @Override 48 | public Long nextIfAvailable(long timeout, TimeUnit unit) { 49 | return nextIfAvailable(); 50 | } 51 | 52 | @Override 53 | public Long next() { 54 | return nextIfAvailable(); 55 | } 56 | 57 | @Override 58 | public void onAvailable(Runnable callback) { 59 | callback.run(); 60 | } 61 | 62 | @Override 63 | public void close() { 64 | e.set(0L); 65 | } 66 | 67 | @Override 68 | public boolean isClosed() { 69 | return e.get() == 0L; 70 | } 71 | 72 | @Override 73 | public Optional getError() { 74 | return Optional.empty(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main - Axon Server Connector Java 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | - connector-*.*.x 9 | 10 | jobs: 11 | build: 12 | name: Test and Build on JDK ${{ matrix.java-version }} 13 | runs-on: ubuntu-latest 14 | continue-on-error: true 15 | 16 | strategy: 17 | matrix: 18 | include: 19 | - java-version: 11 20 | sonar-enabled: false 21 | deploy-enabled: true 22 | - java-version: 17 23 | sonar-enabled: true 24 | deploy-enabled: false 25 | - java-version: 21 26 | sonar-enabled: false 27 | deploy-enabled: false 28 | 29 | steps: 30 | - name: Checkout Code 31 | uses: actions/checkout@v6 32 | 33 | - name: Set up JDK ${{ matrix.java-version }} 34 | uses: actions/setup-java@v5.1.0 35 | with: 36 | distribution: 'zulu' 37 | java-version: ${{ matrix.java-version }} 38 | cache: "maven" 39 | server-id: central 40 | server-username: MAVEN_USERNAME 41 | server-password: MAVEN_PASSWORD 42 | 43 | - name: Build and Test with Sonar Analysis 44 | if: matrix.sonar-enabled 45 | run: | 46 | ./mvnw -B -U -Dstyle.color=always -Pcoverage clean verify \ 47 | org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \ 48 | -Dsonar.projectKey=AxonIQ_axonserver-connector-java 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | SONAR_TOKEN: ${{ secrets.CONNECTOR_SONAR_TOKEN }} 52 | 53 | - name: Build and Test without Sonar Analysis 54 | if: matrix.sonar-enabled != true 55 | run: | 56 | mvn -B -U -Dstyle.color=always clean verify 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | 60 | - name: Deploy to Sonatype 61 | if: success() && matrix.deploy-enabled 62 | run: | 63 | mvn -B -U -Dstyle.color=always deploy -DskipTests=true 64 | env: 65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 66 | MAVEN_USERNAME: ${{ secrets.SONATYPE_TOKEN_ID }} 67 | MAVEN_PASSWORD: ${{ secrets.SONATYPE_TOKEN_PASS }} -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Thank you for your interest in contributing to the Axon Server Connector for Java. To make sure using this project is a 4 | smooth experience for everybody, we've set up a number of guidelines to follow. 5 | 6 | There are different ways in which you can contribute to the connector: 7 | 8 | 1. You can report any bugs, feature requests or ideas about improvements on 9 | our [issue page](https://github.com/AxonIQ/axonserver-connector-java/issues/new/choose). All ideas are welcome. 10 | Please be as exact as possible when reporting bugs. This will help us reproduce and thus solve the problem faster. 11 | 2. If you have created a component for your own application that you think might be useful to include in the connector, 12 | send us a pull request (or a patch / zip containing the source code). We will evaluate it and try to fit it in the 13 | framework. Please make sure code is properly documented using JavaDoc. This helps us to understand what is going on. 14 | 3. If you know of any other way you think you can help us, do not hesitate to send a message to 15 | the [AxonIQ's discussion platform](https://discuss.axoniq.io/). 16 | 17 | ## Code Contributions 18 | 19 | If you're contributing code, please take care of the following: 20 | 21 | ### Contributor Licence Agreement 22 | 23 | To keep everyone out of trouble (both you and us), we require that all contributors (digitally) sign a Contributor 24 | License Agreement. Basically, the agreement says that we may freely use the code you contribute to the Axon Server 25 | Connector for Java, and that we won't hold you liable for any unfortunate side effects that the code may cause. 26 | 27 | To sign the CLA, visit: https://cla-assistant.io/AxonIQ/axonserver-connector-java 28 | 29 | ### Code Style 30 | 31 | We're trying very hard to maintain a consistent style of coding throughout the code base. Think of things like indenting 32 | using 4 spaces, putting opening brackets (the '{') on the same line and putting proper JavaDoc on all non-private 33 | members. 34 | 35 | If you're using IntelliJ IDEA, you can download the code style 36 | definition [here](https://github.com/AxonFramework/AxonFramework/blob/master/axon_code_style.xml). Simply import the XML 37 | file in under "Settings -> Code Style -> Scheme -> Import Scheme". Doing so should make the code style selectable 38 | immediately. 39 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/PersistentStreamSegment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2024. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.axoniq.axonserver.connector.event; 17 | 18 | import io.axoniq.axonserver.connector.ResultStream; 19 | import io.axoniq.axonserver.grpc.event.EventWithToken; 20 | import io.axoniq.axonserver.grpc.streams.PersistentStreamEvent; 21 | 22 | import java.util.Optional; 23 | 24 | /** 25 | * An event stream producing events for one segment of a persistent stream. 26 | * 27 | * @author Marc Gathier 28 | * @since 2024.0.0 29 | */ 30 | public interface PersistentStreamSegment extends ResultStream { 31 | 32 | /** 33 | * Acknowledgement value to indicate that all events processed after a segment is closed 34 | */ 35 | long PENDING_WORK_DONE_MARKER = -45; 36 | 37 | /** 38 | * Registers a callback that will be invoked when Axon Server closes the segment within the persistent 39 | * stream connection. This happens when the number of segments in the persistent stream has changed or when 40 | * Axon Server assigns a segment to another client. 41 | * @param callback the callback to register 42 | */ 43 | void onSegmentClosed(Runnable callback); 44 | 45 | /** 46 | * Sends the last processed token for this segment to Axon Server. Clients may choose to notify each processed event, 47 | * or to only sent progress information after a number of processed events. 48 | * @param token the last processed token for this segment 49 | */ 50 | void acknowledge(long token); 51 | 52 | 53 | void error(String error); 54 | 55 | /** 56 | * Returns the segment number of the stream. 57 | * @return the segment number of this stream 58 | */ 59 | int segment(); 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/transformation/impl/grpc/GrpcEventTransformation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.transformation.impl.grpc; 18 | 19 | import io.axoniq.axonserver.connector.event.transformation.EventTransformation; 20 | import io.axoniq.axonserver.grpc.event.Transformation; 21 | 22 | /** 23 | * Implementation of {@link EventTransformation} that wraps a Proto message representing the transformation, and uses it 24 | * to provide the requested details. 25 | * 26 | * @author Sara Pellegrini 27 | * @since 2023.1.0 28 | */ 29 | public class GrpcEventTransformation implements EventTransformation { 30 | 31 | private final Transformation transformation; 32 | 33 | /** 34 | * Constructs an instance based on the proto message representing the transformation. 35 | * 36 | * @param transformation the proto message containing the details of the transformation. 37 | */ 38 | GrpcEventTransformation(Transformation transformation) { 39 | this.transformation = transformation; 40 | } 41 | 42 | @Override 43 | public String id() { 44 | return transformation.getTransformationId().getId(); 45 | } 46 | 47 | @Override 48 | public String description() { 49 | return transformation.getDescription(); 50 | } 51 | 52 | @Override 53 | public Long lastSequence() { 54 | return transformation.getSequence(); 55 | } 56 | 57 | @Override 58 | public State state() { 59 | return State.valueOf(transformation.getState().name()); 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | return "GrpcEventTransformation{" + 65 | "transformation=" + transformation + 66 | '}'; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/impl/AppendEventsTransactionImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.impl; 18 | 19 | import io.axoniq.axonserver.connector.event.AppendEventsTransaction; 20 | import io.axoniq.axonserver.grpc.event.Confirmation; 21 | import io.axoniq.axonserver.grpc.event.Event; 22 | import io.grpc.Status; 23 | import io.grpc.StatusRuntimeException; 24 | import io.grpc.stub.StreamObserver; 25 | 26 | import java.util.concurrent.CompletableFuture; 27 | 28 | /** 29 | * Implementation of the {@link AppendEventsTransaction} used to append events to the Event Store. 30 | */ 31 | public class AppendEventsTransactionImpl implements AppendEventsTransaction { 32 | 33 | private final StreamObserver stream; 34 | private final CompletableFuture result; 35 | 36 | /** 37 | * Constructs a {@link AppendEventsTransactionImpl} used to append events to the Event Store. 38 | * 39 | * @param stream the {@link StreamObserver} of {@link Event} instances which should be appended 40 | * @param result a {@link CompletableFuture} resolving the {@link Confirmation} of the successful processing of this 41 | * transaction 42 | */ 43 | public AppendEventsTransactionImpl(StreamObserver stream, CompletableFuture result) { 44 | this.stream = stream; 45 | this.result = result; 46 | } 47 | 48 | @Override 49 | public AppendEventsTransaction appendEvent(Event event) { 50 | stream.onNext(event); 51 | return this; 52 | } 53 | 54 | @Override 55 | public CompletableFuture commit() { 56 | stream.onCompleted(); 57 | return result; 58 | } 59 | 60 | @Override 61 | public void rollback() { 62 | stream.onError(new StatusRuntimeException(Status.CANCELLED)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/io/axoniq/axonserver/connector/testutils/MessageFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2021. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.testutils; 18 | 19 | import com.google.protobuf.ByteString; 20 | import io.axoniq.axonserver.grpc.SerializedObject; 21 | import io.axoniq.axonserver.grpc.event.Event; 22 | 23 | import java.util.UUID; 24 | 25 | /** 26 | * Utility class to construct messages, like an {@link Event}. 27 | * 28 | * @author Allard Buijze 29 | */ 30 | public class MessageFactory { 31 | 32 | public static Event createEvent(String payload) { 33 | return createEvent(payload, System.currentTimeMillis()); 34 | } 35 | 36 | public static Event createEvent(String payload, long timestamp) { 37 | return Event.newBuilder().setPayload(SerializedObject.newBuilder() 38 | .setData(ByteString.copyFromUtf8(payload)) 39 | .setType("string")) 40 | .setMessageIdentifier(UUID.randomUUID().toString()) 41 | .setTimestamp(timestamp) 42 | .build(); 43 | } 44 | 45 | public static Event createDomainEvent(String payload, String aggregateIdentifier, long sequence) { 46 | return Event.newBuilder().setPayload(SerializedObject.newBuilder() 47 | .setData(ByteString.copyFromUtf8(payload)) 48 | .setType("string")) 49 | .setAggregateType("Aggregate") 50 | .setAggregateIdentifier(aggregateIdentifier) 51 | .setAggregateSequenceNumber(sequence) 52 | .setMessageIdentifier(UUID.randomUUID().toString()) 53 | .setTimestamp(System.currentTimeMillis()) 54 | .build(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/transformation/event/EventSources.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.transformation.event; 18 | 19 | import io.axoniq.axonserver.connector.event.EventChannel; 20 | import io.axoniq.axonserver.connector.event.transformation.event.impl.TransformableEventIterable; 21 | import io.axoniq.axonserver.connector.event.transformation.event.stream.TokenRangeEvents; 22 | import io.axoniq.axonserver.grpc.event.EventWithToken; 23 | 24 | import java.util.function.Supplier; 25 | 26 | /** 27 | * Provides various ways to get {@link TransformableEventStream}. 28 | * 29 | * @author Sara Pellegrini 30 | * @since 2023.1.0 31 | */ 32 | public interface EventSources { 33 | 34 | /** 35 | * Creates a {@link TransformableEventStream} based on the {@link Iterable} of events. 36 | * 37 | * @param events {@link Iterable} of events 38 | * @return {@link TransformableEventStream} 39 | */ 40 | static TransformableEventStream fromIterable(Iterable events) { 41 | return new TransformableEventIterable(events); 42 | } 43 | 44 | /** 45 | * Creates a {@link TransformableEventStream} based on the {@code eventChannelSupplier} and {@code first} and 46 | * {@code last} event tokens. 47 | * 48 | * @param eventChannelSupplier supplies the {@link EventChannel} 49 | * @param first the first token to be included in the stream, should be -1 to start from the 50 | * beginning 51 | * @param last the last token to be included in the stream 52 | * @return {@link TransformableEventStream} 53 | */ 54 | static TransformableEventStream range(Supplier eventChannelSupplier, long first, long last) { 55 | return EventSources.fromIterable(new TokenRangeEvents(eventChannelSupplier, first, last)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/MessageFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.axoniq.axonserver.connector.ErrorCategory; 20 | import io.axoniq.axonserver.grpc.ErrorMessage; 21 | 22 | /** 23 | * Utility class to build simple messages. 24 | */ 25 | public abstract class MessageFactory { 26 | 27 | private MessageFactory() { 28 | // Utility class 29 | } 30 | 31 | /** 32 | * Build an {@link ErrorMessage}, using the given {@code errorCategory}, {@code client} and {@link Throwable}. 33 | * 34 | * @param errorCategory the {@link ErrorCategory} the constructed {@link ErrorMessage} belongs to 35 | * @param client a {@link String} defining where the constructed {@link ErrorMessage} originates from 36 | * @param t a {@link Throwable} defining the original exception resulting in this {@link ErrorMessage} 37 | * @return an {@link ErrorMessage}, using the given {@code errorCategory}, {@code client} and {@link Throwable} 38 | */ 39 | public static ErrorMessage buildErrorMessage(ErrorCategory errorCategory, String client, Throwable t) { 40 | ErrorMessage.Builder builder = ErrorMessage.newBuilder() 41 | .setLocation(client) 42 | .setErrorCode(errorCategory.errorCode()); 43 | if (t != null) { 44 | builder.setMessage(extractMessage(t)); 45 | builder.addDetails(extractMessage(t)); 46 | while (t.getCause() != null) { 47 | t = t.getCause(); 48 | builder.addDetails(extractMessage(t)); 49 | } 50 | } 51 | return builder.build(); 52 | } 53 | 54 | private static String extractMessage(Throwable t) { 55 | return t.getMessage() == null ? t.getClass().getName() : t.getMessage(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/io/axoniq/axonserver/connector/ResultStreamPublisherTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector; 18 | 19 | import org.reactivestreams.Publisher; 20 | import org.reactivestreams.tck.PublisherVerification; 21 | import org.reactivestreams.tck.TestEnvironment; 22 | import org.testng.annotations.Test; 23 | 24 | /** 25 | * Unit tests for {@link ResultStreamPublisher}. 26 | * 27 | * @author Sara Pellegrini 28 | */ 29 | public class ResultStreamPublisherTest extends PublisherVerification { 30 | 31 | private final TestEnvironment env; 32 | 33 | public ResultStreamPublisherTest() { 34 | this(new TestEnvironment(1_000)); 35 | } 36 | 37 | private ResultStreamPublisherTest(TestEnvironment env) { 38 | super(env); 39 | this.env = env; 40 | } 41 | 42 | @Override 43 | public Publisher createPublisher(long elements) { 44 | return new ResultStreamPublisher<>(() -> new StubResultStream(elements)); 45 | } 46 | 47 | @Override 48 | public Publisher createFailedPublisher() { 49 | return new ResultStreamPublisher<>(FailedResultStream::new); 50 | } 51 | 52 | /** 53 | * Tests the rule 17 of Reactive Stream Specification for Subscription. 54 | * 55 | * @see 56 | * Reactive Stream Specification - Subscription - rule 17 57 | */ 58 | @Test 59 | public void required_spec317_mustSupportACumulativePendingElementCountGreaterThenLongMaxValue() throws Throwable { 60 | final int totalElements = 5; 61 | 62 | activePublisherTest(totalElements, true, pub -> { 63 | final TestEnvironment.ManualSubscriber sub = env.newManualSubscriber(pub); 64 | new Thread(() -> sub.request(Long.MAX_VALUE)).start(); 65 | new Thread(() -> sub.request(Long.MAX_VALUE)).start(); 66 | 67 | sub.nextElements(totalElements); 68 | sub.expectCompletion(); 69 | 70 | try { 71 | env.verifyNoAsyncErrorsNoDelay(); 72 | } finally { 73 | sub.cancel(); 74 | } 75 | }); 76 | } 77 | } -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/transformation/ActiveTransformation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.transformation; 18 | 19 | import java.util.concurrent.CompletableFuture; 20 | 21 | /** 22 | * Class responsible to interact with the active transformation. 23 | * 24 | * @author Sara Pellegrini 25 | * @since 2023.1.0 26 | */ 27 | public interface ActiveTransformation { 28 | 29 | /** 30 | * Accepts a {@link Transformer} to collect the requested changes and to register them into the active 31 | * transformation. 32 | * 33 | * @param transformer the {@link Transformer} used to append the required changes to the active transformation 34 | * @return a {@link CompletableFuture} of the active transformation that completes when all changes have been 35 | * registered. 36 | */ 37 | CompletableFuture transform(Transformer transformer); 38 | 39 | /** 40 | * Requests the transformation to be applied. After this method is invoked, it will not be possible to cancel the 41 | * transformation or to register new changes in it. Axon Server will asynchronously apply the transformation to 42 | * the event store, making the changes effective. 43 | * The returned {@link CompletableFuture} completes when the request to start applying has been received form AS, 44 | * it does not wait the apply process is completed. Depending on the number of transformation actions contained in 45 | * the transformation, the apply process can take a long time before to be completed. 46 | * 47 | * @return a {@link CompletableFuture} of the Event Transformation, that completes only after the start applying 48 | * request has been received by AS. 49 | */ 50 | CompletableFuture startApplying(); 51 | 52 | /** 53 | * Cancels the active transformation. After this method is invoked, it will not be possible to apply the 54 | * transformation or to register new changes in it. 55 | * 56 | * @return a {@link CompletableFuture} of the Event Transformation, that completes only after the cancel request has 57 | * been received by AS. 58 | */ 59 | CompletableFuture cancel(); 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/HeaderAttachingInterceptor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.grpc.CallOptions; 20 | import io.grpc.Channel; 21 | import io.grpc.ClientCall; 22 | import io.grpc.ClientInterceptor; 23 | import io.grpc.ForwardingClientCall; 24 | import io.grpc.Metadata; 25 | import io.grpc.MethodDescriptor; 26 | 27 | /** 28 | * Interceptor around a gRPC request to add header information as metadata to a message. 29 | * 30 | * @param the key and value type of the header this interceptor will attach 31 | * @author Marc Gathier 32 | * @since 4.0 33 | */ 34 | public class HeaderAttachingInterceptor implements ClientInterceptor { 35 | 36 | private final Metadata.Key header; 37 | private final T value; 38 | 39 | /** 40 | * Constructs a {@link HeaderAttachingInterceptor} attaching metadata using the given {@code header} as key, paired 41 | * with the given {@code value} 42 | * 43 | * @param header the {@link Metadata.Key} to attach on intercepted messages 44 | * @param value the object of type {@code T} to attach on intercepted messages 45 | */ 46 | public HeaderAttachingInterceptor(Metadata.Key header, T value) { 47 | this.header = header; 48 | this.value = value; 49 | } 50 | 51 | @Override 52 | public ClientCall interceptCall(MethodDescriptor methodDescriptor, 53 | CallOptions callOptions, 54 | Channel channel) { 55 | ClientCall call = channel.newCall(methodDescriptor, callOptions); 56 | return value == null ? call : new HeaderAttachedCall<>(call); 57 | } 58 | 59 | private class HeaderAttachedCall extends ForwardingClientCall.SimpleForwardingClientCall { 60 | 61 | public HeaderAttachedCall(ClientCall call) { 62 | super(call); 63 | } 64 | 65 | @Override 66 | public void start(Listener responseListener, Metadata headers) { 67 | headers.put(header, value); 68 | super.start(responseListener, headers); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/CloseableReadonlyBuffer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2022. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.axoniq.axonserver.grpc.ErrorMessage; 20 | 21 | import java.util.Optional; 22 | 23 | /** 24 | * A readonly buffer that can be closed from the producing side. 25 | * 26 | * @param the type of messages in this buffer 27 | * @author Milan Savic 28 | * @author Stefan Dragisic 29 | * @author Allard Buijze 30 | * @since 4.6.0 31 | */ 32 | public interface CloseableReadonlyBuffer { 33 | 34 | /** 35 | * Polls the message from this buffer. If the returned {@link Optional} is empty, that doesn't mean that this buffer 36 | * stays empty forever. The {@link #closed()} method should be consolidated to validate this. 37 | * 38 | * @return an {@link Optional} with polled message 39 | */ 40 | Optional poll(); 41 | 42 | /** 43 | * Indicates whether there are messages in the buffer. If this returns {@code false}, that doesn't mean that this 44 | * buffer stays empty forever. The {@link #closed()} method should be consolidated to validate this. 45 | * 46 | * @return {@code true} if buffer is empty, {@code false} otherwise 47 | */ 48 | boolean isEmpty(); 49 | 50 | /** 51 | * Returns the overall capacity of this buffer. 52 | * 53 | * @return the overall capacity of this buffer 54 | */ 55 | int capacity(); 56 | 57 | /** 58 | * Registers an action to be triggered when there is a new message added to the buffer, or the buffer is closed, or 59 | * the buffer is closed with an error. The action can check {@link #poll()}, {@link #closed()}, and {@link #error()} 60 | * to validate this. 61 | * 62 | * @param onAvailable to be invoked when there are changes in this buffer 63 | */ 64 | void onAvailable(Runnable onAvailable); 65 | 66 | /** 67 | * Indicates whether the buffer is closed by the producing side. 68 | * 69 | * @return {@code true} if closed, {@code false} otherwise 70 | */ 71 | boolean closed(); 72 | 73 | /** 74 | * Returns an error from this buffer, if any. 75 | * 76 | * @return an {@link Optional} of an error, if any 77 | */ 78 | Optional error(); 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/command/CommandChannel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.command; 18 | 19 | import io.axoniq.axonserver.connector.Registration; 20 | import io.axoniq.axonserver.grpc.command.Command; 21 | import io.axoniq.axonserver.grpc.command.CommandResponse; 22 | 23 | import java.util.concurrent.CompletableFuture; 24 | import java.util.function.Function; 25 | 26 | /** 27 | * Communication channel with AxonServer for Command related interactions. 28 | */ 29 | public interface CommandChannel { 30 | 31 | /** 32 | * Registers the given {@code handler} to handle incoming commands with given {@code commandNames}, using the given 33 | * {@code loadFactor}. If handlers had already been registered for any of the given command names, this registration 34 | * replaces the existing one for those command names. Other commands remain unaffected. 35 | * 36 | * @param handler the handler to handle incoming commands with 37 | * @param loadFactor the relative load factor for this handler 38 | * @param commandNames the names of the commands to register the handler for 39 | * @return a registration which allows the command handler to be deregistered 40 | */ 41 | Registration registerCommandHandler(Function> handler, 42 | int loadFactor, 43 | String... commandNames); 44 | 45 | /** 46 | * Sends the give Command to AxonServer for routing to an appropriate handler. 47 | * 48 | * @param command the command to send 49 | * @return a CompletableFuture providing the result of command execution 50 | */ 51 | CompletableFuture sendCommand(Command command); 52 | 53 | /** 54 | * Prepares this {@link CommandChannel} to disconnect, by unsubscribing all registered command handlers. Will wait 55 | * with a certain cut off until all acknowledgments of unsubscribing have been received. 56 | *

57 | * This method should be used if a connected client wants to disconnect from AxonServer. 58 | * 59 | * @return a {@link CompletableFuture} of {@link Void} to react when all command handlers have been unsubscribed 60 | */ 61 | CompletableFuture prepareDisconnect(); 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/query/QueryDefinition.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.query; 18 | 19 | import java.lang.reflect.Type; 20 | import java.util.Objects; 21 | 22 | /** 23 | * Definition of a query, constituting out of a {@code queryName} and {@code resultType}. 24 | */ 25 | public class QueryDefinition { 26 | 27 | private final String queryName; 28 | private final String resultType; 29 | 30 | /** 31 | * Construct a {@link QueryDefinition} out of the given {@code queryName} and {@code resultType}. 32 | * 33 | * @param queryName the name of the query 34 | * @param resultType the {@link Type} of result 35 | */ 36 | public QueryDefinition(String queryName, Type resultType) { 37 | this(queryName, resultType.getTypeName()); 38 | } 39 | 40 | /** 41 | * Construct a {@link QueryDefinition} out of the given {@code queryName} and {@code resultType}. 42 | * 43 | * @param queryName the name of the query 44 | * @param resultType the type of the result 45 | */ 46 | public QueryDefinition(String queryName, String resultType) { 47 | this.queryName = queryName; 48 | this.resultType = resultType; 49 | } 50 | 51 | /** 52 | * Return the query name of this definition. 53 | * 54 | * @return the query name of this definition 55 | */ 56 | public String getQueryName() { 57 | return queryName; 58 | } 59 | 60 | /** 61 | * Return the result type of this definition. 62 | * 63 | * @return the result type of this definition 64 | */ 65 | public String getResultType() { 66 | return resultType; 67 | } 68 | 69 | @Override 70 | public boolean equals(Object o) { 71 | if (this == o) { 72 | return true; 73 | } 74 | if (o == null || getClass() != o.getClass()) { 75 | return false; 76 | } 77 | QueryDefinition that = (QueryDefinition) o; 78 | return queryName.equals(that.queryName) && 79 | resultType.equals(that.resultType); 80 | } 81 | 82 | @Override 83 | public int hashCode() { 84 | return Objects.hash(queryName, resultType); 85 | } 86 | 87 | @Override 88 | public String toString() { 89 | return queryName + " : " + resultType; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/ErrorCategory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector; 18 | 19 | import static java.util.Arrays.stream; 20 | 21 | 22 | /** 23 | * Converts an Axon Server Error to the relevant Axon framework exception. 24 | * 25 | * @author Marc Gathier 26 | * @since 4.0 27 | */ 28 | public enum ErrorCategory { 29 | 30 | // Generic errors processing client request 31 | AUTHENTICATION_TOKEN_MISSING("AXONIQ-1000"), 32 | AUTHENTICATION_INVALID_TOKEN("AXONIQ-1001"), 33 | UNSUPPORTED_INSTRUCTION("AXONIQ-1002"), 34 | INSTRUCTION_ACK_ERROR("AXONIQ-1003"), 35 | INSTRUCTION_EXECUTION_ERROR("AXONIQ-1004"), 36 | 37 | //Event publishing errors 38 | INVALID_EVENT_SEQUENCE("AXONIQ-2000"), 39 | NO_EVENT_STORE_MASTER_AVAILABLE("AXONIQ-2100" 40 | ), 41 | EVENT_PAYLOAD_TOO_LARGE( 42 | "AXONIQ-2001" 43 | ), 44 | 45 | //Communication errors 46 | CONNECTION_FAILED("AXONIQ-3001"), 47 | GRPC_MESSAGE_TOO_LARGE("AXONIQ-3002"), 48 | 49 | // Command errors 50 | NO_HANDLER_FOR_COMMAND("AXONIQ-4000"), 51 | COMMAND_EXECUTION_ERROR("AXONIQ-4002"), 52 | COMMAND_DISPATCH_ERROR("AXONIQ-4003"), 53 | CONCURRENCY_EXCEPTION("AXONIQ-4004"), 54 | 55 | //Query errors 56 | NO_HANDLER_FOR_QUERY("AXONIQ-5000"), 57 | QUERY_EXECUTION_ERROR("AXONIQ-5001"), 58 | QUERY_DISPATCH_ERROR("AXONIQ-5002"), 59 | 60 | // Internal errors 61 | DATAFILE_READ_ERROR("AXONIQ-9000"), 62 | INDEX_READ_ERROR("AXONIQ-9001"), 63 | DATAFILE_WRITE_ERROR("AXONIQ-9100"), 64 | INDEX_WRITE_ERROR("AXONIQ-9101"), 65 | DIRECTORY_CREATION_FAILED("AXONIQ-9102"), 66 | VALIDATION_FAILED("AXONIQ-9200"), 67 | TRANSACTION_ROLLED_BACK("AXONIQ-9900"), 68 | 69 | //Default 70 | OTHER("AXONIQ-0001"); 71 | 72 | private final String errorCode; 73 | 74 | /** 75 | * Initializes the ErrorCode using the given {@code code} 76 | * 77 | * @param errorCode the code of the error 78 | */ 79 | ErrorCategory(String errorCode) { 80 | this.errorCode = errorCode; 81 | } 82 | 83 | public static ErrorCategory getFromCode(String code) { 84 | return stream(values()).filter(value -> value.errorCode.equals(code)).findFirst().orElse(OTHER); 85 | } 86 | 87 | public String errorCode() { 88 | return errorCode; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/transformation/event/impl/IterableEventTransformationExecutor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.transformation.event.impl; 18 | 19 | import io.axoniq.axonserver.connector.event.transformation.Appender; 20 | import io.axoniq.axonserver.connector.event.transformation.EventTransformation; 21 | import io.axoniq.axonserver.connector.event.transformation.EventTransformationChannel; 22 | import io.axoniq.axonserver.connector.event.transformation.event.EventTransformationExecutor; 23 | import io.axoniq.axonserver.connector.event.transformation.event.EventTransformer; 24 | import io.axoniq.axonserver.grpc.event.EventWithToken; 25 | 26 | import java.util.concurrent.CompletableFuture; 27 | import java.util.function.Supplier; 28 | 29 | /** 30 | * {@link EventTransformationExecutor} implementation based on an {@link Iterable} of {@link EventWithToken}. 31 | * 32 | * @author Sara Pellegrini 33 | * @since 2023.1.0 34 | */ 35 | public class IterableEventTransformationExecutor implements EventTransformationExecutor { 36 | 37 | private final String transformationDescription; 38 | private final Iterable events; 39 | private final EventTransformer eventTransformer; 40 | 41 | /** 42 | * Constructs an instance based on the specified parameters. 43 | * 44 | * @param transformationDescription the description of the transformation 45 | * @param events the events to be transformed 46 | * @param eventTransformer the transformation function 47 | */ 48 | IterableEventTransformationExecutor(String transformationDescription, 49 | Iterable events, 50 | EventTransformer eventTransformer) { 51 | this.transformationDescription = transformationDescription; 52 | this.events = events; 53 | this.eventTransformer = eventTransformer; 54 | } 55 | 56 | @Override 57 | public CompletableFuture execute(Supplier channelSupplier) { 58 | return channelSupplier.get() 59 | .transform(transformationDescription, this::transform); 60 | } 61 | 62 | private void transform(Appender appender) { 63 | events.forEach(event -> eventTransformer.accept(event, appender)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/transformation/EventTransformation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.transformation; 18 | 19 | /** 20 | * The Event Transformation details. 21 | * 22 | * @author Sara Pellegrini 23 | * @since 2023.1.0 24 | */ 25 | public interface EventTransformation { 26 | 27 | /** 28 | * Returns the identifier of the EventTransformation. 29 | * @return the identifier of the EventTransformation. 30 | */ 31 | String id(); 32 | 33 | /** 34 | * Returns the description of the EventTransformation. 35 | * @return the description of the EventTransformation. 36 | */ 37 | String description(); 38 | 39 | /** 40 | * Each transformation action registered in the transformation has its own incremental sequence. 41 | * An action could represent the deletion of an Event, or its replacement. 42 | * This method returns the sequence of the last transformation action registered in the EventTransformation. 43 | * @return the last sequence of the EventTransformation. 44 | */ 45 | Long lastSequence(); 46 | 47 | /** 48 | * Returns the state of the EventTransformation. 49 | * @return the state of the EventTransformation. 50 | */ 51 | State state(); 52 | 53 | /** 54 | * The state of the transformation 55 | */ 56 | enum State { 57 | 58 | /** 59 | * A transformation is in ACTIVE state when it is still open to accept new transformation actions. 60 | * A new transformation is created in ACTIVE state. 61 | */ 62 | ACTIVE, 63 | 64 | /** 65 | * A transformation is in APPLYING state when the process to apply the transformation actions against the event 66 | * store is in progress. In this state, the transformation cannot accept any further transformation action. 67 | */ 68 | APPLYING, 69 | 70 | /** 71 | * A transformation is in APPLIED state when all the transformation actions registered in it have been applied 72 | * to the event store. 73 | */ 74 | APPLIED, 75 | 76 | /** 77 | * A transformation is in CANCELLED state when it has been discarded before to be applied. In this state, the 78 | * transformation cannot accept any further transformation action, and cannot be applied. 79 | */ 80 | CANCELLED 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/BufferingReplyChannel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2022. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.axoniq.axonserver.connector.ErrorCategory; 20 | import io.axoniq.axonserver.connector.ReplyChannel; 21 | import io.axoniq.axonserver.grpc.ErrorMessage; 22 | 23 | /** 24 | * A {@link ReplyChannel} implementation that uses a given {@link CloseableBuffer buffer} to buffer send, complete and 25 | * complete-with-error. {@code ACK}s and {@code NACK}s are delegated via {@code delegate ReplyChannel}. 26 | * 27 | * @param the type of messages this {@link ReplyChannel} deals with 28 | * @author Milan Savic 29 | * @author Stefan Dragisic 30 | * @author Allard Buijze 31 | * @since 4.6.0 32 | */ 33 | public class BufferingReplyChannel implements ReplyChannel { 34 | 35 | private final ReplyChannel delegate; 36 | private final CloseableBuffer buffer; 37 | 38 | /** 39 | * Instantiates this {@link BufferingReplyChannel} with given {@code delegate} and {@code buffer}. 40 | * 41 | * @param delegate used to delegate {@code ack}s and {@code nack}s 42 | * @param buffer used to buffer sends, completes and completes with error 43 | */ 44 | public BufferingReplyChannel(ReplyChannel delegate, CloseableBuffer buffer) { 45 | this.delegate = delegate; 46 | this.buffer = buffer; 47 | } 48 | 49 | @Override 50 | public void send(T outboundMessage) { 51 | buffer.put(outboundMessage); 52 | } 53 | 54 | @Override 55 | public void sendAck() { 56 | delegate.sendAck(); 57 | } 58 | 59 | @Override 60 | public void sendNack(ErrorMessage errorMessage) { 61 | delegate.sendNack(errorMessage); 62 | } 63 | 64 | @Override 65 | public void complete() { 66 | buffer.close(); 67 | } 68 | 69 | @Override 70 | public void completeWithError(ErrorMessage errorMessage) { 71 | buffer.closeExceptionally(errorMessage); 72 | } 73 | 74 | @Override 75 | public void completeWithError(ErrorCategory errorCategory, String message) { 76 | ErrorMessage error = ErrorMessage.newBuilder() 77 | .setErrorCode(errorCategory.errorCode()) 78 | .setMessage(message) 79 | .build(); 80 | buffer.closeExceptionally(error); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AxonServer Connector for Java 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/io.axoniq/axonserver-connector-java)](https://central.sonatype.com/artifact/io.axoniq/axonserver-connector-java) 4 | ![Build Status](https://github.com/AxonIQ/axonserver-connector-java/workflows/AxonServer%20Connector%20Java/badge.svg?branch=master) 5 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=AxonIQ_axonserver-connector-java&metric=alert_status&token=7f2878de3251f0b89dd985bd2fa6dff72c0a7697)](https://sonarcloud.io/dashboard?id=AxonIQ_axonserver-connector-java) 6 | 7 | The AxonServer Connector for Java serves the purpose of connecting JVM based application 8 | to [AxonServer](https://axoniq.io/product-overview/axon-server). 9 | 10 | As such it aims to provide a clean and clear solution that ties into 11 | the [AxonServer API](https://github.com/AxonIQ/axon-server-api), which is written 12 | in [ProtoBuf](https://developers.google.com/protocol-buffers). It would thus allow a straightforward connection with 13 | AxonServer, without necessarily using [Axon Framework](https://github.com/AxonFramework/AxonFramework). On top of this, 14 | it can be used as a clear starting point to build your own connector to AxonServer in your preferred language. 15 | 16 | For more information on anything Axon, please visit our website, [http://axoniq.io](http://axoniq.io). 17 | 18 | ## Receiving help 19 | 20 | Are you having trouble using this connector, or implementing your own language specific version of it? We'd like to help 21 | you out the best we can! There are a couple of things to consider when you're traversing anything Axon: 22 | 23 | * There is a [forum](https://discuss.axoniq.io/) to support you in the case the reference guide did not sufficiently 24 | answer your question. Axon Framework and Server developers will help out on a best effort basis. Know that any support 25 | from contributors on posted question is very much appreciated on the forum. 26 | * Next to the forum we also monitor Stack Overflow for any questions which are tagged with `axon`. 27 | 28 | ## Feature requests and issue reporting 29 | 30 | We use GitHub's [issue tracking system](https://github.com/AxonIQ/axonserver-connector-java/issues) for new feature 31 | request, framework enhancements and bugs. Prior to filing an issue, please verify that it's not already reported by 32 | someone else. 33 | 34 | When filing bugs: 35 | 36 | * A description of your setup and what's happening helps us to figure out what the issue might be 37 | * Do not forget to provide the framework version you're using 38 | * If possible, share a stack trace, using the Markdown semantic ``` 39 | 40 | When filing features: 41 | 42 | * A description of the envisioned addition or enhancement should be provided 43 | * (Pseudo-)Code snippets showing what it might look like help us understand your suggestion better 44 | * If you have any thoughts on where to plug this into the framework, that would be very helpful too 45 | * Lastly, we value contributions to the framework highly. So please provide a Pull Request as well! 46 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/AsyncRegistration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.axoniq.axonserver.connector.AxonServerException; 20 | import io.axoniq.axonserver.connector.ErrorCategory; 21 | import io.axoniq.axonserver.connector.Registration; 22 | 23 | import java.util.concurrent.CompletableFuture; 24 | import java.util.concurrent.ExecutionException; 25 | import java.util.concurrent.TimeUnit; 26 | import java.util.concurrent.TimeoutException; 27 | import java.util.function.Supplier; 28 | 29 | /** 30 | * Asynchronous implementation of the {@link Registration}. 31 | */ 32 | public class AsyncRegistration implements Registration { 33 | 34 | private final CompletableFuture requestAck; 35 | private final Supplier> cancelAction; 36 | 37 | /** 38 | * Construct a {@link AsyncRegistration}, using the given {@code requestAck} on {@link #awaitAck(long, TimeUnit)} 39 | * and {@link #onAck(Runnable)}, and the given {@code cancelAction} on {@link #cancel()}. 40 | * 41 | * @param requestAck the {@link CompletableFuture} to wait for on #awaitAck(long, TimeUnit) 42 | * @param cancelAction the {@link Supplier} of the {@link CompletableFuture} to retrieve on {@link #cancel()} 43 | */ 44 | public AsyncRegistration(CompletableFuture requestAck, Supplier> cancelAction) { 45 | this.requestAck = requestAck; 46 | this.cancelAction = cancelAction; 47 | } 48 | 49 | @Override 50 | public CompletableFuture cancel() { 51 | return cancelAction.get(); 52 | } 53 | 54 | @Override 55 | public Registration awaitAck(long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { 56 | try { 57 | requestAck.get(timeout, unit); 58 | } catch (ExecutionException e) { 59 | if (e.getCause() instanceof AxonServerException) { 60 | throw (AxonServerException) e.getCause(); 61 | } else { 62 | throw new AxonServerException( 63 | ErrorCategory.INSTRUCTION_ACK_ERROR, "An instruction returned a failed acknowledgement", "", e 64 | ); 65 | } 66 | } 67 | return this; 68 | } 69 | 70 | @Override 71 | public Registration onAck(Runnable runnable) { 72 | requestAck.thenRun(runnable); 73 | return this; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/GrpcBufferingInterceptor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.grpc.CallOptions; 20 | import io.grpc.Channel; 21 | import io.grpc.ClientCall; 22 | import io.grpc.ClientInterceptor; 23 | import io.grpc.ForwardingClientCall; 24 | import io.grpc.Metadata; 25 | import io.grpc.MethodDescriptor; 26 | 27 | /** 28 | * Interceptor that immediately requests a number of messages from the server, to increase the flow of messages. 29 | *

30 | * As an additional message is requested by default each time a message is received, setting an initial request amount 31 | * will allow that number of messages to be "in transit" before the server stops sending more. 32 | */ 33 | public class GrpcBufferingInterceptor implements ClientInterceptor { 34 | 35 | private final int additionalBuffer; 36 | 37 | /** 38 | * Initialize the interceptor to ask for {@code additionalBuffer} amount of messages from the server. 39 | * 40 | * @param additionalBuffer the number of messages the server may send before waiting for permits to be renewed 41 | */ 42 | public GrpcBufferingInterceptor(int additionalBuffer) { 43 | this.additionalBuffer = additionalBuffer; 44 | } 45 | 46 | @Override 47 | public ClientCall interceptCall(MethodDescriptor method, 48 | CallOptions callOptions, 49 | Channel next) { 50 | ClientCall call = next.newCall(method, callOptions); 51 | if (additionalBuffer == 0 || method.getType().serverSendsOneMessage()) { 52 | return call; 53 | } 54 | return new AdditionalMessageRequestingCall<>(call, additionalBuffer); 55 | } 56 | 57 | private static class AdditionalMessageRequestingCall 58 | extends ForwardingClientCall.SimpleForwardingClientCall { 59 | 60 | private final int additionalBuffer; 61 | 62 | public AdditionalMessageRequestingCall(ClientCall call, int additionalBuffer) { 63 | super(call); 64 | this.additionalBuffer = additionalBuffer; 65 | } 66 | 67 | @Override 68 | public void start(Listener responseListener, Metadata headers) { 69 | super.start(responseListener, headers); 70 | request(additionalBuffer); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/io/axoniq/axonserver/connector/impl/BufferingReplyChannelTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2022. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.axoniq.axonserver.connector.ErrorCategory; 20 | import io.axoniq.axonserver.connector.ReplyChannel; 21 | import io.axoniq.axonserver.grpc.ErrorMessage; 22 | import org.junit.jupiter.api.*; 23 | 24 | import static org.mockito.Mockito.*; 25 | 26 | /** 27 | * Tests for the {@link BufferingReplyChannel}. 28 | */ 29 | class BufferingReplyChannelTest { 30 | 31 | private BufferingReplyChannel bufferingReplyChannel; 32 | private ReplyChannel delegate; 33 | private CloseableBuffer buffer; 34 | 35 | @BeforeEach 36 | void setUp() { 37 | delegate = mock(ReplyChannel.class); 38 | buffer = mock(CloseableBuffer.class); 39 | bufferingReplyChannel = new BufferingReplyChannel<>(delegate, buffer); 40 | } 41 | 42 | @Test 43 | void bufferSend() { 44 | bufferingReplyChannel.send("message"); 45 | verify(buffer).put("message"); 46 | verifyNoInteractions(delegate); 47 | } 48 | 49 | @Test 50 | void bufferComplete() { 51 | bufferingReplyChannel.complete(); 52 | verify(buffer).close(); 53 | verifyNoInteractions(delegate); 54 | } 55 | 56 | @Test 57 | void bufferCompleteWithError() { 58 | ErrorMessage errorMessage = ErrorMessage.getDefaultInstance(); 59 | bufferingReplyChannel.completeWithError(errorMessage); 60 | verify(buffer).closeExceptionally(errorMessage); 61 | 62 | bufferingReplyChannel.completeWithError(ErrorCategory.OTHER, "msg"); 63 | verify(buffer).closeExceptionally(ErrorMessage.newBuilder() 64 | .setErrorCode(ErrorCategory.OTHER.errorCode()) 65 | .setMessage("msg") 66 | .build()); 67 | 68 | verifyNoInteractions(delegate); 69 | } 70 | 71 | @Test 72 | void delegateAck() { 73 | bufferingReplyChannel.sendAck(); 74 | verify(delegate).sendAck(); 75 | verifyNoInteractions(buffer); 76 | } 77 | 78 | @Test 79 | void delegateNack() { 80 | ErrorMessage errorMessage = ErrorMessage.getDefaultInstance(); 81 | bufferingReplyChannel.sendNack(errorMessage); 82 | verify(delegate).sendNack(errorMessage); 83 | verifyNoInteractions(buffer); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/FutureStreamObserver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.grpc.stub.StreamObserver; 20 | 21 | import java.util.concurrent.CompletableFuture; 22 | 23 | /** 24 | * An implementation of both a {@link CompletableFuture} and {@link StreamObserver}. This future will complete 25 | * successfully on {@link #onNext(Object)} or on {@link #onCompleted()} (granted that the given {@code 26 | * valueWhenNoResult} is not of type {@link Throwable}). On {@link #onError(Throwable)}, this future will {@link 27 | * #completeExceptionally(Throwable)}. 28 | * 29 | * @param the type of result this {@link CompletableFuture} and {@link StreamObserver} resolves 30 | */ 31 | public class FutureStreamObserver extends CompletableFuture implements StreamObserver { 32 | 33 | private final Object valueWhenNoResult; 34 | 35 | /** 36 | * Construct a {@link FutureStreamObserver}, using the given {@code valueWhenNoResult} if no result is returned. 37 | * 38 | * @param valueWhenNoResult the object of type {@code T} to return if nothing is returned by this {@link 39 | * StreamObserver} 40 | */ 41 | public FutureStreamObserver(T valueWhenNoResult) { 42 | this.valueWhenNoResult = valueWhenNoResult; 43 | } 44 | 45 | /** 46 | * Construct a {@link FutureStreamObserver}, using the given {@code valueWhenNoResult} if no result is returned. 47 | * This future will complete exceptionally when the {@code valueWhenNoResult} is used. 48 | * 49 | * @param valueWhenNoResult the {@link Throwable} to return if nothing is returned by this {@link StreamObserver} 50 | */ 51 | public FutureStreamObserver(Throwable valueWhenNoResult) { 52 | this.valueWhenNoResult = valueWhenNoResult; 53 | } 54 | 55 | @Override 56 | public void onNext(T value) { 57 | complete(value); 58 | } 59 | 60 | @Override 61 | public void onError(Throwable t) { 62 | if (!isDone()) { 63 | completeExceptionally(t); 64 | } 65 | } 66 | 67 | @Override 68 | public void onCompleted() { 69 | if (!isDone()) { 70 | if (valueWhenNoResult instanceof Throwable) { 71 | completeExceptionally((Throwable) valueWhenNoResult); 72 | } else { 73 | //noinspection unchecked 74 | complete((T) valueWhenNoResult); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/ServerAddress.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import java.util.Objects; 20 | 21 | /** 22 | * Definition of an AxonServer address, defining the {@link #getGrpcPort()} and {@link #getHostName()}. 23 | */ 24 | public class ServerAddress { 25 | 26 | /** 27 | * A default {@link ServerAddress}, using {@link #getHostName() host} {@code "localhost"} and 28 | * {@link #getGrpcPort() gRPC port} {@code 8124}. 29 | */ 30 | public static final ServerAddress DEFAULT = new ServerAddress(); 31 | 32 | private final int grpcPort; 33 | private final String host; 34 | 35 | /** 36 | * Create a {@link ServerAddress} with the default host {@code localhost} and port number {@code 8124}. 37 | */ 38 | public ServerAddress() { 39 | this("localhost"); 40 | } 41 | 42 | /** 43 | * Create a {@link ServerAddress} with the given {@code host} and port number {@code 8124}. 44 | */ 45 | public ServerAddress(String host) { 46 | this(host, 8124); 47 | } 48 | 49 | /** 50 | * Create a {@link ServerAddress} with the given {@code host} and given {@code port} number. 51 | */ 52 | public ServerAddress(String host, int grpcPort) { 53 | this.grpcPort = grpcPort; 54 | this.host = host; 55 | } 56 | 57 | /** 58 | * Return the gRPR port defined in this {@link ServerAddress} instance. 59 | * 60 | * @return the gRPR port defined in this {@link ServerAddress} instance 61 | */ 62 | public int getGrpcPort() { 63 | return grpcPort; 64 | } 65 | 66 | /** 67 | * Return the host name defined in this {@link ServerAddress} instance. 68 | * 69 | * @return the host name defined in this {@link ServerAddress} instance 70 | */ 71 | public String getHostName() { 72 | return host; 73 | } 74 | 75 | @Override 76 | public boolean equals(Object o) { 77 | if (this == o) { 78 | return true; 79 | } 80 | if (o == null || getClass() != o.getClass()) { 81 | return false; 82 | } 83 | ServerAddress that = (ServerAddress) o; 84 | return grpcPort == that.grpcPort && 85 | host.equals(that.host); 86 | } 87 | 88 | @Override 89 | public int hashCode() { 90 | return Objects.hash(grpcPort, host); 91 | } 92 | 93 | @Override 94 | public String toString() { 95 | return host + ":" + grpcPort; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/test/java/io/axoniq/axonserver/connector/command/CommandChannelTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.command; 18 | 19 | import io.axoniq.axonserver.connector.AxonServerConnection; 20 | import io.axoniq.axonserver.connector.AxonServerConnectionFactory; 21 | import io.axoniq.axonserver.connector.Registration; 22 | import io.axoniq.axonserver.connector.impl.ServerAddress; 23 | import io.axoniq.axonserver.grpc.command.CommandResponse; 24 | import org.junit.jupiter.api.AfterEach; 25 | import org.junit.jupiter.api.BeforeEach; 26 | import org.junit.jupiter.api.Test; 27 | 28 | import java.util.concurrent.CompletableFuture; 29 | import java.util.concurrent.TimeUnit; 30 | 31 | import static io.axoniq.axonserver.connector.impl.ObjectUtils.silently; 32 | import static io.axoniq.axonserver.connector.testutils.AssertUtils.assertWithin; 33 | import static org.junit.jupiter.api.Assertions.assertFalse; 34 | import static org.junit.jupiter.api.Assertions.assertNotNull; 35 | import static org.junit.jupiter.api.Assertions.assertTrue; 36 | 37 | class CommandChannelTest { 38 | 39 | private AxonServerConnectionFactory connectionFactory1; 40 | private AxonServerConnection connection1; 41 | 42 | @BeforeEach 43 | void setUp() { 44 | connectionFactory1 = AxonServerConnectionFactory.forClient(getClass().getSimpleName(), 45 | "client1") 46 | .routingServers(new ServerAddress("127:0.0.1")) 47 | .forceReconnectViaRoutingServers(false) 48 | .reconnectInterval(500, TimeUnit.MILLISECONDS) 49 | .build(); 50 | connection1 = connectionFactory1.connect("default"); 51 | } 52 | 53 | @AfterEach 54 | void tearDown() { 55 | silently(connectionFactory1, AxonServerConnectionFactory::shutdown); 56 | } 57 | 58 | @Test 59 | void testSubscribeWithMalformedUrl() throws InterruptedException { 60 | CommandChannel commandChannel = connection1.commandChannel(); 61 | 62 | assertFalse(connection1.isConnected()); 63 | assertWithin(1, TimeUnit.SECONDS, ()-> assertTrue(connection1.isConnectionFailed())); 64 | 65 | Registration result = commandChannel 66 | .registerCommandHandler(r -> CompletableFuture.completedFuture(CommandResponse.getDefaultInstance()), 100, "test"); 67 | assertNotNull(result); 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/query/SubscriptionQueryResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.query; 18 | 19 | import io.axoniq.axonserver.connector.ResultStream; 20 | import io.axoniq.axonserver.grpc.query.QueryResponse; 21 | import io.axoniq.axonserver.grpc.query.QueryUpdate; 22 | 23 | import java.util.concurrent.CompletableFuture; 24 | 25 | /** 26 | * Interface describing the results of a subscription query. This type of query consists of an initial response, 27 | * representing the state of a projection at the start of the query, and a stream of updates which represent the updates 28 | * to the model since the query started. 29 | */ 30 | public interface SubscriptionQueryResult { 31 | 32 | /** 33 | * Returns a CompletableFuture that completes when the initial result of the query is available. If an error 34 | * occurred while querying, the CompletableFuture completes exceptionally. 35 | *

36 | * Invoking this method will send a request for the initial result, if that hasn't been requested before. Subsequent 37 | * invocations of this method will return the same CompletableFuture instance. 38 | *

39 | * Note that calling this method will not prevent the {@link #initialResults()} call from retrieving a new set 40 | * of initial results, or vice versa. 41 | * 42 | * @return a CompletableFuture that completes with the initial result of the subscription query 43 | * @deprecated in favor of {@link #initialResults()}, which returns a stream of results. 44 | */ 45 | @Deprecated(since = "2025.2.0", forRemoval = true) 46 | CompletableFuture initialResult(); 47 | 48 | /** 49 | * Returns a ResultStream that represent the initial result of the subscription query. 50 | *

51 | * Invoking this method will send a request for the initial result if that hasn't been requested before. Further 52 | * invocations of this method will return the same ResultStream instance. 53 | * 54 | * @return a ResultStream that provides the initial result of the subscription query 55 | */ 56 | ResultStream initialResults(); 57 | 58 | /** 59 | * Returns the stream of updates to the queried projection. The stream can be read in a blocking and non-blocking 60 | * fashion, as desired by downstream processes. 61 | *

62 | * Multiple invocation of this method will return the same stream instance. 63 | * 64 | * @return a stream of updates 65 | */ 66 | ResultStream updates(); 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/transformation/EventTransformationChannel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.transformation; 18 | 19 | import java.util.concurrent.CompletableFuture; 20 | 21 | /** 22 | * Communication channel for Event Transformation related interactions with AxonServer. 23 | * 24 | * @author Sara Pellegrini 25 | * @since 2023.1.0 26 | */ 27 | public interface EventTransformationChannel { 28 | 29 | /** 30 | * Returns the completable future of the all transformations. 31 | * 32 | * @return the completable future of the all transformations. 33 | */ 34 | CompletableFuture> transformations(); 35 | 36 | /** 37 | * Returns the completable future of the active transformation. 38 | * 39 | * @return The completable future of the active transformation. 40 | */ 41 | CompletableFuture activeTransformation(); 42 | 43 | /** 44 | * Starts a new transformation to append events change actions. 45 | * It 46 | * 47 | * @param description the description of the transformation to start. 48 | * @return The completable future of the active transformation reference onto which to register events 49 | * transformation actions. 50 | */ 51 | CompletableFuture newTransformation(String description); 52 | 53 | /** 54 | * Starts the compaction of the event store. The invocation of this method requests Axon Server to permanently 55 | * delete all the obsoleted version of the event store, in order to save storage space. 56 | * 57 | * @return a CompletableFuture that completes when the compaction request has been received by Axon Server. 58 | */ 59 | CompletableFuture startCompacting(); 60 | 61 | /** 62 | * This method conveniently composes the creation of a new transformation, the registration of all required changes 63 | * and the request to apply them to the event store, in a single invocation. 64 | * 65 | * @param description the description of the transformation 66 | * @param transformer the transformer used to register the required changes 67 | * @return a CompletableFuture of the EventTransformation 68 | */ 69 | default CompletableFuture transform(String description, Transformer transformer) { 70 | return newTransformation(description) 71 | .thenCompose(transformation -> transformation.transform(transformer)) 72 | .thenCompose(ActiveTransformation::startApplying); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/Registration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector; 18 | 19 | import java.util.concurrent.CompletableFuture; 20 | import java.util.concurrent.TimeUnit; 21 | import java.util.concurrent.TimeoutException; 22 | 23 | /** 24 | * Interface describing an instruction to perform a registration, which can be cancelled. 25 | */ 26 | @FunctionalInterface 27 | public interface Registration { 28 | 29 | /** 30 | * Cancel the registration from which this instance was returned. Does nothing if the registration has already been 31 | * cancelled, or when the registration was undone by another mechanism (such as a new registration overriding this 32 | * one). 33 | * 34 | * @return a {@link CompletableFuture} of {@link Void} to react when {@code this} {@link Registration} has been 35 | * canceled 36 | */ 37 | CompletableFuture cancel(); 38 | 39 | /** 40 | * Wait for the acknowledgement of the original instruction {@code this} {@link Registration} corresponds to. Allows 41 | * for the addition of further logic to {@code this Registration}, like invoking {@link #onAck(Runnable)} for 42 | * example. 43 | * 44 | * @param timeout the duration to wait until the operation has been acknowledged 45 | * @param unit the {@link TimeUnit} for the given {@code timeout} to wait until the operation has been 46 | * acknowledged 47 | * @return {@code this} {@link Registration} to support a fluent API 48 | * @throws TimeoutException is thrown when the given {@code timeout} and {@code unit} is surpassed 49 | * @throws InterruptedException is thrown when the thread waiting for the acknowledgement is interrupted 50 | */ 51 | default Registration awaitAck(long timeout, TimeUnit unit) throws TimeoutException, InterruptedException { 52 | return this; 53 | } 54 | 55 | /** 56 | * Registers the given {@code runnable} to {@code this} {@link Registration} to be executed when the acknowledgement 57 | * of {@code this} {@link Registration} is received. Allows for the addition of further logic to {@code this 58 | * Registration}, like invoking {@link #awaitAck(long, TimeUnit)} for example. 59 | * 60 | * @param runnable the {@link Runnable} to execute when the acknowledgement of {@code this} {@link Registration} is 61 | * received 62 | * @return {@code this} {@link Registration} to support a fluent API 63 | */ 64 | default Registration onAck(Runnable runnable) { 65 | runnable.run(); 66 | return this; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/io/axoniq/axonserver/connector/control/FakeProcessorInstructionHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2022. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.control; 18 | 19 | import java.util.concurrent.CompletableFuture; 20 | 21 | /** 22 | * Fake implementation of {@link ProcessorInstructionHandler} that simulates a client processing the instruction. 23 | * 24 | * @author Marc Gathier 25 | */ 26 | public class FakeProcessorInstructionHandler implements ProcessorInstructionHandler { 27 | 28 | private final CompletableFuture voidCompletableFuture = new CompletableFuture<>(); 29 | private final CompletableFuture booleanCompletableFuture = new CompletableFuture<>(); 30 | 31 | public FakeProcessorInstructionHandler() { 32 | } 33 | 34 | @Override 35 | public CompletableFuture releaseSegment(int segmentId) { 36 | return booleanCompletableFuture; 37 | } 38 | 39 | @Override 40 | public CompletableFuture splitSegment(int segmentId) { 41 | return booleanCompletableFuture; 42 | } 43 | 44 | @Override 45 | public CompletableFuture mergeSegment(int segmentId) { 46 | return booleanCompletableFuture; 47 | } 48 | 49 | @Override 50 | public CompletableFuture pauseProcessor() { 51 | return voidCompletableFuture; 52 | } 53 | 54 | @Override 55 | public CompletableFuture startProcessor() { 56 | return voidCompletableFuture; 57 | } 58 | 59 | /** 60 | * Simulates a successful execution of the instruction. 61 | */ 62 | public void performSuccessfully() { 63 | voidCompletableFuture.complete(null); 64 | booleanCompletableFuture.complete(true); 65 | } 66 | 67 | /** 68 | * Simulates a failed execution of the instruction. 69 | * The operation failed because it is not possible to perform it, the completable future is successfully completed 70 | * with false. 71 | */ 72 | public void performFailing() { 73 | voidCompletableFuture.completeExceptionally(new RuntimeException("Failed while performing action")); 74 | booleanCompletableFuture.complete(false); 75 | } 76 | 77 | /** 78 | * Simulates a failed execution of the instruction. 79 | * The operation failed with an exception and the completable future completes exceptionally. 80 | * 81 | * @param error the exception thrown during the instruction execution. 82 | */ 83 | public void completeExceptionally(Throwable error) { 84 | voidCompletableFuture.completeExceptionally(error); 85 | booleanCompletableFuture.completeExceptionally(error); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/ReconnectConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import java.util.concurrent.TimeUnit; 20 | 21 | /** 22 | * Container of configuration variables used when reconnecting to a channel. 23 | */ 24 | public class ReconnectConfiguration { 25 | 26 | private final long connectTimeout; 27 | private final long reconnectInterval; 28 | private final boolean forcePlatformReconnect; 29 | private final TimeUnit timeUnit; 30 | 31 | /** 32 | * Construct a new {@link ReconnectConfiguration}. 33 | * 34 | * @param connectTimeout a {@code long} defining the connect timeout 35 | * @param reconnectInterval a {@code long} defining the interval when to reconnect 36 | * @param forcePlatformReconnect a {@code boolean} defining whether a platform reconnect should be enforced 37 | * @param timeUnit the {@link TimeUnit} used for both the {@code connectTimeout} and {@code 38 | * reconnectInterval} 39 | */ 40 | public ReconnectConfiguration(long connectTimeout, 41 | long reconnectInterval, 42 | boolean forcePlatformReconnect, 43 | TimeUnit timeUnit) { 44 | this.connectTimeout = connectTimeout; 45 | this.reconnectInterval = reconnectInterval; 46 | this.forcePlatformReconnect = forcePlatformReconnect; 47 | this.timeUnit = timeUnit; 48 | } 49 | 50 | /** 51 | * Return the configured connect timeout. 52 | * 53 | * @return the configured connect timeout 54 | */ 55 | public long getConnectTimeout() { 56 | return connectTimeout; 57 | } 58 | 59 | /** 60 | * Return the configured reconnect timeout. 61 | * 62 | * @return the configured reconnect timeout 63 | */ 64 | public long getReconnectInterval() { 65 | return reconnectInterval; 66 | } 67 | 68 | /** 69 | * Return whether a platform reconnect should be enforced. 70 | * 71 | * @return whether a platform reconnect should be enforced 72 | */ 73 | public boolean isForcePlatformReconnect() { 74 | return forcePlatformReconnect; 75 | } 76 | 77 | /** 78 | * Return the configured {@link TimeUnit} used by {@link #getConnectTimeout()} and {@link #getReconnectInterval()}. 79 | * 80 | * @return the configured {@link TimeUnit} used by {@link #getConnectTimeout()} and {@link #getReconnectInterval()} 81 | */ 82 | public TimeUnit getTimeUnit() { 83 | return timeUnit; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/control/ProcessorInstructionHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.control; 18 | 19 | import java.util.concurrent.CompletableFuture; 20 | 21 | /** 22 | * Interface describing the instructions AxonServer can send for Event Processor management. 23 | */ 24 | public interface ProcessorInstructionHandler { 25 | 26 | /** 27 | * Instruction to release the segment with given {@code segmentId}. If the local node doesn't have the given segment 28 | * claimed, it should take precautions to prevent claiming it immediately. 29 | *

30 | * The return future must be completed with a {@code true} after the segment has been successfully released. When 31 | * failing to release the segment, the CompletableFuture must complete with either {@code false} or exceptionally. 32 | * 33 | * @param segmentId the id of the segment to release 34 | * @return a CompletableFuture that completes to true or false, depending on the successful release of the segment 35 | */ 36 | CompletableFuture releaseSegment(int segmentId); 37 | 38 | /** 39 | * Instruction to split the segment with given {@code segmentId}. If the local node doesn't have a claim on the 40 | * given segment, it must try to claim it. 41 | * 42 | * @param segmentId the identifier of the segment to split 43 | * @return a CompletableFuture that completes to true or false, depending on the successful split of the segment 44 | */ 45 | CompletableFuture splitSegment(int segmentId); 46 | 47 | /** 48 | * Instruction to merge the segment with given {@code segmentId} with its counterpart. If the local node doesn't 49 | * have a claim on the segment with given {@code segmentId} or its counterpart, then it must attempt to claim both. 50 | * 51 | * @param segmentId the identifier of the segment to merge 52 | * @return a CompletableFuture that completes to true or false, depending on the successful merge of the segment 53 | */ 54 | CompletableFuture mergeSegment(int segmentId); 55 | 56 | /** 57 | * Instruction to pause the processor for which this handler was registered. All claims for segments by the 58 | * processor must be released. 59 | * 60 | * @return a CompletableFuture that completes when the processor has been paused. 61 | */ 62 | CompletableFuture pauseProcessor(); 63 | 64 | /** 65 | * Instruction to start the processor for which this handler was registered. 66 | * 67 | * @return a CompletableFuture that completes when the processor has been started. 68 | */ 69 | CompletableFuture startProcessor(); 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/SnapshotChannel.java: -------------------------------------------------------------------------------- 1 | package io.axoniq.axonserver.connector.event; 2 | 3 | import io.axoniq.axonserver.connector.ResultStream; 4 | import io.axoniq.axonserver.grpc.event.dcb.AddSnapshotRequest; 5 | import io.axoniq.axonserver.grpc.event.dcb.AddSnapshotResponse; 6 | import io.axoniq.axonserver.grpc.event.dcb.DeleteSnapshotsRequest; 7 | import io.axoniq.axonserver.grpc.event.dcb.DeleteSnapshotsResponse; 8 | import io.axoniq.axonserver.grpc.event.dcb.GetLastSnapshotRequest; 9 | import io.axoniq.axonserver.grpc.event.dcb.GetLastSnapshotResponse; 10 | import io.axoniq.axonserver.grpc.event.dcb.ListSnapshotsRequest; 11 | import io.axoniq.axonserver.grpc.event.dcb.ListSnapshotsResponse; 12 | 13 | import java.util.concurrent.CompletableFuture; 14 | 15 | /** 16 | * Communication channel for Snapshot-related interactions with AxonServer. This interface provides operations for 17 | * managing snapshots, including adding, deleting, listing, and retrieving snapshots. 18 | */ 19 | public interface SnapshotChannel { 20 | 21 | /** 22 | * Adds a snapshot to the store. If a snapshot with the same key and sequence already exists, it will be replaced. 23 | * When the prune flag is set to true, all snapshots with the same key and a sequence number less than the provided 24 | * sequence will be removed. 25 | * 26 | * @param request The request containing the snapshot to add, including key, sequence, prune flag, and snapshot 27 | * data 28 | * @return A CompletableFuture that resolves to an AddSnapshotResponse when the operation completes 29 | */ 30 | CompletableFuture addSnapshot(AddSnapshotRequest request); 31 | 32 | /** 33 | * Deletes snapshots within a specified sequence range for a given key. The range is inclusive at the lower bound 34 | * (fromSequence) and exclusive at the upper bound (toSequence). 35 | * 36 | * @param request The request containing the key and sequence range to delete 37 | * @return A CompletableFuture that resolves to a DeleteSnapshotsResponse when the operation completes 38 | */ 39 | CompletableFuture deleteSnapshots(DeleteSnapshotsRequest request); 40 | 41 | /** 42 | * Lists snapshots for a given key within a specified sequence range. The range is inclusive at the lower bound 43 | * (fromSequence) and exclusive at the upper bound (toSequence). Snapshots are returned in ascending order by 44 | * sequence number. 45 | * 46 | * @param request The request containing the key and sequence range to list 47 | * @return A ResultStream of ListSnapshotsResponse objects containing the matching snapshots 48 | */ 49 | ResultStream listSnapshots(ListSnapshotsRequest request); 50 | 51 | /** 52 | * Retrieves the snapshot with the highest sequence number for a given key. If no snapshots exist for the key, the 53 | * response will not contain a snapshot. 54 | * 55 | * @param request The request containing the key to get the last snapshot for 56 | * @return A CompletableFuture that resolves to a GetLastSnapshotResponse when the operation completes 57 | */ 58 | CompletableFuture getLastSnapshot(GetLastSnapshotRequest request); 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/io/axoniq/axonserver/connector/impl/SynchronizedRequestStreamTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.grpc.stub.ClientCallStreamObserver; 20 | import org.junit.jupiter.api.AfterEach; 21 | import org.junit.jupiter.api.BeforeEach; 22 | import org.junit.jupiter.api.RepeatedTest; 23 | import org.mockito.InOrder; 24 | import org.mockito.Mockito; 25 | 26 | import java.util.concurrent.ExecutorService; 27 | import java.util.concurrent.Executors; 28 | import java.util.concurrent.TimeUnit; 29 | 30 | import static org.mockito.ArgumentMatchers.*; 31 | import static org.mockito.Mockito.atLeastOnce; 32 | import static org.mockito.Mockito.mock; 33 | 34 | class SynchronizedRequestStreamTest { 35 | 36 | private ExecutorService executorService; 37 | private ClientCallStreamObserver mockObserver; 38 | private SynchronizedRequestStream testSubject; 39 | 40 | @BeforeEach 41 | void setUp() { 42 | executorService = Executors.newFixedThreadPool(3); 43 | mockObserver = mock(ClientCallStreamObserver.class); 44 | testSubject = new SynchronizedRequestStream<>(mockObserver); 45 | } 46 | 47 | @AfterEach 48 | void tearDown() { 49 | executorService.shutdownNow(); 50 | } 51 | 52 | @RepeatedTest(10) 53 | void testConcurrentWriteAndCompleteSuppressesExceptions() throws InterruptedException { 54 | for (int i = 0; i < 10; i++) { 55 | String message = "Message " + i; 56 | executorService.submit(() -> testSubject.onNext(message)); 57 | if (i == 5) { 58 | executorService.submit(() -> testSubject.onCompleted()); 59 | } 60 | } 61 | 62 | executorService.shutdown(); 63 | executorService.awaitTermination(5, TimeUnit.SECONDS); 64 | 65 | InOrder inOrder = Mockito.inOrder(mockObserver); 66 | inOrder.verify(mockObserver, atLeastOnce()).onNext(anyString()); 67 | inOrder.verify(mockObserver).onCompleted(); 68 | inOrder.verifyNoMoreInteractions(); 69 | } 70 | 71 | @RepeatedTest(10) 72 | void testConcurrentWriteAndCompleteWithErrorSuppressesExceptions() throws InterruptedException { 73 | for (int i = 0; i < 10; i++) { 74 | String message = "Message " + i; 75 | executorService.submit(() -> testSubject.onNext(message)); 76 | if (i == 5) { 77 | executorService.submit(() -> testSubject.onError(new RuntimeException("Faking an exception"))); 78 | } 79 | } 80 | 81 | executorService.shutdown(); 82 | executorService.awaitTermination(5, TimeUnit.SECONDS); 83 | 84 | InOrder inOrder = Mockito.inOrder(mockObserver); 85 | inOrder.verify(mockObserver, atLeastOnce()).onNext(anyString()); 86 | inOrder.verify(mockObserver).onError(isA(RuntimeException.class)); 87 | inOrder.verifyNoMoreInteractions(); 88 | } 89 | } -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/transformation/impl/DefaultActiveTransformation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.transformation.impl; 18 | 19 | import io.axoniq.axonserver.connector.event.transformation.ActiveTransformation; 20 | import io.axoniq.axonserver.connector.event.transformation.EventTransformation; 21 | import io.axoniq.axonserver.connector.event.transformation.Transformer; 22 | import io.axoniq.axonserver.connector.event.transformation.impl.EventTransformationService.TransformationStream; 23 | 24 | import java.util.concurrent.CompletableFuture; 25 | 26 | /** 27 | * Default implementation of the {@link ActiveTransformation} 28 | * 29 | * @author Sara Pellegrini 30 | * @since 2023.1.0 31 | */ 32 | public class DefaultActiveTransformation implements ActiveTransformation { 33 | 34 | private final String transformationId; 35 | private final Long currentSequence; 36 | private final EventTransformationService service; 37 | 38 | /** 39 | * Constructs an instance based on the specified parameters. 40 | * 41 | * @param transformationId the identifier of the active transformation 42 | * @param currentSequence the current last sequence, used as validation that no action has been lost 43 | * @param service the {@link EventTransformationService} used to communicate with Axon Server 44 | */ 45 | DefaultActiveTransformation(String transformationId, 46 | Long currentSequence, 47 | EventTransformationService service) { 48 | this.transformationId = transformationId; 49 | this.currentSequence = currentSequence; 50 | this.service = service; 51 | } 52 | 53 | @Override 54 | public CompletableFuture transform(Transformer transformer) { 55 | TransformationStream transformationStream = service.transformationStream(transformationId); //open stream 56 | TransformationStreamAppender appender = new TransformationStreamAppender(transformationStream, currentSequence); 57 | transformer.transform(appender); //execute transformation 58 | return appender.complete() //close stream 59 | .thenApply(seq -> new DefaultActiveTransformation(transformationId, 60 | seq, 61 | service)); 62 | } 63 | 64 | @Override 65 | public CompletableFuture startApplying() { 66 | return service.startApplying(transformationId, currentSequence) 67 | .thenCompose(unused -> service.transformationById(transformationId)); 68 | } 69 | 70 | @Override 71 | public CompletableFuture cancel() { 72 | return service.cancel(transformationId) 73 | .thenCompose(unused -> service.transformationById(transformationId)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/buffer/FlowControlledDisposableReadonlyBuffer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2022. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl.buffer; 18 | 19 | import io.axoniq.axonserver.connector.FlowControl; 20 | import io.axoniq.axonserver.connector.impl.CloseableReadonlyBuffer; 21 | import io.axoniq.axonserver.connector.impl.DisposableReadonlyBuffer; 22 | import io.axoniq.axonserver.grpc.ErrorMessage; 23 | 24 | import java.util.Optional; 25 | import java.util.concurrent.atomic.AtomicBoolean; 26 | 27 | /** 28 | * Connects a {@link FlowControl} instance with a {@link DisposableReadonlyBuffer} assuming that {@link 29 | * FlowControl#request(long) requesting} from the flow control will trigger a replenishment of the buffer. 30 | * 31 | * @param the type of messages this buffer deals with 32 | * @author Milan Savic 33 | * @author Stefan Dragisic 34 | * @author Allard Buijze 35 | * @since 4.6.0 36 | */ 37 | public class FlowControlledDisposableReadonlyBuffer implements DisposableReadonlyBuffer { 38 | 39 | private final FlowControl flowControl; 40 | private final CloseableReadonlyBuffer buffer; 41 | private final AtomicBoolean started = new AtomicBoolean(); 42 | 43 | /** 44 | * Instantiates this buffer with the given {@code flowControl} and {@code buffer}. 45 | * 46 | * @param flowControl used for {@code buffer} replenishment 47 | * @param buffer used for message retrieval on {@link FlowControl#request(long)} 48 | */ 49 | public FlowControlledDisposableReadonlyBuffer(FlowControl flowControl, CloseableReadonlyBuffer buffer) { 50 | this.flowControl = flowControl; 51 | this.buffer = buffer; 52 | } 53 | 54 | @Override 55 | public Optional poll() { 56 | replenishIfNotStarted(); 57 | Optional element = buffer.poll(); 58 | element.ifPresent(e -> markConsumed()); 59 | return element; 60 | } 61 | 62 | @Override 63 | public boolean isEmpty() { 64 | return buffer.isEmpty(); 65 | } 66 | 67 | @Override 68 | public int capacity() { 69 | return buffer.capacity(); 70 | } 71 | 72 | @Override 73 | public void onAvailable(Runnable onAvailable) { 74 | buffer.onAvailable(onAvailable); 75 | } 76 | 77 | @Override 78 | public void dispose() { 79 | flowControl.cancel(); 80 | } 81 | 82 | @Override 83 | public boolean closed() { 84 | return buffer.closed(); 85 | } 86 | 87 | @Override 88 | public Optional error() { 89 | return buffer.error(); 90 | } 91 | 92 | private void replenishIfNotStarted() { 93 | if (started.compareAndSet(false, true)) { 94 | flowControl.request(buffer.capacity()); 95 | } 96 | } 97 | 98 | private void markConsumed() { 99 | if (!closed()) { 100 | flowControl.request(1); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/buffer/BlockingCloseableBuffer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2022. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl.buffer; 18 | 19 | import io.axoniq.axonserver.connector.impl.CloseableBuffer; 20 | import io.axoniq.axonserver.grpc.ErrorMessage; 21 | 22 | import java.util.Optional; 23 | import java.util.concurrent.BlockingQueue; 24 | import java.util.concurrent.LinkedBlockingQueue; 25 | import java.util.concurrent.atomic.AtomicReference; 26 | 27 | /** 28 | * An implementation of the {@link CloseableBuffer} that uses a {@link LinkedBlockingQueue} as the backing buffer. 29 | * 30 | * @param the type of messages in this buffer 31 | * @author Milan Savic 32 | * @author Stefan Dragisic 33 | * @author Allard Buijze 34 | * @since 4.6.0 35 | */ 36 | public class BlockingCloseableBuffer implements CloseableBuffer { 37 | 38 | private static final int DEFAULT_CAPACITY = 32; 39 | 40 | private final BlockingQueue buffer = new LinkedBlockingQueue<>(DEFAULT_CAPACITY); 41 | private volatile boolean closed = false; 42 | private final AtomicReference errorRef = new AtomicReference<>(); 43 | private final AtomicReference onAvailableRef = new AtomicReference<>(); 44 | 45 | @Override 46 | public Optional poll() { 47 | return Optional.ofNullable(buffer.poll()); 48 | } 49 | 50 | @Override 51 | public boolean isEmpty() { 52 | return buffer.isEmpty(); 53 | } 54 | 55 | @Override 56 | public int capacity() { 57 | return DEFAULT_CAPACITY; 58 | } 59 | 60 | /** 61 | * Returns the number of elements in this buffer. 62 | * 63 | * @return the number of elements in this buffer 64 | */ 65 | public int size() { 66 | return buffer.size(); 67 | } 68 | 69 | @Override 70 | public void onAvailable(Runnable onAvailable) { 71 | onAvailableRef.set(onAvailable); 72 | if (!isEmpty() || closed) { 73 | notifyOnAvailable(); 74 | } 75 | } 76 | 77 | @Override 78 | public void put(T message) { 79 | try { 80 | buffer.put(message); 81 | notifyOnAvailable(); 82 | } catch (InterruptedException e) { 83 | Thread.currentThread().interrupt(); 84 | } 85 | } 86 | 87 | @Override 88 | public boolean closed() { 89 | return closed; 90 | } 91 | 92 | @Override 93 | public Optional error() { 94 | return Optional.ofNullable(errorRef.get()); 95 | } 96 | 97 | @Override 98 | public void close() { 99 | closed = true; 100 | notifyOnAvailable(); 101 | } 102 | 103 | @Override 104 | public void closeExceptionally(ErrorMessage errorMessage) { 105 | errorRef.set(errorMessage); 106 | close(); 107 | } 108 | 109 | protected void notifyOnAvailable() { 110 | Runnable onAvailable = onAvailableRef.get(); 111 | if (onAvailable != null) { 112 | onAvailable.run(); 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/PersistentStreamCallbacks.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2024. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.axoniq.axonserver.connector.event; 17 | 18 | import java.util.function.Consumer; 19 | import java.util.function.IntConsumer; 20 | 21 | /** 22 | * Definitions of the callbacks to be added to a persistent stream. 23 | * 24 | * @author Marc Gathier 25 | * @since 2024.0.0 26 | */ 27 | public class PersistentStreamCallbacks { 28 | 29 | private final Consumer onSegmentOpened; 30 | 31 | private final Consumer onSegmentClosed; 32 | private final Consumer onAvailable; 33 | 34 | private final Consumer onClosed; 35 | 36 | /** 37 | * Instantiates the callbacks object. 38 | * 39 | * @param onSegmentOpened callback that the connector invokes when a persistent stream segment is opened 40 | * @param onSegmentClosed callback that the connector invokes when a persistent stream segment is closed 41 | * @param onAvailable callback that the connector invokes when an event is available on a persistent stream 42 | * segment 43 | * @param onClosed callback that the connector invokes when the persistent stream is closed 44 | */ 45 | public PersistentStreamCallbacks(Consumer onSegmentOpened, Consumer onSegmentClosed, 46 | Consumer onAvailable, 47 | Consumer onClosed) { 48 | this.onSegmentOpened = onSegmentOpened; 49 | this.onSegmentClosed = onSegmentClosed; 50 | this.onAvailable = onAvailable; 51 | this.onClosed = onClosed; 52 | } 53 | 54 | /** 55 | * Returns the callback that the connector invokes when the persistent stream is closed. 56 | * 57 | * @return callback that the connector invokes when the persistent stream is closed 58 | */ 59 | public Consumer onClosed() { 60 | return onClosed; 61 | } 62 | 63 | /** 64 | * Returns the callback that the connector invokes when a persistent stream segment is opened. 65 | * 66 | * @return callback that the connector invokes when a persistent stream segment is opened 67 | */ 68 | public Consumer onSegmentOpened() { 69 | return onSegmentOpened; 70 | } 71 | 72 | /** 73 | * Returns the callback that the connector invokes when a persistent stream segment is closed. 74 | * 75 | * @return callback that the connector invokes when a persistent stream segment is closed 76 | */ 77 | public Consumer onSegmentClosed() { 78 | return onSegmentClosed; 79 | } 80 | 81 | /** 82 | * Returns the callback that the connector invokes when an event is available on a persistent stream segment. 83 | * 84 | * @return callback that the connector invokes when an event is available on a persistent stream segment 85 | */ 86 | public Consumer onAvailable() { 87 | return onAvailable; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/transformation/event/impl/TransformableEventIterable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.transformation.event.impl; 18 | 19 | import io.axoniq.axonserver.connector.event.transformation.event.EventTransformationExecutor; 20 | import io.axoniq.axonserver.connector.event.transformation.event.EventTransformer; 21 | import io.axoniq.axonserver.connector.event.transformation.event.TransformableEventStream; 22 | import io.axoniq.axonserver.grpc.event.EventWithToken; 23 | 24 | import java.util.Iterator; 25 | import java.util.NoSuchElementException; 26 | import java.util.Optional; 27 | import java.util.concurrent.atomic.AtomicReference; 28 | import java.util.function.Predicate; 29 | 30 | /** 31 | * {@link TransformableEventStream} implementation that is based on an {@link Iterable}. 32 | * 33 | * @author Sara Pellegrini 34 | * @since 2023.1.0 35 | */ 36 | public class TransformableEventIterable implements TransformableEventStream { 37 | 38 | private final Iterable events; 39 | 40 | /** 41 | * Constructs an instance based on the specified {@link Iterable} 42 | * @param events the {@link Iterable} of {@link EventWithToken}s used to retrieve events 43 | */ 44 | public TransformableEventIterable(Iterable events) { 45 | this.events = events; 46 | } 47 | 48 | @Override 49 | public TransformableEventStream filter(Predicate predicate) { 50 | return new TransformableEventIterable(() -> new Iterator() { 51 | private final Iterator iterator = events.iterator(); 52 | private final AtomicReference next = new AtomicReference<>(); 53 | 54 | { //prefetch the initial result 55 | nextMatchingThePredicate().ifPresent(next::set); 56 | } 57 | 58 | @Override 59 | public boolean hasNext() { 60 | return next.get() != null; 61 | } 62 | 63 | @Override 64 | public EventWithToken next() { 65 | if (!hasNext()) { 66 | throw new NoSuchElementException(); 67 | } 68 | EventWithToken current = next.get(); 69 | next.set(nextMatchingThePredicate().orElse(null)); 70 | return current; 71 | } 72 | 73 | private Optional nextMatchingThePredicate() { 74 | while (iterator.hasNext()) { 75 | EventWithToken current = iterator.next(); 76 | if (predicate.test(current)) { 77 | return Optional.of(current); 78 | } 79 | } 80 | return Optional.empty(); 81 | } 82 | 83 | 84 | }); 85 | } 86 | 87 | @Override 88 | public EventTransformationExecutor transform(String transformationDescription, 89 | EventTransformer eventTransformer) { 90 | return new IterableEventTransformationExecutor(transformationDescription, events, eventTransformer); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/test/java/io/axoniq/axonserver/connector/event/impl/BufferedAggregateEventStreamTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.impl; 18 | 19 | import io.axoniq.axonserver.connector.impl.StreamClosedException; 20 | import io.axoniq.axonserver.grpc.event.Event; 21 | import io.axoniq.axonserver.grpc.event.GetAggregateEventsRequest; 22 | import io.grpc.stub.ClientCallStreamObserver; 23 | import org.junit.jupiter.api.*; 24 | 25 | import java.lang.reflect.Field; 26 | import java.lang.reflect.Modifier; 27 | 28 | import static org.junit.jupiter.api.Assertions.*; 29 | import static org.mockito.Mockito.*; 30 | 31 | /** 32 | * Test class validationg the {@link BufferedAggregateEventStream}. 33 | * 34 | * @author Allard Buijze 35 | */ 36 | class BufferedAggregateEventStreamTest { 37 | 38 | private BufferedAggregateEventStream testSubject; 39 | private ClientCallStreamObserver clientCallStreamObserver; 40 | 41 | @SuppressWarnings("unchecked") 42 | @BeforeEach 43 | void setUp() { 44 | System.setProperty("AGGREGATE_TAKE_EVENT_TIMEOUT_MILLIS", "100"); 45 | testSubject = new BufferedAggregateEventStream(10, 1); 46 | clientCallStreamObserver = mock(ClientCallStreamObserver.class); 47 | testSubject.beforeStart(clientCallStreamObserver); 48 | } 49 | 50 | @AfterAll 51 | static void afterAll() { 52 | System.setProperty("AGGREGATE_TAKE_EVENT_TIMEOUT_MILLIS", "10000"); 53 | } 54 | 55 | @Test 56 | void testEventStreamPropagatesErrorOnHasNext() { 57 | testSubject.onError(new RuntimeException("Mock")); 58 | 59 | assertThrows(StreamClosedException.class, () -> testSubject.hasNext()); 60 | } 61 | 62 | @Test 63 | void testEventStreamPropagatesErrorOnHasNextAfterReadingAvailableEvents() throws InterruptedException { 64 | testSubject.onNext(Event.getDefaultInstance()); 65 | testSubject.onNext(Event.newBuilder().setAggregateSequenceNumber(1).build()); 66 | testSubject.onError(new RuntimeException("Mock")); 67 | 68 | assertTrue(testSubject.hasNext()); 69 | assertEquals(Event.getDefaultInstance(), testSubject.next()); 70 | assertTrue(testSubject.hasNext()); 71 | assertEquals(Event.newBuilder().setAggregateSequenceNumber(1).build(), testSubject.next()); 72 | assertThrows(StreamClosedException.class, () -> testSubject.hasNext()); 73 | } 74 | 75 | @Test 76 | void throwsExceptionOnTimeoutWhileRetrievingEvents() throws Exception { 77 | // Push messages 78 | for (int i = 0; i < 20; i++) { 79 | testSubject.onNext(Event.newBuilder().setAggregateSequenceNumber(i).build()); 80 | testSubject.hasNext(); 81 | testSubject.next(); 82 | } 83 | 84 | // Now, wait while there is no message 85 | RuntimeException exception = assertThrows(RuntimeException.class, 86 | () -> testSubject.hasNext()); 87 | assertEquals( 88 | "Was unable to load aggregate due to timeout while waiting for events. Last sequence number received: 19", 89 | exception.getMessage()); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/SynchronizedRequestStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.grpc.stub.ClientCallStreamObserver; 20 | 21 | import java.util.concurrent.atomic.AtomicBoolean; 22 | import java.util.concurrent.locks.ReentrantLock; 23 | 24 | /** 25 | * Lock-based synchronized implementation of a {@link ClientCallStreamObserver}. Acts as a wrapper of another {@code 26 | * ClientCallStreamObserver}, to which all of operations will be delegated, adding synchronization logic to {@link 27 | * #onNext(Object)}, {@link #onCompleted()} and {@link #onError(Throwable)}. 28 | * 29 | * @param the type of value returned by this stream 30 | */ 31 | public class SynchronizedRequestStream extends ClientCallStreamObserver { 32 | 33 | private final ClientCallStreamObserver delegate; 34 | private final ReentrantLock lock = new ReentrantLock(); 35 | private final AtomicBoolean halfClosed = new AtomicBoolean(false); 36 | 37 | /** 38 | * Instantiate a {@link SynchronizedRequestStream}, delegating all operations to the given {@code requestStream} 39 | * 40 | * @param requestStream the {@link ClientCallStreamObserver} to delegate method invocations to 41 | */ 42 | public SynchronizedRequestStream(ClientCallStreamObserver requestStream) { 43 | delegate = requestStream; 44 | } 45 | 46 | @Override 47 | public void cancel(String message, Throwable cause) { 48 | halfClosed.set(true); 49 | delegate.cancel(message, cause); 50 | } 51 | 52 | @Override 53 | public boolean isReady() { 54 | return delegate.isReady(); 55 | } 56 | 57 | @Override 58 | public void setOnReadyHandler(Runnable onReadyHandler) { 59 | delegate.setOnReadyHandler(onReadyHandler); 60 | } 61 | 62 | @Override 63 | public void disableAutoInboundFlowControl() { 64 | delegate.disableAutoInboundFlowControl(); 65 | } 66 | 67 | @Override 68 | public void request(int count) { 69 | delegate.request(count); 70 | } 71 | 72 | @Override 73 | public void setMessageCompression(boolean enable) { 74 | delegate.setMessageCompression(enable); 75 | } 76 | 77 | @Override 78 | public void onNext(T value) { 79 | inLock(() -> delegate.onNext(value)); 80 | } 81 | 82 | @Override 83 | public void disableAutoRequestWithInitial(int request) { 84 | delegate.disableAutoRequestWithInitial(request); 85 | } 86 | 87 | @Override 88 | public void onError(Throwable t) { 89 | inLock(() -> { 90 | delegate.onError(t); 91 | halfClosed.set(true); 92 | }); 93 | } 94 | 95 | @Override 96 | public void onCompleted() { 97 | inLock(() -> { 98 | delegate.onCompleted(); 99 | halfClosed.set(true); 100 | }); 101 | } 102 | 103 | private void inLock(Runnable action) { 104 | try { 105 | lock.lock(); 106 | if (!halfClosed.get()) { 107 | action.run(); 108 | } 109 | } finally { 110 | lock.unlock(); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/impl/CloseAwareReplyChannel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2022. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.axoniq.axonserver.connector.ErrorCategory; 20 | import io.axoniq.axonserver.connector.ReplyChannel; 21 | import io.axoniq.axonserver.grpc.ErrorMessage; 22 | 23 | import java.util.concurrent.atomic.AtomicBoolean; 24 | 25 | /** 26 | * A {@link ReplyChannel} implementation that will trigger a given {@link Runnable action} when it is closed. 27 | * 28 | * @param the type of messages flowing through this {@link ReplyChannel} 29 | * @author Milan Savic 30 | * @author Stefan Dragisic 31 | * @author Allard Buijze 32 | * @since 4.6.0 33 | */ 34 | public class CloseAwareReplyChannel implements ReplyChannel { 35 | 36 | private final ReplyChannel delegate; 37 | private final Runnable onClose; 38 | private final AtomicBoolean closed = new AtomicBoolean(); 39 | 40 | /** 41 | * Instantiates this {@link ReplyChannel} with given {@code delegate}. 42 | * 43 | * @param delegate the delegate 44 | */ 45 | public CloseAwareReplyChannel(ReplyChannel delegate) { 46 | this(delegate, () -> { 47 | }); 48 | } 49 | 50 | /** 51 | * Instantiates this {@link ReplyChannel} with given {@code delegate} and {@code onClose} {@link Runnable} execution 52 | * to be executed once the channel is closed. 53 | * 54 | * @param delegate the delegate 55 | * @param onClose to be executed when channel is closed 56 | */ 57 | public CloseAwareReplyChannel(ReplyChannel delegate, Runnable onClose) { 58 | this.delegate = delegate; 59 | this.onClose = () -> { 60 | closed.set(true); 61 | onClose.run(); 62 | }; 63 | } 64 | 65 | @Override 66 | public void send(T outboundMessage) { 67 | delegate.send(outboundMessage); 68 | } 69 | 70 | @Override 71 | public void sendLast(T outboundMessage) { 72 | delegate.sendLast(outboundMessage); 73 | onClose.run(); 74 | } 75 | 76 | @Override 77 | public void sendAck() { 78 | delegate.sendAck(); 79 | } 80 | 81 | @Override 82 | public void sendNack() { 83 | delegate.sendNack(); 84 | } 85 | 86 | @Override 87 | public void sendNack(ErrorMessage errorMessage) { 88 | delegate.sendNack(errorMessage); 89 | } 90 | 91 | @Override 92 | public void complete() { 93 | delegate.complete(); 94 | onClose.run(); 95 | } 96 | 97 | @Override 98 | public void completeWithError(ErrorMessage errorMessage) { 99 | delegate.completeWithError(errorMessage); 100 | onClose.run(); 101 | } 102 | 103 | @Override 104 | public void completeWithError(ErrorCategory errorCategory, String message) { 105 | delegate.completeWithError(errorCategory, message); 106 | onClose.run(); 107 | } 108 | 109 | /** 110 | * Indicates whether this {@link ReplyChannel} is closed. 111 | * 112 | * @return {@code true} if closed, {@code false} otherwise 113 | */ 114 | public boolean isClosed() { 115 | return closed.get(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/transformation/event/stream/TokenRangeEvents.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.transformation.event.stream; 18 | 19 | import io.axoniq.axonserver.connector.event.EventChannel; 20 | import io.axoniq.axonserver.connector.event.EventStream; 21 | import io.axoniq.axonserver.connector.impl.StreamClosedException; 22 | import io.axoniq.axonserver.grpc.event.EventWithToken; 23 | 24 | import java.util.Iterator; 25 | import java.util.NoSuchElementException; 26 | import java.util.concurrent.atomic.AtomicReference; 27 | import java.util.function.Supplier; 28 | 29 | /** 30 | * {@link EventWithToken}s' {@link Iterable} that start from a given initial token and completes at the last token. 31 | * 32 | * @author Sara Pellegrini 33 | * @since 2023.0.0 34 | */ 35 | public class TokenRangeEvents implements Iterable { 36 | 37 | private final Supplier eventChannel; 38 | 39 | private final long firstToken; 40 | 41 | private final long lastToken; 42 | 43 | /** 44 | * Constructs an instance based on the specified parameters. 45 | * 46 | * @param eventChannel the channel used to open a stream to Axon Server for reading events 47 | * @param firstToken the first token to include 48 | * @param lastToken the last token to include 49 | */ 50 | public TokenRangeEvents(Supplier eventChannel, long firstToken, long lastToken) { 51 | this.eventChannel = eventChannel; 52 | this.firstToken = firstToken; 53 | this.lastToken = lastToken; 54 | } 55 | 56 | private static EventWithToken next(EventStream eventStream) { 57 | try { 58 | return eventStream.next(); 59 | } catch (InterruptedException e) { 60 | Thread.currentThread().interrupt(); 61 | eventStream.close(); 62 | throw new RuntimeException(e); 63 | } 64 | } 65 | 66 | @Override 67 | public Iterator iterator() { 68 | return events(firstToken, lastToken); 69 | } 70 | 71 | private Iterator events(long firstToken, long lastToken) { 72 | EventStream eventStream = eventChannel.get().openStream(firstToken, 10); 73 | AtomicReference nextRef = new AtomicReference<>(TokenRangeEvents.next(eventStream)); 74 | return new Iterator() { 75 | @Override 76 | public boolean hasNext() { 77 | return nextRef.get().getToken() <= lastToken; 78 | } 79 | 80 | @Override 81 | public EventWithToken next() { 82 | if (!hasNext()) { 83 | throw new NoSuchElementException(); 84 | } 85 | if (eventStream.isClosed()) { 86 | throw new StreamClosedException(eventStream.getError().orElse(null)); 87 | } 88 | EventWithToken current = nextRef.get(); 89 | if (current.getToken() == lastToken) { 90 | nextRef.set(current.toBuilder().setToken(lastToken + 1).build()); 91 | eventStream.close(); 92 | } else { 93 | nextRef.set(TokenRangeEvents.next(eventStream)); 94 | } 95 | return current; 96 | } 97 | }; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/test/java/io/axoniq/axonserver/connector/impl/AsyncRegistrationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.axoniq.axonserver.connector.AxonServerException; 20 | import io.axoniq.axonserver.connector.ErrorCategory; 21 | import org.junit.jupiter.api.BeforeEach; 22 | import org.junit.jupiter.api.Test; 23 | 24 | import java.util.concurrent.CompletableFuture; 25 | import java.util.concurrent.ExecutionException; 26 | import java.util.concurrent.TimeUnit; 27 | import java.util.concurrent.TimeoutException; 28 | import java.util.concurrent.atomic.AtomicBoolean; 29 | 30 | import static org.junit.jupiter.api.Assertions.assertEquals; 31 | import static org.junit.jupiter.api.Assertions.assertFalse; 32 | import static org.junit.jupiter.api.Assertions.assertSame; 33 | import static org.junit.jupiter.api.Assertions.assertThrows; 34 | import static org.junit.jupiter.api.Assertions.assertTrue; 35 | 36 | class AsyncRegistrationTest { 37 | 38 | 39 | private CompletableFuture future; 40 | private CompletableFuture cancelFuture; 41 | private AsyncRegistration testSubject; 42 | 43 | @BeforeEach 44 | void setUp() { 45 | future = new CompletableFuture<>(); 46 | cancelFuture = new CompletableFuture<>(); 47 | 48 | testSubject = new AsyncRegistration(future, () -> cancelFuture); 49 | } 50 | 51 | @Test 52 | void testAwaitDoesNotWaitOnCompletedFuture() throws TimeoutException, InterruptedException { 53 | future.complete(null); 54 | 55 | assertSame(testSubject, testSubject.awaitAck(1, TimeUnit.SECONDS)); 56 | } 57 | 58 | @Test 59 | void testAwaitTimeoutThrowsException() { 60 | assertThrows(TimeoutException.class, () -> testSubject.awaitAck(10, TimeUnit.MILLISECONDS)); 61 | } 62 | 63 | @Test 64 | void testAwaitRethrowsAxonServerException() { 65 | AxonServerException thrown = new AxonServerException(ErrorCategory.OTHER, "Testing", ""); 66 | future.completeExceptionally(thrown); 67 | 68 | try { 69 | testSubject.awaitAck(1, TimeUnit.MILLISECONDS); 70 | } catch (Exception e) { 71 | assertEquals(AxonServerException.class, e.getClass()); 72 | assertSame(thrown, e); 73 | } 74 | } 75 | 76 | @Test 77 | void testAwaitWrapsOtherExceptionInAxonServerException() { 78 | Exception thrown = new Exception("Testing"); 79 | future.completeExceptionally(thrown); 80 | 81 | try { 82 | testSubject.awaitAck(1, TimeUnit.MILLISECONDS); 83 | } catch (Exception e) { 84 | assertEquals(AxonServerException.class, e.getClass()); 85 | assertEquals(ErrorCategory.INSTRUCTION_ACK_ERROR, ((AxonServerException) e).getErrorCategory()); 86 | assertEquals(ExecutionException.class, e.getCause().getClass()); 87 | assertSame(thrown, e.getCause().getCause()); 88 | } 89 | } 90 | 91 | @Test 92 | void testOnAckExecutesAtCompletion() { 93 | AtomicBoolean result = new AtomicBoolean(); 94 | testSubject.onAck(() -> result.set(true)); 95 | 96 | assertFalse(result.get()); 97 | 98 | future.complete(null); 99 | 100 | assertTrue(result.get()); 101 | } 102 | 103 | @Test 104 | void cancelReturnsFutureIndependentOfRequestAck() { 105 | assertSame(cancelFuture, testSubject.cancel()); 106 | future.complete(null); 107 | assertSame(cancelFuture, testSubject.cancel()); 108 | } 109 | 110 | } -------------------------------------------------------------------------------- /src/test/java/io/axoniq/axonserver/connector/impl/buffer/FlowControlledDisposableReadonlyBufferTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2022. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl.buffer; 18 | 19 | import io.axoniq.axonserver.connector.FlowControl; 20 | import io.axoniq.axonserver.connector.impl.CloseableReadonlyBuffer; 21 | import io.axoniq.axonserver.grpc.ErrorMessage; 22 | import org.junit.jupiter.api.*; 23 | 24 | import java.util.Optional; 25 | 26 | import static org.junit.jupiter.api.Assertions.*; 27 | import static org.mockito.Mockito.*; 28 | 29 | /** 30 | * Tests for the {@link FlowControlledDisposableReadonlyBuffer}. 31 | */ 32 | class FlowControlledDisposableReadonlyBufferTest { 33 | 34 | private FlowControl flowControl; 35 | private CloseableReadonlyBuffer buffer; 36 | private FlowControlledDisposableReadonlyBuffer flowControlledBuffer; 37 | 38 | @BeforeEach 39 | void setUp() { 40 | flowControl = mock(FlowControl.class); 41 | buffer = mock(CloseableReadonlyBuffer.class); 42 | flowControlledBuffer = new FlowControlledDisposableReadonlyBuffer<>(flowControl, buffer); 43 | } 44 | 45 | @Test 46 | void testPolling() { 47 | when(buffer.poll()).thenReturn(Optional.of("v1")) 48 | .thenReturn(Optional.empty()) 49 | .thenReturn(Optional.of("v2")) 50 | .thenReturn(Optional.of("v3")); 51 | when(buffer.capacity()).thenReturn(5); 52 | when(buffer.closed()).thenReturn(false) 53 | .thenReturn(false) 54 | .thenReturn(true); 55 | 56 | assertEquals(Optional.of("v1"), flowControlledBuffer.poll()); 57 | assertEquals(Optional.empty(), flowControlledBuffer.poll()); 58 | assertEquals(Optional.of("v2"), flowControlledBuffer.poll()); 59 | assertEquals(Optional.of("v3"), flowControlledBuffer.poll()); 60 | 61 | verify(flowControl).request(5); // initial 62 | verify(flowControl, times(2)).request(1); // replenish 63 | verifyNoMoreInteractions(flowControl); // no more replenishment, buffer is closed 64 | } 65 | 66 | @Test 67 | void testIsEmpty() { 68 | when(buffer.isEmpty()).thenReturn(false) 69 | .thenReturn(true); 70 | 71 | assertFalse(flowControlledBuffer.isEmpty()); 72 | assertTrue(flowControlledBuffer.isEmpty()); 73 | } 74 | 75 | @Test 76 | void testCapacity() { 77 | when(buffer.capacity()).thenReturn(23); 78 | 79 | assertEquals(23, flowControlledBuffer.capacity()); 80 | } 81 | 82 | @Test 83 | void testOnAvailablePropagated() { 84 | Runnable onAvailable = () -> { 85 | }; 86 | flowControlledBuffer.onAvailable(onAvailable); 87 | 88 | verify(buffer).onAvailable(onAvailable); 89 | } 90 | 91 | @Test 92 | void testDisposePropagated() { 93 | flowControlledBuffer.dispose(); 94 | 95 | verify(flowControl).cancel(); 96 | } 97 | 98 | @Test 99 | void testClosed() { 100 | when(buffer.closed()).thenReturn(false) 101 | .thenReturn(true); 102 | 103 | assertFalse(flowControlledBuffer.closed()); 104 | assertTrue(flowControlledBuffer.closed()); 105 | } 106 | 107 | @Test 108 | void testError() { 109 | when(buffer.error()).thenReturn(Optional.of(ErrorMessage.getDefaultInstance())); 110 | 111 | assertEquals(Optional.of(ErrorMessage.getDefaultInstance()), flowControlledBuffer.error()); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/query/QueryHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.query; 18 | 19 | import io.axoniq.axonserver.connector.FlowControl; 20 | import io.axoniq.axonserver.connector.Registration; 21 | import io.axoniq.axonserver.connector.ReplyChannel; 22 | import io.axoniq.axonserver.connector.impl.NoopFlowControl; 23 | import io.axoniq.axonserver.grpc.query.QueryRequest; 24 | import io.axoniq.axonserver.grpc.query.QueryResponse; 25 | import io.axoniq.axonserver.grpc.query.QueryUpdate; 26 | import io.axoniq.axonserver.grpc.query.SubscriptionQuery; 27 | 28 | /** 29 | * Interface representing a component that can handle queries. 30 | */ 31 | @FunctionalInterface 32 | public interface QueryHandler { 33 | 34 | /** 35 | * Handle the given {@code query}, using given {@code responseHandler} to send the response(s). 36 | *

37 | * Note that the query must be completed using {@link ReplyChannel#complete()} or {@link 38 | * ReplyChannel#sendLast(Object)}. 39 | * 40 | * @param query the message representing the query request 41 | * @param responseHandler to handler to send responses with 42 | */ 43 | void handle(QueryRequest query, ReplyChannel responseHandler); 44 | 45 | /** 46 | * Handle the given {@code query}, using the given {@code responseHandler} to send the response(s). This operation 47 | * is flow control aware, hence messages should be sent via {@code responseHandler} when requested. 48 | *

49 | * Note that the query must be completed using {@link ReplyChannel#complete()} or {@link 50 | * ReplyChannel#sendLast(Object)}. 51 | * 52 | * @param query the message representing the query request 53 | * @param responseHandler the handler to send responses with 54 | * @return a {@link FlowControl} to request more responses and cancel sending responses 55 | */ 56 | default FlowControl stream(QueryRequest query, ReplyChannel responseHandler) { 57 | handle(query, responseHandler); 58 | return NoopFlowControl.INSTANCE; 59 | } 60 | 61 | /** 62 | * Registers an incoming subscription query request, represented by given {@code query}, using given {@code 63 | * updateHandler} to send updates when the projection for this query changes. 64 | *

65 | * If this handler doesn't support subscription queries for the given {@code query}, it should return {@code null}. 66 | * Otherwise, it must return a handle that can be used to cancel the subscription query. 67 | * 68 | * @param query the message representing the query 69 | * @param updateHandler to handler to send updates with 70 | * @return a registration to cancel the subscription, or {@code null} if this handler doesn't support the 71 | * subscription query 72 | */ 73 | default Registration registerSubscriptionQuery(SubscriptionQuery query, UpdateHandler updateHandler) { 74 | return null; 75 | } 76 | 77 | /** 78 | * Interface describing a stream of updates to a subscription query. 79 | */ 80 | interface UpdateHandler { 81 | 82 | /** 83 | * Send the given {@code queryUpdate} in response to the subscription query this handler was provided for. 84 | * 85 | * @param queryUpdate the update to send 86 | */ 87 | void sendUpdate(QueryUpdate queryUpdate); 88 | 89 | /** 90 | * Indicates the subscription query has completed, meaning no more updates are to be expected. The component 91 | * sending the subscription query is requested to close the subscription. 92 | */ 93 | void complete(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/io/axoniq/axonserver/connector/impl/AbstractIncomingInstructionStreamTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2022. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.impl; 18 | 19 | import io.axoniq.axonserver.connector.InstructionHandler; 20 | import io.axoniq.axonserver.grpc.FlowControl; 21 | import io.axoniq.axonserver.grpc.InstructionAck; 22 | import io.grpc.stub.CallStreamObserver; 23 | import io.grpc.stub.ClientCallStreamObserver; 24 | import org.junit.jupiter.api.*; 25 | 26 | import java.util.concurrent.atomic.AtomicReference; 27 | import java.util.function.Consumer; 28 | 29 | import static org.junit.jupiter.api.Assertions.*; 30 | import static org.mockito.Mockito.*; 31 | 32 | /** 33 | * Test class validating the {{@link AbstractIncomingInstructionStream}}. 34 | * 35 | * @author Steven van Beelen 36 | */ 37 | class AbstractIncomingInstructionStreamTest { 38 | 39 | private static final String CLIENT_ID = "clientId"; 40 | private static final int PERMITS = 100; 41 | private static final int PERMITS_BATCH = 100; 42 | 43 | @Test 44 | void testOnCompleted() { 45 | AtomicReference disconnectHandlerThrowable = new AtomicReference<>(); 46 | AbstractIncomingInstructionStream testSubject = new TestAbstractIncomingInstructionStreamImpl( 47 | CLIENT_ID, PERMITS, PERMITS_BATCH, disconnectHandlerThrowable::set, r -> { 48 | }, true 49 | ); 50 | //noinspection unchecked 51 | ClientCallStreamObserver mockedClientCallStreamObserver = mock(ClientCallStreamObserver.class); 52 | 53 | // Given 54 | testSubject.beforeStart(mockedClientCallStreamObserver); 55 | // When 56 | testSubject.onCompleted(); 57 | // Then 58 | assertTrue(disconnectHandlerThrowable.get() instanceof StreamUnexpectedlyCompletedException); 59 | verify(mockedClientCallStreamObserver).onCompleted(); 60 | } 61 | 62 | private static class TestAbstractIncomingInstructionStreamImpl 63 | extends AbstractIncomingInstructionStream { 64 | 65 | private final boolean unregisterOutboundStreamResponse; 66 | 67 | public TestAbstractIncomingInstructionStreamImpl(String clientId, 68 | int permits, 69 | int permitsBatch, 70 | Consumer disconnectHandler, 71 | Consumer> beforeStartHandler, 72 | boolean unregisterOutboundStreamResponse) { 73 | super(clientId, permits, permitsBatch, disconnectHandler, beforeStartHandler); 74 | this.unregisterOutboundStreamResponse = unregisterOutboundStreamResponse; 75 | } 76 | 77 | @Override 78 | protected Object buildAckMessage(InstructionAck ack) { 79 | return null; 80 | } 81 | 82 | @Override 83 | protected String getInstructionId(Object instruction) { 84 | return null; 85 | } 86 | 87 | @Override 88 | protected InstructionHandler getHandler(Object msgIn) { 89 | return null; 90 | } 91 | 92 | @Override 93 | protected boolean unregisterOutboundStream(CallStreamObserver expected) { 94 | return this.unregisterOutboundStreamResponse; 95 | } 96 | 97 | @Override 98 | protected Object buildFlowControlMessage(FlowControl flowControl) { 99 | return null; 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/AggregateEventStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event; 18 | 19 | import io.axoniq.axonserver.connector.impl.StreamClosedException; 20 | import io.axoniq.axonserver.grpc.event.Event; 21 | 22 | import java.util.Spliterator; 23 | import java.util.function.Consumer; 24 | import java.util.stream.Stream; 25 | import java.util.stream.StreamSupport; 26 | 27 | /** 28 | * A stream of Events for a single Aggregate. The operations on this stream are blocking and intended for Event Sourcing 29 | * purposes, where an Aggregate's state needs to be reconstructed based on historic events. 30 | */ 31 | public interface AggregateEventStream { 32 | 33 | /** 34 | * Returns the next available event, possibly blocking until one becomes available for reading. 35 | * 36 | * @return the next event 37 | * @throws InterruptedException if the current thread was interrupted while waiting for an event to become 38 | * available 39 | * @throws StreamClosedException when the stream has reached the end or was closed for another reason 40 | * @see #hasNext() to verify availability of events 41 | */ 42 | Event next() throws InterruptedException; 43 | 44 | /** 45 | * Indicates whether a new event is available. This method may block while waiting for a confirmation if an event is 46 | * available for reading. 47 | * 48 | * @return {@code true} if a message is available, or {@code false} if the stream has reached the end 49 | * @throws StreamClosedException if the stream has been closed prematurely because of an error or on client request 50 | */ 51 | boolean hasNext(); 52 | 53 | /** 54 | * Close this stream for further reading, notifying the provider of Events to stop streaming them. Any event already 55 | * emitted by the sender may still be consumed. 56 | *

57 | * Note that after calling {@code cancel}, {@link #hasNext()} may throw a {@link StreamClosedException} if the 58 | * cancellation caused the stream to close before the last message was received. 59 | */ 60 | void cancel(); 61 | 62 | /** 63 | * Returns a Stream that consumes the Events from this instance. Note that this instance should not be read from in 64 | * parallel to the returned stream, as this may provide undefined results on the availability of events in either 65 | * stream instance. 66 | * 67 | * @return a Stream containing the Events contained in this stream 68 | */ 69 | default Stream asStream() { 70 | return StreamSupport.stream(new Spliterator() { 71 | @Override 72 | public boolean tryAdvance(Consumer action) { 73 | if (hasNext()) { 74 | try { 75 | action.accept(next()); 76 | return true; 77 | } catch (InterruptedException e) { 78 | Thread.currentThread().interrupt(); 79 | return false; 80 | } 81 | } 82 | return false; 83 | } 84 | 85 | @Override 86 | public Spliterator trySplit() { 87 | return null; 88 | } 89 | 90 | @Override 91 | public long estimateSize() { 92 | return Integer.MAX_VALUE; 93 | } 94 | 95 | @Override 96 | public int characteristics() { 97 | return Spliterator.NONNULL & Spliterator.ORDERED & Spliterator.IMMUTABLE & Spliterator.DISTINCT; 98 | } 99 | }, false); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/test/java/io/axoniq/axonserver/connector/AxonServerConnectionFactoryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector; 18 | 19 | import io.axoniq.axonserver.connector.impl.ServerAddress; 20 | import org.junit.jupiter.api.*; 21 | import org.testng.reporters.Files; 22 | 23 | import java.io.ByteArrayOutputStream; 24 | import java.io.IOException; 25 | import java.io.InputStream; 26 | import java.io.PrintStream; 27 | 28 | import static org.junit.jupiter.api.Assertions.*; 29 | 30 | /** 31 | * Test class validating the {@link AxonServerConnectionFactory}. 32 | * 33 | * @author Steven van Beelen 34 | */ 35 | class AxonServerConnectionFactoryTest { 36 | 37 | private static final String TEST_COMPONENT_NAME = "some-component-name"; 38 | private static final String TEST_CONTEXT = "some-context"; 39 | 40 | private final ByteArrayOutputStream systemOut = new ByteArrayOutputStream(); 41 | private final PrintStream originalSystemOut = System.out; 42 | 43 | private String downloadMessage; 44 | private AxonServerConnectionFactory testSubject; 45 | 46 | @BeforeEach 47 | void setUp() throws IOException { 48 | System.setOut(new PrintStream(systemOut)); 49 | 50 | try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream("axonserver_download.txt")) { 51 | // noinspection DataFlowIssue 52 | downloadMessage = Files.readFile(inputStream); 53 | } 54 | } 55 | 56 | @AfterEach 57 | void tearDown() { 58 | if (testSubject != null) { 59 | testSubject.shutdown(); 60 | } 61 | System.setOut(originalSystemOut); 62 | System.clearProperty("axon.axonserver.suppressDownloadMessage"); 63 | } 64 | 65 | @Test 66 | void downloadMessageIsNotSuppressedForDefaultServerAddress() { 67 | assertFalse(downloadMessage.isEmpty()); 68 | 69 | testSubject = AxonServerConnectionFactory.forClient(TEST_COMPONENT_NAME) 70 | .routingServers(ServerAddress.DEFAULT) 71 | .build(); 72 | 73 | testSubject.connect(TEST_CONTEXT); 74 | String replace = systemOut.toString().replace("\r\n", "\n"); 75 | assertTrue(replace.contains(downloadMessage)); 76 | } 77 | 78 | @Test 79 | void downloadMessageIsSuppressedForNoneDefaultServerAddress() { 80 | assertFalse(downloadMessage.isEmpty()); 81 | 82 | ServerAddress customAddress = new ServerAddress("my-host", 4218); 83 | testSubject = AxonServerConnectionFactory.forClient(TEST_COMPONENT_NAME) 84 | .routingServers(customAddress) 85 | .build(); 86 | 87 | testSubject.connect(TEST_CONTEXT); 88 | String replace = systemOut.toString().replace("\r\n", "\n"); 89 | assertFalse(replace.contains(downloadMessage)); 90 | } 91 | 92 | @Test 93 | void downloadMessageIsSuppressedWhenSuppressionPropertyIsSet() { 94 | assertFalse(downloadMessage.isEmpty()); 95 | 96 | System.setProperty("axon.axonserver.suppressDownloadMessage", "true"); 97 | testSubject = AxonServerConnectionFactory.forClient(TEST_COMPONENT_NAME) 98 | .routingServers(ServerAddress.DEFAULT) 99 | .build(); 100 | 101 | testSubject.connect(TEST_CONTEXT); 102 | String replace = systemOut.toString().replace("\r\n", "\n"); 103 | assertFalse(replace.contains(downloadMessage)); 104 | } 105 | } -------------------------------------------------------------------------------- /src/main/java/io/axoniq/axonserver/connector/event/impl/BufferedAggregateEventStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. AxonIQ 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.axoniq.axonserver.connector.event.impl; 18 | 19 | import io.axoniq.axonserver.connector.event.AggregateEventStream; 20 | import io.axoniq.axonserver.connector.impl.FlowControlledBuffer; 21 | import io.axoniq.axonserver.connector.impl.StreamClosedException; 22 | import io.axoniq.axonserver.connector.impl.StreamTimeoutException; 23 | import io.axoniq.axonserver.grpc.FlowControl; 24 | import io.axoniq.axonserver.grpc.event.Event; 25 | import io.axoniq.axonserver.grpc.event.GetAggregateEventsRequest; 26 | 27 | import java.util.concurrent.TimeUnit; 28 | 29 | /** 30 | * Buffering implementation of the {@link AggregateEventStream} used for Event Sourced Aggregates. 31 | */ 32 | public class BufferedAggregateEventStream 33 | extends FlowControlledBuffer 34 | implements AggregateEventStream { 35 | 36 | private static final Event TERMINAL_MESSAGE = Event.newBuilder().setAggregateSequenceNumber(-1729).build(); 37 | private static final int TAKE_TIMEOUT_MILLIS = Integer.parseInt( 38 | System.getProperty("AGGREGATE_TAKE_EVENT_TIMEOUT_MILLIS", "10000") 39 | ); 40 | 41 | private Event peeked; 42 | private long lastSequenceNumber = -1; 43 | 44 | /** 45 | * Constructs a {@link BufferedAggregateEventStream} with a buffer size of {@code 512} and a {@code refillBatch} of 46 | * 16. 47 | */ 48 | public BufferedAggregateEventStream() { 49 | this(512, 16); 50 | } 51 | 52 | /** 53 | * Constructs a {@link BufferedAggregateEventStream} with the given {@code bufferSize}. 54 | * 55 | * @param bufferSize the buffer size of the aggregate event stream 56 | */ 57 | public BufferedAggregateEventStream(int bufferSize, int refillBatch) { 58 | super("unused", bufferSize, refillBatch); 59 | } 60 | 61 | @Override 62 | public Event next() throws InterruptedException { 63 | Event taken; 64 | if (peeked != null) { 65 | taken = peeked; 66 | peeked = null; 67 | } else { 68 | taken = take(); 69 | } 70 | if (taken != null) { 71 | lastSequenceNumber = taken.getAggregateSequenceNumber(); 72 | } 73 | return taken; 74 | } 75 | 76 | @Override 77 | public boolean hasNext() { 78 | if (peeked != null) { 79 | return true; 80 | } 81 | try { 82 | peeked = tryTake(TAKE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS, true); 83 | } catch (InterruptedException e) { 84 | cancel(); 85 | Thread.currentThread().interrupt(); 86 | return false; 87 | } catch (StreamTimeoutException e) { 88 | throw new RuntimeException(String.format( 89 | "Was unable to load aggregate due to timeout while waiting for events. Last sequence number received: %d", 90 | lastSequenceNumber 91 | ), e); 92 | } 93 | if (peeked == null) { 94 | Throwable errorResult = getErrorResult(); 95 | if (errorResult != null) { 96 | throw new StreamClosedException(errorResult); 97 | } 98 | } 99 | return peeked != null; 100 | } 101 | 102 | @Override 103 | public void cancel() { 104 | outboundStream().cancel("Request cancelled by client", null); 105 | } 106 | 107 | @Override 108 | protected GetAggregateEventsRequest buildFlowControlMessage(FlowControl flowControl) { 109 | // no app-level flow control available on this request 110 | return null; 111 | } 112 | 113 | @Override 114 | protected Event terminalMessage() { 115 | return TERMINAL_MESSAGE; 116 | } 117 | 118 | } 119 | --------------------------------------------------------------------------------