├── pytest.ini ├── requirements.txt ├── sources ├── package │ └── aeronpy │ │ ├── __init__.py │ │ ├── data.py │ │ ├── archive.py │ │ ├── context.py │ │ ├── publication.py │ │ ├── subscription.py │ │ ├── exclusive_publication.py │ │ ├── exclusive_publication.pyi │ │ ├── publication.pyi │ │ ├── subscription.pyi │ │ ├── data.pyi │ │ ├── context.pyi │ │ ├── archive.pyi │ │ ├── _exclusive_publication.hpp │ │ ├── _data.cpp │ │ ├── _context.hpp │ │ ├── _data.hpp │ │ ├── _subscription.hpp │ │ ├── _publication.hpp │ │ ├── _publication.cpp │ │ ├── _exclusive_publication.cpp │ │ ├── _subscription.cpp │ │ ├── _archive.hpp │ │ ├── _context.cpp │ │ └── _archive.cpp └── CMakeLists.txt ├── PKG-INFO ├── .gitmodules ├── tests ├── package │ └── aeronpy │ │ ├── archive.properties │ │ ├── exclusive_publication_test.py │ │ ├── publication_test.py │ │ ├── archive_test.py │ │ ├── context_test.py │ │ └── subscription_test.py └── CMakeLists.txt ├── samples ├── basic_subscriber.py └── basic_publisher.py ├── CMakeLists.txt ├── external └── CMakeLists.txt ├── README.md ├── setup.py └── LICENSE /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -d -n1 3 | testpaths = tests/package 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest==3.8.2 2 | pytest-xdist==1.24.0 3 | PyHamcrest==1.9.0 4 | Cython==0.29 5 | aeron-python-driver==1.11.2 6 | twine 7 | wheel 8 | 9 | -------------------------------------------------------------------------------- /sources/package/aeronpy/__init__.py: -------------------------------------------------------------------------------- 1 | from .archive import * 2 | from .context import * 3 | from .data import * 4 | from .exclusive_publication import * 5 | from .publication import * 6 | from .subscription import * 7 | from pkgutil import extend_path 8 | 9 | __path__ = extend_path(__path__, __name__) 10 | -------------------------------------------------------------------------------- /PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.0 2 | Name: aeron_python 3 | Version: 1.0.0 4 | Summary: Python bindings for Aeron 5 | Home-page: https://github.com/fairtide/aeron-python 6 | Author: Fairtide Pte 7 | Author-email: UNKNOWN 8 | License: Apache 2.0 9 | Description: Python binfings for Aeron messaging 10 | Platform: UNKNOWN 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "external/pybind11"] 2 | path = external/pybind11 3 | url = https://github.com/pybind/pybind11.git 4 | [submodule "external/aeron"] 5 | path = external/aeron 6 | url = https://github.com/real-logic/aeron.git 7 | [submodule "external/aeron-archive-client"] 8 | path = external/aeron-archive-client 9 | url = https://github.com/fairtide/aeron-archive-client.git 10 | [submodule "external/fmt"] 11 | path = external/fmt 12 | url = https://github.com/fmtlib/fmt.git 13 | -------------------------------------------------------------------------------- /tests/package/aeronpy/archive.properties: -------------------------------------------------------------------------------- 1 | aeron.archive.control.channel=aeron:udp?endpoint=localhost:8010 2 | aeron.archive.control.mtu.length=1408 3 | aeron.archive.control.response.channel=aeron:udp?endpoint=localhost:22335 4 | aeron.archive.control.response.stream.id=20 5 | aeron.archive.control.stream.id=10 6 | aeron.archive.control.term.buffer.length=65536 7 | aeron.archive.control.term.buffer.sparse=true 8 | aeron.archive.local.control.channel=aeron:ipc 9 | aeron.archive.local.control.stream.id=11 10 | aeron.archive.message.timeout=2000000000 11 | aeron.archive.recording.events.channel=aeron:udp?endpoint=localhost:22336 12 | aeron.archive.recording.events.stream.id=30 13 | -------------------------------------------------------------------------------- /sources/package/aeronpy/data.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Fairtide Pte. Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from _data import * -------------------------------------------------------------------------------- /sources/package/aeronpy/archive.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Fairtide Pte. Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from _archive import * -------------------------------------------------------------------------------- /sources/package/aeronpy/context.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Fairtide Pte. Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from _context import * -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_test(NAME run-tests 2 | COMMAND ${PYTHON_EXECUTABLE} -m pytest ${CMAKE_CURRENT_SOURCE_DIR} --junitxml=run-tests.xml -d -n1) 3 | 4 | set_property(TEST run-tests 5 | PROPERTY 6 | ENVIRONMENT "PYTHONPATH=$ENV{PYTHONPATH}:${CMAKE_CURRENT_SOURCE_DIR}/package:${CMAKE_SOURCE_DIR}/sources/package:${CMAKE_BINARY_DIR}/sources") 7 | set_property(TEST run-tests 8 | APPEND PROPERTY 9 | ENVIRONMENT "BINARY_DIR=${CMAKE_BINARY_DIR}") 10 | 11 | add_custom_target(install-dependencies 12 | COMMAND ${PYTHON_EXECUTABLE} -m pip install Cython 13 | COMMAND ${PYTHON_EXECUTABLE} -m pip install -r ${CMAKE_SOURCE_DIR}/requirements.txt) 14 | -------------------------------------------------------------------------------- /sources/package/aeronpy/publication.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Fairtide Pte. Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from _publication import * -------------------------------------------------------------------------------- /sources/package/aeronpy/subscription.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Fairtide Pte. Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from _subscription import * -------------------------------------------------------------------------------- /sources/package/aeronpy/exclusive_publication.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Fairtide Pte. Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from _exclusive_publication import * -------------------------------------------------------------------------------- /sources/package/aeronpy/exclusive_publication.pyi: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Fairtide Pte. Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Union, NoReturn 16 | 17 | 18 | class ExclusivePublication: 19 | channel = ... # type: str 20 | stream_id = ... # type: int 21 | 22 | def offer(self, data: Union[bytearray, str]) -> NoReturn: ... 23 | def close(self) -> NoReturn: ... 24 | 25 | def __bool__(self) -> bool: ... -------------------------------------------------------------------------------- /sources/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include_directories(${PYTHON_INCLUDE_DIRS}) 2 | include_directories(${PYBIND11_INCLUDE_DIRS}) 3 | include_directories(${FMT_INCLUDE_DIRS}) 4 | include_directories(${AERON_INCLUDE_DIRS}) 5 | include_directories(${AERON_ARCHIVE_CLIENT_INCLUDE_DIRS}) 6 | 7 | set(CMAKE_SHARED_MODULE_PREFIX) 8 | 9 | set(modules_SRC "") 10 | file(GLOB_RECURSE modules_SRC "*.cpp") 11 | 12 | foreach(module_file ${modules_SRC}) 13 | get_filename_component (name_without_extension ${module_file} NAME_WE) 14 | add_library(${name_without_extension} MODULE 15 | ${modules_SRC}) 16 | 17 | target_link_libraries(${name_without_extension} 18 | ${PYTHON_LIBRARIES} 19 | ${SYSCALL_INTERCEPT_LIBRARIES} 20 | ${CMAKE_THREAD_LIBS_INIT} 21 | ${RT_LIBRARY} 22 | ${FMT_LIBRARIES} 23 | ${AERON_LIBRARIES} 24 | ${AERON_ARCHIVE_CLIENT_LIBRARIES}) 25 | 26 | add_dependencies(${name_without_extension} 27 | fmt 28 | aeron 29 | aeron-archive-client) 30 | 31 | list(APPEND modules ${name_without_extension}) 32 | endforeach() -------------------------------------------------------------------------------- /sources/package/aeronpy/publication.pyi: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Fairtide Pte. Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Union, NoReturn 16 | 17 | 18 | class Publication: 19 | channel = ... # type: str 20 | stream_id = ... # type: int 21 | session_id = ... # type: int 22 | 23 | is_connected = ... # type: bool 24 | is_closed = ... # type: bool 25 | is_original = ... # type: bool 26 | 27 | 28 | def offer(self, data: Union[bytearray, str]) -> NoReturn: ... 29 | def close(self) -> NoReturn: ... 30 | 31 | def __bool__(self) -> bool: ... 32 | def __str__(self)-> str: ... -------------------------------------------------------------------------------- /sources/package/aeronpy/subscription.pyi: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Fairtide Pte. Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import Callable, List, NoReturn, Union 16 | from .data import Header, Image 17 | 18 | 19 | class Subscription: 20 | channel = ... # type: str 21 | stream_id = ... # type: int 22 | 23 | is_connected = ... # type: bool 24 | is_closed = ... # type: bool 25 | 26 | images = ... # type: List[Image] 27 | 28 | def poll(self, handler: Union[Callable[[memoryview], NoReturn], Callable[[memoryview, Header], NoReturn]], fragment_limit: int=10) -> int: ... 29 | def poll_eos(self) -> int: ... 30 | 31 | -------------------------------------------------------------------------------- /sources/package/aeronpy/data.pyi: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Fairtide Pte. Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import NoReturn 16 | 17 | 18 | class Image: 19 | session_id = ... # type: int 20 | 21 | is_end_of_stream = ... # type: bool 22 | is_closed = ... # type: bool 23 | 24 | def close(self) -> NoReturn: ... 25 | 26 | 27 | class Header: 28 | stream_id = ... # type: int 29 | session_id = ... # type: int 30 | 31 | def __str__(self) -> str: ... 32 | 33 | 34 | NOT_CONNECTED = ... # type: int 35 | BACK_PRESSURED = ... # type: int 36 | ADMIN_ACTION = ... # type: int 37 | PUBLICATION_CLOSED = ... # type: int 38 | MAX_POSITION_EXCEEDED = ... # type: int -------------------------------------------------------------------------------- /samples/basic_subscriber.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from argparse import ArgumentParser, ArgumentError 3 | from time import sleep 4 | 5 | from aeronpy import Context 6 | 7 | 8 | def main(): 9 | try: 10 | parser = ArgumentParser() 11 | parser.add_argument('-p', '--prefix', default=None) 12 | parser.add_argument('-c', '--channel', default='aeron:udp?endpoint=localhost:40123') 13 | parser.add_argument('-s', '--stream_id', type=int, default=1) 14 | 15 | args = parser.parse_args() 16 | context = Context( 17 | aeron_dir=args.prefix, 18 | new_subscription_handler=lambda *args: print(f'new subscription {args}'), 19 | available_image_handler=lambda *args: print(f'available image {args}'), 20 | unavailable_image_handler=lambda *args: print(f'unavailable image {args}')) 21 | 22 | subscription = context.add_subscription(args.channel, args.stream_id) 23 | while True: 24 | fragments_read = subscription.poll(lambda data: print(bytes(data))) 25 | if fragments_read == 0: 26 | eos_count = subscription.poll_eos(lambda *args: print(f'end of stream: {args}')) 27 | if eos_count > 0: 28 | break 29 | 30 | sleep(0.1) 31 | 32 | return 0 33 | 34 | except ArgumentError as e: 35 | print(e, file=sys.stderr) 36 | return -1 37 | except Exception as e: 38 | print(e, file=sys.stderr) 39 | return -2 40 | 41 | 42 | if __name__ == '__main__': 43 | exit(main()) 44 | -------------------------------------------------------------------------------- /sources/package/aeronpy/context.pyi: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Fairtide Pte. Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from datetime import timedelta 16 | from typing import Callable, NoReturn 17 | from .data import Image 18 | from .exclusive_publication import ExclusivePublication 19 | from .publication import Publication 20 | from .subscription import Subscription 21 | 22 | 23 | class Context: 24 | def __init__(self, 25 | aeron_dir: str=None, 26 | media_driver_timeout: timedelta=None, 27 | resource_linger_timeout: timedelta=None, 28 | user_conductor_agent_invoker: bool=False, 29 | error_handler: Callable[[Exception], NoReturn]=None, 30 | new_publication_handler: Callable[[str, int, int, int], NoReturn]=None, 31 | new_exclusive_publication_handler: Callable[[str, int, int, int], NoReturn]=None, 32 | new_subscription_handler: Callable[[str, int, int], NoReturn]=None, 33 | available_image_handler: Callable[[Image], NoReturn]=None, 34 | unavailable_image_handler: Callable[[Image], NoReturn]=None, 35 | **kwargs) -> NoReturn: ... 36 | 37 | def add_subscription(self, channel: str, stream_id: int) -> Subscription: ... 38 | def add_publication(self, channel: str, stream_id: int) -> Publication: ... 39 | def add_exclusive_publication(self, channel: str, stream_id: int) -> ExclusivePublication: ... -------------------------------------------------------------------------------- /samples/basic_publisher.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from argparse import ArgumentParser, ArgumentError 3 | from datetime import timedelta 4 | from time import sleep 5 | 6 | from aeronpy import Context, BACK_PRESSURED, NOT_CONNECTED, ADMIN_ACTION, PUBLICATION_CLOSED 7 | 8 | 9 | def main(): 10 | try: 11 | parser = ArgumentParser() 12 | parser.add_argument('-p', '--prefix', default=None) 13 | parser.add_argument('-c', '--channel', default='aeron:udp?endpoint=localhost:40123') 14 | parser.add_argument('-s', '--stream_id', type=int, default=1) 15 | parser.add_argument('-m', '--messages', type=int, default=sys.maxsize) 16 | parser.add_argument('-l', '--linger', type=int, default=60*60*1000) 17 | 18 | args = parser.parse_args() 19 | context = Context( 20 | aeron_dir=args.prefix, 21 | resource_linger_timeout=timedelta(milliseconds=args.linger), 22 | new_publication_handler=lambda *args: print(f'new publication - {args}')) 23 | 24 | publication = context.add_publication(args.channel, args.stream_id) 25 | for i in range(args.messages): 26 | result = publication.offer(f'Hallo World! {i}') 27 | if result == BACK_PRESSURED: 28 | print('Offer failed due to back pressure') 29 | elif result == NOT_CONNECTED: 30 | print('Offer failed because publisher is not connected to subscriber') 31 | elif result == ADMIN_ACTION: 32 | print('Offer failed because of an administration action in the system') 33 | elif result == PUBLICATION_CLOSED: 34 | print('Offer failed publication is closed') 35 | else: 36 | print('yay!') 37 | 38 | sleep(1) 39 | 40 | return 0 41 | 42 | except ArgumentError as e: 43 | print(e, file=sys.stderr) 44 | return -1 45 | except Exception as e: 46 | print(e, file=sys.stderr) 47 | return -2 48 | 49 | 50 | if __name__ == '__main__': 51 | exit(main()) 52 | -------------------------------------------------------------------------------- /sources/package/aeronpy/archive.pyi: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Fairtide Pte. Ltd. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import NoReturn, Optional 16 | from .exclusive_publication import ExclusivePublication 17 | from .publication import Publication 18 | from .subscription import Subscription 19 | 20 | 21 | class Recording: 22 | id = ... # type: int 23 | position = ... # type: int 24 | 25 | def replay(self, channel: str, stream_id: int, position: int = 0) -> Subscription: ... 26 | def truncate(self, position: int) -> NoReturn: ... 27 | 28 | 29 | class Archive: 30 | def __init__( 31 | config_file: str=None, 32 | aeron_dir: str=None, 33 | message_timeout_ns: int=None, 34 | control_request_channel: str=None, 35 | control_request_stream_id: int=None, 36 | control_response_channel: str=None, 37 | control_response_stream_id: int=None, 38 | recording_events_channel: str=None, 39 | recording_events_stream_id: int=None, 40 | control_term_buffer_sparse: bool=None, 41 | control_term_buffer_length: int=None, 42 | control_mtu_length: int=None) -> NoReturn: ... 43 | 44 | def find(self, recording_id: int) -> Optional[Recording]: ... 45 | def find_last(self, channel: str, stream_id: int) -> Optional[Recording]: ... 46 | 47 | def add_recorded_publication(self, channel: str, stream_id: int) -> Publication: ... 48 | def add_recorded_exclusive_publication(self, channel: str, stream_id: int) -> ExclusivePublication: ... 49 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.7) 2 | project(aeron-python) 3 | 4 | set(PYTHON_VERSION 3 CACHE STRING "Target version of python") 5 | message(STATUS "Requested python version: ${PYTHON_VERSION}") 6 | 7 | set(RELEASE_VERSION unknown CACHE STRING "Target release version") 8 | message(STATUS "Release version: ${RELEASE_VERSION}") 9 | 10 | # register modules 11 | include(CTest) 12 | include(ExternalProject) 13 | 14 | set(CMAKE_CXX_STANDARD 14) 15 | add_definitions(-Wno-unknown-attributes -fvisibility=hidden -fPIC) 16 | 17 | if(${CMAKE_SYSTEM_NAME} MATCHES "Linux") 18 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pthread") 19 | set(RT_LIBRARY "rt") 20 | endif() 21 | 22 | # find boost 23 | find_package(Boost COMPONENTS system REQUIRED) 24 | include_directories(${Boost_INCLUDE_DIRS}) 25 | 26 | # find python 27 | find_package(PythonInterp ${PYTHON_VERSION}) 28 | find_package(PythonLibs ${PYTHON_VERSION}) 29 | 30 | if (PYTHONINTERP_FOUND) 31 | message(STATUS "Found Python interpreter: ${PYTHON_VERSION_STRING}") 32 | else () 33 | message(FATAL_ERROR "Python not found!") 34 | endif () 35 | 36 | if (PYTHONLIBS_FOUND) 37 | message(STATUS "Found Python libs: ${PYTHONLIBS_VERSION_STRING} in ${PYTHON_LIBRARIES}") 38 | else () 39 | message(FATAL_ERROR "Python dev libs not found!") 40 | endif () 41 | 42 | # setup CMake to run tests 43 | enable_testing() 44 | 45 | # include subdirectories 46 | add_subdirectory(external) 47 | add_subdirectory(sources) 48 | add_subdirectory(tests) 49 | 50 | # define package build targets 51 | add_custom_target(build-package 52 | COMMAND 53 | tar -cvf ${CMAKE_BINARY_DIR}/aeron_python.tar 54 | sources 55 | external 56 | tests 57 | CMakeLists.txt 58 | LICENSE 59 | README.md 60 | setup.py 61 | PKG-INFO 62 | COMMAND gzip ${CMAKE_BINARY_DIR}/aeron_python.tar 63 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) 64 | 65 | 66 | add_custom_target(build-wheel 67 | COMMAND 68 | ${CMAKE_COMMAND} -E env 69 | CMAKE=${CMAKE_COMMAND} 70 | CC=${CMAKE_C_COMPILER} 71 | CXX=${CMAKE_CXX_COMPILER} 72 | ${PYTHON_EXECUTABLE} setup.py bdist_wheel -d ${CMAKE_BINARY_DIR} -b ${CMAKE_BINARY_DIR}/wheel_build 73 | WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") 74 | -------------------------------------------------------------------------------- /sources/package/aeronpy/_exclusive_publication.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Fairtide Pte. Ltd. 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 | #pragma once 18 | 19 | #include 20 | #include 21 | 22 | #include 23 | 24 | 25 | /** 26 | * @brief Represents interop proxy for Aeron exclusive publication. 27 | */ 28 | class exclusive_publication final 29 | { 30 | public: 31 | /** 32 | * @brief 33 | * @param aeron_exclusive_publication 34 | */ 35 | exclusive_publication(std::shared_ptr aeron_exclusive_publication); 36 | 37 | /** 38 | * @brief 39 | * @return 40 | */ 41 | const std::string& channel() const; 42 | /** 43 | * @brief 44 | * @return 45 | */ 46 | int64_t stream_id() const; 47 | /** 48 | * 49 | * @return 50 | */ 51 | int32_t session_id() const; 52 | /** 53 | * 54 | * @return 55 | */ 56 | int32_t initial_term_id() const; 57 | /** 58 | * @brief 59 | * @return 60 | */ 61 | bool is_connected() const; 62 | /** 63 | * @brief 64 | * @return 65 | */ 66 | bool is_closed() const; 67 | /** 68 | * @brief 69 | * @return 70 | */ 71 | bool is_original() const; 72 | 73 | /** 74 | * Offers a data block to open stream this publisher is for. 75 | * @param data Data block to be offered to the stream. 76 | * @return Number of bytes sent or BACK_PRESSURED or NOT_CONNECTED. 77 | */ 78 | int64_t offer(pybind11::object data); 79 | 80 | /** 81 | * @brief 82 | */ 83 | void close(); 84 | 85 | /** 86 | * @brief 87 | * @return 88 | */ 89 | bool __bool__() const; 90 | /** 91 | * @brief 92 | * @return 93 | */ 94 | std::string __str__() const; 95 | 96 | private: 97 | std::shared_ptr aeron_exclusive_publication_; 98 | 99 | }; 100 | -------------------------------------------------------------------------------- /sources/package/aeronpy/_data.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Fairtide Pte. Ltd. 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 | #include "_data.hpp" 18 | 19 | #include 20 | #include 21 | 22 | using namespace std; 23 | using namespace aeron; 24 | using namespace fmt; 25 | namespace py = pybind11; 26 | 27 | int32_t image::session_id(Image& self) 28 | { 29 | return self.sessionId(); 30 | } 31 | 32 | bool image::is_end_of_stream(Image& self) 33 | { 34 | return self.isEndOfStream(); 35 | } 36 | 37 | bool image::is_closed(Image& self) 38 | { 39 | return self.isClosed(); 40 | } 41 | 42 | void image::close(Image& self) 43 | { 44 | return self.close(); 45 | } 46 | 47 | string image::__str__(Image& self) 48 | { 49 | return format("image: session_id:[{}]", self.sessionId()); 50 | } 51 | 52 | int32_t header::stream_id(Header& self) 53 | { 54 | return self.streamId(); 55 | } 56 | 57 | int32_t header::session_id(Header& self) 58 | { 59 | return self.sessionId(); 60 | } 61 | 62 | string header::__str__(Header& self) 63 | { 64 | return format("header: stream_id:[{}] session_id:[{}]", self.streamId(), self.sessionId()); 65 | } 66 | 67 | PYBIND11_MODULE(_data, m) 68 | { 69 | py::class_(m, "Image") 70 | .def_property_readonly("session_id", &image::session_id) 71 | .def_property_readonly("is_end_of_stream", &image::is_end_of_stream) 72 | .def_property_readonly("is_closed", &image::is_closed) 73 | .def("close", &image::close) 74 | .def("__str__", &image::__str__); 75 | 76 | py::class_
(m, "Header") 77 | .def_property_readonly("stream_id", &header::session_id) 78 | .def_property_readonly("session_id", &header::session_id) 79 | .def("__str__", &header::__str__); 80 | 81 | m.attr("NOT_CONNECTED") = NOT_CONNECTED; 82 | m.attr("BACK_PRESSURED") = BACK_PRESSURED; 83 | m.attr("ADMIN_ACTION") = ADMIN_ACTION; 84 | m.attr("PUBLICATION_CLOSED") = PUBLICATION_CLOSED; 85 | m.attr("MAX_POSITION_EXCEEDED") = MAX_POSITION_EXCEEDED; 86 | 87 | } -------------------------------------------------------------------------------- /sources/package/aeronpy/_context.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Fairtide Pte. Ltd. 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 | #pragma once 18 | 19 | #include "_exclusive_publication.hpp" 20 | #include "_publication.hpp" 21 | #include "_subscription.hpp" 22 | 23 | #include 24 | #include 25 | 26 | #include 27 | #include 28 | #include 29 | 30 | 31 | /** 32 | * @brief Provides a helper for exposing Aeron to python. 33 | */ 34 | class context final 35 | { 36 | public: 37 | /** 38 | * @brief Creates a new instance of Aeron interop context. 39 | * @details 40 | * This constructor creates and configures underlying Aeron client. Configuration options can be passed 41 | * through kwargs. 42 | */ 43 | explicit context(pybind11::kwargs args); 44 | 45 | /** 46 | * @brief Adds and waits for initialisation of a subscription. 47 | * @param channel Subscribed channel identifier. 48 | * @param stream_id Subscribed stream number. 49 | * @return An interop proxy for added subscription. 50 | */ 51 | subscription add_subscription(const std::string& channel, int32_t stream_id); 52 | /** 53 | * @brief Adds and waits for initialisation of a publication. 54 | * @param channel Channel this publication should be on. 55 | * @param stream_id Id of stream this publication should be on. 56 | * @return An interop proxy for added publication. 57 | */ 58 | publication add_publication(const std::string& channel, int32_t stream_id); 59 | /** 60 | * @brief Adds and waits for initialisation of an exclusive publication. 61 | * @details 62 | * Added publication will have unique session. 63 | * @param channel Channel the publication should be on. 64 | * @param stream_id Id of stream this publication should be on. 65 | * @return An interop proxy for added publication. 66 | */ 67 | exclusive_publication add_exclusive_publication(const std::string& channel, int32_t stream_id); 68 | 69 | private: 70 | aeron::Context aeron_context_; 71 | std::shared_ptr aeron_instance_; 72 | 73 | }; -------------------------------------------------------------------------------- /sources/package/aeronpy/_data.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Fairtide Pte. Ltd. 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 | #pragma once 18 | 19 | #include 20 | 21 | #include 22 | 23 | 24 | /** 25 | * @brief Helper for interop with Aeron Image type. 26 | */ 27 | class image final 28 | { 29 | public: 30 | /** 31 | * @brief Gets session id. 32 | * @param self An instance of **aeron::Image**. 33 | * @return Session id associated with the image. 34 | */ 35 | static int32_t session_id(aeron::Image& self); 36 | /** 37 | * @brief Indicates if stream related to the image ended. 38 | * @param self An instance of **aeron::Image**. 39 | * @return True if stream ended, false otherwise. 40 | */ 41 | static bool is_end_of_stream(aeron::Image& self); 42 | /** 43 | * @brief Indicates if the image is closed. 44 | * @param self An instance of **aeron::Image**. 45 | * @return True if image is closed, false otherwise. 46 | */ 47 | static bool is_closed(aeron::Image& self); 48 | /** 49 | * @brief Closes the image and it's session. 50 | * @param self An instance of **aeron::Image**. 51 | */ 52 | static void close(aeron::Image& self); 53 | 54 | /** 55 | * @brief Produces debug textual representation of the image. 56 | * @param self An instance of **aeron::Image**. 57 | * @return Debug textual representation of the image. 58 | */ 59 | static std::string __str__(aeron::Image& self); 60 | 61 | }; 62 | 63 | /** 64 | * @brief A helper for interop with Aeron Header type. 65 | */ 66 | class header final 67 | { 68 | public: 69 | /** 70 | * @brief Gets stream id. 71 | * @param self An instance of **aeron::Header** this method will be affixed to. 72 | * @return Stream id. 73 | */ 74 | static int32_t stream_id(aeron::Header& self); 75 | /** 76 | * @brief Gets session id. 77 | * @param self An instance of **aeron::Header** this method will be affixed to. 78 | * @return Session id. 79 | */ 80 | static int32_t session_id(aeron::Header& self); 81 | 82 | /** 83 | * @brief Produces debug textual representation of the header. 84 | * @param self An instance of **aeron::Header** this method will be affixed to. 85 | * @return Debug textual representation of the header. 86 | */ 87 | static std::string __str__(aeron::Header& self); 88 | 89 | }; 90 | -------------------------------------------------------------------------------- /sources/package/aeronpy/_subscription.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Fairtide Pte. Ltd. 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 | #pragma once 18 | 19 | #include 20 | #include 21 | 22 | #include 23 | #include 24 | #include 25 | 26 | 27 | /** 28 | * @brief Represents an interop proxy for an Aeron subscription. 29 | */ 30 | class subscription final 31 | { 32 | public: 33 | /** 34 | * @brief Creates a new instance of proxy wrapping given aeron subscription. 35 | * @param aeron_subscription Aeron subscription to wrap. 36 | */ 37 | subscription(std::shared_ptr aeron_subscription); 38 | 39 | /** 40 | * @brief Gets channel definition this subscription is for. 41 | * @return Channel definition this subscription is for. 42 | */ 43 | const std::string& channel() const; 44 | /** 45 | * @brief Gets stream identifier this subscription is for. 46 | * @return Stream identifier this subscription is for. 47 | */ 48 | int64_t stream_id() const; 49 | /** 50 | * @brief Indicates if subscription is connected to publisher or not. 51 | * @return True if connected, false otherwise. 52 | */ 53 | bool is_connected() const; 54 | /** 55 | * @brief 56 | * @return 57 | */ 58 | bool is_closed() const; 59 | 60 | /** 61 | * @brief Gets a list of images this subscription currently handles. 62 | * @details 63 | * Each image represents a different session within the channel/stream. 64 | * 65 | * @return A list of images. 66 | */ 67 | std::vector images() const; 68 | 69 | /** 70 | * @brief 71 | * @param handler 72 | * @param fragment_limit 73 | * @return 74 | */ 75 | int poll(pybind11::function handler, int fragment_limit); 76 | /** 77 | * @brief 78 | * @return 79 | */ 80 | int poll_eos(pybind11::object handler); 81 | 82 | /** 83 | * @brief 84 | * @return 85 | */ 86 | bool __bool__() const; 87 | /** 88 | * @brief Produces debug, textual representation of this subscription. 89 | * @return Debug, textual representation of this subscription. 90 | */ 91 | std::string __str__() const; 92 | 93 | private: 94 | bool is_complete_poll_handler(pybind11::function& handler); 95 | 96 | std::shared_ptr aeron_subscription_; 97 | 98 | }; 99 | -------------------------------------------------------------------------------- /sources/package/aeronpy/_publication.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Fairtide Pte. Ltd. 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 | #pragma once 18 | 19 | #include 20 | #include 21 | 22 | #include 23 | #include 24 | 25 | 26 | /** 27 | * @brief Represents interop proxy for Aeron publication. 28 | */ 29 | class publication final 30 | { 31 | public: 32 | /** 33 | * @brief Creates proxy object for given Aeron publication. 34 | * @param aeron_publication A pointer to base Aeron publication. 35 | */ 36 | publication(std::shared_ptr aeron_publication); 37 | 38 | /** 39 | * @brief Gets channel name this publisher is for. 40 | * @return Channel name. 41 | */ 42 | const std::string& channel() const; 43 | /** 44 | * @brief Gets stream id this publisher is for. 45 | * @return Stream id. 46 | */ 47 | int64_t stream_id() const; 48 | /** 49 | * @brief Gets session id this publisher is for. 50 | * @return Session id. 51 | */ 52 | int32_t session_id() const; 53 | /** 54 | * @brief 55 | * @return 56 | */ 57 | int32_t initial_term_id() const; 58 | /** 59 | * @brief Indicates if publication is connected to media driver. 60 | * @return True if connected to media driver, False otherwise. 61 | */ 62 | bool is_connected() const; 63 | /** 64 | * @brief Indicates if the underlying publication is closed. 65 | * @return True if closed, False otherwise. 66 | */ 67 | bool is_closed() const; 68 | /** 69 | * @brief Indicates if the publication is original publication. 70 | * @return True if publication is original, false otherwise. 71 | */ 72 | bool is_original() const; 73 | 74 | /** 75 | * Offers a data block to open stream this publisher is for. 76 | * @param data Data block to be offered to the stream. 77 | * @return Number of bytes sent or BACK_PRESSURED or NOT_CONNECTED. 78 | */ 79 | int64_t offer(pybind11::object data); 80 | /** 81 | * @brief Closes the underlying publication. 82 | */ 83 | void close(); 84 | 85 | /** 86 | * @brief Checks state of the publisher. 87 | * @return True if publisher is connected and is ready to offer data. 88 | */ 89 | bool __bool__() const; 90 | /** 91 | * @brief Provides debug description of publication. 92 | * @return Debug description of publication. 93 | */ 94 | std::string __str__() const; 95 | 96 | private: 97 | std::shared_ptr aeron_publication_; 98 | 99 | }; 100 | -------------------------------------------------------------------------------- /external/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # pybind11 2 | set(PYBIND11_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/pybind11/include PARENT_SCOPE) 3 | 4 | # fmt 5 | ExternalProject_Add(fmt 6 | SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/fmt 7 | CMAKE_ARGS 8 | -DCMAKE_BUILD_TYPE=Release 9 | -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} 10 | -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} 11 | -DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM} 12 | -DCMAKE_GENERATOR=${CMAKE_GENERATOR} 13 | -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS} 14 | -DCMAKE_EXE_LINKER_FLAGS=${CMAKE_EXE_LINKER_FLAGS} 15 | -DCMAKE_POSITION_INDEPENDENT_CODE=ON 16 | BUILD_COMMAND ${CMAKE_COMMAND} --build . --target fmt 17 | INSTALL_COMMAND "" 18 | BUILD_BYPRODUCTS 19 | "${CMAKE_CURRENT_BINARY_DIR}/fmt-prefix/src/fmt-build/libfmt.a") 20 | 21 | ExternalProject_Get_Property(fmt SOURCE_DIR) 22 | set(FMT_INCLUDE_DIRS ${SOURCE_DIR}/include PARENT_SCOPE) 23 | 24 | ExternalProject_Get_Property(fmt BINARY_DIR) 25 | message(STATUS ${BINARY_DIR}) 26 | set(FMT_LIBRARIES ${BINARY_DIR}/libfmt.a PARENT_SCOPE) 27 | 28 | # aeron 29 | ExternalProject_Add(aeron 30 | SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/aeron 31 | CMAKE_ARGS 32 | -DCMAKE_BUILD_TYPE=Release 33 | -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} 34 | -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} 35 | -DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM} 36 | -DCMAKE_GENERATOR=${CMAKE_GENERATOR} 37 | -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS} 38 | -DCMAKE_EXE_LINKER_FLAGS=${CMAKE_EXE_LINKER_FLAGS} 39 | -DCMAKE_POSITION_INDEPENDENT_CODE=ON 40 | BUILD_COMMAND ${CMAKE_COMMAND} --build . --target aeron_client 41 | INSTALL_COMMAND "" 42 | BUILD_BYPRODUCTS 43 | "${CMAKE_CURRENT_BINARY_DIR}/aeron-prefix/src/aeron-build/lib/libaeron_client.a") 44 | 45 | ExternalProject_Get_Property(aeron SOURCE_DIR) 46 | set(AERON_INCLUDE_DIRS ${SOURCE_DIR}/aeron-client/src/main/cpp PARENT_SCOPE) 47 | 48 | ExternalProject_Get_Property(aeron BINARY_DIR) 49 | set(AERON_LIBRARIES "${BINARY_DIR}/lib/libaeron_client.a" PARENT_SCOPE) 50 | 51 | # aeron archive client 52 | ExternalProject_Add(aeron-archive-client 53 | SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/aeron-archive-client 54 | CMAKE_ARGS 55 | -DCMAKE_BUILD_TYPE=Release 56 | -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} 57 | -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} 58 | -DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM} 59 | -DCMAKE_GENERATOR=${CMAKE_GENERATOR} 60 | -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS} 61 | -DCMAKE_EXE_LINKER_FLAGS=${CMAKE_EXE_LINKER_FLAGS} 62 | -DCMAKE_POSITION_INDEPENDENT_CODE=ON 63 | -DBOOST_ROOT=${BOOST_ROOT} 64 | BUILD_COMMAND ${CMAKE_COMMAND} --build . --target aeron_archive_client 65 | INSTALL_COMMAND "" 66 | BUILD_BYPRODUCTS 67 | "${CMAKE_CURRENT_BINARY_DIR}/aeron-archive-client-prefix/src/aeron-archive-client-build/src/libaeron_archive_client.a") 68 | 69 | ExternalProject_Get_Property(aeron-archive-client SOURCE_DIR) 70 | ExternalProject_Get_Property(aeron-archive-client BINARY_DIR) 71 | set(AERON_ARCHIVE_CLIENT_INCLUDE_DIRS 72 | "${SOURCE_DIR}/src" 73 | "${BINARY_DIR}/src/sbe" 74 | "${BINARY_DIR}/deps/sbe/src/sbe_project/sbe-tool/src/main/cpp" PARENT_SCOPE) 75 | set(AERON_ARCHIVE_CLIENT_LIBRARIES "${BINARY_DIR}/src/libaeron_archive_client.a" PARENT_SCOPE) 76 | 77 | -------------------------------------------------------------------------------- /tests/package/aeronpy/exclusive_publication_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | from time import sleep 3 | from hamcrest import * 4 | from pytest import fixture 5 | from tempfile import _get_candidate_names as temp_dir_candidates, tempdir 6 | from aeronpy import Context, NOT_CONNECTED, PUBLICATION_CLOSED 7 | from aeronpy.driver import media_driver 8 | 9 | 10 | @fixture(scope='module') 11 | def aeron_directory(): 12 | temp_dirs = temp_dir_candidates() 13 | 14 | where = os.path.join(tempdir, next(temp_dirs)) 15 | with media_driver.launch(aeron_directory_name=where): 16 | yield where 17 | 18 | 19 | @fixture() 20 | def stream_id(request): 21 | if 'stream_id' not in request.session.__dict__: 22 | request.session.stream_id = 1 23 | else: 24 | request.session.stream_id = request.session.stream_id + 1 25 | return request.session.stream_id 26 | 27 | 28 | @fixture() 29 | def context(aeron_directory): 30 | return Context(aeron_dir=aeron_directory) 31 | 32 | 33 | @fixture() 34 | def ipc_subscriber(context, stream_id): 35 | return context.add_subscription('aeron:ipc', stream_id) 36 | 37 | 38 | @fixture() 39 | def mcast_subscriber(context, stream_id): 40 | return context.add_subscription('aeron:udp?endpoint=224.0.1.1:40456', stream_id) 41 | 42 | 43 | def test_offer__ipc__no_subscribers(aeron_directory, stream_id): 44 | context = Context(aeron_dir=aeron_directory) 45 | publication = context.add_exclusive_publication('aeron:ipc', stream_id) 46 | assert_that(publication.is_connected, is_(False)) 47 | 48 | result = publication.offer(b'abc') 49 | assert_that(result, is_(equal_to(NOT_CONNECTED))) 50 | 51 | 52 | def test_offer__ipc__message_too_large(aeron_directory, ipc_subscriber, stream_id): 53 | context = Context(aeron_dir=aeron_directory) 54 | publication = context.add_exclusive_publication('aeron:ipc', stream_id) 55 | 56 | blob = bytearray(50 * 1024 * 1024) 57 | assert_that(calling(publication.offer).with_args(blob), raises(RuntimeError)) 58 | 59 | 60 | def test_offer__ipc__closed(aeron_directory, ipc_subscriber, stream_id): 61 | context = Context(aeron_dir=aeron_directory) 62 | publication = context.add_exclusive_publication('aeron:ipc', stream_id) 63 | publication.close() 64 | assert_that(publication.is_closed, is_(True)) 65 | 66 | result = publication.offer(b'abc') 67 | assert_that(result, is_(equal_to(PUBLICATION_CLOSED))) 68 | 69 | 70 | def test_offer__ipc(aeron_directory, ipc_subscriber, stream_id): 71 | context = Context(aeron_dir=aeron_directory) 72 | publication = context.add_exclusive_publication('aeron:ipc', stream_id) 73 | result = publication.offer(b'abc') 74 | assert_that(result, is_(greater_than_or_equal_to(0))) 75 | 76 | 77 | def test_offer__multicast(aeron_directory, mcast_subscriber, stream_id): 78 | context = Context(aeron_dir=aeron_directory) 79 | publication = context.add_exclusive_publication('aeron:udp?endpoint=224.0.1.1:40456|ttl=0', stream_id) 80 | sleep(0.5) 81 | 82 | result = publication.offer(b'abc') 83 | assert_that(result, is_(greater_than_or_equal_to(0))) 84 | 85 | 86 | def test_offer__multiple_publishers__same_stream(aeron_directory, ipc_subscriber, stream_id): 87 | context = Context(aeron_dir=aeron_directory) 88 | publication_1 = context.add_exclusive_publication('aeron:ipc', stream_id) 89 | publication_2 = context.add_exclusive_publication('aeron:ipc', stream_id) 90 | assert_that(publication_1.session_id, is_not(equal_to(publication_2.session_id))) 91 | 92 | result = publication_1.offer(b'abc') 93 | assert_that(result, is_(greater_than_or_equal_to(0))) 94 | 95 | result = publication_2.offer(b'def') 96 | assert_that(result, is_(greater_than_or_equal_to(0))) 97 | 98 | sleep(0.5) 99 | 100 | messages = list() 101 | ipc_subscriber.poll(lambda data: messages.append(data)) 102 | 103 | assert_that(messages, has_length(equal_to(2))) 104 | assert_that(bytes(messages[0]), is_(equal_to(b'abc'))) 105 | assert_that(bytes(messages[1]), is_(equal_to(b'def'))) 106 | 107 | -------------------------------------------------------------------------------- /sources/package/aeronpy/_publication.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Fairtide Pte. Ltd. 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 | #include "_publication.hpp" 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | using namespace std; 24 | using namespace aeron; 25 | using namespace fmt; 26 | namespace py = pybind11; 27 | 28 | 29 | publication::publication(shared_ptr aeron_publication) 30 | : 31 | aeron_publication_(aeron_publication) 32 | { 33 | 34 | } 35 | 36 | const string& publication::channel() const 37 | { 38 | return aeron_publication_->channel(); 39 | } 40 | 41 | int64_t publication::stream_id() const 42 | { 43 | return aeron_publication_->streamId(); 44 | } 45 | 46 | int32_t publication::session_id() const 47 | { 48 | return aeron_publication_->sessionId(); 49 | } 50 | 51 | int32_t publication::initial_term_id() const 52 | { 53 | return aeron_publication_->initialTermId(); 54 | } 55 | 56 | bool publication::is_connected() const 57 | { 58 | return aeron_publication_->isConnected(); 59 | } 60 | 61 | bool publication::is_closed() const 62 | { 63 | return aeron_publication_->isClosed(); 64 | } 65 | 66 | bool publication::is_original() const 67 | { 68 | return aeron_publication_->isOriginal(); 69 | } 70 | 71 | int64_t publication::offer(py::object data) 72 | { 73 | if (py::isinstance(data)) 74 | { 75 | py::buffer buffer = data; 76 | py::buffer_info info = buffer.request(false); 77 | 78 | AtomicBuffer aeron_buffer(reinterpret_cast(info.ptr), info.size * info.itemsize); 79 | return aeron_publication_->offer(aeron_buffer); 80 | } 81 | else if (py::isinstance(data)) 82 | { 83 | auto characters = data.cast(); 84 | 85 | AtomicBuffer aeron_buffer( 86 | const_cast( 87 | reinterpret_cast(characters.data())), characters.length()); 88 | return aeron_publication_->offer(aeron_buffer); 89 | } 90 | 91 | throw py::type_error("unsupported data type!"); 92 | } 93 | 94 | void publication::close() 95 | { 96 | aeron_publication_->close(); 97 | } 98 | 99 | bool publication::__bool__() const 100 | { 101 | return aeron_publication_ && aeron_publication_->isConnected(); 102 | } 103 | 104 | string publication::__str__() const 105 | { 106 | return format("publication: channel:[{}] stream_id:[{}] session_id:[{}]", 107 | aeron_publication_->channel(), 108 | aeron_publication_->streamId(), 109 | aeron_publication_->sessionId()); 110 | } 111 | 112 | PYBIND11_MODULE(_publication, m) 113 | { 114 | py::class_(m, "Publication") 115 | .def_property_readonly("channel", &publication::channel) 116 | .def_property_readonly("stream_id", &publication::stream_id) 117 | .def_property_readonly("session_id", &publication::session_id) 118 | .def_property_readonly("is_connected", &publication::is_connected) 119 | .def_property_readonly("is_closed", &publication::is_closed) 120 | .def_property_readonly("is_original", &publication::is_original) 121 | .def("offer", &publication::offer, 122 | py::arg("data")) 123 | .def("close", &publication::close) 124 | .def("__bool__", &publication::__bool__) 125 | .def("__str__", &publication::__str__); 126 | 127 | } 128 | 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 2 | [![image](https://img.shields.io/pypi/v/aeron-python.svg)](https://pypi.org/project/aeron-python/) 3 | 4 | ### Introduction 5 | 6 | This repository provides unofficial Python bindings for Aeron and Aeron archive client. Providing minimalistic, pythonic interface 7 | for sending and receiving messages through Aeron. 8 | 9 | ### Installation and usage 10 | 11 | For installation use [pipenv](https://docs.pipenv.org): 12 | ``` 13 | $ pipenv install aeronpy 14 | ``` 15 | 16 | or pip: 17 | ``` 18 | $ pip install aeronpy 19 | ``` 20 | 21 | ### Compilation and packaging 22 | 23 | #### Requirements 24 | 25 | To build, following prerequisites are necessary: 26 | * Linux, OSX or Windows host 27 | * Python 3.x with headers and development libraries 28 | * Modern C++ 14 compatible compiler - clang 3.9+/gcc 7+/vcpp 2017+ 29 | * CMake 3.7 or newer 30 | * make or ninja / visual studio 31 | * Modern version of boost 32 | * JDK 8+ 33 | 34 | #### Compilation 35 | 36 | ##### cmake approach 37 | 38 | * Clone this repository into a local directory: 39 | ``` 40 | $ git clone 41 | ``` 42 | 43 | * Create a build directory. 44 | * From the build directory invoke project configuration: 45 | ``` 46 | $ cmake 47 | ``` 48 | * Invoke a build for all compilation targets: 49 | ``` 50 | $ cmake --build . --target all 51 | ``` 52 | * Invoke unit tests with **ctest**: 53 | ``` 54 | $ ctest --verbose 55 | ``` 56 | 57 | * To build source package: 58 | ``` 59 | $ cmake --build . --target build-package 60 | ``` 61 | 62 | * To build a [wheel](https://www.python.org/dev/peps/pep-0427/): 63 | ``` 64 | $ cmake --build . --target build-wheel 65 | ``` 66 | 67 | ##### setup tools approach 68 | 69 | It is possible to build and install this repository as a source package using setuptools: 70 | 71 | Clone this repository into a local directory: 72 | ``` 73 | $ git clone ... 74 | ``` 75 | 76 | Run pip against newly cloned repository 77 | ``` 78 | $ python3 -m pip install 79 | ``` 80 | 81 | In this way setup tools will automatically trigger cmake configuration, compilation and installation. 82 | 83 | ### Troubleshooting 84 | 85 | * **`CnC file not created:` when creating `Context`** 86 | 87 | This indicates that Aeron driver is not currently running in your system. You can either checkout Aeron from it's [official github repository](https://github.com/real-logic/aeron) and build is by yourself or use appropriate pre-built package available [here](https://bintray.com/lukaszlaszko/aeron/aeron-driver#files). 88 | 89 | Once downloaded and unpacked run: 90 | ``` 91 | $ /bin/aeron-driver 92 | ``` 93 | 94 | * **macOS Mojave & Anaconda** 95 | 96 | It appears that there are some problems with this sort of setup at this moment. An attempt to load shared object module linked against Anaconda 5.3 results with following exception being thrown: 97 | ``` 98 | Fatal Python error: PyThreadState_Get: no current thread 99 | ``` 100 | during a call to [`PyModule_Create`](https://docs.python.org/3.6/c-api/module.html#c.PyModule_Create). 101 | 102 | This is resolve this issue use non-Anaconda, standard python distribution from [python.org](https://www.python.org/downloads/release/python-367/). 103 | 104 | 105 | ### License 106 | 107 | Copyright 2018 Fairtide Pte. Ltd. 108 | 109 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 110 | 111 | http://www.apache.org/licenses/LICENSE-2.0 112 | 113 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 114 | -------------------------------------------------------------------------------- /tests/package/aeronpy/publication_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | from time import sleep 3 | from hamcrest import * 4 | from pytest import fixture 5 | from tempfile import _get_candidate_names as temp_dir_candidates, tempdir 6 | from aeronpy import Context, NOT_CONNECTED, PUBLICATION_CLOSED 7 | from aeronpy.driver import media_driver 8 | 9 | 10 | @fixture(scope='module') 11 | def aeron_directory(): 12 | temp_dirs = temp_dir_candidates() 13 | 14 | where = os.path.join(tempdir, next(temp_dirs)) 15 | with media_driver.launch(aeron_directory_name=where): 16 | yield where 17 | 18 | 19 | @fixture() 20 | def stream_id(request): 21 | if 'stream_id' not in request.session.__dict__: 22 | request.session.stream_id = 1 23 | else: 24 | request.session.stream_id = request.session.stream_id + 1 25 | return request.session.stream_id 26 | 27 | 28 | @fixture() 29 | def context(aeron_directory): 30 | return Context(aeron_dir=aeron_directory) 31 | 32 | 33 | @fixture() 34 | def ipc_subscriber(context, stream_id): 35 | return context.add_subscription('aeron:ipc', stream_id) 36 | 37 | 38 | @fixture() 39 | def mcast_subscriber(context, stream_id): 40 | return context.add_subscription('aeron:udp?endpoint=224.0.1.1:40456', stream_id) 41 | 42 | 43 | def test_str_(aeron_directory): 44 | context = Context(aeron_dir=aeron_directory) 45 | publication = context.add_publication('aeron:ipc', 9845) 46 | 47 | assert_that(str(publication), contains_string('aeron:ipc')) 48 | assert_that(str(publication), contains_string('9845')) 49 | 50 | 51 | def test_offer__ipc__no_subscribers(aeron_directory): 52 | context = Context(aeron_dir=aeron_directory) 53 | publication = context.add_publication('aeron:ipc', 50) 54 | assert_that(publication.is_connected, is_(False)) 55 | 56 | result = publication.offer(b'abc') 57 | assert_that(result, is_(equal_to(NOT_CONNECTED))) 58 | 59 | 60 | def test_offer__ipc__message_too_large(aeron_directory, ipc_subscriber, stream_id): 61 | context = Context(aeron_dir=aeron_directory) 62 | publication = context.add_publication('aeron:ipc', stream_id) 63 | 64 | blob = bytearray(50 * 1024 * 1024) 65 | assert_that(calling(publication.offer).with_args(blob), raises(RuntimeError)) 66 | 67 | 68 | def test_offer__ipc__closed(aeron_directory, ipc_subscriber, stream_id): 69 | context = Context(aeron_dir=aeron_directory) 70 | publication = context.add_publication('aeron:ipc', stream_id) 71 | publication.close() 72 | assert_that(publication.is_closed, is_(True)) 73 | 74 | result = publication.offer(b'abc') 75 | assert_that(result, is_(equal_to(PUBLICATION_CLOSED))) 76 | 77 | 78 | def test_offer__ipc(aeron_directory, ipc_subscriber, stream_id): 79 | context = Context(aeron_dir=aeron_directory) 80 | publication = context.add_publication('aeron:ipc', stream_id) 81 | result = publication.offer(b'abc') 82 | assert_that(result, is_(greater_than_or_equal_to(0))) 83 | 84 | 85 | def test_offer__multicast(aeron_directory, mcast_subscriber, stream_id): 86 | context = Context(aeron_dir=aeron_directory) 87 | publication = context.add_publication('aeron:udp?endpoint=224.0.1.1:40456|ttl=0', stream_id) 88 | sleep(0.5) 89 | 90 | result = publication.offer(b'abc') 91 | assert_that(result, is_(greater_than_or_equal_to(0))) 92 | 93 | 94 | def test_offer__multiple_publishers__same_stream(aeron_directory, ipc_subscriber, stream_id): 95 | context = Context(aeron_dir=aeron_directory) 96 | publication_1 = context.add_publication('aeron:ipc', stream_id) 97 | publication_2 = context.add_publication('aeron:ipc', stream_id) 98 | assert_that(publication_1.session_id, is_(equal_to(publication_2.session_id))) 99 | 100 | result = publication_1.offer(b'abc') 101 | assert_that(result, is_(greater_than_or_equal_to(0))) 102 | 103 | result = publication_2.offer(b'def') 104 | assert_that(result, is_(greater_than_or_equal_to(0))) 105 | 106 | sleep(0.5) 107 | 108 | messages = list() 109 | ipc_subscriber.poll(lambda data: messages.append(data)) 110 | 111 | assert_that(messages, has_length(equal_to(2))) 112 | assert_that(bytes(messages[0]), is_(equal_to(b'abc'))) 113 | assert_that(bytes(messages[1]), is_(equal_to(b'def'))) 114 | 115 | -------------------------------------------------------------------------------- /sources/package/aeronpy/_exclusive_publication.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Fairtide Pte. Ltd. 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 | #include "_exclusive_publication.hpp" 18 | 19 | #include 20 | 21 | using namespace std; 22 | using namespace aeron; 23 | using namespace fmt; 24 | namespace py = pybind11; 25 | 26 | 27 | exclusive_publication::exclusive_publication(shared_ptr aeron_exclusive_publication) 28 | : 29 | aeron_exclusive_publication_(aeron_exclusive_publication) 30 | { 31 | 32 | } 33 | 34 | const string& exclusive_publication::channel() const 35 | { 36 | return aeron_exclusive_publication_->channel(); 37 | } 38 | 39 | int64_t exclusive_publication::stream_id() const 40 | { 41 | return aeron_exclusive_publication_->streamId(); 42 | } 43 | 44 | int32_t exclusive_publication::session_id() const 45 | { 46 | return aeron_exclusive_publication_->sessionId(); 47 | } 48 | 49 | int32_t exclusive_publication::initial_term_id() const 50 | { 51 | return aeron_exclusive_publication_->initialTermId(); 52 | } 53 | 54 | bool exclusive_publication::is_connected() const 55 | { 56 | return aeron_exclusive_publication_->isConnected(); 57 | } 58 | 59 | bool exclusive_publication::is_closed() const 60 | { 61 | return aeron_exclusive_publication_->isClosed(); 62 | } 63 | 64 | bool exclusive_publication::is_original() const 65 | { 66 | return aeron_exclusive_publication_->isOriginal(); 67 | } 68 | 69 | int64_t exclusive_publication::offer(py::object data) 70 | { 71 | if (py::isinstance(data)) 72 | { 73 | py::buffer buffer = data; 74 | py::buffer_info info = buffer.request(false); 75 | 76 | AtomicBuffer aeron_buffer(reinterpret_cast(info.ptr), info.size * info.itemsize); 77 | return aeron_exclusive_publication_->offer(aeron_buffer); 78 | } 79 | else if (py::isinstance(data)) 80 | { 81 | auto characters = data.cast(); 82 | 83 | AtomicBuffer aeron_buffer( 84 | const_cast( 85 | reinterpret_cast(characters.data())), characters.length()); 86 | return aeron_exclusive_publication_->offer(aeron_buffer); 87 | } 88 | 89 | throw py::type_error("unsupported data type!"); 90 | } 91 | 92 | void exclusive_publication::close() 93 | { 94 | aeron_exclusive_publication_->close(); 95 | } 96 | 97 | bool exclusive_publication::__bool__() const 98 | { 99 | return aeron_exclusive_publication_ && aeron_exclusive_publication_->isConnected(); 100 | } 101 | 102 | string exclusive_publication::__str__() const 103 | { 104 | return format("exclusive publication: hannel:[{}] stream_id:[{}] session_id:[{}]", 105 | aeron_exclusive_publication_->channel(), 106 | aeron_exclusive_publication_->streamId(), 107 | aeron_exclusive_publication_->sessionId()); 108 | } 109 | 110 | PYBIND11_MODULE(_exclusive_publication, m) 111 | { 112 | py::class_(m, "ExclusivePublication") 113 | .def_property_readonly("channel", &exclusive_publication::channel) 114 | .def_property_readonly("stream_id", &exclusive_publication::stream_id) 115 | .def_property_readonly("session_id", &exclusive_publication::session_id) 116 | .def_property_readonly("is_connected", &exclusive_publication::is_connected) 117 | .def_property_readonly("is_closed", &exclusive_publication::is_closed) 118 | .def_property_readonly("is_original", &exclusive_publication::is_original) 119 | .def("offer", &exclusive_publication::offer, 120 | py::arg("data")) 121 | .def("close", &exclusive_publication::close) 122 | .def("__bool__", &exclusive_publication::__bool__) 123 | .def("__str__", &exclusive_publication::__str__); 124 | 125 | } 126 | 127 | -------------------------------------------------------------------------------- /sources/package/aeronpy/_subscription.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Fairtide Pte. Ltd. 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 | #include "_subscription.hpp" 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | using namespace std; 24 | using namespace std::chrono; 25 | using namespace aeron; 26 | using namespace fmt; 27 | namespace py = pybind11; 28 | 29 | 30 | subscription::subscription(shared_ptr aeron_subscription) 31 | : 32 | aeron_subscription_(aeron_subscription) 33 | { 34 | 35 | } 36 | 37 | const string& subscription::channel() const 38 | { 39 | return aeron_subscription_->channel(); 40 | } 41 | 42 | int64_t subscription::stream_id() const 43 | { 44 | return aeron_subscription_->streamId(); 45 | } 46 | 47 | bool subscription::is_connected() const 48 | { 49 | return aeron_subscription_->isConnected(); 50 | } 51 | 52 | bool subscription::is_closed() const 53 | { 54 | return aeron_subscription_->isClosed(); 55 | } 56 | 57 | vector subscription::images() const 58 | { 59 | 60 | return *aeron_subscription_->images(); 61 | } 62 | 63 | int subscription::poll(py::function handler, int fragment_limit) 64 | { 65 | return aeron_subscription_->poll( 66 | [&, this](auto& buffer, auto offset, auto length, auto& header) 67 | { 68 | py::gil_scoped_acquire gil_guard; 69 | 70 | auto data_info = py::buffer_info( 71 | buffer.buffer() + offset, 72 | sizeof(uint8_t), 73 | py::format_descriptor::format(), 74 | length); 75 | 76 | if (is_complete_poll_handler(handler)) // expected performance hit 77 | handler(py::memoryview(data_info), header); 78 | else 79 | handler(py::memoryview(data_info)); 80 | }, 81 | fragment_limit); 82 | } 83 | 84 | int subscription::poll_eos(py::object handler) 85 | { 86 | return aeron_subscription_->pollEndOfStreams([&](auto& image) 87 | { 88 | py::gil_scoped_acquire gil_guard; 89 | 90 | if (handler) 91 | handler(image); 92 | }); 93 | } 94 | 95 | bool subscription::__bool__() const 96 | { 97 | return aeron_subscription_ && aeron_subscription_->isConnected(); 98 | } 99 | 100 | string subscription::__str__() const 101 | { 102 | return format("subscription: channel:[{}] stream:[{}]", 103 | aeron_subscription_->channel(), 104 | aeron_subscription_->streamId()); 105 | } 106 | 107 | bool subscription::is_complete_poll_handler(py::function& handler) 108 | { 109 | static constexpr auto inspect_module = "inspect"; 110 | static constexpr auto signature_attribute = "signature"; 111 | static constexpr auto parameters_attribute = "parameters"; 112 | 113 | auto inspect = py::module::import(inspect_module); 114 | auto signature_func = inspect.attr(signature_attribute); 115 | auto signature = signature_func(handler); 116 | 117 | return py::len(signature.attr(parameters_attribute)) > 1u; 118 | } 119 | 120 | PYBIND11_MODULE(_subscription, m) 121 | { 122 | static constexpr auto default_fragment_limit = 10; 123 | 124 | py::class_(m, "Subscription") 125 | .def_property_readonly("channel", &subscription::channel) 126 | .def_property_readonly("stream_id", &subscription::stream_id) 127 | .def_property_readonly("is_connected", &subscription::is_connected) 128 | .def_property_readonly("is_closed", &subscription::is_closed) 129 | .def_property_readonly("images", &subscription::images) 130 | .def("poll", &subscription::poll, 131 | py::arg("handler"), 132 | py::arg("fragment_limit") = default_fragment_limit, 133 | py::call_guard()) 134 | .def("poll_eos", &subscription::poll_eos, 135 | py::arg("handler") = py::none(), 136 | py::call_guard()) 137 | .def("__bool__", &subscription::__bool__) 138 | .def("__str__", &subscription::__str__); 139 | 140 | } 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | import multiprocessing 5 | import platform 6 | import subprocess 7 | 8 | from setuptools import setup, Extension, find_packages, findall 9 | from setuptools.command.build_ext import build_ext 10 | from distutils.version import LooseVersion 11 | 12 | 13 | class CMakeExtension(Extension): 14 | def __init__(self, name, sourcedir=''): 15 | Extension.__init__(self, name, sources=[]) 16 | self.sourcedir = os.path.abspath(sourcedir) 17 | 18 | 19 | class CMakeBuild(build_ext): 20 | def run(self): 21 | env = os.environ.copy() 22 | cmake = env['CMAKE'] if 'CMAKE' in env else 'cmake' 23 | 24 | try: 25 | out = subprocess.check_output([cmake, '--version']) 26 | except OSError: 27 | raise RuntimeError("CMake must be installed to build the following extensions: " + 28 | ", ".join(e.name for e in self.extensions)) 29 | 30 | if platform.system() == "Windows": 31 | cmake_version = LooseVersion(re.search(r'version\s*([\d.]+)', out.decode()).group(1)) 32 | if cmake_version < '3.1.0': 33 | raise RuntimeError("CMake >= 3.1.0 is required on Windows") 34 | 35 | for ext in self.extensions: 36 | self.build_extension(ext) 37 | 38 | def build_extension(self, ext): 39 | extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name))) 40 | cmake_args = ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=' + extdir, 41 | '-DPYTHON_EXECUTABLE=' + sys.executable] 42 | 43 | cfg = 'Debug' if self.debug else 'Release' 44 | build_args = ['--config', cfg] 45 | 46 | if platform.system() == "Windows": 47 | cmake_args += ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}'.format(cfg.upper(), extdir)] 48 | if sys.maxsize > 2**32: 49 | cmake_args += ['-A', 'x64'] 50 | build_args += ['--', '/m'] 51 | else: 52 | cmake_args += ['-DCMAKE_BUILD_TYPE=' + cfg] 53 | build_args += ['--', '-j', str(multiprocessing.cpu_count())] 54 | 55 | env = os.environ.copy() 56 | env['CXXFLAGS'] = '{} -DVERSION_INFO=\\"{}\\"'.format(env.get('CXXFLAGS', ''), 57 | self.distribution.get_version()) 58 | 59 | if 'CC' in env: 60 | cmake_args += ['-DCMAKE_C_COMPILER=' + env['CC']] 61 | if 'CXX' in env: 62 | cmake_args += ['-DCMAKE_CXX_COMPILER=' + env['CXX']] 63 | 64 | if not os.path.exists(self.build_temp): 65 | os.makedirs(self.build_temp) 66 | 67 | cmake = env['CMAKE'] if 'CMAKE' in env else 'cmake' 68 | subprocess.check_call([cmake, ext.sourcedir] + cmake_args, cwd=self.build_temp, env=env) 69 | subprocess.check_call([cmake, '--build', '.'] + build_args, cwd=self.build_temp) 70 | 71 | packagedir = os.path.join(ext.sourcedir, 'sources/package') 72 | self.distribution.packages = find_packages(packagedir) 73 | self.distribution.package_dir = {package:os.path.join(packagedir, package.replace('.', '/')) 74 | for package in self.distribution.packages} 75 | self.distribution.package_data = {package:[filename 76 | for filename in findall(self.distribution.package_dir[package]) 77 | if os.path.splitext(filename)[1] == '.pyi'] 78 | for package in self.distribution.packages} 79 | 80 | 81 | class PackageInfo(object): 82 | def __init__(self): 83 | here, _ = os.path.split(__file__) 84 | filename = os.path.join(here, 'PKG-INFO') 85 | 86 | with open(filename, 'r') as pkg_info: 87 | self.__fields = dict() 88 | for line in pkg_info: 89 | key, value = line.split(':', maxsplit=1) # type: (str, str) 90 | self.__fields[key.strip()] = value.strip() 91 | 92 | @property 93 | def version(self): 94 | return self.__fields['Version'] 95 | 96 | @property 97 | def author(self): 98 | return self.__fields['Author'] 99 | 100 | @property 101 | def author_email(self): 102 | return self.__fields['Author-email'] 103 | 104 | @property 105 | def home_page(self): 106 | return self.__fields['Home-page'] 107 | 108 | @property 109 | def license(self): 110 | return self.__fields['License'] 111 | 112 | 113 | info = PackageInfo() 114 | setup( 115 | name='aeronpy', 116 | version=info.version, 117 | author=info.author, 118 | author_email=info.author_email, 119 | url=info.home_page, 120 | license=info.license, 121 | description='Python bindings for Aeron', 122 | long_description='', 123 | ext_modules=[CMakeExtension('aeronpy')], 124 | cmdclass=dict(build_ext=CMakeBuild), 125 | zip_safe=False, 126 | python_requires='>=3.6.*' 127 | ) 128 | -------------------------------------------------------------------------------- /tests/package/aeronpy/archive_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | from hamcrest import * 3 | from pytest import fixture 4 | from tempfile import _get_candidate_names as temp_dir_candidates, tempdir 5 | from time import sleep 6 | from aeronpy import Archive 7 | from aeronpy.driver import archiving_media_driver 8 | 9 | 10 | @fixture() 11 | def aeron_directory(): 12 | temp_dirs = temp_dir_candidates() 13 | 14 | where = os.path.join(tempdir, next(temp_dirs)) 15 | where_archive = os.path.join(tempdir, next(temp_dirs)) 16 | with archiving_media_driver.launch(aeron_directory_name=where, archive_directory_name=where_archive): 17 | yield where 18 | 19 | 20 | @fixture() 21 | def config_file(): 22 | here, _ = os.path.split(__file__) 23 | return os.path.join(here, 'archive.properties') 24 | 25 | 26 | def test__archive_create(aeron_directory): 27 | archive = Archive(aeron_dir=aeron_directory) 28 | assert_that(archive, is_not(None)) 29 | 30 | 31 | def test__archive_create__with_config(aeron_directory, config_file): 32 | archive = Archive(config_file=config_file, aeron_dir=aeron_directory) 33 | assert_that(archive, is_not(None)) 34 | 35 | 36 | def test__archive_add_recorded_publication(aeron_directory): 37 | archive = Archive(aeron_dir=aeron_directory, aeron_archive_dir=aeron_directory) 38 | recording = archive.find_last('aeron:ipc', 5000) 39 | assert_that(recording, is_(None)) 40 | 41 | publication = archive.add_recorded_publication('aeron:ipc', 5000) 42 | 43 | sleep(0.5) 44 | 45 | recording = archive.find_last('aeron:ipc', 5000) 46 | assert_that(recording, is_not(None)) 47 | assert_that(recording.id, is_(equal_to(0))) 48 | 49 | result = publication.offer(b'abc') 50 | assert_that(result, is_(greater_than(0))) 51 | 52 | sleep(0.5) 53 | 54 | assert_that(recording.position, is_(equal_to(result))) 55 | 56 | 57 | def test__archive_add_recorded_exclusive_publication(aeron_directory): 58 | archive = Archive(aeron_dir=aeron_directory, aeron_archive_dir=aeron_directory) 59 | recording = archive.find_last('aeron:ipc', 5000) 60 | assert_that(recording, is_(None)) 61 | 62 | publication = archive.add_recorded_exclusive_publication('aeron:ipc', 5000) 63 | 64 | sleep(0.5) 65 | 66 | recording = archive.find_last('aeron:ipc', 5000) 67 | assert_that(recording, is_not(None)) 68 | assert_that(recording.id, is_(equal_to(0))) 69 | 70 | result = publication.offer(b'abc') 71 | assert_that(result, is_(greater_than(0))) 72 | 73 | sleep(0.5) 74 | 75 | assert_that(recording.position, is_(equal_to(result))) 76 | 77 | 78 | def test__recording_find(aeron_directory): 79 | archive = Archive(aeron_dir=aeron_directory, aeron_archive_dir=aeron_directory) 80 | publication = archive.add_recorded_publication('aeron:ipc', 5000) 81 | sleep(0.5) 82 | 83 | recording = archive.find(0) 84 | assert_that(recording, is_not(None)) 85 | assert_that(recording.position, is_(equal_to(0))) 86 | 87 | 88 | def test__recording_replay(aeron_directory): 89 | archive = Archive(aeron_dir=aeron_directory, aeron_archive_dir=aeron_directory) 90 | publication = archive.add_recorded_publication('aeron:ipc', 5000) 91 | offer_result = publication.offer(b'abc') 92 | assert_that(offer_result, is_(greater_than(0))) 93 | 94 | offer_result = publication.offer(b'def') 95 | assert_that(offer_result, is_(greater_than(0))) 96 | 97 | sleep(0.5) 98 | 99 | recording = archive.find_last('aeron:ipc', 5000) 100 | subscription = recording.replay('aeron:ipc', 6000) 101 | assert_that(archive.find_last('aeron:ipc', 6000), is_(None)) 102 | 103 | replayed = list() 104 | subscription.poll(lambda data: replayed.append(bytes(data))) 105 | assert_that(replayed, has_length(2)) 106 | assert_that(replayed, has_items(equal_to(b'abc'), equal_to(b'def'))) 107 | 108 | 109 | def test__recording_replay__from_position(aeron_directory): 110 | archive = Archive(aeron_dir=aeron_directory, aeron_archive_dir=aeron_directory) 111 | publication = archive.add_recorded_publication('aeron:ipc', 5000) 112 | offer_result = publication.offer(b'abc') 113 | assert_that(offer_result, is_(greater_than(0))) 114 | 115 | offer_result = publication.offer(b'def') 116 | assert_that(offer_result, is_(greater_than(0))) 117 | 118 | sleep(0.5) 119 | 120 | recording = archive.find_last('aeron:ipc', 5000) 121 | subscription = recording.replay('aeron:ipc', 6000, 64) 122 | assert_that(archive.find_last('aeron:ipc', 6000), is_(None)) 123 | 124 | replayed = list() 125 | subscription.poll(lambda data: replayed.append(bytes(data))) 126 | assert_that(replayed, has_length(1)) 127 | assert_that(replayed, has_items(equal_to(b'def'))) 128 | 129 | 130 | def test__recording_replay__from_position__not_aligned(aeron_directory): 131 | archive = Archive(aeron_dir=aeron_directory, aeron_archive_dir=aeron_directory) 132 | publication = archive.add_recorded_publication('aeron:ipc', 5000) 133 | offer_result = publication.offer(b'abc') 134 | assert_that(offer_result, is_(greater_than(0))) 135 | 136 | offer_result = publication.offer(b'def') 137 | assert_that(offer_result, is_(greater_than(0))) 138 | 139 | sleep(0.5) 140 | 141 | recording = archive.find_last('aeron:ipc', 5000) 142 | assert_that(calling(recording.replay).with_args('aeron:ipc', 6000, 50), raises(RuntimeError)) 143 | -------------------------------------------------------------------------------- /sources/package/aeronpy/_archive.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Fairtide Pte. Ltd. 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 | #pragma once 18 | 19 | #include "_exclusive_publication.hpp" 20 | #include "_publication.hpp" 21 | #include "_subscription.hpp" 22 | 23 | #include 24 | #include 25 | #include 26 | 27 | #include 28 | #include 29 | 30 | 31 | /** 32 | * @brief Represents existing Aeron recording. 33 | */ 34 | class recording final 35 | { 36 | public: 37 | /** 38 | * @brief Creates an instance of a **recording** with given id. 39 | * @param aeron_archive An instance of archive this recording belongs to. 40 | * @param id Recording id. 41 | */ 42 | explicit recording(std::shared_ptr aeron_archive, int64_t id); 43 | 44 | /** 45 | * @brief Gets id of the recording. 46 | * @return Id of the recording. 47 | */ 48 | int64_t id() const; 49 | /** 50 | * @brief Gets current position in the recording. 51 | * @return Current position in the recording. 52 | */ 53 | int64_t position() const; 54 | 55 | /** 56 | * @brief Initiates replay from the stream. 57 | * @details 58 | * Depending on archive configuration this will initiate replay from the local or remote host. Returned subscription 59 | * allows consumption of replayed stream. The stream will be delivered on the given **channel** and **stream**. 60 | * 61 | * @param channel Channel to replay recorded stream on. 62 | * @param stream_id Stream to replay the recording as. 63 | * @param position Position within the recording to transmi the first replay message from. 64 | * @return A subscription allowing consumption of replayed stream. 65 | */ 66 | subscription replay(const std::string& channel, int32_t stream_id, int64_t position); 67 | /** 68 | * @brief Truncates the recording up to the given position. 69 | * @details 70 | * This removes all messages before given position from the recording. 71 | * 72 | * @param position The first position in the modified recording. 73 | */ 74 | void truncate(int64_t position); 75 | 76 | /** 77 | * @brief Provides debug description of the recording. 78 | * @return Debug description of the recording. 79 | */ 80 | std::string __str__() const; 81 | 82 | private: 83 | std::shared_ptr aeron_archive_; 84 | int64_t id_; 85 | 86 | }; 87 | 88 | /** 89 | * @brief Provides an interop proxy for interaction with Aeron archive. 90 | * @details 91 | * The aeron-archive is an service which enables Aeron data stream recording and replay support from an archive. 92 | */ 93 | class archive final 94 | { 95 | public: 96 | /** 97 | * @brief Creates an instance of an **archive**. 98 | * @details 99 | * This creates an instance of an **archive** configured according to passed options. Configured archive allows 100 | * accessing existing recordings and initiating new ones. 101 | * 102 | * @param args Configuration options for this archive. 103 | */ 104 | explicit archive(pybind11::kwargs args); 105 | 106 | /** 107 | * @brief Attempts to get recording associated with the specified id. 108 | * @param recording_id The id of recording to look for. 109 | * @return On success, recording associated to the id. On failure, None. 110 | */ 111 | std::unique_ptr find(int64_t recording_id); 112 | /** 113 | * @brief Attempts to find last recording for given channel and stream. 114 | * @param channel Channel for which 115 | * @param stream_id Stream to reply on. 116 | * @param position Position to start reply from. 117 | * @return On success, latest recording for given channel and stream. On failure, None. 118 | */ 119 | std::unique_ptr find_last(const std::string& channel, int32_t stream_id); 120 | 121 | /** 122 | * @brief Adds a publication with automatic archive recording. 123 | * @details 124 | * This creates a publication in a similar way to **context::add_publication** wit automatic registration of 125 | * stream recording. Multiple publications share the same session. 126 | * 127 | * @param channel Channel to open the publication on. 128 | * @param stream_id Stream to open the publication on. 129 | * @return An instance of publisher through which data can be offered to the stream. 130 | */ 131 | publication add_recorded_publication(const std::string& channel, int32_t stream_id); 132 | /** 133 | * @brief Adds an exclusive publication with automatic recording. 134 | * @details 135 | * This method behaves indetically to **context::add_exclusive_publication**, with addional registration 136 | * of stream recording. Each publication will have its unique session. 137 | * 138 | * @param channel Channel to open the publication on. 139 | * @param stream_id Stream to open the publication on. 140 | * @return An instance of publisher through which data can be offered to the stream. 141 | */ 142 | exclusive_publication add_recorded_exclusive_publication(const std::string& channel, int32_t stream_id); 143 | 144 | /** 145 | * @brief Provides debug description of the archive. 146 | * @return Debug description of the archive. 147 | */ 148 | std::string __str__() const; 149 | 150 | private: 151 | int64_t find_latest_recording_id(const std::string& channel, int32_t streamId); 152 | 153 | int64_t recording_id_; 154 | std::shared_ptr aeron_archive_; 155 | 156 | }; 157 | 158 | 159 | -------------------------------------------------------------------------------- /tests/package/aeronpy/context_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | from time import sleep 4 | from hamcrest import * 5 | from pytest import fixture 6 | from tempfile import _get_candidate_names as temp_dir_candidates, tempdir 7 | from aeronpy import Context 8 | from aeronpy.driver import media_driver 9 | 10 | 11 | @fixture() 12 | def aeron_directory(): 13 | temp_dirs = temp_dir_candidates() 14 | 15 | where = os.path.join(tempdir, next(temp_dirs)) 16 | with media_driver.launch(aeron_directory_name=where): 17 | yield where 18 | 19 | 20 | def test_create__default(aeron_directory): 21 | context = Context(aeron_dir=aeron_directory) 22 | assert_that(context, is_not(None)) 23 | 24 | 25 | def test_create__wrong_parameter_type(): 26 | assert_that(calling(Context).with_args(aeron_dir=datetime(2018, 1, 1)), raises(RuntimeError)) 27 | assert_that(calling(Context).with_args(media_driver_timeout='abc'), raises(RuntimeError)) 28 | assert_that(calling(Context).with_args(resource_linger_timeout='abc'), raises(RuntimeError)) 29 | assert_that(calling(Context).with_args(user_conductor_agent_invoker='abc'), raises(RuntimeError)) 30 | assert_that(calling(Context).with_args(error_handler='abc'), raises(TypeError)) 31 | assert_that(calling(Context).with_args(new_publication_handler='abc'), raises(TypeError)) 32 | assert_that(calling(Context).with_args(new_exclusive_publication_handler='abc'), raises(TypeError)) 33 | assert_that(calling(Context).with_args(new_subscription_handler='abc'), raises(TypeError)) 34 | assert_that(calling(Context).with_args(available_image_handler='abc'), raises(TypeError)) 35 | assert_that(calling(Context).with_args(unavailable_image_handler='abc'), raises(TypeError)) 36 | 37 | 38 | def test_add_subscription(aeron_directory): 39 | context = Context(aeron_dir=aeron_directory) 40 | subscription = context.add_subscription('aeron:ipc', 1000) 41 | assert_that(subscription, is_not(None)) 42 | assert_that(subscription.channel, is_('aeron:ipc')) 43 | assert_that(subscription.stream_id, is_(1000)) 44 | 45 | 46 | def test_add_subscription__with_handlers(aeron_directory): 47 | class Handler: 48 | def __init__(self): 49 | self.subscriptions = list() 50 | 51 | def on_new_subscription(self, *args): 52 | self.subscriptions.append(args) 53 | 54 | handler = Handler() 55 | context = Context( 56 | aeron_dir=aeron_directory, 57 | new_subscription_handler=handler.on_new_subscription) 58 | subscription = context.add_subscription('aeron:ipc', 546) 59 | 60 | assert_that(handler.subscriptions, is_not(empty())) 61 | assert_that(handler.subscriptions, has_length(equal_to(1))) 62 | assert_that(handler.subscriptions[0][0], is_('aeron:ipc')) 63 | assert_that(handler.subscriptions[0][1], is_(546)) 64 | 65 | 66 | def test_add_publication(aeron_directory): 67 | context = Context(aeron_dir=aeron_directory) 68 | publication = context.add_publication('aeron:ipc', 2000) 69 | assert_that(publication, is_not(None)) 70 | assert_that(publication.channel, is_('aeron:ipc')) 71 | assert_that(publication.stream_id, is_(2000)) 72 | 73 | 74 | def test_add_publication__wrong_channel(aeron_directory): 75 | context = Context(aeron_dir=aeron_directory) 76 | assert_that(calling(context.add_publication).with_args('wrong channel', 1), raises(RuntimeError)) 77 | 78 | 79 | def test_add_publication__with_handler(aeron_directory): 80 | class Handler(object): 81 | def __init__(self): 82 | self.publications = list() 83 | 84 | def on_new_publication(self, *args): 85 | self.publications.append(args) 86 | 87 | handler = Handler() 88 | context = Context( 89 | aeron_dir=aeron_directory, 90 | new_publication_handler=handler.on_new_publication) 91 | publication = context.add_publication('aeron:ipc', 3000) 92 | 93 | assert_that(handler.publications, is_not(empty())) 94 | assert_that(handler.publications, has_length(equal_to(1))) 95 | assert_that(handler.publications[0][0], is_(equal_to('aeron:ipc'))) 96 | assert_that(handler.publications[0][1], is_(equal_to(3000))) 97 | 98 | 99 | def test_add_exclusive_publication(aeron_directory): 100 | context = Context(aeron_dir=aeron_directory) 101 | publication = context.add_exclusive_publication('aeron:ipc', 4000) 102 | assert_that(publication, is_not(None)) 103 | assert_that(publication.channel, is_('aeron:ipc')) 104 | assert_that(publication.stream_id, is_(4000)) 105 | 106 | 107 | def test_add_exclusive_publication__with_handler(aeron_directory): 108 | class Handler(object): 109 | def __init__(self): 110 | self.publications = list() 111 | 112 | def on_new_exclusive_publication(self, *args): 113 | self.publications.append(args) 114 | 115 | handler = Handler() 116 | context = Context( 117 | aeron_dir=aeron_directory, 118 | new_exclusive_publication_handler=handler.on_new_exclusive_publication) 119 | publication = context.add_exclusive_publication('aeron:ipc', 5000) 120 | 121 | assert_that(handler.publications, is_not(empty())) 122 | assert_that(handler.publications, has_length(equal_to(1))) 123 | assert_that(handler.publications[0][0], is_(equal_to('aeron:ipc'))) 124 | assert_that(handler.publications[0][1], is_(equal_to(5000))) 125 | 126 | 127 | def test_image_available_unavailable_callbacks(aeron_directory): 128 | class Handler(object): 129 | def __init__(self): 130 | self.available_images = list() 131 | self.unavailable_images = list() 132 | 133 | def on_image_available(self, *args): 134 | self.available_images.append(args) 135 | 136 | def on_image_unavailable(self, *args): 137 | self.unavailable_images.append(args) 138 | 139 | handler = Handler() 140 | context = Context( 141 | aeron_dir=aeron_directory, 142 | available_image_handler=handler.on_image_available, 143 | unavailable_image_handler=handler.on_image_unavailable) 144 | subscription = context.add_subscription('aeron:ipc', 6000) 145 | 146 | publication_context = Context(aeron_dir=aeron_directory) 147 | publication = publication_context.add_publication('aeron:ipc', 6000) 148 | sleep(1) 149 | 150 | publication.close() 151 | del publication 152 | sleep(1) 153 | 154 | assert_that(handler.available_images, is_not(empty())) 155 | assert_that(handler.unavailable_images, is_not(empty())) 156 | 157 | 158 | -------------------------------------------------------------------------------- /tests/package/aeronpy/subscription_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import timedelta 3 | from time import sleep 4 | from hamcrest import * 5 | from pytest import fixture 6 | from tempfile import _get_candidate_names as temp_dir_candidates, tempdir 7 | from aeronpy import Context 8 | from aeronpy.driver import media_driver 9 | 10 | 11 | @fixture() 12 | def aeron_directory(scope='module'): 13 | temp_dirs = temp_dir_candidates() 14 | 15 | where = os.path.join(tempdir, next(temp_dirs)) 16 | with media_driver.launch(aeron_directory_name=where): 17 | yield where 18 | 19 | 20 | 21 | @fixture() 22 | def stream_id(request): 23 | if 'stream_id' not in request.session.__dict__: 24 | request.session.stream_id = 1 25 | else: 26 | request.session.stream_id = request.session.stream_id + 1 27 | return request.session.stream_id 28 | 29 | 30 | @fixture() 31 | def context(aeron_directory): 32 | return Context(aeron_dir=aeron_directory, resource_linger_timeout=timedelta(milliseconds=10)) 33 | 34 | 35 | @fixture() 36 | def ipc_publication(context, stream_id): 37 | return context.add_publication('aeron:ipc', stream_id) 38 | 39 | 40 | @fixture() 41 | def ipc_publication_1(ipc_publication): 42 | return ipc_publication 43 | 44 | 45 | @fixture() 46 | def ipc_publication_2(context, stream_id): 47 | return context.add_exclusive_publication('aeron:ipc', stream_id) 48 | 49 | 50 | @fixture() 51 | def mcast_publication(context, stream_id): 52 | return context.add_exclusive_publication('aeron:udp?endpoint=224.0.1.1:40456|ttl=0', stream_id) 53 | 54 | 55 | def test_create__no_publisher(aeron_directory, stream_id): 56 | # always connected 57 | context = Context(aeron_dir=aeron_directory) 58 | subscription = context.add_subscription('aeron:ipc', stream_id) 59 | sleep(0.5) 60 | 61 | assert_that(subscription.is_connected, is_(True)) 62 | assert_that(subscription.images, is_(empty())) 63 | 64 | 65 | def test_create__with_publisher(aeron_directory, ipc_publication, stream_id): 66 | # always connected 67 | context = Context(aeron_dir=aeron_directory) 68 | subscription = context.add_subscription('aeron:ipc', stream_id) 69 | sleep(0.5) 70 | 71 | assert_that(subscription.images, is_not(empty())) 72 | assert_that(subscription.images, has_length(1)) 73 | assert_that(subscription.images[0].session_id, ipc_publication.session_id) 74 | 75 | 76 | def test_poll__not_connected(aeron_directory, stream_id): 77 | # always connected 78 | context = Context(aeron_dir=aeron_directory) 79 | subscription = context.add_subscription('aeron:ipc', stream_id) 80 | 81 | received = list() 82 | result = subscription.poll(lambda data: received.append(data)) 83 | assert_that(result, is_(0)) 84 | 85 | 86 | def test_poll__no_data(aeron_directory, ipc_publication, stream_id): 87 | context = Context(aeron_dir=aeron_directory) 88 | subscription = context.add_subscription('aeron:ipc', stream_id) 89 | sleep(0.5) 90 | 91 | assert_that(subscription.images, is_not(empty())) 92 | assert_that(subscription.images, has_length(1)) 93 | assert_that(subscription.images[0].session_id, ipc_publication.session_id) 94 | 95 | received = list() 96 | result = subscription.poll(lambda data: received.append(data)) 97 | assert_that(result, is_(0)) 98 | 99 | 100 | def test_poll__single_fragment(aeron_directory, ipc_publication, stream_id): 101 | context = Context(aeron_dir=aeron_directory) 102 | subscription = context.add_subscription('aeron:ipc', stream_id) 103 | sleep(0.5) 104 | 105 | ipc_publication.offer(b'abc') 106 | ipc_publication.offer(b'cde') 107 | ipc_publication.offer(b'efg') 108 | 109 | received = list() 110 | result = subscription.poll(lambda data: received.append(bytes(data)), fragment_limit=1) 111 | assert_that(result, is_(equal_to(1))) 112 | assert_that(received, has_length(1)) 113 | assert_that(received[0], is_(equal_to(b'abc'))) 114 | 115 | result = subscription.poll(lambda data: received.append(bytes(data)), fragment_limit=1) 116 | assert_that(result, is_(equal_to(1))) 117 | assert_that(received, has_length(2)) 118 | assert_that(received[-1], is_(equal_to(b'cde'))) 119 | 120 | 121 | def test_poll__multiple_fragments(aeron_directory, ipc_publication, stream_id): 122 | context = Context(aeron_dir=aeron_directory) 123 | subscription = context.add_subscription('aeron:ipc', stream_id) 124 | sleep(0.5) 125 | 126 | ipc_publication.offer(b'abc') 127 | ipc_publication.offer(b'cde') 128 | ipc_publication.offer(b'efg') 129 | 130 | received = list() 131 | result = subscription.poll(lambda data: received.append(bytes(data)), fragment_limit=3) 132 | assert_that(result, is_(equal_to(3))) 133 | assert_that(received, has_length(3)) 134 | assert_that(received[0], is_(equal_to(b'abc'))) 135 | assert_that(received[1], is_(equal_to(b'cde'))) 136 | assert_that(received[2], is_(equal_to(b'efg'))) 137 | 138 | 139 | def test_poll__multiple_sessions(aeron_directory, ipc_publication_1, ipc_publication_2, stream_id): 140 | context = Context(aeron_dir=aeron_directory) 141 | subscription = context.add_subscription('aeron:ipc', stream_id) 142 | sleep(0.5) 143 | 144 | assert_that(subscription.images, has_length(2)) 145 | ipc_publication_1.offer(b'abc') 146 | ipc_publication_2.offer(b'cde') 147 | 148 | received = list() 149 | result = subscription.poll(lambda data, header: received.append((header.session_id, bytes(data)))) 150 | assert_that(result, is_(equal_to(2))) 151 | assert_that(received, has_length(2)) 152 | 153 | 154 | def test_poll_eos__no_data(aeron_directory, ipc_publication, stream_id): 155 | context = Context(aeron_dir=aeron_directory) 156 | subscription = context.add_subscription('aeron:ipc', stream_id) 157 | result = subscription.poll_eos() 158 | assert_that(result, is_(0)) 159 | 160 | 161 | # def test_poll_eos__single_image(aeron_directory): 162 | # context = Context(aeron_dir=aeron_directory) 163 | # subscription = context.add_subscription('aeron:udp?endpoint=localhost:40456|ttl=0', 199) 164 | # publication = context.add_publication('aeron:udp?endpoint=localhost:40456|ttl=0', 199) 165 | # sleep(0.5) 166 | # 167 | # publication.offer(b'abc') 168 | # sleep(0.5) 169 | # 170 | # result = subscription.poll(lambda _: None) 171 | # assert_that(result, is_(1)) 172 | # 173 | # publication.close() 174 | # del publication 175 | # sleep(1) 176 | # 177 | # finished = list() 178 | # 179 | # result = subscription.poll_eos(lambda image: finished.append(image.session_id)) 180 | # assert_that(result, is_(1)) 181 | 182 | 183 | # def test_poll_eos__multiple_images(ipc_publication): 184 | # pass -------------------------------------------------------------------------------- /sources/package/aeronpy/_context.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Fairtide Pte. Ltd. 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 | #include "_context.hpp" 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | #include 24 | #include 25 | #include 26 | 27 | using namespace std; 28 | using namespace std::chrono; 29 | using namespace aeron; 30 | using namespace fmt; 31 | namespace py = pybind11; 32 | 33 | 34 | context::context(py::kwargs args) 35 | { 36 | static constexpr auto aeron_dir_key = "aeron_dir"; 37 | static constexpr auto media_driver_timeout_key = "media_driver_timeout"; 38 | static constexpr auto resource_linger_timeout_key = "resource_linger_timeout"; 39 | static constexpr auto use_conductor_agent_invoker_key = "user_conductor_agent_invoker"; 40 | 41 | static constexpr auto error_handler_key = "error_handler"; 42 | static constexpr auto new_publication_handler_key = "new_publication_handler"; 43 | static constexpr auto new_exclusive_publication_handler_key = "new_exclusive_publication_handler"; 44 | static constexpr auto new_subscription_handler_key = "new_subscription_handler"; 45 | static constexpr auto available_image_handler_key = "available_image_handler"; 46 | static constexpr auto unavailable_image_handler_key = "unavailable_image_handler"; 47 | 48 | static constexpr auto default_aeron_dir_var = "AERON_DIR"; 49 | 50 | // defaults from environment variables 51 | if(auto default_aeron_dir = getenv(default_aeron_dir_var)) 52 | aeron_context_.aeronDir(default_aeron_dir); 53 | 54 | // context properties 55 | if (args.contains(aeron_dir_key)) 56 | { 57 | if (!args[aeron_dir_key].is_none()) 58 | { 59 | auto aeron_dir = args[aeron_dir_key].cast(); 60 | aeron_context_.aeronDir(aeron_dir); 61 | } 62 | } 63 | if (args.contains(media_driver_timeout_key)) 64 | { 65 | auto timeout = args[media_driver_timeout_key].cast(); 66 | aeron_context_.mediaDriverTimeout(timeout.count()); 67 | } 68 | if (args.contains(resource_linger_timeout_key)) 69 | { 70 | auto timeout = args[resource_linger_timeout_key].cast(); 71 | aeron_context_.resourceLingerTimeout(timeout.count()); 72 | } 73 | if (args.contains(use_conductor_agent_invoker_key)) 74 | { 75 | auto use_conductor_agent_invoker = args[use_conductor_agent_invoker_key].cast(); 76 | aeron_context_.useConductorAgentInvoker(use_conductor_agent_invoker); 77 | } 78 | 79 | // callbacks 80 | if (args.contains(error_handler_key)) 81 | { 82 | auto handler = args[error_handler_key]; 83 | if (!py::isinstance(handler)) 84 | { 85 | auto reason = format("{} has to be a function!", error_handler_key); 86 | throw py::type_error(reason); 87 | } 88 | 89 | aeron_context_.errorHandler([handler = handler.cast()](auto& error) 90 | { 91 | py::gil_scoped_acquire gil_guard; 92 | handler(error); 93 | }); 94 | } 95 | if (args.contains(new_publication_handler_key)) 96 | { 97 | auto handler = args[new_publication_handler_key]; 98 | if (!py::isinstance(handler)) 99 | { 100 | auto reason = format("{} has to be a function!", new_publication_handler_key); 101 | throw py::type_error(reason); 102 | } 103 | 104 | aeron_context_.newPublicationHandler( 105 | [handler = handler.cast()]( 106 | auto& channel, auto stream_id, auto session_id, auto correlation_id) 107 | { 108 | py::gil_scoped_acquire gil_guard; 109 | handler(channel, stream_id, session_id, correlation_id); 110 | }); 111 | } 112 | if (args.contains(new_exclusive_publication_handler_key)) 113 | { 114 | auto handler = args[new_exclusive_publication_handler_key]; 115 | if (!py::isinstance(handler)) 116 | { 117 | auto reason = format("{} has to be a function!", new_exclusive_publication_handler_key); 118 | throw py::type_error(reason); 119 | } 120 | 121 | aeron_context_.newExclusivePublicationHandler( 122 | [handler = handler.cast()]( 123 | auto& channel, auto stream_id, auto session_id, auto correlation_id) 124 | { 125 | py::gil_scoped_acquire gil_guard; 126 | handler(channel, stream_id, session_id, correlation_id); 127 | }); 128 | } 129 | if (args.contains(new_subscription_handler_key)) 130 | { 131 | auto handler = args[new_subscription_handler_key]; 132 | if (!py::isinstance(handler)) 133 | { 134 | auto reason = format("{} has to be a function!", new_subscription_handler_key); 135 | throw py::type_error(reason); 136 | } 137 | 138 | aeron_context_.newSubscriptionHandler( 139 | [handler = handler.cast()]( 140 | auto &channel, auto stream_id, auto correlation_id) 141 | { 142 | py::gil_scoped_acquire gil_guard; 143 | handler(channel, stream_id, correlation_id); 144 | }); 145 | } 146 | if (args.contains(available_image_handler_key)) 147 | { 148 | auto handler = args[available_image_handler_key]; 149 | if (!py::isinstance(handler)) 150 | { 151 | auto reason = format("{} has to be a function!", available_image_handler_key); 152 | throw py::type_error(reason); 153 | } 154 | 155 | aeron_context_.availableImageHandler( 156 | [handler = handler.cast()](auto& image) 157 | { 158 | py::gil_scoped_acquire gil_guard; 159 | handler(image); 160 | }); 161 | } 162 | if (args.contains(unavailable_image_handler_key)) 163 | { 164 | auto handler = args[unavailable_image_handler_key]; 165 | if (!py::isinstance(handler)) 166 | { 167 | auto reason = format("{} has to be a function!", unavailable_image_handler_key); 168 | throw py::type_error(reason); 169 | } 170 | 171 | aeron_context_.unavailableImageHandler( 172 | [handler = handler.cast()](auto& image) 173 | { 174 | py::gil_scoped_acquire gil_guard; 175 | handler(image); 176 | }); 177 | } 178 | 179 | aeron_instance_ = Aeron::connect(aeron_context_); 180 | } 181 | 182 | subscription context::add_subscription(const string& channel, int32_t stream_id) 183 | { 184 | auto id = aeron_instance_->addSubscription(channel, stream_id); 185 | auto subscription = aeron_instance_->findSubscription(id); 186 | 187 | // wait for the subscription to be valid 188 | while (!subscription) 189 | { 190 | std::this_thread::yield(); 191 | subscription = aeron_instance_->findSubscription(id); 192 | } 193 | 194 | return subscription; 195 | } 196 | 197 | publication context::add_publication(const string& channel, int32_t stream_id) 198 | { 199 | auto id = aeron_instance_->addPublication(channel, stream_id); 200 | auto publication = aeron_instance_->findPublication(id); 201 | 202 | // wait for the subscription to be valid 203 | while (!publication) 204 | { 205 | std::this_thread::yield(); 206 | publication = aeron_instance_->findPublication(id); 207 | } 208 | 209 | return publication; 210 | } 211 | 212 | exclusive_publication context::add_exclusive_publication(const std::string &channel, int32_t stream_id) 213 | { 214 | auto id = aeron_instance_->addExclusivePublication(channel, stream_id); 215 | auto publication = aeron_instance_->findExclusivePublication(id); 216 | 217 | // wait for the subscription to be valid 218 | while (!publication) 219 | { 220 | std::this_thread::yield(); 221 | publication = aeron_instance_->findExclusivePublication(id); 222 | } 223 | 224 | return publication; 225 | } 226 | 227 | PYBIND11_MODULE(_context, m) 228 | { 229 | py::class_(m, "Context") 230 | .def(py::init()) 231 | .def("add_subscription", &context::add_subscription, 232 | py::arg("channel"), 233 | py::arg("stream_id"), 234 | py::call_guard(), 235 | py::keep_alive<0, 1>()) 236 | .def("add_publication", &context::add_publication, 237 | py::arg("channel"), 238 | py::arg("stream_id"), 239 | py::call_guard(), 240 | py::keep_alive<0, 1>()) 241 | .def("add_exclusive_publication", &context::add_exclusive_publication, 242 | py::arg("channel"), 243 | py::arg("stream_id"), 244 | py::call_guard(), 245 | py::keep_alive<0, 1>()); 246 | 247 | } 248 | 249 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /sources/package/aeronpy/_archive.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Fairtide Pte. Ltd. 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 | #include "_archive.hpp" 18 | 19 | #include 20 | #include 21 | #include 22 | 23 | #include 24 | 25 | using namespace std; 26 | using namespace fmt; 27 | namespace py = pybind11; 28 | 29 | 30 | recording::recording(shared_ptr aeron_archive, int64_t id) 31 | : 32 | aeron_archive_(aeron_archive), 33 | id_(id) 34 | { 35 | 36 | } 37 | 38 | int64_t recording::id() const 39 | { 40 | return id_; 41 | } 42 | 43 | int64_t recording::position() const 44 | { 45 | return aeron_archive_->getRecordingPosition(id_); 46 | } 47 | 48 | subscription recording::replay(const string& channel, int32_t stream_id, int64_t position) 49 | { 50 | auto subscription = aeron_archive_->replay( 51 | id_, 52 | position, 53 | numeric_limits::max(), 54 | channel, 55 | stream_id); 56 | 57 | return subscription; 58 | } 59 | 60 | void recording::truncate(int64_t position) 61 | { 62 | aeron_archive_->truncateRecording(id_, position); 63 | } 64 | 65 | string recording::__str__() const 66 | { 67 | return format("recording: id:[{}]", id_); 68 | } 69 | 70 | archive::archive(pybind11::kwargs args) 71 | { 72 | static constexpr auto config_file_key = "config_file"; 73 | static constexpr auto aeron_dir_key = "aeron_dir"; 74 | static constexpr auto message_timeout_ns_key = "message_timeout_ns"; 75 | static constexpr auto control_request_channel_key = "control_request_channel"; 76 | static constexpr auto control_request_stream_id_key = "control_request_stream_id"; 77 | static constexpr auto control_response_channel_key = "control_response_channel"; 78 | static constexpr auto control_response_stream_id_key = "control_response_stream_id"; 79 | static constexpr auto recording_events_channel_key = "recording_events_channel"; 80 | static constexpr auto recording_events_stream_id_key = "recording_events_stream_id"; 81 | static constexpr auto control_term_buffer_sparse_key = "control_term_buffer_sparse"; 82 | static constexpr auto control_term_buffer_length_key = "control_term_buffer_length"; 83 | static constexpr auto control_mtu_length_key = "control_mtu_length"; 84 | 85 | static constexpr auto default_aeron_dir_var = "AERON_DIR"; 86 | 87 | // aeron archive context initialisation 88 | unique_ptr aeron_archive_context; 89 | if (args.contains(config_file_key)) 90 | { 91 | auto config_file = args[config_file_key].cast(); 92 | aeron::archive::Configuration configuration(config_file); 93 | 94 | aeron_archive_context = make_unique(configuration); 95 | } 96 | else 97 | { 98 | aeron_archive_context = make_unique(); 99 | } 100 | 101 | // defaults from environment variables 102 | if(auto default_aeron_dir = getenv(default_aeron_dir_var)) 103 | aeron_archive_context->aeronDirectoryName(default_aeron_dir); 104 | 105 | // context properties 106 | if (args.contains(aeron_dir_key)) 107 | { 108 | auto aeron_dir = args[aeron_dir_key].cast(); 109 | aeron_archive_context->aeronDirectoryName(aeron_dir); 110 | } 111 | if (args.contains(message_timeout_ns_key)) 112 | { 113 | auto message_timeout_ns = args[message_timeout_ns_key].cast(); 114 | aeron_archive_context->messageTimeoutNs(message_timeout_ns); 115 | } 116 | if (args.contains(control_request_channel_key)) 117 | { 118 | auto control_request_channel = args[control_request_channel_key].cast(); 119 | aeron_archive_context->controlRequestChannel(control_request_channel); 120 | } 121 | if (args.contains(control_request_stream_id_key)) 122 | { 123 | auto control_request_stream_id = args[control_request_stream_id_key].cast(); 124 | aeron_archive_context->controlRequestStreamId(control_request_stream_id); 125 | } 126 | if (args.contains(control_response_channel_key)) 127 | { 128 | auto control_response_channel = args[control_response_channel_key].cast(); 129 | aeron_archive_context->controlResponseChannel(control_response_channel); 130 | } 131 | if (args.contains(control_response_stream_id_key)) 132 | { 133 | auto control_response_stream_id = args[control_response_stream_id_key].cast(); 134 | aeron_archive_context->controlResponseStreamId(control_response_stream_id); 135 | } 136 | if (args.contains(recording_events_channel_key)) 137 | { 138 | auto recording_events_channel = args[recording_events_channel_key].cast(); 139 | aeron_archive_context->recordingEventsChannel(recording_events_channel); 140 | } 141 | if (args.contains(recording_events_stream_id_key)) 142 | { 143 | auto recording_events_stream_id = args[recording_events_stream_id_key].cast(); 144 | aeron_archive_context->recordingEventsStreamId(recording_events_stream_id); 145 | } 146 | if (args.contains(control_term_buffer_sparse_key)) 147 | { 148 | auto control_term_buffer_sparse = args[control_term_buffer_sparse_key].cast(); 149 | aeron_archive_context->controlTermBufferSparse(control_term_buffer_sparse_key); 150 | } 151 | if (args.contains(control_term_buffer_length_key)) 152 | { 153 | auto control_term_buffer_length = args[control_term_buffer_length_key].cast(); 154 | aeron_archive_context->controlTermBufferLength(control_term_buffer_length); 155 | } 156 | if (args.contains(control_mtu_length_key)) 157 | { 158 | auto control_mtu_length = args[control_mtu_length_key].cast(); 159 | aeron_archive_context->controlMtuLength(control_mtu_length); 160 | } 161 | 162 | aeron_archive_ = aeron::archive::AeronArchive::connect(*aeron_archive_context); 163 | } 164 | 165 | unique_ptr archive::find(int64_t recording_id) 166 | { 167 | auto consumer = [&]( 168 | auto controlSession_id, 169 | auto correlation_id, 170 | auto recording_id, 171 | auto start_timestamp, 172 | auto stop_timestamp, 173 | auto start_position, 174 | auto stop_position, 175 | auto initial_termId, 176 | auto segment_file_length, 177 | auto term_buffer_length, 178 | auto mtu_length, 179 | auto session_id, 180 | auto stream_id, 181 | auto& stripped_channel, 182 | auto& original_channel, 183 | auto& source_identity) 184 | { 185 | 186 | }; 187 | 188 | auto found_count = aeron_archive_->listRecording(recording_id, consumer); 189 | if (found_count <= 0) 190 | return unique_ptr(); 191 | 192 | return make_unique(aeron_archive_, recording_id); 193 | } 194 | 195 | unique_ptr archive::find_last(const string& channel, int32_t stream_id) 196 | { 197 | int64_t last_recording_id = -1; 198 | 199 | auto consumer = [&]( 200 | auto controlSession_id, 201 | auto correlation_id, 202 | auto recording_id, 203 | auto start_timestamp, 204 | auto stop_timestamp, 205 | auto start_position, 206 | auto stop_position, 207 | auto initial_termId, 208 | auto segment_file_length, 209 | auto term_buffer_length, 210 | auto mtu_length, 211 | auto session_id, 212 | auto stream_id, 213 | auto& stripped_channel, 214 | auto& original_channel, 215 | auto& source_identity) 216 | { 217 | last_recording_id = recording_id; 218 | }; 219 | 220 | auto found_count = aeron_archive_->listRecordingsForUri( 221 | 0, 222 | numeric_limits::max(), 223 | channel, 224 | stream_id, 225 | consumer); 226 | if (found_count <= 0) 227 | return unique_ptr(); 228 | 229 | return make_unique(aeron_archive_, last_recording_id); 230 | } 231 | 232 | publication archive::add_recorded_publication(const string& channel, int32_t stream_id) 233 | { 234 | return aeron_archive_->addRecordedPublication(channel, stream_id); 235 | } 236 | 237 | exclusive_publication archive::add_recorded_exclusive_publication(const string &channel, int32_t stream_id) 238 | { 239 | return aeron_archive_->addRecordedExclusivePublication(channel, stream_id); 240 | } 241 | 242 | string archive::__str__() const 243 | { 244 | return format("archive: aeron_dir:[{}]", aeron_archive_->context().aeronDirectoryName()); 245 | } 246 | 247 | PYBIND11_MODULE(_archive, m) 248 | { 249 | static constexpr auto default_position = 0; 250 | 251 | py::class_(m, "Recording") 252 | .def_property_readonly("id", &recording::id) 253 | .def_property_readonly("position", &recording::position) 254 | .def("replay", &recording::replay, 255 | py::arg("channel"), 256 | py::arg("stream_id"), 257 | py::arg("position") = default_position, 258 | py::call_guard(), 259 | py::keep_alive<0, 1>()) 260 | .def("truncate", &recording::truncate, 261 | py::arg("position")) 262 | .def("__str__", &recording::__str__); 263 | 264 | py::class_(m, "Archive") 265 | .def(py::init()) 266 | .def("find", &archive::find, 267 | py::arg("recording_id"), 268 | py::call_guard(), 269 | py::keep_alive<0, 1>()) 270 | .def("find_last", &archive::find_last, 271 | py::arg("channel"), 272 | py::arg("stream_id"), 273 | py::call_guard(), 274 | py::keep_alive<0, 1>()) 275 | .def("add_recorded_publication", &archive::add_recorded_publication, 276 | py::arg("channel"), 277 | py::arg("stream_id"), 278 | py::call_guard(), 279 | py::keep_alive<0, 1>()) 280 | .def("add_recorded_exclusive_publication", &archive::add_recorded_exclusive_publication, 281 | py::arg("channel"), 282 | py::arg("stream_id"), 283 | py::call_guard(), 284 | py::keep_alive<0, 1>()) 285 | .def("__str__", &archive::__str__); 286 | 287 | } 288 | 289 | --------------------------------------------------------------------------------