diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /messagebus |
Publish
Diffstat (limited to 'messagebus')
617 files changed, 45569 insertions, 0 deletions
diff --git a/messagebus/.gitignore b/messagebus/.gitignore new file mode 100644 index 00000000000..b79b059b0a7 --- /dev/null +++ b/messagebus/.gitignore @@ -0,0 +1,7 @@ +.classpath +.project +messagebus-lib.iml +target +/pom.xml.build +Makefile +Testing diff --git a/messagebus/CMakeLists.txt b/messagebus/CMakeLists.txt new file mode 100644 index 00000000000..c66874e4ec4 --- /dev/null +++ b/messagebus/CMakeLists.txt @@ -0,0 +1,26 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_define_module( + DEPENDS + fastos + vespalog + config_cloudconfig + vespalib + staging_vespalib + fnet + slobrok + slobrok_slobrokserver + + LIBS + src/vespa/messagebus + src/vespa/messagebus/network + src/vespa/messagebus/routing + src/vespa/messagebus/testlib + + APPS + src/apps/printversion + src/binref + + TESTS + src/tests + test +) diff --git a/messagebus/OWNERS b/messagebus/OWNERS new file mode 100644 index 00000000000..631c3b2dd30 --- /dev/null +++ b/messagebus/OWNERS @@ -0,0 +1,2 @@ +dybdahl +balder diff --git a/messagebus/README b/messagebus/README new file mode 100644 index 00000000000..55518dcab92 --- /dev/null +++ b/messagebus/README @@ -0,0 +1 @@ +This is the generic Vespa message bus infrastructure. diff --git a/messagebus/pom.xml b/messagebus/pom.xml new file mode 100644 index 00000000000..16868d49e7c --- /dev/null +++ b/messagebus/pom.xml @@ -0,0 +1,98 @@ +<?xml version="1.0"?> +<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>com.yahoo.vespa</groupId> + <artifactId>parent</artifactId> + <version>6-SNAPSHOT</version> + <relativePath>../parent/pom.xml</relativePath> + </parent> + <artifactId>messagebus</artifactId> + <version>6-SNAPSHOT</version> + <packaging>jar</packaging> + <dependencies> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>vespajlib</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>component</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>config</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>configdefinitions</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>jrt</artifactId> + <version>${project.version}</version> + </dependency> + </dependencies> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <compilerArgs> + <arg>-Werror</arg> + <arg>-Xlint:all</arg> + <arg>-Xlint:-serial</arg> + </compilerArgs> + </configuration> + </plugin> + <plugin> + <artifactId>maven-assembly-plugin</artifactId> + <configuration> + <descriptorRefs> + <descriptorRef>jar-with-dependencies</descriptorRef> + </descriptorRefs> + </configuration> + <executions> + <execution> + <id>make-assembly</id> + <phase>package</phase> + <!-- append to the packaging phase. --> + <goals> + <goal>single</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>com.yahoo.vespa</groupId> + <artifactId>config-class-plugin</artifactId> + <version>${project.version}</version> + <configuration> + <defFilesDirectories>src/main/config/</defFilesDirectories> + </configuration> + <executions> + <execution> + <id>config-gen</id> + <goals> + <goal>config-gen</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/messagebus/src/.gitignore b/messagebus/src/.gitignore new file mode 100644 index 00000000000..3b9f1ee8e62 --- /dev/null +++ b/messagebus/src/.gitignore @@ -0,0 +1,5 @@ +Makefile.ini +config_command.sh +doxygen +messagebus.mak +project.dsw diff --git a/messagebus/src/Doxyfile b/messagebus/src/Doxyfile new file mode 100644 index 00000000000..c0392277d2a --- /dev/null +++ b/messagebus/src/Doxyfile @@ -0,0 +1,1257 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +# Doxyfile 1.4.7 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project +# +# All text after a hash (#) is considered a comment and will be ignored +# The format is: +# TAG = value [value, ...] +# For lists items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (" ") + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded +# by quotes) that should identify the project. + +PROJECT_NAME = messagebus + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. +# This could be handy for archiving the generated documentation or +# if some version control system is used. + +PROJECT_NUMBER = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) +# base path where the generated documentation will be put. +# If a relative path is entered, it will be relative to the location +# where doxygen was started. If left blank the current directory will be used. + +OUTPUT_DIRECTORY = ../../doc + +# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create +# 4096 sub-directories (in 2 levels) under the output directory of each output +# format and will distribute the generated files over these directories. +# Enabling this option can be useful when feeding doxygen a huge amount of +# source files, where putting all generated files in the same directory would +# otherwise cause performance problems for the file system. + +CREATE_SUBDIRS = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# The default language is English, other supported languages are: +# Brazilian, Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, +# Dutch, Finnish, French, German, Greek, Hungarian, Italian, Japanese, +# Japanese-en (Japanese with English messages), Korean, Korean-en, Norwegian, +# Polish, Portuguese, Romanian, Russian, Serbian, Slovak, Slovene, Spanish, +# Swedish, and Ukrainian. + +OUTPUT_LANGUAGE = English + +# This tag can be used to specify the encoding used in the generated output. +# The encoding is not always determined by the language that is chosen, +# but also whether or not the output is meant for Windows or non-Windows users. +# In case there is a difference, setting the USE_WINDOWS_ENCODING tag to YES +# forces the Windows encoding (this is the default for the Windows binary), +# whereas setting the tag to NO uses a Unix-style encoding (the default for +# all platforms other than Windows). + +USE_WINDOWS_ENCODING = NO + +# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will +# include brief member descriptions after the members that are listed in +# the file and class documentation (similar to JavaDoc). +# Set to NO to disable this. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend +# the brief description of a member or function before the detailed description. +# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator +# that is used to form the text in various listings. Each string +# in this list, if found as the leading text of the brief description, will be +# stripped from the text and the result after processing the whole list, is +# used as the annotated text. Otherwise, the brief description is used as-is. +# If left blank, the following values are used ("$name" is automatically +# replaced with the name of the entity): "The $name class" "The $name widget" +# "The $name file" "is" "provides" "specifies" "contains" +# "represents" "a" "an" "the" + +ABBREVIATE_BRIEF = + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# Doxygen will generate a detailed section even if there is only a brief +# description. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full +# path before files name in the file list and in the header files. If set +# to NO the shortest path that makes the file name unique will be used. + +FULL_PATH_NAMES = YES + +# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag +# can be used to strip a user-defined part of the path. Stripping is +# only done if one of the specified strings matches the left-hand part of +# the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the +# path to strip. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of +# the path mentioned in the documentation of a class, which tells +# the reader which header file to include in order to use a class. +# If left blank only the name of the header file containing the class +# definition is used. Otherwise one should specify the include paths that +# are normally passed to the compiler using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter +# (but less readable) file names. This can be useful is your file systems +# doesn't support long names like on DOS, Mac, or CD-ROM. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen +# will interpret the first line (until the first dot) of a JavaDoc-style +# comment as the brief description. If set to NO, the JavaDoc +# comments will behave just like the Qt-style comments (thus requiring an +# explicit @brief command for a brief description. + +JAVADOC_AUTOBRIEF = YES + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen +# treat a multi-line C++ special comment block (i.e. a block of //! or /// +# comments) as a brief description. This used to be the default behaviour. +# The new default is to treat a multi-line C++ comment block as a detailed +# description. Set this tag to YES if you prefer the old behaviour instead. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the DETAILS_AT_TOP tag is set to YES then Doxygen +# will output the detailed description near the top, like JavaDoc. +# If set to NO, the detailed description appears after the member +# documentation. + +DETAILS_AT_TOP = NO + +# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented +# member inherits the documentation from any documented member that it +# re-implements. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce +# a new page for each member. If set to NO, the documentation of a member will +# be part of the file/class/namespace that contains it. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. +# Doxygen uses this value to replace tabs by spaces in code fragments. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that acts +# as commands in the documentation. An alias has the form "name=value". +# For example adding "sideeffect=\par Side Effects:\n" will allow you to +# put the command \sideeffect (or @sideeffect) in the documentation, which +# will result in a user-defined paragraph with heading "Side Effects:". +# You can put \n's in the value part of an alias to insert newlines. + +ALIASES = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C +# sources only. Doxygen will then generate output that is more tailored for C. +# For instance, some of the names that are used will be different. The list +# of all members will be omitted, etc. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java +# sources only. Doxygen will then generate output that is more tailored for Java. +# For instance, namespaces will be presented as packages, qualified scopes +# will look different, etc. + +OPTIMIZE_OUTPUT_JAVA = NO + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want to +# include (a tag file for) the STL sources as input, then you should +# set this tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); v.s. +# func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. + +BUILTIN_STL_SUPPORT = NO + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES, then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. + +DISTRIBUTE_GROUP_DOC = NO + +# Set the SUBGROUPING tag to YES (the default) to allow class member groups of +# the same type (for instance a group of public functions) to be put as a +# subgroup of that type (e.g. under the Public Functions section). Set it to +# NO to prevent subgrouping. Alternatively, this can be done per class using +# the \nosubgrouping command. + +SUBGROUPING = YES + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in +# documentation are documented, even if no documentation was available. +# Private class members and static file members will be hidden unless +# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES + +EXTRACT_ALL = NO + +# If the EXTRACT_PRIVATE tag is set to YES all private members of a class +# will be included in the documentation. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_STATIC tag is set to YES all static members of a file +# will be included in the documentation. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) +# defined locally in source files will be included in the documentation. +# If set to NO only classes defined in header files are included. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. When set to YES local +# methods, which are defined in the implementation section but not in +# the interface are included in the documentation. +# If set to NO (the default) only methods in the interface are included. + +EXTRACT_LOCAL_METHODS = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all +# undocumented members of documented classes, files or namespaces. +# If set to NO (the default) these members will be included in the +# various overviews, but no documentation section is generated. +# This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. +# If set to NO (the default) these classes will be included in the various +# overviews. This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all +# friend (class|struct|union) declarations. +# If set to NO (the default) these declarations will be included in the +# documentation. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any +# documentation blocks found inside the body of a function. +# If set to NO (the default) these blocks will be appended to the +# function's detailed documentation block. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation +# that is typed after a \internal command is included. If the tag is set +# to NO (the default) then the documentation will be excluded. +# Set it to YES to include the internal documentation. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate +# file names in lower-case letters. If set to YES upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. + +CASE_SENSE_NAMES = YES + +# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen +# will show members with their full class and namespace scopes in the +# documentation. If set to YES the scope will be hidden. + +HIDE_SCOPE_NAMES = NO + +# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen +# will put a list of the files that are included by a file in the documentation +# of that file. + +SHOW_INCLUDE_FILES = YES + +# If the INLINE_INFO tag is set to YES (the default) then a tag [inline] +# is inserted in the documentation for inline members. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen +# will sort the (detailed) documentation of file and class members +# alphabetically by member name. If set to NO the members will appear in +# declaration order. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the +# brief documentation of file, namespace and class members alphabetically +# by member name. If set to NO (the default) the members will appear in +# declaration order. + +SORT_BRIEF_DOCS = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be +# sorted by fully-qualified names, including namespaces. If set to +# NO (the default), the class list will be sorted only by class name, +# not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the +# alphabetical list. + +SORT_BY_SCOPE_NAME = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or +# disable (NO) the todo list. This list is created by putting \todo +# commands in the documentation. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or +# disable (NO) the test list. This list is created by putting \test +# commands in the documentation. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or +# disable (NO) the bug list. This list is created by putting \bug +# commands in the documentation. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or +# disable (NO) the deprecated list. This list is created by putting +# \deprecated commands in the documentation. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional +# documentation sections, marked by \if sectionname ... \endif. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines +# the initial value of a variable or define consists of for it to appear in +# the documentation. If the initializer consists of more lines than specified +# here it will be hidden. Use a value of 0 to hide initializers completely. +# The appearance of the initializer of individual variables and defines in the +# documentation can be controlled using \showinitializer or \hideinitializer +# command in the documentation regardless of this setting. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated +# at the bottom of the documentation of classes and structs. If set to YES the +# list will mention the files that were used to generate the documentation. + +SHOW_USED_FILES = YES + +# If the sources in your project are distributed over multiple directories +# then setting the SHOW_DIRECTORIES tag to YES will show the directory hierarchy +# in the documentation. The default is NO. + +SHOW_DIRECTORIES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from the +# version control system). Doxygen will invoke the program by executing (via +# popen()) the command <command> <input-file>, where <command> is the value of +# the FILE_VERSION_FILTER tag, and <input-file> is the name of an input file +# provided by doxygen. Whatever the program writes to standard output +# is used as the file version. See the manual for examples. + +FILE_VERSION_FILTER = + +#--------------------------------------------------------------------------- +# configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated +# by doxygen. Possible values are YES and NO. If left blank NO is used. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated by doxygen. Possible values are YES and NO. If left blank +# NO is used. + +WARNINGS = YES + +# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings +# for undocumented members. If EXTRACT_ALL is set to YES then this flag will +# automatically be disabled. + +WARN_IF_UNDOCUMENTED = YES + +# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some +# parameters in a documented function, or documenting parameters that +# don't exist or using markup commands wrongly. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be abled to get warnings for +# functions that are documented, but have no documentation for their parameters +# or return value. If set to NO (the default) doxygen will only warn about +# wrong or incomplete parameter documentation, but not about the absence of +# documentation. + +WARN_NO_PARAMDOC = NO + +# The WARN_FORMAT tag determines the format of the warning messages that +# doxygen can produce. The string should contain the $file, $line, and $text +# tags, which will be replaced by the file and line number from which the +# warning originated and the warning text. Optionally the format may contain +# $version, which will be replaced by the version of the file (if it could +# be obtained via FILE_VERSION_FILTER) + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning +# and error messages should be written. If left blank the output is written +# to stderr. + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag can be used to specify the files and/or directories that contain +# documented source files. You may enter file names like "myfile.cpp" or +# directories like "/usr/src/myproject". Separate the files or directories +# with spaces. + +INPUT = messagebus + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank the following patterns are tested: +# *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx +# *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.py + +FILE_PATTERNS = *.h \ + *.cpp + +# The RECURSIVE tag can be used to turn specify whether or not subdirectories +# should be searched for input files as well. Possible values are YES and NO. +# If left blank NO is used. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. + +EXCLUDE = messagebus/testlib \ + messagebus/sanity.cpp \ + messagebus/config-messagebus.h \ + messagebus/config-messagebus.cpp + +# The EXCLUDE_SYMLINKS tag can be used select whether or not files or +# directories that are symbolic links (a Unix filesystem feature) are excluded +# from the input. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. Note that the wildcards are matched +# against the file with absolute path, so to exclude all test directories +# for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or +# directories that contain example code fragments that are included (see +# the \include command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank all files are included. + +EXAMPLE_PATTERNS = + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude +# commands irrespective of the value of the RECURSIVE tag. +# Possible values are YES and NO. If left blank NO is used. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or +# directories that contain image that are included in the documentation (see +# the \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command <filter> <input-file>, where <filter> +# is the value of the INPUT_FILTER tag, and <input-file> is the name of an +# input file. Doxygen will then use the output that the filter program writes +# to standard output. If FILTER_PATTERNS is specified, this tag will be +# ignored. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: +# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further +# info on how filters are used. If FILTER_PATTERNS is empty, INPUT_FILTER +# is applied to all files. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will be used to filter the input files when producing source +# files to browse (i.e. when SOURCE_BROWSER is set to YES). + +FILTER_SOURCE_FILES = NO + +#--------------------------------------------------------------------------- +# configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will +# be generated. Documented entities will be cross-referenced with these sources. +# Note: To get rid of all source code in the generated output, make sure also +# VERBATIM_HEADERS is set to NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body +# of functions and classes directly in the documentation. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct +# doxygen to hide any special comment blocks from generated source code +# fragments. Normal C and C++ comments will always remain visible. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES (the default) +# then for each documented function all documented +# functions referencing it will be listed. + +REFERENCED_BY_RELATION = YES + +# If the REFERENCES_RELATION tag is set to YES (the default) +# then for each documented function all documented entities +# called/used by that function will be listed. + +REFERENCES_RELATION = YES + +# If the REFERENCES_LINK_SOURCE tag is set to YES (the default) +# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from +# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will +# link to the source code. Otherwise they will link to the documentstion. + +REFERENCES_LINK_SOURCE = YES + +# If the USE_HTAGS tag is set to YES then the references to source code +# will point to the HTML generated by the htags(1) tool instead of doxygen +# built-in source browser. The htags tool is part of GNU's global source +# tagging system (see http://www.gnu.org/software/global/global.html). You +# will need version 4.8.6 or higher. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen +# will generate a verbatim copy of the header file for each class for +# which an include is specified. Set to NO to disable this. + +VERBATIM_HEADERS = YES + +#--------------------------------------------------------------------------- +# configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index +# of all compounds will be generated. Enable this if the project +# contains a lot of classes, structs, unions or interfaces. + +ALPHABETICAL_INDEX = NO + +# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then +# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns +# in which this list will be split (can be a number in the range [1..20]) + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all +# classes will be put under the same header in the alphabetical index. +# The IGNORE_PREFIX tag can be used to specify one or more prefixes that +# should be ignored while generating the index headers. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES (the default) Doxygen will +# generate HTML output. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `html' will be used as the default path. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for +# each generated HTML page (for example: .htm,.php,.asp). If it is left blank +# doxygen will generate files with .html extension. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a personal HTML header for +# each generated HTML page. If it is left blank doxygen will generate a +# standard header. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a personal HTML footer for +# each generated HTML page. If it is left blank doxygen will generate a +# standard footer. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading +# style sheet that is used by each HTML page. It can be used to +# fine-tune the look of the HTML output. If the tag is left blank doxygen +# will generate a default style sheet. Note that doxygen will try to copy +# the style sheet file to the HTML output directory, so don't put your own +# stylesheet in the HTML output directory as well, or it will be erased! + +HTML_STYLESHEET = + +# If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes, +# files or namespaces will be aligned in HTML using tables. If set to +# NO a bullet list will be used. + +HTML_ALIGN_MEMBERS = YES + +# If the GENERATE_HTMLHELP tag is set to YES, additional index files +# will be generated that can be used as input for tools like the +# Microsoft HTML help workshop to generate a compressed HTML help file (.chm) +# of the generated HTML documentation. + +GENERATE_HTMLHELP = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can +# be used to specify the file name of the resulting .chm file. You +# can add a path in front of the file if the result should not be +# written to the html output directory. + +CHM_FILE = + +# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can +# be used to specify the location (absolute path including file name) of +# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run +# the HTML help compiler on the generated index.hhp. + +HHC_LOCATION = + +# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag +# controls if a separate .chi index file is generated (YES) or that +# it should be included in the master .chm file (NO). + +GENERATE_CHI = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag +# controls whether a binary table of contents is generated (YES) or a +# normal table of contents (NO) in the .chm file. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members +# to the contents of the HTML help documentation and to the tree view. + +TOC_EXPAND = NO + +# The DISABLE_INDEX tag can be used to turn on/off the condensed index at +# top of each HTML page. The value NO (the default) enables the index and +# the value YES disables it. + +DISABLE_INDEX = NO + +# This tag can be used to set the number of enum values (range [1..20]) +# that doxygen will group on one line in the generated HTML documentation. + +ENUM_VALUES_PER_LINE = 4 + +# If the GENERATE_TREEVIEW tag is set to YES, a side panel will be +# generated containing a tree-like index structure (just like the one that +# is generated for HTML Help). For this to work a browser that supports +# JavaScript, DHTML, CSS and frames is required (for instance Mozilla 1.0+, +# Netscape 6.0+, Internet explorer 5.0+, or Konqueror). Windows users are +# probably better off using the HTML help feature. + +GENERATE_TREEVIEW = NO + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be +# used to set the initial width (in pixels) of the frame in which the tree +# is shown. + +TREEVIEW_WIDTH = 250 + +#--------------------------------------------------------------------------- +# configuration options related to the LaTeX output +#--------------------------------------------------------------------------- + +# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will +# generate Latex output. + +GENERATE_LATEX = NO + +# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `latex' will be used as the default path. + +LATEX_OUTPUT = latex + +# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be +# invoked. If left blank `latex' will be used as the default command name. + +LATEX_CMD_NAME = latex + +# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to +# generate index for LaTeX. If left blank `makeindex' will be used as the +# default command name. + +MAKEINDEX_CMD_NAME = makeindex + +# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact +# LaTeX documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_LATEX = NO + +# The PAPER_TYPE tag can be used to set the paper type that is used +# by the printer. Possible values are: a4, a4wide, letter, legal and +# executive. If left blank a4wide will be used. + +PAPER_TYPE = a4wide + +# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX +# packages that should be included in the LaTeX output. + +EXTRA_PACKAGES = + +# The LATEX_HEADER tag can be used to specify a personal LaTeX header for +# the generated latex document. The header should contain everything until +# the first chapter. If it is left blank doxygen will generate a +# standard header. Notice: only use this tag if you know what you are doing! + +LATEX_HEADER = + +# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated +# is prepared for conversion to pdf (using ps2pdf). The pdf file will +# contain links (just like the HTML output) instead of page references +# This makes the output suitable for online browsing using a pdf viewer. + +PDF_HYPERLINKS = NO + +# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of +# plain latex in the generated Makefile. Set this option to YES to get a +# higher quality PDF documentation. + +USE_PDFLATEX = NO + +# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. +# command to the generated LaTeX files. This will instruct LaTeX to keep +# running if errors occur, instead of asking the user for help. +# This option is also used when generating formulas in HTML. + +LATEX_BATCHMODE = NO + +# If LATEX_HIDE_INDICES is set to YES then doxygen will not +# include the index chapters (such as File Index, Compound Index, etc.) +# in the output. + +LATEX_HIDE_INDICES = NO + +#--------------------------------------------------------------------------- +# configuration options related to the RTF output +#--------------------------------------------------------------------------- + +# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output +# The RTF output is optimized for Word 97 and may not look very pretty with +# other RTF readers or editors. + +GENERATE_RTF = NO + +# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `rtf' will be used as the default path. + +RTF_OUTPUT = rtf + +# If the COMPACT_RTF tag is set to YES Doxygen generates more compact +# RTF documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_RTF = NO + +# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated +# will contain hyperlink fields. The RTF file will +# contain links (just like the HTML output) instead of page references. +# This makes the output suitable for online browsing using WORD or other +# programs which support those fields. +# Note: wordpad (write) and others do not support links. + +RTF_HYPERLINKS = NO + +# Load stylesheet definitions from file. Syntax is similar to doxygen's +# config file, i.e. a series of assignments. You only have to provide +# replacements, missing definitions are set to their default value. + +RTF_STYLESHEET_FILE = + +# Set optional variables used in the generation of an rtf document. +# Syntax is similar to doxygen's config file. + +RTF_EXTENSIONS_FILE = + +#--------------------------------------------------------------------------- +# configuration options related to the man page output +#--------------------------------------------------------------------------- + +# If the GENERATE_MAN tag is set to YES (the default) Doxygen will +# generate man pages + +GENERATE_MAN = NO + +# The MAN_OUTPUT tag is used to specify where the man pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `man' will be used as the default path. + +MAN_OUTPUT = man + +# The MAN_EXTENSION tag determines the extension that is added to +# the generated man pages (default is the subroutine's section .3) + +MAN_EXTENSION = .3 + +# If the MAN_LINKS tag is set to YES and Doxygen generates man output, +# then it will generate one additional man file for each entity +# documented in the real man page(s). These additional files +# only source the real man page, but without them the man command +# would be unable to find the correct page. The default is NO. + +MAN_LINKS = NO + +#--------------------------------------------------------------------------- +# configuration options related to the XML output +#--------------------------------------------------------------------------- + +# If the GENERATE_XML tag is set to YES Doxygen will +# generate an XML file that captures the structure of +# the code including all documentation. + +GENERATE_XML = NO + +# The XML_OUTPUT tag is used to specify where the XML pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `xml' will be used as the default path. + +XML_OUTPUT = xml + +# The XML_SCHEMA tag can be used to specify an XML schema, +# which can be used by a validating XML parser to check the +# syntax of the XML files. + +XML_SCHEMA = + +# The XML_DTD tag can be used to specify an XML DTD, +# which can be used by a validating XML parser to check the +# syntax of the XML files. + +XML_DTD = + +# If the XML_PROGRAMLISTING tag is set to YES Doxygen will +# dump the program listings (including syntax highlighting +# and cross-referencing information) to the XML output. Note that +# enabling this will significantly increase the size of the XML output. + +XML_PROGRAMLISTING = YES + +#--------------------------------------------------------------------------- +# configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- + +# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will +# generate an AutoGen Definitions (see autogen.sf.net) file +# that captures the structure of the code including all +# documentation. Note that this feature is still experimental +# and incomplete at the moment. + +GENERATE_AUTOGEN_DEF = NO + +#--------------------------------------------------------------------------- +# configuration options related to the Perl module output +#--------------------------------------------------------------------------- + +# If the GENERATE_PERLMOD tag is set to YES Doxygen will +# generate a Perl module file that captures the structure of +# the code including all documentation. Note that this +# feature is still experimental and incomplete at the +# moment. + +GENERATE_PERLMOD = NO + +# If the PERLMOD_LATEX tag is set to YES Doxygen will generate +# the necessary Makefile rules, Perl scripts and LaTeX code to be able +# to generate PDF and DVI output from the Perl module output. + +PERLMOD_LATEX = NO + +# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be +# nicely formatted so it can be parsed by a human reader. This is useful +# if you want to understand what is going on. On the other hand, if this +# tag is set to NO the size of the Perl module output will be much smaller +# and Perl will parse it just the same. + +PERLMOD_PRETTY = YES + +# The names of the make variables in the generated doxyrules.make file +# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. +# This is useful so different doxyrules.make files included by the same +# Makefile don't overwrite each other's variables. + +PERLMOD_MAKEVAR_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- + +# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will +# evaluate all C-preprocessor directives found in the sources and include +# files. + +ENABLE_PREPROCESSING = YES + +# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro +# names in the source code. If set to NO (the default) only conditional +# compilation will be performed. Macro expansion can be done in a controlled +# way by setting EXPAND_ONLY_PREDEF to YES. + +MACRO_EXPANSION = NO + +# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES +# then the macro expansion is limited to the macros specified with the +# PREDEFINED and EXPAND_AS_DEFINED tags. + +EXPAND_ONLY_PREDEF = NO + +# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files +# in the INCLUDE_PATH (see below) will be search if a #include is found. + +SEARCH_INCLUDES = YES + +# The INCLUDE_PATH tag can be used to specify one or more directories that +# contain include files that are not input files but should be processed by +# the preprocessor. + +INCLUDE_PATH = + +# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard +# patterns (like *.h and *.hpp) to filter out the header-files in the +# directories. If left blank, the patterns specified with FILE_PATTERNS will +# be used. + +INCLUDE_FILE_PATTERNS = + +# The PREDEFINED tag can be used to specify one or more macro names that +# are defined before the preprocessor is started (similar to the -D option of +# gcc). The argument of the tag is a list of macros of the form: name +# or name=definition (no spaces). If the definition and the = are +# omitted =1 is assumed. To prevent a macro definition from being +# undefined via #undef or recursively expanded use the := operator +# instead of the = operator. + +PREDEFINED = IAM_DOXYGEN + +# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then +# this tag can be used to specify a list of macro names that should be expanded. +# The macro definition that is found in the sources will be used. +# Use the PREDEFINED tag if you want to use a different macro definition. + +EXPAND_AS_DEFINED = + +# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then +# doxygen's preprocessor will remove all function-like macros that are alone +# on a line, have an all uppercase name, and do not end with a semicolon. Such +# function macros are typically used for boiler-plate code, and will confuse +# the parser if not removed. + +SKIP_FUNCTION_MACROS = YES + +#--------------------------------------------------------------------------- +# Configuration::additions related to external references +#--------------------------------------------------------------------------- + +# The TAGFILES option can be used to specify one or more tagfiles. +# Optionally an initial location of the external documentation +# can be added for each tagfile. The format of a tag file without +# this location is as follows: +# TAGFILES = file1 file2 ... +# Adding location for the tag files is done as follows: +# TAGFILES = file1=loc1 "file2 = loc2" ... +# where "loc1" and "loc2" can be relative or absolute paths or +# URLs. If a location is present for each tag, the installdox tool +# does not have to be run to correct the links. +# Note that each tag file must have a unique name +# (where the name does NOT include the path) +# If a tag file is not located in the directory in which doxygen +# is run, you must also specify the path to the tagfile here. + +TAGFILES = + +# When a file name is specified after GENERATE_TAGFILE, doxygen will create +# a tag file that is based on the input files it reads. + +GENERATE_TAGFILE = + +# If the ALLEXTERNALS tag is set to YES all external classes will be listed +# in the class index. If set to NO only the inherited external classes +# will be listed. + +ALLEXTERNALS = NO + +# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed +# in the modules index. If set to NO, only the current project's groups will +# be listed. + +EXTERNAL_GROUPS = YES + +# The PERL_PATH should be the absolute path and name of the perl script +# interpreter (i.e. the result of `which perl'). + +PERL_PATH = /usr/bin/perl + +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- + +# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will +# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base +# or super classes. Setting the tag to NO turns the diagrams off. Note that +# this option is superseded by the HAVE_DOT option below. This is only a +# fallback. It is recommended to install and use dot, since it yields more +# powerful graphs. + +CLASS_DIAGRAMS = YES + +# If set to YES, the inheritance and collaboration graphs will hide +# inheritance and usage relations if the target is undocumented +# or is not a class. + +HIDE_UNDOC_RELATIONS = YES + +# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is +# available from the path. This tool is part of Graphviz, a graph visualization +# toolkit from AT&T and Lucent Bell Labs. The other options in this section +# have no effect if this option is set to NO (the default) + +HAVE_DOT = NO + +# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect inheritance relations. Setting this tag to YES will force the +# the CLASS_DIAGRAMS tag to NO. + +CLASS_GRAPH = YES + +# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect implementation dependencies (inheritance, containment, and +# class references variables) of the class with other documented classes. + +COLLABORATION_GRAPH = YES + +# If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for groups, showing the direct groups dependencies + +GROUP_GRAPHS = YES + +# If the UML_LOOK tag is set to YES doxygen will generate inheritance and +# collaboration diagrams in a style similar to the OMG's Unified Modeling +# Language. + +UML_LOOK = NO + +# If set to YES, the inheritance and collaboration graphs will show the +# relations between templates and their instances. + +TEMPLATE_RELATIONS = NO + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT +# tags are set to YES then doxygen will generate a graph for each documented +# file showing the direct and indirect include dependencies of the file with +# other documented files. + +INCLUDE_GRAPH = YES + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and +# HAVE_DOT tags are set to YES then doxygen will generate a graph for each +# documented header file showing the documented files that directly or +# indirectly include this file. + +INCLUDED_BY_GRAPH = YES + +# If the CALL_GRAPH and HAVE_DOT tags are set to YES then doxygen will +# generate a call dependency graph for every global function or class method. +# Note that enabling this option will significantly increase the time of a run. +# So in most cases it will be better to enable call graphs for selected +# functions only using the \callgraph command. + +CALL_GRAPH = NO + +# If the CALLER_GRAPH and HAVE_DOT tags are set to YES then doxygen will +# generate a caller dependency graph for every global function or class method. +# Note that enabling this option will significantly increase the time of a run. +# So in most cases it will be better to enable caller graphs for selected +# functions only using the \callergraph command. + +CALLER_GRAPH = NO + +# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen +# will graphical hierarchy of all classes instead of a textual one. + +GRAPHICAL_HIERARCHY = YES + +# If the DIRECTORY_GRAPH, SHOW_DIRECTORIES and HAVE_DOT tags are set to YES +# then doxygen will show the dependencies a directory has on other directories +# in a graphical way. The dependency relations are determined by the #include +# relations between the files in the directories. + +DIRECTORY_GRAPH = YES + +# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images +# generated by dot. Possible values are png, jpg, or gif +# If left blank png will be used. + +DOT_IMAGE_FORMAT = png + +# The tag DOT_PATH can be used to specify the path where the dot tool can be +# found. If left blank, it is assumed the dot tool can be found in the path. + +DOT_PATH = + +# The DOTFILE_DIRS tag can be used to specify one or more directories that +# contain dot files that are included in the documentation (see the +# \dotfile command). + +DOTFILE_DIRS = + +# The MAX_DOT_GRAPH_WIDTH tag can be used to set the maximum allowed width +# (in pixels) of the graphs generated by dot. If a graph becomes larger than +# this value, doxygen will try to truncate the graph, so that it fits within +# the specified constraint. Beware that most browsers cannot cope with very +# large images. + +MAX_DOT_GRAPH_WIDTH = 1024 + +# The MAX_DOT_GRAPH_HEIGHT tag can be used to set the maximum allows height +# (in pixels) of the graphs generated by dot. If a graph becomes larger than +# this value, doxygen will try to truncate the graph, so that it fits within +# the specified constraint. Beware that most browsers cannot cope with very +# large images. + +MAX_DOT_GRAPH_HEIGHT = 1024 + +# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the +# graphs generated by dot. A depth value of 3 means that only nodes reachable +# from the root by following a path via at most 3 edges will be shown. Nodes +# that lay further from the root node will be omitted. Note that setting this +# option to 1 or 2 may greatly reduce the computation time needed for large +# code bases. Also note that a graph may be further truncated if the graph's +# image dimensions are not sufficient to fit the graph (see MAX_DOT_GRAPH_WIDTH +# and MAX_DOT_GRAPH_HEIGHT). If 0 is used for the depth value (the default), +# the graph is not depth-constrained. + +MAX_DOT_GRAPH_DEPTH = 0 + +# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent +# background. This is disabled by default, which results in a white background. +# Warning: Depending on the platform used, enabling this option may lead to +# badly anti-aliased labels on the edges of a graph (i.e. they become hard to +# read). + +DOT_TRANSPARENT = NO + +# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output +# files in one run (i.e. multiple -o and -T options on the command line). This +# makes dot run faster, but since only newer versions of dot (>1.8.10) +# support this, this feature is disabled by default. + +DOT_MULTI_TARGETS = NO + +# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will +# generate a legend page explaining the meaning of the various boxes and +# arrows in the dot generated graphs. + +GENERATE_LEGEND = YES + +# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will +# remove the intermediate dot files that are used to generate +# the various graphs. + +DOT_CLEANUP = YES + +#--------------------------------------------------------------------------- +# Configuration::additions related to the search engine +#--------------------------------------------------------------------------- + +# The SEARCHENGINE tag specifies whether or not a search engine should be +# used. If set to NO the values of all tags below this one will be ignored. + +SEARCHENGINE = NO diff --git a/messagebus/src/apps/printversion/.gitignore b/messagebus/src/apps/printversion/.gitignore new file mode 100644 index 00000000000..46d3c9812ec --- /dev/null +++ b/messagebus/src/apps/printversion/.gitignore @@ -0,0 +1,4 @@ +/.depend +/Makefile +/printversion +messagebus_printversion_app diff --git a/messagebus/src/apps/printversion/CMakeLists.txt b/messagebus/src/apps/printversion/CMakeLists.txt new file mode 100644 index 00000000000..2576eef901e --- /dev/null +++ b/messagebus/src/apps/printversion/CMakeLists.txt @@ -0,0 +1,8 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_printversion_app + SOURCES + printversion.cpp + INSTALL bin + DEPENDS + messagebus +) diff --git a/messagebus/src/apps/printversion/printversion.cpp b/messagebus/src/apps/printversion/printversion.cpp new file mode 100644 index 00000000000..8401653fc51 --- /dev/null +++ b/messagebus/src/apps/printversion/printversion.cpp @@ -0,0 +1,19 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/messagebus/vtag.h> +#include <stdio.h> +#include <vespa/vespalib/component/version.h> + +int main(int, char **) +{ + printf("version tag: %s\n", mbus::VersionTag); + printf("version tag date: %s\n", mbus::VersionTagDate); + printf("version tag system: %s\n", mbus::VersionTagSystem); + printf("version tag system rev: %s\n", mbus::VersionTagSystemRev); + printf("version tag builder: %s\n", mbus::VersionTagBuilder); + printf("nice version:\n\t"); + mbus::Vtag::printVersionNice(); + printf("\n"); + printf("currentVersion object: %s\n", mbus::Vtag::currentVersion.toString().c_str()); + return 0; +} diff --git a/messagebus/src/binref/.gitignore b/messagebus/src/binref/.gitignore new file mode 100644 index 00000000000..cfb0e619824 --- /dev/null +++ b/messagebus/src/binref/.gitignore @@ -0,0 +1,3 @@ +.depend +Makefile +testrun.sh diff --git a/messagebus/src/binref/CMakeLists.txt b/messagebus/src/binref/CMakeLists.txt new file mode 100644 index 00000000000..5c90dd5bfcc --- /dev/null +++ b/messagebus/src/binref/CMakeLists.txt @@ -0,0 +1 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. diff --git a/messagebus/src/main/config/messagebus.def b/messagebus/src/main/config/messagebus.def new file mode 100644 index 00000000000..c88e4ebbe6b --- /dev/null +++ b/messagebus/src/main/config/messagebus.def @@ -0,0 +1,30 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +version=2 +namespace=messagebus + +# Name of the protocol that uses this routing table. All +# instances of message bus must support all named protocols. +routingtable[].protocol string + +# A protocol-unique name for a hop. +routingtable[].hop[].name string + +# The selector string of a hop, this string typically contains +# routing policy references on the form [policy-name:parameter]. +# The protocol for the routing table must support all named +# policies. +routingtable[].hop[].selector string + +# List of recipients for a hop. These strings may contain +# wildcards to allow the network layer to choose any single +# matching service. +routingtable[].hop[].recipient[] string + +# Whether or not to ignore the result from this hop. +routingtable[].hop[].ignoreresult bool default=false + +# A protocol-unique name for a route. +routingtable[].route[].name string + +# An array of hop names that together make up the route. +routingtable[].route[].hop[] string diff --git a/messagebus/src/main/java/com/yahoo/messagebus/AllPassThrottlePolicy.java b/messagebus/src/main/java/com/yahoo/messagebus/AllPassThrottlePolicy.java new file mode 100644 index 00000000000..72f13ac2a1e --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/AllPassThrottlePolicy.java @@ -0,0 +1,22 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +/** + * This is an implementation of the {@link ThrottlePolicy} that passes all requests (no real throttling). + * @author <a href="mailto:dybdahl@yahoo-inc.com">Haakon Dybdahl</a> + */ +public class AllPassThrottlePolicy implements ThrottlePolicy +{ + @Override + public boolean canSend(Message msg, int pendingCount) { + return true; + } + + @Override + public void processMessage(Message msg) { + } + + @Override + public void processReply(Reply reply) { + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/CallStack.java b/messagebus/src/main/java/com/yahoo/messagebus/CallStack.java new file mode 100644 index 00000000000..c4a1f2d9097 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/CallStack.java @@ -0,0 +1,83 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Stack; + +/** + * An wrapper around a stack of frame objects that is aware of the message that owns it. It contains functionality to + * move the content of itself to another, never to copy, since a callback is unique and might be counted by + * implementations such as Resender. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class CallStack { + + private Deque<StackFrame> stack = new ArrayDeque<>(); + + /** + * Push a handler onto the callstack of this message with a given context. + * + * @param handler The reply handler to store. + * @param context The context to be associated with the message for that handler. + */ + public void push(ReplyHandler handler, Object context) { + stack.push(new StackFrame(handler, context)); + } + + /** + * Pop a frame from this stack. The handler part of the frame will be returned and the context part will be set on + * the given reply. Invoke this method on an empty stack and terrible things will happen. + * + * @param routable The routable that will have its context set. + * @return The next handler on the stack. + */ + public ReplyHandler pop(Routable routable) { + StackFrame frame = stack.pop(); + routable.setContext(frame.context); + return frame.handler; + } + + /** + * Swap the content of this and the argument stack. + * + * @param other The stack to swap content with. + */ + public void swap(CallStack other) { + Deque<StackFrame> tmp = stack; + stack = other.stack; + other.stack = tmp; + } + + /** + * Clear this call stack. This method should only be used when you are certain that it is safe to just throw away + * the stack. It has similar effects to stopping a thread, you need to know where it is safe to do so. + */ + public void clear() { + stack.clear(); + } + + /** + * Returns the number of elements of the callstack. + * + * @return The number of elements. + */ + public int size() { + return stack.size(); + } + + /** + * Helper class that holds stack frame data. + */ + private static class StackFrame { + + private final ReplyHandler handler; + private final Object context; + + public StackFrame(ReplyHandler handler, Object context) { + this.handler = handler; + this.context = context; + } + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/ConfigAgent.java b/messagebus/src/main/java/com/yahoo/messagebus/ConfigAgent.java new file mode 100755 index 00000000000..386441e381c --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/ConfigAgent.java @@ -0,0 +1,114 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.config.subscription.ConfigSubscriber; +import com.yahoo.config.subscription.ConfigURI; +import com.yahoo.messagebus.MessagebusConfig; +import com.yahoo.messagebus.routing.HopSpec; +import com.yahoo.messagebus.routing.RouteSpec; +import com.yahoo.messagebus.routing.RoutingSpec; +import com.yahoo.messagebus.routing.RoutingTableSpec; + +/** + * This class implements subscription to message bus config. To use configuration one must implement the {@link + * ConfigHandler} interface. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ConfigAgent implements ConfigSubscriber.SingleSubscriber<MessagebusConfig>{ + private final ConfigURI configURI; + private final ConfigHandler handler; + private ConfigSubscriber subscriber; + + /** + * Create a config agent that will obtain config for the given handler and configure it programmatically. + * + * @param configId the config id we want to use + * @param handler the handler that should be configured + */ + public ConfigAgent(String configId, ConfigHandler handler) { + this.configURI = ConfigURI.createFromId(configId); + this.handler = handler; + } + + /** + * Create a config agent that will obtain config for the given handler and configure it programmatically. + * + * @param configURI the config URI we want to use + * @param handler the handler that should be configured + */ + public ConfigAgent(ConfigURI configURI, ConfigHandler handler) { + this.configURI = configURI; + this.handler = handler; + } + + /** + * Create a config agent that will configure the given handler with the given config. + * + * @param config the config we want to use + * @param handler the handler that should be configured + */ + public ConfigAgent(MessagebusConfig config, ConfigHandler handler) { + this.configURI = null; + this.handler = handler; + configure(config); + } + + /** + * Force reload config. Only necessary for testing or if subscribing to + * config using files. + */ + public void reload(long generation) { + if (subscriber != null) { + subscriber.reload(generation); + } + } + + /** + * Start listening for config updates. This method will not return until the handler has been configured at least + * once unless an exception is thrown. + */ + public void subscribe() { + if (configURI != null) { + subscriber = new ConfigSubscriber(configURI.getSource()); + subscriber.subscribe(this, MessagebusConfig.class, configURI.getConfigId()); + } + } + + @Override + public void configure(MessagebusConfig config) { + RoutingSpec routing = new RoutingSpec(); + for (int table = 0; table < config.routingtable().size(); table++) { + MessagebusConfig.Routingtable tableConfig = config.routingtable(table); + RoutingTableSpec tableSpec = new RoutingTableSpec(tableConfig.protocol()); + for (int hop = 0; hop < tableConfig.hop().size(); hop++) { + MessagebusConfig.Routingtable.Hop hopConfig = tableConfig.hop(hop); + HopSpec hopSpec = new HopSpec(hopConfig.name(), hopConfig.selector()); + for (int recipient = 0; recipient < hopConfig.recipient().size(); recipient++) { + hopSpec.addRecipient(hopConfig.recipient(recipient)); + } + hopSpec.setIgnoreResult(hopConfig.ignoreresult()); + tableSpec.addHop(hopSpec); + } + for (int route = 0; route < tableConfig.route().size(); route++) { + MessagebusConfig.Routingtable.Route routeConfig = tableConfig.route(route); + RouteSpec routeSpec = new RouteSpec(routeConfig.name()); + for (int hop = 0; hop < routeConfig.hop().size(); hop++) { + routeSpec.addHop(routeConfig.hop(hop)); + } + tableSpec.addRoute(routeSpec); + } + routing.addTable(tableSpec); + } + handler.setupRouting(routing); + } + + /** + * Shuts down the config agent by unsubscribing to the messagebus config. + */ + public void shutdown() { + if (subscriber != null) { + subscriber.close(); + } + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/ConfigHandler.java b/messagebus/src/main/java/com/yahoo/messagebus/ConfigHandler.java new file mode 100644 index 00000000000..6e16b593b5e --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/ConfigHandler.java @@ -0,0 +1,22 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.messagebus.routing.RoutingSpec; + +/** + * This class declares those methods required to be a handler for an instance of the {@link ConfigAgent} class. + * Instead of declaring separate subscribers and handlers for all types of configurations, this pair is intended to hold + * everything. Extend this handler whenever new configs are added to {@link ConfigAgent}. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface ConfigHandler { + + /** + * Sets the routing specification for this client. This will be done synchronously during initialization, and then + * subsequently whenever an updated configuration is available. + * + * @param spec The routing specification. + */ + public void setupRouting(RoutingSpec spec); +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/DestinationSession.java b/messagebus/src/main/java/com/yahoo/messagebus/DestinationSession.java new file mode 100644 index 00000000000..7104d596015 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/DestinationSession.java @@ -0,0 +1,143 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.log.LogLevel; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; + +/** + * A session supporting receiving and replying to messages. A destination is expected to reply to every message + * received. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class DestinationSession implements MessageHandler { + + private static Logger log = Logger.getLogger(DestinationSession.class.getName()); + private final AtomicBoolean destroyed = new AtomicBoolean(false); + private final String name; + private final boolean broadcastName; + private final MessageBus mbus; + private final MessageHandler msgHandler; + + /** + * This constructor is package private since only MessageBus is supposed to instantiate it. + * + * @param mbus The message bus that created this instance. + * @param params The parameter object for this session. + */ + DestinationSession(MessageBus mbus, DestinationSessionParams params) { + this.mbus = mbus; + this.name = params.getName(); + this.broadcastName = params.getBroadcastName(); + this.msgHandler = params.getMessageHandler(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (destroy()) { + log.log(LogLevel.WARNING, "DestinationSession destroyed by finalizer, please review application shutdown logic."); + } + } finally { + super.finalize(); + } + } + + /** + * Sets the destroyed flag to true. The very first time this method is called, it cleans up all its dependencies. + * Even if you retain a reference to this object, all of its content is allowed to be garbage collected. + * + * @return True if content existed and was destroyed. + */ + public boolean destroy() { + if (!destroyed.getAndSet(true)) { + close(); + return true; + } + return false; + } + + /** + * This method unregisters this session from message bus, effectively disabling any more messages from being + * delivered to the message handler. After unregistering, this method calls {@link com.yahoo.messagebus.MessageBus#sync()} + * as to ensure that there are no threads currently entangled in the handler. + * + * This method will deadlock if you call it from the message handler. + */ + public void close() { + mbus.unregisterSession(name, broadcastName); + mbus.sync(); + } + + /** + * Conveniece method for acknowledging a message back to the sender. + * + * This is equivalent to: + * <pre> + * Reply ack = new EmptyReply(); + * ack.swapState(msg); + * reply(ack); + * </pre> + * + * Messages should be acknowledged when + * <ul> + * <li>this destination has safely and permanently applied the message, or + * <li>an intermediate determines that the purpose of the message is fullfilled without forwarding the message + * </ul> + * + * @param msg The message to acknowledge back to the sender. + * @see #reply + */ + public void acknowledge(Message msg) { + Reply ack = new EmptyReply(); + msg.swapState(ack); + reply(ack); + } + + /** + * Sends a reply to a message. The reply will propagate back to the original sender, prefering the same route as it + * used to reach the detination. + * + * @param reply The reply, created from the message this is a reply to. + */ + public void reply(Reply reply) { + ReplyHandler handler = reply.popHandler(); + handler.handleReply(reply); + } + + /** + * Returns the message handler of this session. + * + * @return The message handler. + */ + public MessageHandler getMessageHandler() { + return msgHandler; + } + + /** + * Returns the connection spec string for this session. This returns a combination of the owning message bus' own + * spec string and the name of this session. + * + * @return The connection string. + */ + public String getConnectionSpec() { + return mbus.getConnectionSpec() + "/" + name; + } + + /** + * Returns the name of this session. + * + * @return The session name. + */ + public String getName() { + return name; + } + + @Override + public void handleMessage(Message msg) { + msgHandler.handleMessage(msg); + } + +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/DestinationSessionParams.java b/messagebus/src/main/java/com/yahoo/messagebus/DestinationSessionParams.java new file mode 100755 index 00000000000..5a721b26e4f --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/DestinationSessionParams.java @@ -0,0 +1,99 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +/** + * To facilitate several configuration parameters to the {@link MessageBus#createDestinationSession(DestinationSessionParams)}, + * all parameters are held by this class. This class has reasonable default values for each parameter. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class DestinationSessionParams { + + // The session name to register with message bus. + private String name = "destination"; + + // Whether or not to broadcast name on network. + private boolean broadcastName = true; + + // The handler to receive incoming messages. + private MessageHandler handler = null; + + /** + * Constructs a new instance of this class with default values. + */ + public DestinationSessionParams() { + // empty + } + + /** + * Implements the copy constructor. + * + * @param params The object to copy. + */ + public DestinationSessionParams(DestinationSessionParams params) { + name = params.name; + broadcastName = params.broadcastName; + handler = params.handler; + } + + /** + * Returns the name to register with message bus. + * + * @return The name. + */ + public String getName() { + return name; + } + + /** + * Sets the name to register with message bus. + * + * @param name The name to set. + * @return This, to allow chaining. + */ + public DestinationSessionParams setName(String name) { + this.name = name; + return this; + } + + /** + * Returns whether or not to broadcast the name of this session on the network. + * + * @return True to broadcast, false otherwise. + */ + public boolean getBroadcastName() { + return broadcastName; + } + + /** + * Sets whether or not to broadcast the name of this session on the network. + * + * @param broadcastName True to broadcast, false otherwise. + * @return This, to allow chaining. + */ + public DestinationSessionParams setBroadcastName(boolean broadcastName) { + this.broadcastName = broadcastName; + return this; + } + + /** + * Returns the handler to receive incoming messages. + * + * @return The handler. + */ + public MessageHandler getMessageHandler() { + return handler; + } + + /** + * Sets the handler to recive incoming messages. + * + * @param handler The handler to set. + * @return This, to allow chaining. + */ + public DestinationSessionParams setMessageHandler(MessageHandler handler) { + this.handler = handler; + return this; + } + +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/DynamicThrottlePolicy.java b/messagebus/src/main/java/com/yahoo/messagebus/DynamicThrottlePolicy.java new file mode 100644 index 00000000000..9d6af3a84a9 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/DynamicThrottlePolicy.java @@ -0,0 +1,256 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus;
+
+import com.yahoo.concurrent.SystemTimer;
+import com.yahoo.concurrent.Timer;
+import com.yahoo.log.LogLevel;
+import java.util.logging.Logger;
+
+/**
+ * This is an implementatin of the {@link ThrottlePolicy} that offers dynamic limits to the number of pending messages a
+ * {@link SourceSession} is allowed to have.
+ *
+ * <b>NOTE:</b> By context, "pending" is refering to the number of sent messages that have not been replied to yet.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class DynamicThrottlePolicy extends StaticThrottlePolicy {
+
+ private static final long IDLE_TIME_MILLIS = 60000;
+ private final Timer timer;
+ private int numSent = 0;
+ private int numOk = 0;
+ private double resizeRate = 3;
+ private long resizeTime = 0;
+ private long timeOfLastMessage;
+ private double efficiencyThreshold = 1.0;
+ private double windowSizeIncrement = 20;
+ private double windowSize = windowSizeIncrement;
+ private double minWindowSize = windowSizeIncrement;
+ private double maxWindowSize = Integer.MAX_VALUE;
+ private double windowSizeBackOff = 0.9;
+ private double weight = 1.0;
+ private double localMaxThroughput = 0;
+ private double maxThroughput = 0;
+ private static final Logger log = Logger.getLogger(DynamicThrottlePolicy.class.getName());
+
+ /**
+ * Constructs a new instance of this policy and sets the appropriate default values of member data.
+ */
+ public DynamicThrottlePolicy() {
+ this(SystemTimer.INSTANCE);
+ }
+
+ /**
+ * Constructs a new instance of this class using the given clock to calculate efficiency.
+ *
+ * @param timer The timer to use.
+ */
+ public DynamicThrottlePolicy(Timer timer) {
+ this.timer = timer;
+ this.timeOfLastMessage = timer.milliTime();
+ }
+
+ public double getWindowSizeIncrement() {
+ return windowSizeIncrement;
+ }
+
+ public double getWindowSizeBackOff() {
+ return windowSizeBackOff;
+ }
+
+ public void setMaxThroughput(double maxThroughput) {
+ this.maxThroughput = maxThroughput;
+ }
+
+ @Override
+ public boolean canSend(Message msg, int pendingCount) {
+ if (!super.canSend(msg, pendingCount)) {
+ return false;
+ }
+ long time = timer.milliTime();
+ double elapsed = (time - timeOfLastMessage);
+ if (elapsed > IDLE_TIME_MILLIS) {
+ windowSize = Math.min(windowSize, pendingCount + windowSizeIncrement);
+ }
+ timeOfLastMessage = time;
+ return pendingCount < windowSize;
+ }
+
+ @Override
+ public void processMessage(Message msg) {
+ super.processMessage(msg);
+ if (++numSent < windowSize * resizeRate) {
+ return;
+ }
+
+ long time = timer.milliTime();
+ double elapsed = time - resizeTime;
+ resizeTime = time;
+
+ double throughput = numOk / elapsed;
+ numSent = 0;
+ numOk = 0;
+
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "windowSize " + windowSize + " throughput " + throughput);
+ }
+
+ if (maxThroughput > 0 && throughput > maxThroughput * 0.95) {
+ // No need to increase window when we're this close to max.
+ } else if (throughput > localMaxThroughput * 1.01) {
+ localMaxThroughput = throughput;
+ windowSize += weight*windowSizeIncrement;
+ } else {
+ // scale up/down throughput for comparing to window size
+ double period = 1;
+ while(throughput * period/windowSize < 2) {
+ period *= 10;
+ }
+ while(throughput * period/windowSize > 2) {
+ period *= 0.1;
+ }
+ double efficiency = throughput*period/windowSize;
+ if (efficiency < efficiencyThreshold) {
+ double newSize = Math.min(windowSize,throughput * period);
+ windowSize = Math.min(windowSize * windowSizeBackOff, windowSize - 2* windowSizeIncrement);
+ localMaxThroughput = 0;
+ } else {
+ windowSize += weight*windowSizeIncrement;
+ }
+ }
+ windowSize = Math.max(minWindowSize, windowSize);
+ windowSize = Math.min(maxWindowSize, windowSize);
+ }
+
+ @Override
+ public void processReply(Reply reply) {
+ super.processReply(reply);
+ if (!reply.hasErrors()) {
+ ++numOk;
+ }
+ }
+
+ /**
+ * Sets the lower efficiency threshold at which the algorithm should perform window size back off. Efficiency is
+ * the correlation between throughput and window size. The algorithm will increase the window size until efficiency
+ * drops below the efficiency of the local maxima times this value.
+ *
+ * @param efficiencyThreshold The limit to set.
+ * @return This, to allow chaining.
+ * @see #setWindowSizeBackOff(double)
+ */
+ public DynamicThrottlePolicy setEfficiencyThreshold(double efficiencyThreshold) {
+ this.efficiencyThreshold = efficiencyThreshold;
+ return this;
+ }
+
+ /**
+ * Sets the step size used when increasing window size.
+ *
+ * @param windowSizeIncrement The step size to set.
+ * @return This, to allow chaining.
+ */
+ public DynamicThrottlePolicy setWindowSizeIncrement(double windowSizeIncrement) {
+ this.windowSizeIncrement = windowSizeIncrement;
+ return this;
+ }
+
+ /**
+ * Sets the factor of window size to back off to when the algorithm determines that efficiency is not increasing.
+ * A value of 1 means that there is no back off from the local maxima, and means that the algorithm will fail to
+ * reduce window size to something lower than a previous maxima. This value is capped to the [0, 1] range.
+ *
+ * @param windowSizeBackOff The back off to set.
+ * @return This, to allow chaining.
+ */
+ public DynamicThrottlePolicy setWindowSizeBackOff(double windowSizeBackOff) {
+ this.windowSizeBackOff = Math.max(0, Math.min(1, windowSizeBackOff));
+ return this;
+ }
+
+ /**
+ * Sets the rate at which the window size is updated. The larger the value, the less responsive the resizing
+ * becomes. However, the smaller the value, the less accurate the measurements become.
+ *
+ * @param resizeRate The rate to set.
+ * @return This, to allow chaining.
+ */
+ public DynamicThrottlePolicy setResizeRate(double resizeRate) {
+ this.resizeRate = resizeRate;
+ return this;
+ }
+
+ /**
+ * Sets the weight for this client. The larger the value, the more resources
+ * will be allocated to this clients. Resources are shared between clients
+ * proportiannally to their weights.
+ *
+ * @param weight The weight to set.
+ * @return This, to allow chaining.
+ */
+ public DynamicThrottlePolicy setWeight(double weight) {
+ this.weight = weight;
+ return this;
+ }
+
+ /**
+ * Sets the maximium number of pending operations allowed at any time, in
+ * order to avoid using too much resources.
+ *
+ * @param max The max to set.
+ * @return This, to allow chaining.
+ */
+ public DynamicThrottlePolicy setMaxWindowSize(double max) {
+ this.maxWindowSize = max;
+ return this;
+ }
+
+ /**
+ * Get the maximum number of pending operations allowed at any time.
+ *
+ * @return The maximum number of operations.
+ */
+ public double getMaxWindowSize() {
+ return maxWindowSize;
+ }
+
+
+ /**
+ * Sets the minimium number of pending operations allowed at any time, in
+ * order to keep a level of performance.
+ *
+ * @param min The min to set.
+ * @return This, to allow chaining.
+ */
+ public DynamicThrottlePolicy setMinWindowSize(double min) {
+ this.minWindowSize = min;
+ return this;
+ }
+
+ /**
+ * Get the minimum number of pending operations allowed at any time.
+ *
+ * @return The minimum number of operations.
+ */
+ public double getMinWindowSize() {
+ return minWindowSize;
+ }
+
+ public DynamicThrottlePolicy setMaxPendingCount(int maxCount) {
+ super.setMaxPendingCount(maxCount);
+ maxWindowSize = maxCount;
+ return this;
+ }
+
+
+ /**
+ * Returns the maximum number of pending messages allowed.
+ *
+ * @return The max limit.
+ */
+ public int getMaxPendingCount() {
+ return (int)windowSize;
+ }
+
+}
diff --git a/messagebus/src/main/java/com/yahoo/messagebus/EmptyReply.java b/messagebus/src/main/java/com/yahoo/messagebus/EmptyReply.java new file mode 100644 index 00000000000..cb674920458 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/EmptyReply.java @@ -0,0 +1,34 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.text.Utf8String; + +/** + * The empty reply is the only concrete implementation of a message that is offered by the MessageBus. It is used to + * generate replies to events that occur within the messagebus, and since the messagebus by design knows nothing about + * the messages that have been implemented by the users it requires a class such as this. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class EmptyReply extends Reply { + private final Utf8String PROTOCOL = new Utf8String(""); + + /** + * Implements the getType() function of the root class Routable to identify this reply as the reserved type '0'. + * + * @return The number '0'. + */ + public int getType() { + return 0; + } + + /** + * Implements the getProtocol() function of Routable to identify this reply as the reserved type. This is done by an + * empty string. + * + * @return The string "". + */ + public Utf8String getProtocol() { + return PROTOCOL; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/Error.java b/messagebus/src/main/java/com/yahoo/messagebus/Error.java new file mode 100644 index 00000000000..b66c398224c --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/Error.java @@ -0,0 +1,86 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +/** + * This class implements the pair (code, message) that is used in Reply to hold errors. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class Error { + + private final int code; + private final String message; + private final String service; + + /** + * This is the constructor used by anyone adding an error to a message. One does not manually need to set the + * service name of an error, so ignore the other constructor when creating your own error instance. + * + * @param code The numerical code of this error. + * @param message The description of this error. + */ + public Error(int code, String message) { + this.code = code; + this.message = message; + service = null; + } + + /** + * This constructor is used by the network layer to properly tag deserialized errors with the hostname of whatever + * service produced the error. This constructor should NOT be used when manually creating errors. + * + * @param code The numerical code of this error. + * @param message The description of this error. + * @param service The service name of this error. + */ + public Error(int code, String message, String service) { + this.code = code; + this.message = message; + this.service = service; + } + + /** + * Return the numerical code of this error. + * + * @return The numerical code. + */ + public int getCode() { + return code; + } + + /** + * Return the description of this error. + * + * @return The description. + */ + public String getMessage() { + return message; + } + + /** + * Returns the name of the service on which this error occured. + * + * @return The service name. + */ + public String getService() { + return service; + } + + /** + * Returns whether or not this error is fatal, i.e. getCode() >= ErrorCode.FATAL_ERROR. + * + * @return True, if this error is fatal. + */ + public boolean isFatal() { + return code >= ErrorCode.FATAL_ERROR; + } + + @Override + public String toString() { + String name = ErrorCode.getName(code); + return "[" + + (name != null ? name : code) + " @ " + + (service != null ? service : "localhost") + + "]: " + message; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/ErrorCode.java b/messagebus/src/main/java/com/yahoo/messagebus/ErrorCode.java new file mode 100644 index 00000000000..5999fcb9521 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/ErrorCode.java @@ -0,0 +1,129 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +/** + * This interface contains the reserved error codes that are used for errors that occur within the messagebus. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class ErrorCode { + + /** The code is here for completeness. */ + public static final int NONE = 0; + + /** A general transient error, resending is possible. */ + public static final int TRANSIENT_ERROR = 100000; + + /** Sending was rejected because throttler capacity is full. */ + public static final int SEND_QUEUE_FULL = TRANSIENT_ERROR + 1; + + /** No addresses found for the services of the message route. */ + public static final int NO_ADDRESS_FOR_SERVICE = TRANSIENT_ERROR + 2; + + /** A connection problem occured while sending. */ + public static final int CONNECTION_ERROR = TRANSIENT_ERROR + 3; + + /** The session specified for the message is unknown. */ + public static final int UNKNOWN_SESSION = TRANSIENT_ERROR + 4; + + /** The recipient session is busy. */ + public static final int SESSION_BUSY = TRANSIENT_ERROR + 5; + + /** Sending aborted by route verification. */ + public static final int SEND_ABORTED = TRANSIENT_ERROR + 6; + + /** Version handshake failed for any reason. */ + public static final int HANDSHAKE_FAILED = TRANSIENT_ERROR + 7; + + /** An application specific transient error. */ + public static final int APP_TRANSIENT_ERROR = TRANSIENT_ERROR + 50000; + + /** A general non-recoverable error, resending is not possible. */ + public static final int FATAL_ERROR = 200000; + + /** Sending was rejected because throttler is closed. */ + public static final int SEND_QUEUE_CLOSED = FATAL_ERROR + 1; + + /** The route of the message is illegal. */ + public static final int ILLEGAL_ROUTE = FATAL_ERROR + 2; + + /** No services found for the message route. */ + public static final int NO_SERVICES_FOR_ROUTE = FATAL_ERROR + 3; + + /** The selected service was out of service. */ + public static final int SERVICE_OOS = FATAL_ERROR + 4; + + /** An error occured while encoding the message. */ + public static final int ENCODE_ERROR = FATAL_ERROR + 5; + + /** A fatal network error occured while sending. */ + public static final int NETWORK_ERROR = FATAL_ERROR + 6; + + /** The protocol specified for the message is unknown. */ + public static final int UNKNOWN_PROTOCOL = FATAL_ERROR + 7; + + /** An error occured while decoding the message. */ + public static final int DECODE_ERROR = FATAL_ERROR + 8; + + /** A timeout occured while sending. */ + public static final int TIMEOUT = FATAL_ERROR + 9; + + /** The target is running an incompatible version. */ + public static final int INCOMPATIBLE_VERSION = FATAL_ERROR + 10; + + /** The policy specified in a route is unknown. */ + public static final int UNKNOWN_POLICY = FATAL_ERROR + 11; + + /** The network was shut down when attempting to send. */ + public static final int NETWORK_SHUTDOWN = FATAL_ERROR + 12; + + /** Exception thrown by routing policy. */ + public static final int POLICY_ERROR = FATAL_ERROR + 13; + + /** An error occured while sequencing a message. */ + public static final int SEQUENCE_ERROR = FATAL_ERROR + 14; + + /** An application specific non-recoverable error. */ + public static final int APP_FATAL_ERROR = FATAL_ERROR + 50000; + + /** No error codes are allowed to be this big. */ + public static final int ERROR_LIMIT = APP_FATAL_ERROR + 50000; + + /** + * Translates the given error code into its symbolic name. + * + * @param error The error code to translate. + * @return The symbolic name. + */ + public static String getName(int error) { + switch (error) { + case APP_FATAL_ERROR : return "APP_FATAL_ERROR"; + case APP_TRANSIENT_ERROR : return "APP_TRANSIENT_ERROR"; + case CONNECTION_ERROR : return "CONNECTION_ERROR"; + case DECODE_ERROR : return "DECODE_ERROR"; + case ENCODE_ERROR : return "ENCODE_ERROR"; + case FATAL_ERROR : return "FATAL_ERROR"; + case HANDSHAKE_FAILED : return "HANDSHAKE_FAILED"; + case ILLEGAL_ROUTE : return "ILLEGAL_ROUTE"; + case INCOMPATIBLE_VERSION : return "INCOMPATIBLE_VERSION"; + case NETWORK_ERROR : return "NETWORK_ERROR"; + case NETWORK_SHUTDOWN : return "NETWORK_SHUTDOWN"; + case NO_ADDRESS_FOR_SERVICE : return "NO_ADDRESS_FOR_SERVICE"; + case NO_SERVICES_FOR_ROUTE : return "NO_SERVICES_FOR_ROUTE"; + case NONE : return "NONE"; + case POLICY_ERROR : return "POLICY_ERROR"; + case SEND_ABORTED : return "SEND_ABORTED"; + case SEND_QUEUE_CLOSED : return "SEND_QUEUE_CLOSED"; + case SEND_QUEUE_FULL : return "SEND_QUEUE_FULL"; + case SEQUENCE_ERROR : return "SEQUENCE_ERROR"; + case SERVICE_OOS : return "SERVICE_OOS"; + case SESSION_BUSY : return "SESSION_BUSY"; + case TIMEOUT : return "TIMEOUT"; + case TRANSIENT_ERROR : return "TRANSIENT_ERROR"; + case UNKNOWN_POLICY : return "UNKNOWN_POLICY"; + case UNKNOWN_PROTOCOL : return "UNKNOWN_PROTOCOL"; + case UNKNOWN_SESSION : return "UNKNOWN_SESSION"; + default : return "UNKNOWN(" + error + ")"; + } + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/IntermediateSession.java b/messagebus/src/main/java/com/yahoo/messagebus/IntermediateSession.java new file mode 100644 index 00000000000..2ebb72e2518 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/IntermediateSession.java @@ -0,0 +1,141 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.log.LogLevel; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; + +/** + * A session which supports receiving, forwarding and acknowledgement of messages. An intermediate session is expacted + * to either forward or acknowledge every message received. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class IntermediateSession implements MessageHandler, ReplyHandler { + + private static final Logger log = Logger.getLogger(IntermediateSession.class.getName()); + private final AtomicBoolean destroyed = new AtomicBoolean(false); + private final String name; + private final boolean broadcastName; + private final MessageHandler msgHandler; + private final ReplyHandler replyHandler; + private final MessageBus mbus; + + /** + * This constructor is declared package private since only MessageBus is supposed to instantiate it. + * + * @param mbus The message bus that created this instance. + * @param params The parameter object for this session. + */ + IntermediateSession(MessageBus mbus, IntermediateSessionParams params) { + this.mbus = mbus; + this.name = params.getName(); + this.broadcastName = params.getBroadcastName(); + this.msgHandler = params.getMessageHandler(); + this.replyHandler= params.getReplyHandler(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (destroy()) { + log.log(LogLevel.WARNING, "IntermediateSession destroyed by finalizer, please review application shutdown logic."); + } + } finally { + super.finalize(); + } + } + + /** + * Sets the destroyed flag to true. The very first time this method is called, it cleans up all its dependencies. + * Even if you retain a reference to this object, all of its content is allowed to be garbage collected. + * + * @return True if content existed and was destroyed. + */ + public boolean destroy() { + if (!destroyed.getAndSet(true)) { + close(); + return true; + } + return false; + } + + /** + * This method unregisters this session from message bus, effectively disabling any more messages from being + * delivered to the message handler. After unregistering, this method calls {@link com.yahoo.messagebus.MessageBus#sync()} + * as to ensure that there are no threads currently entangled in the handler. + * + * This method will deadlock if you call it from the message or reply handler. + */ + public void close() { + mbus.unregisterSession(name, broadcastName); + mbus.sync(); + } + + /** + * Forwards a routable to the next hop in its route. This method will never block. + * @param routable the routable to forward. + */ + public void forward(Routable routable) { + if (routable instanceof Reply) { + Reply reply = (Reply)routable; + ReplyHandler handler = reply.popHandler(); + handler.handleReply(reply); + } else { + routable.pushHandler(this); + mbus.handleMessage((Message)routable); + } + } + + /** + * Returns the message handler of this session. + * + * @return The message handler. + */ + public MessageHandler getMessageHandler() { + return msgHandler; + } + + /** + * Returns the reply handler of this session. + * + * @return The reply handler. + */ + public ReplyHandler getReplyHandler() { + return replyHandler; + } + + /** + * Returns the connection spec string for this session. This returns a combination of the owning message bus' own + * spec string and the name of this session. + * + * @return The connection string. + */ + public String getConnectionSpec() { + return mbus.getConnectionSpec() + "/" + name; + } + + /** + * Returns the name of this session. + * + * @return The session name. + */ + public String getName() { + return name; + } + + @Override + public void handleMessage(Message msg) { + msgHandler.handleMessage(msg); + } + + @Override + public void handleReply(Reply reply) { + if (destroyed.get()) { + reply.discard(); + } else { + replyHandler.handleReply(reply); + } + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/IntermediateSessionParams.java b/messagebus/src/main/java/com/yahoo/messagebus/IntermediateSessionParams.java new file mode 100755 index 00000000000..fb3f1b3bb4c --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/IntermediateSessionParams.java @@ -0,0 +1,122 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +/** + * To facilitate several configuration parameters to the {@link MessageBus#createIntermediateSession(IntermediateSessionParams)}, + * all parameters are held by this class. This class has reasonable default values for each parameter. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class IntermediateSessionParams { + + // The session name to register with message bus. + private String name = "intermediate"; + + // Whether or not to broadcast name on network. + private boolean broadcastName = true; + + // The handler to receive incoming replies. + private ReplyHandler replyHandler = null; + + // The handler to receive incoming messages. + private MessageHandler msgHandler = null; + + /** + * Constructs a new instance of this class with default values. + */ + public IntermediateSessionParams() { + // empty + } + + /** + * Implements the copy constructor. + * + * @param params The object to copy. + */ + public IntermediateSessionParams(IntermediateSessionParams params) { + name = params.name; + broadcastName = params.broadcastName; + replyHandler = params.replyHandler; + msgHandler = params.msgHandler; + } + + /** + * Returns the name to register with message bus. + * + * @return The name. + */ + public String getName() { + return name; + } + + /** + * Sets the name to register with message bus. + * + * @param name The name to set. + * @return This, to allow chaining. + */ + public IntermediateSessionParams setName(String name) { + this.name = name; + return this; + } + + /** + * Returns whether or not to broadcast the name of this session on the network. + * + * @return True to broadcast, false otherwise. + */ + public boolean getBroadcastName() { + return broadcastName; + } + + /** + * Returns the handler to receive incoming replies. + * + * @return The handler. + */ + public ReplyHandler getReplyHandler() { + return replyHandler; + } + + /** + * Sets the handler to recive incoming replies. + * + * @param handler The handler to set. + * @return This, to allow chaining. + */ + public IntermediateSessionParams setReplyHandler(ReplyHandler handler) { + replyHandler = handler; + return this; + } + + /** + * Returns the handler to receive incoming messages. + * + * @return The handler. + */ + public MessageHandler getMessageHandler() { + return msgHandler; + } + + /** + * Sets the handler to recive incoming messages. + * + * @param handler The handler to set. + * @return This, to allow chaining. + */ + public IntermediateSessionParams setMessageHandler(MessageHandler handler) { + msgHandler = handler; + return this; + } + + /** + * Sets whether or not to broadcast the name of this session on the network. + * + * @param broadcastName True to broadcast, false otherwise. + * @return This, to allow chaining. + */ + public IntermediateSessionParams setBroadcastName(boolean broadcastName) { + this.broadcastName = broadcastName; + return this; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/Message.java b/messagebus/src/main/java/com/yahoo/messagebus/Message.java new file mode 100644 index 00000000000..8ebb1f2a227 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/Message.java @@ -0,0 +1,244 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.concurrent.SystemTimer; +import com.yahoo.messagebus.routing.Route; + +/** + * <p>A message is a child of Routable, it is not a reply, and it has a sequencing identifier. Furthermore, a message + * contains a retry counter that holds what retry the message is currently on. See the method comment {@link #getRetry} + * for more information.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public abstract class Message extends Routable { + + private Route route = null; + private long timeReceived = 0; + private long timeRemaining = 0; + private boolean retryEnabled = true; + private int retry = 0; + + @Override + public void swapState(Routable rhs) { + super.swapState(rhs); + if (rhs instanceof Message) { + Message msg = (Message)rhs; + + Route route = this.route; + this.route = msg.route; + msg.route = route; + + boolean retryEnabled = this.retryEnabled; + this.retryEnabled = msg.retryEnabled; + msg.retryEnabled = retryEnabled; + + int retry = this.retry; + this.retry = msg.retry; + msg.retry = retry; + + long timeReceived = this.timeReceived; + this.timeReceived = msg.timeReceived; + msg.timeReceived = timeReceived; + + long timeRemaining = this.timeRemaining; + this.timeRemaining = msg.timeRemaining; + msg.timeRemaining = timeRemaining; + } + } + + /** + * <p>Return the route of this routable.</p> + * + * @return The route. + */ + public Route getRoute() { + return route; + } + + /** + * <p>Set a new route for this routable.</p> + * + * @param route The new route. + * @return This, to allow chaining. + */ + public Message setRoute(Route route) { + this.route = new Route(route); + return this; + } + + /** + * <p>Returns the timestamp for when this message was last seen by message bus. If you are using this to determine + * message expiration, you should use {@link #isExpired()} instead.</p> + * + * @return The timestamp this was last seen. + */ + public long getTimeReceived() { + return timeReceived; + } + + /** + * <p>Sets the timestamp for when this message was last seen by message bus to the given time in milliseconds since + * epoch. Please see comment on {@link #isExpired()} for more information on how to determine whether or not a + * message has expired. You should never need to call this method yourself, as it is touched automatically whenever + * message bus encounters a new message.</p> + * + * @param timeReceived The time received in milliseconds. + * @return This, to allow chaining. + */ + public Message setTimeReceived(long timeReceived) { + this.timeReceived = timeReceived; + return this; + } + + /** + * <p>This is a convenience method to call {@link #setTimeReceived(long)} passing the current time as argument.</p> + * + * @return This, to allow chaining. + */ + public Message setTimeReceivedNow() { + return setTimeReceived(SystemTimer.INSTANCE.milliTime()); + } + + /** + * <p>Returns the number of milliseconds that remain before this message times out. This value is only updated by + * the network layer, and is therefore not current. If you are trying to determine message expiration, use {@link + * #isExpired()} instead.</p> + * + * @return The remaining time in milliseconds. + */ + public long getTimeRemaining() { + return timeRemaining; + } + + /** + * <p>Sets the numer of milliseconds that remain before this message times out. Please see comment on {@link + * #isExpired()} for more information on how to determine whether or not a message has expired.</p> + * + * @param timeRemaining The number of milliseconds until expiration. + * @return This, to allow chaining. + */ + public Message setTimeRemaining(long timeRemaining) { + this.timeRemaining = timeRemaining; + return this; + } + + /** + * <p>Returns the number of milliseconds that remain right now before this message times out. This is a function of + * {@link #getTimeReceived()}, {@link #getTimeRemaining()} and current time. Whenever a message is transmitted by + * message bus, a new remaining time is calculated and serialized as <code>timeRemaining = timeRemaining - + * (currentTime - timeReceived)</code>. This means that we are doing an over-estimate of remaining time, as we are + * only factoring in the time used by the application above message bus.</p> + * + * @return The remaining time in milliseconds. + */ + public long getTimeRemainingNow() { + return timeRemaining - (SystemTimer.INSTANCE.milliTime() - timeReceived); + } + + /** + * <p>Returns whether or not this message has expired.</p> + * + * @return True if {@link #getTimeRemainingNow()} is less than or equal to zero. + */ + public boolean isExpired() { + return getTimeRemainingNow() <= 0; + } + + /** + * <p>Returns whether or not this message contains a sequence identifier that should be respected, i.e. whether or + * not this message requires sequencing.</p> + * + * @return True to enable sequencing. + * @see #getSequenceId() + */ + public boolean hasSequenceId() { + return false; + } + + /** + * <p>Returns the identifier used to order messages. Any two messages that have the same sequence id are ensured to + * arrive at the recipient in the order they were sent by the client. This value is only respected if the {@link + * #hasSequenceId()} method returns true.</p> + * + * @return The sequence identifier. + */ + public long getSequenceId() { + return 0; + } + + /** + * <p>Returns whether or not this message contains a sequence bucket that should be respected, i.e. whether or not + * this message requires bucket-level sequencing.</p> + * + * @return True to enable bucket sequencing. + * @see #getBucketSequence() + */ + public boolean hasBucketSequence() { + return false; + } + + /** + * <p>Returns the identifier used to order message buckets. Any two messages that have the same bucket sequence are + * ensured to arrive at the NEXT peer in the order they were sent by THIS peer. This value is only respected if the + * {@link #hasBucketSequence()} method returns true.</p> + * + * @return The bucket sequence. + */ + public long getBucketSequence() { + return 0; + } + + /** + * <p>Obtain the approximate size of this message object in bytes. This enables messagebus to track the size of the + * send queue in both memory usage and item count. This method returns 1 by default, and must be overridden to + * enable message size tracking.</p> + * + * @return 1 + */ + public int getApproxSize() { + return 1; + } + + /** + * <p>Sets whether or not this message can be resent.</p> + * + * @param enabled Resendable flag. + */ + public void setRetryEnabled(boolean enabled) { + retryEnabled = enabled; + } + + /** + * <p>Returns whether or not this message can be resent.</p> + * + * @return True if this can be resent. + */ + public boolean getRetryEnabled() { + return retryEnabled; + } + + /** + * <p>Returns the number of times the sending of this message has been retried. This is available for inspection so + * that clients may implement logic to control resending.</p> + * + * @return The retry count. + * @see Reply#setRetryDelay This method can be used to request resending that differs from the default. + */ + public int getRetry() { + return retry; + } + + /** + * <p>Sets the number of times the sending of this message has been retried. This method only makes sense to modify + * BEFORE sending it, since its value is not serialized back into any reply that it may create.</p> + * + * @param retry The retry count. + * @return This, to allow chaining. + */ + public Message setRetry(int retry) { + this.retry = retry; + return this; + } +} + diff --git a/messagebus/src/main/java/com/yahoo/messagebus/MessageBus.java b/messagebus/src/main/java/com/yahoo/messagebus/MessageBus.java new file mode 100644 index 00000000000..729bef7985f --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/MessageBus.java @@ -0,0 +1,595 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.concurrent.CopyOnWriteHashMap; +import com.yahoo.log.LogLevel; +import com.yahoo.messagebus.metrics.MessageBusMetricSet; +import com.yahoo.messagebus.network.Network; +import com.yahoo.messagebus.network.NetworkOwner; +import com.yahoo.messagebus.routing.*; +import com.yahoo.text.Utf8Array; +import com.yahoo.text.Utf8String; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Logger; + +/** + * <p>A message bus contains the factory for creating sessions to send, receive + * and forward messages.</p> + * + * <p>There are three types of sessions:</p> + * <ul><li>{@link SourceSession Source sessions} sends messages and receives + * replies</li> + * <li>{@link IntermediateSession Intermediate sessions} receives messages on + * their way to their final destination, and may decide to forward the messages + * or reply directly.</li> + * <li>{@link DestinationSession Destination sessions} are the final recipient + * of messages, and are expected to reply to every one of them, but may not + * forward messages.</li></ul> + * + * <p>A message bus is configured with a {@link Protocol protocol}. This table + * enumerates the permissible routes from intermediates to destinations and the + * messaging semantics of each hop.</p> + * + * <p>The responsibilities of a message bus are:</p> + * <ul> <li>Assign a route to every send message from its routing table</li> + * <li>Deliver every message it <i>accepts</i> to the next hop on its route on a + * best effort basis, <i>or</i> deliver a <i>failure reply</i>.</li> + * <li>Deliver replies back to message sources through all the intermediate + * hops.</li></ul> + * + * <p>A runtime will typically</p> + * <ul><li>Create a message bus implementation and set properties on this + * implementation once.</li> + * <li>Create sessions using that message bus many places.</li></ul> + * + * @author btratseth + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class MessageBus implements ConfigHandler, NetworkOwner, MessageHandler, ReplyHandler { + + private static Logger log = Logger.getLogger(MessageBus.class.getName()); + private final AtomicBoolean destroyed = new AtomicBoolean(false); + private final ProtocolRepository protocolRepository = new ProtocolRepository(); + private final AtomicReference<Map<String, RoutingTable>> tablesRef = new AtomicReference<Map<String, RoutingTable>>(null); + private final CopyOnWriteHashMap<String, MessageHandler> sessions = new CopyOnWriteHashMap<String, MessageHandler>(); + private final Network net; + private final Messenger msn; + private final Resender resender; + private int maxPendingCount = 0; + private int maxPendingSize = 0; + private int pendingCount = 0; + private int pendingSize = 0; + private MessageBusMetricSet metrics = new MessageBusMetricSet(); + + /** + * <p>Convenience constructor that proxies {@link #MessageBus(Network, + * MessageBusParams)} by adding the given protocols to a default {@link + * MessageBusParams} object.</p> + * + * @param net The network to associate with. + * @param protocols An array of protocols to register. + */ + public MessageBus(Network net, List<Protocol> protocols) { + this(net, new MessageBusParams().addProtocols(protocols)); + } + + /** + * <p>Constructs an instance of message bus. This requires a network object + * that it will associate with. This assignment may not change during the + * lifetime of this message bus.</p> + * + * @param net The network to associate with. + * @param params The parameters that controls this bus. + */ + public MessageBus(Network net, MessageBusParams params) { + // Add all known protocols to the repository. + maxPendingCount = params.getMaxPendingCount(); + maxPendingSize = params.getMaxPendingSize(); + for (int i = 0, len = params.getNumProtocols(); i < len; ++i) { + protocolRepository.putProtocol(params.getProtocol(i)); + + if (params.getProtocol(i).getMetrics() != null) { + metrics.protocols.addMetric(params.getProtocol(i).getMetrics()); + } + } + + // Attach and start network. + this.net = net; + net.attach(this); + if (!net.waitUntilReady(120)) { + throw new IllegalStateException("Network failed to become ready in time."); + } + + // Start messenger. + msn = new Messenger(); + + RetryPolicy retryPolicy = params.getRetryPolicy(); + if (retryPolicy != null) { + resender = new Resender(retryPolicy); + msn.addRecurrentTask(new ResenderTask(resender)); + } else { + resender = null; + } + + msn.start(); + } + + /** + * <p>Returns the metrics used by this messagebus.</p> + * + * @return The metric set. + */ + public MessageBusMetricSet getMetrics() { + return metrics; + } + + @Override + protected void finalize() throws Throwable { + try { + if (destroy()) { + log.log(LogLevel.WARNING, "MessageBus destroyed by finalizer, please review application shutdown logic."); + } + } finally { + super.finalize(); + } + } + + /** + * <p>Sets the destroyed flag to true. The very first time this method is + * called, it cleans up all its dependencies. Even if you retain a reference + * to this object, all of its content is allowed to be garbage + * collected.</p> + * + * @return True if content existed and was destroyed. + */ + public boolean destroy() { + if (!destroyed.getAndSet(true)) { + protocolRepository.clearPolicyCache(); + net.shutdown(); + msn.destroy(); + if (resender != null) { + resender.destroy(); + } + return true; + } + return false; + } + + /** + * <p>Synchronize with internal threads. This method will handshake with all + * internal threads. This has the implicit effect of waiting for all active + * callbacks. Note that this method should never be invoked from a callback + * since that would make the thread wait for itself... forever. This method + * is typically used to untangle during session shutdown.</p> + */ + public void sync() { + msn.sync(); + net.sync(); + } + + /** + * <p>This is a convenience method to call {@link + * #createSourceSession(SourceSessionParams)} with default values for the + * {@link SourceSessionParams} object.</p> + * + * @param handler The reply handler to receive the replies for the session. + * @return The created session. + */ + public SourceSession createSourceSession(ReplyHandler handler) { + return createSourceSession(new SourceSessionParams().setReplyHandler(handler)); + } + + /** + * <p>This is a convenience method to call {@link + * #createSourceSession(SourceSessionParams)} by first assigning the reply + * handler to the parameter object.</p> + * + * @param handler The reply handler to receive the replies for the session. + * @param params The parameters to control the session. + * @return The created session. + */ + public SourceSession createSourceSession(ReplyHandler handler, SourceSessionParams params) { + return createSourceSession(new SourceSessionParams(params).setReplyHandler(handler)); + } + + /** + * <p>Creates a source session on top of this message bus.</p> + * + * @param params The parameters to control the session. + * @return The created session. + */ + public SourceSession createSourceSession(SourceSessionParams params) { + if (destroyed.get()) { + throw new IllegalStateException("Object is destroyed."); + } + return new SourceSession(this, params); + } + + /** + * <p>This is a convenience method to call {@link + * #createIntermediateSession(IntermediateSessionParams)} with default + * values for the {@link IntermediateSessionParams} object.</p> + * + * @param name The local unique name for the created session. + * @param broadcastName Whether or not to broadcast this session's name on + * the network. + * @param msgHandler The handler to receive the messages for the session. + * @param replyHandler The handler to received the replies for the session. + * @return The created session. + */ + public IntermediateSession createIntermediateSession(String name, + boolean broadcastName, + MessageHandler msgHandler, + ReplyHandler replyHandler) { + return createIntermediateSession( + new IntermediateSessionParams() + .setName(name) + .setBroadcastName(broadcastName) + .setMessageHandler(msgHandler) + .setReplyHandler(replyHandler)); + } + + /** + * <p>Creates an intermediate session on top of this message bus using the + * given handlers and parameter object.</p> + * + * @param params The parameters to control the session. + * @return The created session. + */ + public synchronized IntermediateSession createIntermediateSession(IntermediateSessionParams params) { + if (destroyed.get()) { + throw new IllegalStateException("Object is destroyed."); + } + if (sessions.containsKey(params.getName())) { + throw new IllegalArgumentException("Name '" + params.getName() + "' is not unique."); + } + IntermediateSession session = new IntermediateSession(this, params); + sessions.put(params.getName(), session); + if (params.getBroadcastName()) { + net.registerSession(params.getName()); + } + return session; + } + + /** + * <p>This is a convenience method to call {@link + * #createDestinationSession(DestinationSessionParams)} with default values + * for the {@link DestinationSessionParams} object.</p> + * + * @param name The local unique name for the created session. + * @param broadcastName Whether or not to broadcast this session's name on + * the network. + * @param handler The handler to receive the messages for the session. + * @return The created session. + */ + public DestinationSession createDestinationSession(String name, + boolean broadcastName, + MessageHandler handler) { + return createDestinationSession( + new DestinationSessionParams() + .setName(name) + .setBroadcastName(broadcastName) + .setMessageHandler(handler)); + } + + /** + * <p>Creates a destination session on top of this message bus using the + * given handlers and parameter object.</p> + * + * @param params The parameters to control the session. + * @return The created session. + */ + public synchronized DestinationSession createDestinationSession(DestinationSessionParams params) { + if (destroyed.get()) { + throw new IllegalStateException("Object is destroyed."); + } + if (sessions.containsKey(params.getName())) { + throw new IllegalArgumentException("Name '" + params.getName() + "' is not unique."); + } + DestinationSession session = new DestinationSession(this, params); + sessions.put(params.getName(), session); + if (params.getBroadcastName()) { + net.registerSession(params.getName()); + } + return session; + } + + /** + * <p>This method is invoked by the {@link + * com.yahoo.messagebus.IntermediateSession#destroy()} to unregister + * sessions from receiving data from message bus.</p> + * + * @param name The name of the session to remove. + * @param broadcastName Whether or not session name was broadcast. + */ + public synchronized void unregisterSession(String name, boolean broadcastName) { + if (broadcastName) { + net.unregisterSession(name); + } + sessions.remove(name); + } + + private boolean doAccounting() { + return (maxPendingCount > 0 || maxPendingSize > 0); + } + /** + * <p>This method handles choking input data so that message bus does not + * blindly accept everything. This prevents an application running + * out-of-memory in case it fail to choke input data itself. If this method + * returns false, it means that it should be rejected.</p> + * + * @param msg The message to count. + * @return True if the message was accepted. + */ + private boolean checkPending(Message msg) { + boolean busy = false; + int size = msg.getApproxSize(); + + if (doAccounting()) { + synchronized (this) { + busy = ((maxPendingCount > 0 && pendingCount >= maxPendingCount) || + (maxPendingSize > 0 && pendingSize >= maxPendingSize)); + if (!busy) { + pendingCount++; + pendingSize += size; + } + } + } + if (busy) { + return false; + } + msg.setContext(size); + msg.pushHandler(this); + return true; + } + + @Override + public void handleMessage(Message msg) { + if (resender != null && msg.hasBucketSequence()) { + deliverError(msg, ErrorCode.SEQUENCE_ERROR, "Bucket sequences not supported when resender is enabled."); + return; + } + SendProxy proxy = new SendProxy(this, net, resender); + msn.deliverMessage(msg, proxy); + } + + @Override + public void handleReply(Reply reply) { + if (destroyed.get()) { + reply.discard(); + return; + } + if (doAccounting()) { + synchronized (this) { + --pendingCount; + pendingSize -= (Integer)reply.getContext(); + } + } + deliverReply(reply, reply.popHandler()); + } + + @Override + public void deliverMessage(Message msg, String session) { + MessageHandler msgHandler = sessions.get(session); + if (msgHandler == null) { + deliverError(msg, ErrorCode.UNKNOWN_SESSION, "Session '" + session + "' does not exist."); + } else if (!checkPending(msg)) { + deliverError(msg, ErrorCode.SESSION_BUSY, "Session '" + net.getConnectionSpec() + "/" + session + + "' is busy, try again later."); + } else { + msn.deliverMessage(msg, msgHandler); + } + } + + /** + * <p>Adds a protocol to the internal repository of protocols, replacing any + * previous instance of the protocol and clearing the associated routing + * policy cache.</p> + * + * @param protocol The protocol to add. + */ + public void putProtocol(Protocol protocol) { + protocolRepository.putProtocol(protocol); + } + + @Override + public Protocol getProtocol(Utf8Array name) { + return protocolRepository.getProtocol(name.toString()); + } + + public Protocol getProtocol(Utf8String name) { + return getProtocol((Utf8Array)name); + } + + @Override + public void deliverReply(Reply reply, ReplyHandler handler) { + msn.deliverReply(reply, handler); + } + + @Override + public void setupRouting(RoutingSpec spec) { + Map<String, RoutingTable> tables = new HashMap<String, RoutingTable>(); + for (int i = 0, len = spec.getNumTables(); i < len; ++i) { + RoutingTableSpec table = spec.getTable(i); + String name = table.getProtocol(); + if (!protocolRepository.hasProtocol(name)) { + log.log(LogLevel.INFO, "Protocol '" + name + "' is not supported, ignoring routing table."); + continue; + } + tables.put(name, new RoutingTable(table)); + } + tablesRef.set(tables); + protocolRepository.clearPolicyCache(); + } + + /** + * <p>Returns the resender that is running within this message bus.</p> + * + * @return The resender. + */ + public Resender getResender() { + return resender; + } + + /** + * <p>Returns the number of messages received that have not been replied to + * yet.</p> + * + * @return The pending count. + */ + public synchronized int getPendingCount() { + return pendingCount; + } + + /** + * <p>Returns the size of messages received that have not been replied to + * yet.</p> + * + * @return The pending size. + */ + public synchronized int getPendingSize() { + return pendingSize; + } + + /** + * <p>Sets the maximum number of messages that can be received without being + * replied to yet.</p> + * + * @param maxCount The max count. + */ + public void setMaxPendingCount(int maxCount) { + maxPendingCount = maxCount; + } + + /** + * Gets maximum number of messages that can be received without being + * replied to yet. + */ + public int getMaxPendingCount() { + return maxPendingCount; + } + + /** + * <p>Sets the maximum size of messages that can be received without being + * replied to yet.</p> + * + * @param maxSize The max size. + */ + public void setMaxPendingSize(int maxSize) { + maxPendingSize = maxSize; + } + + /** + * Gets maximum combined size of messages that can be received without + * being replied to yet. + */ + public int getMaxPendingSize() { + return maxPendingSize; + } + + /** + * <p>Returns a named routing table, may return null.</p> + * + * @param name The name of the routing table to return. + * @return The routing table object. + */ + public RoutingTable getRoutingTable(String name) { + Map<String, RoutingTable> tables = tablesRef.get(); + if (tables == null) { + return null; + } + return tables.get(name); + } + /** + * <p>Returns a named routing table, may return null.</p> + * + * @param name The name of the routing table to return. + * @return The routing table object. + */ + public RoutingTable getRoutingTable(Utf8String name) { + + return getRoutingTable(name.toString()); + } + + /** + * <p>Returns a routing policy that corresponds to the argument protocol + * name, policy name and policy parameter. This will cache reuse all + * policies as soon as they are first requested.</p> + * + * @param protocolName The name of the protocol to invoke {@link Protocol#createPolicy(String,String)} on. + * @param policyName The name of the routing policy to retrieve. + * @param policyParam The parameter for the routing policy to retrieve. + * @return A corresponding routing policy, or null. + */ + public RoutingPolicy getRoutingPolicy(String protocolName, String policyName, String policyParam) { + return protocolRepository.getRoutingPolicy(protocolName, policyName, policyParam); + } + + /** + * <p>Returns a routing policy that corresponds to the argument protocol + * name, policy name and policy parameter. This will cache reuse all + * policies as soon as they are first requested.</p> + * + * @param protocolName The name of the protocol to invoke {@link Protocol#createPolicy(String,String)} on. + * @param policyName The name of the routing policy to retrieve. + * @param policyParam The parameter for the routing policy to retrieve. + * @return A corresponding routing policy, or null. + */ + public RoutingPolicy getRoutingPolicy(Utf8String protocolName, String policyName, String policyParam) { + return protocolRepository.getRoutingPolicy(protocolName.toString(), policyName, policyParam); + } + + /** + * <p>Returns the connection spec string for the network layer of this + * message bus. This is merely a proxy of the same function in the network + * layer.</p> + * + * @return The connection string. + */ + public String getConnectionSpec() { + return net.getConnectionSpec(); + } + + /** + * <p>Constructs and schedules a Reply containing an error to the handler of the given Message.</p> + * + * @param msg The message to reply to. + * @param errCode The code of the error to set. + * @param errMsg The message of the error to set. + */ + private void deliverError(Message msg, int errCode, String errMsg) { + Reply reply = new EmptyReply(); + reply.swapState(msg); + reply.addError(new Error(errCode, errMsg)); + deliverReply(reply, reply.popHandler()); + } + + /** + * <p>Implements a task for running the resender in the messenger + * thread. This task acts as a proxy for the resender, allowing the task to + * be deleted without affecting the resender itself.</p> + */ + private static class ResenderTask implements Messenger.Task { + + final Resender resender; + + ResenderTask(Resender resender) { + this.resender = resender; + } + + public void destroy() { + // empty + } + + public void run() { + resender.resendScheduled(); + } + + } +} + diff --git a/messagebus/src/main/java/com/yahoo/messagebus/MessageBusParams.java b/messagebus/src/main/java/com/yahoo/messagebus/MessageBusParams.java new file mode 100755 index 00000000000..767f2cd0ec1 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/MessageBusParams.java @@ -0,0 +1,146 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.messagebus.routing.RetryPolicy; +import com.yahoo.messagebus.routing.RetryTransientErrorsPolicy; + +import java.util.ArrayList; +import java.util.List; + +/** + * To facilitate several configuration parameters to the {@link MessageBus} constructor, all parameters are held by this + * class. This class has reasonable default values for each parameter. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class MessageBusParams { + + private final List<Protocol> protocols = new ArrayList<Protocol>(); + private RetryPolicy retryPolicy; + private int maxPendingCount; + private int maxPendingSize; + + /** + * Constructs a new instance of this parameter object with default values for all members. + */ + public MessageBusParams() { + retryPolicy = new RetryTransientErrorsPolicy(); + maxPendingCount = 1024; + maxPendingSize = 128 * 1024 * 1024; + } + + /** + * Implements the copy constructor. + * + * @param params The object to copy. + */ + public MessageBusParams(MessageBusParams params) { + protocols.addAll(params.protocols); + retryPolicy = params.retryPolicy; + maxPendingCount = params.maxPendingCount; + maxPendingSize = params.maxPendingSize; + } + + /** + * Returns the retry policy for the resender. + * + * @return The policy. + */ + public RetryPolicy getRetryPolicy() { + return retryPolicy; + } + + /** + * Sets the retry policy for the resender. + * + * @param retryPolicy The policy to set. + * @return This, to allow chaining. + */ + public MessageBusParams setRetryPolicy(RetryPolicy retryPolicy) { + this.retryPolicy = retryPolicy; + return this; + } + + /** + * Adds a new protocol to this. + * + * @param protocol The protocol to add. + * @return This, to allow chaining. + */ + public MessageBusParams addProtocol(Protocol protocol) { + protocols.add(protocol); + return this; + } + + /** + * Registers multiple protocols with this by calling {@link #addProtocol(Protocol)} multiple times. + * + * @param protocols The protocols to register. + * @return This, to allow chaining. + */ + public MessageBusParams addProtocols(List<Protocol> protocols) { + for (Protocol protocol : protocols) { + addProtocol(protocol); + } + return this; + } + + /** + * Returns the number of protocols that are contained in this. + * + * @return The number of protocols. + */ + public int getNumProtocols() { + return protocols.size(); + } + + /** + * Returns the protocol at the given index. + * + * @param i The index of the protocol to return. + * @return The protocol object. + */ + public Protocol getProtocol(int i) { + return protocols.get(i); + } + + /** + * Returns the maximum number of pending messages. + * + * @return The count limit. + */ + public int getMaxPendingCount() { + return maxPendingCount; + } + + /** + * Sets the maximum number of allowed pending messages. + * + * @param maxCount The count limit to set. + * @return This, to allow chaining. + */ + public MessageBusParams setMaxPendingCount(int maxCount) { + this.maxPendingCount = maxCount; + return this; + } + + /** + * Returns the maximum number of bytes allowed for pending messages. + * + * @return The size limit. + */ + public int getMaxPendingSize() { + return maxPendingSize; + } + + /** + * Sets the maximum number of bytes allowed for pending messages. + * + * @param maxSize The size limit to set. + * @return This, to allow chaining. + */ + public MessageBusParams setMaxPendingSize(int maxSize) { + this.maxPendingSize = maxSize; + return this; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/MessageHandler.java b/messagebus/src/main/java/com/yahoo/messagebus/MessageHandler.java new file mode 100755 index 00000000000..3b59611c258 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/MessageHandler.java @@ -0,0 +1,19 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +/** + * All classes that wants to handle messages that move through the messagebus need to implement this interface. + * As opposed to the {@link ReplyHandler} which handles replies as they return from the receiver to the sender, this + * interface is intended for handling messages as they travel from the sender to the receiver. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface MessageHandler { + + /** + * This function is called when a message arrives. + * + * @param message The message that arrived. + */ + public void handleMessage(Message message); +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/Messenger.java b/messagebus/src/main/java/com/yahoo/messagebus/Messenger.java new file mode 100755 index 00000000000..17d4b06b8bf --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/Messenger.java @@ -0,0 +1,304 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.log.LogLevel; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; + +/** + * <p>This class implements a single thread that is able to process arbitrary + * tasks. Tasks are enqueued using the synchronized {@link #enqueue(Task)} + * method, and are run in the order they were enqueued.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class Messenger implements Runnable { + + private static final Logger log = Logger.getLogger(Messenger.class.getName()); + private final AtomicBoolean destroyed = new AtomicBoolean(false); + private final List<Task> children = new ArrayList<>(); + private final Queue<Task> queue = new ArrayDeque<>(); + private final Thread thread = new Thread(this, "Messenger"); + + public Messenger() { + thread.setDaemon(true); + } + + /** + * <p>Adds a recurrent task to this that is to be run for every iteration of + * the main loop. This task must be very light-weight as to not block the + * messenger. Note that this method is NOT thread-safe, so it should NOT be + * used after calling {@link #start()}.</p> + * + * @param task The task to add. + */ + public void addRecurrentTask(final Task task) { + children.add(task); + } + + /** + * <p>Starts the internal thread. This must be done AFTER all recurrent + * tasks have been added.</p> + * + * @see #addRecurrentTask(Task) + */ + public void start() { + thread.start(); + } + + /** + * <p>Convenience method to post a {@link Task} that delivers a {@link + * Message} to a {@link MessageHandler} to the queue of tasks to be + * executed.</p> + * + * @param msg The message to send. + * @param handler The handler to send to. + */ + public void deliverMessage(final Message msg, final MessageHandler handler) { + enqueue(new MessageTask(msg, handler)); + } + + /** + * <p>Convenience method to post a {@link Task} that delivers a {@link + * Reply} to a {@link ReplyHandler} to the queue of tasks to be + * executed.</p> + * + * @param reply The reply to return. + * @param handler The handler to return to. + */ + public void deliverReply(final Reply reply, final ReplyHandler handler) { + enqueue(new ReplyTask(reply, handler)); + } + + /** + * <p>Enqueues the given task in the list of tasks that this worker is to + * process. If this thread has been destroyed previously, this method + * invokes {@link Messenger.Task#destroy()}.</p> + * + * @param task The task to enqueue. + */ + public void enqueue(final Task task) { + if (destroyed.get()) { + task.destroy(); + return; + } + synchronized (this) { + queue.offer(task); + if (queue.size() == 1) { + notify(); + } + } + } + + /** + * <p>Handshakes with the internal thread. If this method is called using + * the messenger thread, this will deadlock.</p> + */ + public void sync() { + if (Thread.currentThread() == thread) { + return; // no need to wait for self + } + final SyncTask task = new SyncTask(); + enqueue(task); + task.await(); + } + + /** + * <p>Sets the destroyed flag to true. The very first time this method is + * called, it cleans up all its dependencies. Even if you retain a + * reference to this object, all of its content is allowed to be garbage + * collected.</p> + * + * @return True if content existed and was destroyed. + */ + public boolean destroy() { + boolean done = false; + enqueue(Terminate.INSTANCE); + if (!destroyed.getAndSet(true)) { + try { + synchronized (this) { + while (!queue.isEmpty()) { + wait(); + } + } + thread.join(); + } catch (final InterruptedException e) { + // ignore + } + done = true; + } + return done; + } + + @Override + public void run() { + while (true) { + Task task = null; + synchronized (this) { + if (queue.isEmpty()) { + try { + wait(100); + } catch (final InterruptedException e) { + continue; + } + } + if (queue.size() > 0) { + task = queue.poll(); + } + } + if (task == Terminate.INSTANCE) { + break; + } + if (task != null) { + try { + task.run(); + } catch (final Exception e) { + log.log(LogLevel.ERROR, "An exception was thrown while running " + task.getClass().getName(), e); + } + try { + task.destroy(); + } catch (final Exception e) { + log.warning("An exception was thrown while destroying " + task.getClass().getName() + ": " + + e.toString()); + log.warning("Someone, somewhere might have to wait indefinetly for something."); + } + } + for (final Task child : children) { + child.run(); + } + } + for (final Task child : children) { + child.destroy(); + } + synchronized (this) { + while (!queue.isEmpty()) { + final Task task = queue.poll(); + task.destroy(); + } + notify(); + } + } + + @Override + protected void finalize() throws Throwable { + try { + if (destroy()) { + log.log(LogLevel.WARNING, "Messenger destroyed by finalizer, please review application shutdown logic."); + } + } finally { + super.finalize(); + } + } + + /** + * <p>Defines the required interface for tasks to be posted to this + * worker.</p> + */ + public interface Task { + + /** + * <p>This method is called when being executed.</p> + */ + public void run(); + + /** + * <p>This method is called for all tasks, even if {@link #run()} was + * never called.</p> + */ + public void destroy(); + } + + private static class MessageTask implements Task { + + final MessageHandler handler; + Message msg; + + MessageTask(final Message msg, final MessageHandler handler) { + this.msg = msg; + this.handler = handler; + } + + @Override + public void run() { + final Message msg = this.msg; + this.msg = null; + handler.handleMessage(msg); + } + + @Override + public void destroy() { + if (msg != null) { + msg.discard(); + } + } + } + + private static class ReplyTask implements Task { + + final ReplyHandler handler; + Reply reply; + + ReplyTask(final Reply reply, final ReplyHandler handler) { + this.reply = reply; + this.handler = handler; + } + + @Override + public void run() { + final Reply reply = this.reply; + this.reply = null; + handler.handleReply(reply); + } + + @Override + public void destroy() { + if (reply != null) { + reply.discard(); + } + } + } + + private static class SyncTask implements Task { + + final CountDownLatch latch = new CountDownLatch(1); + + @Override + public void run() { + // empty + } + + @Override + public void destroy() { + latch.countDown(); + } + + public void await() { + try { + latch.await(); + } catch (final InterruptedException e) { + // ignore + } + } + } + + private static class Terminate implements Task { + + static final Terminate INSTANCE = new Terminate(); + + @Override + public void run() { + // empty + } + + @Override + public void destroy() { + // empty + } + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/Protocol.java b/messagebus/src/main/java/com/yahoo/messagebus/Protocol.java new file mode 100644 index 00000000000..4e96c9af959 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/Protocol.java @@ -0,0 +1,54 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.component.Version; +import com.yahoo.messagebus.metrics.MetricSet; +import com.yahoo.messagebus.routing.RoutingPolicy; + +/** + * Interface implemented by the concrete application message protocol. + * + * @author <a href="bratseth@yahoo-inc.com">Jon Bratseth</a> + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface Protocol { + + /** + * Returns a global unique name for this protocol. + * + * @return The name. + */ + public String getName(); + + /** + * Encodes the protocol specific data of a routable into a byte array. + * + * @param version The version to encode for. + * @param routable The routable to encode. + * @return The encoded data. + */ + public byte[] encode(Version version, Routable routable); + + /** + * Decodes the protocol specific data into a routable of the correct type. + * + * @param version The version of the serialized routable. + * @param payload The payload to decode from. + * @return The decoded routable. + */ + public Routable decode(Version version, byte[] payload); + + /** + * Create a policy of the named type with the named param passed to the constructor of that policy. + * + * @param name The name of the policy to create. + * @param param The parameter to that policy's constructor. + * @return The created policy. + */ + public RoutingPolicy createPolicy(String name, String param); + + /** + * Returns the metrics associated with this protocol. + */ + MetricSet getMetrics(); +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/ProtocolRepository.java b/messagebus/src/main/java/com/yahoo/messagebus/ProtocolRepository.java new file mode 100755 index 00000000000..949a8486fb7 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/ProtocolRepository.java @@ -0,0 +1,109 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus;
+
+import com.yahoo.concurrent.CopyOnWriteHashMap;
+import com.yahoo.log.LogLevel;
+import com.yahoo.messagebus.routing.RoutingPolicy;
+import com.yahoo.text.Utf8String;
+
+import java.util.logging.Logger;
+
+/**
+ * Implements a thread-safe repository for protocols and their routing policies. This manages an internal cache of
+ * routing policies so that similarly referenced policy directives share the same instance of a policy.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ProtocolRepository {
+
+ private static final Logger log = Logger.getLogger(ProtocolRepository.class.getName());
+ private final CopyOnWriteHashMap<String, Protocol> protocols = new CopyOnWriteHashMap<>();
+ private final CopyOnWriteHashMap<String, RoutingPolicy> routingPolicyCache = new CopyOnWriteHashMap<>();
+
+ /**
+ * Registers a protocol with this repository. This will overwrite any protocol that was registered earlier that has
+ * the same name. If this method detects a protocol replacement, it will clear its internal routing policy cache.
+ *
+ * @param protocol The protocol to register.
+ */
+ public void putProtocol(Protocol protocol) {
+ if (protocols.put(protocol.getName(), protocol) != null) {
+ routingPolicyCache.clear();
+ }
+ }
+
+ /**
+ * Returns whether or not this repository contains a protocol with the given name. Given the concurrent nature of
+ * things, one should not invoke this method followed by {@link #getProtocol(String)} and expect the return value to
+ * be non-null. Instead just get the protocol and compare it to null.
+ *
+ * @param name The name to check for.
+ * @return True if the named protocol is registered.
+ */
+ public boolean hasProtocol(String name) {
+ return protocols.containsKey(name);
+ }
+
+ /**
+ * Returns the protocol whose name matches the given argument. This method will return null if no such protocol has
+ * been registered.
+ *
+ * @param name The name of the protocol to return.
+ * @return The protocol registered, or null.
+ */
+ public Protocol getProtocol(String name) {
+ return protocols.get(name);
+ }
+
+ /**
+ * Creates and returns a routing policy that matches the given arguments. If a routing policy has been created
+ * previously using the exact same parameters, this method will returned that cached instance instead of creating
+ * another. Not that when you replace a protocol using {@link #putProtocol(Protocol)} the policy cache is cleared.
+ *
+ * @param protocolName The name of the protocol whose routing policy to create.
+ * @param policyName The name of the routing policy to create.
+ * @param policyParam The parameter to pass to the routing policy constructor.
+ * @return The created routing policy.
+ */
+ public RoutingPolicy getRoutingPolicy(String protocolName, String policyName, String policyParam) {
+ String cacheKey = protocolName + "." + policyName + "." + policyParam;
+ RoutingPolicy ret = routingPolicyCache.get(cacheKey);
+ if (ret != null) {
+ return ret;
+ }
+ synchronized (this) {
+ Protocol protocol = getProtocol(protocolName);
+ if (protocol == null) {
+ log.log(LogLevel.ERROR, "Protocol '" + protocolName + "' not supported.");
+ return null;
+ }
+ try {
+ ret = protocol.createPolicy(policyName, policyParam);
+ } catch (RuntimeException e) {
+ log.log(LogLevel.ERROR, "Protcol '" + protocolName + "' threw an exception: " + e.getMessage(), e);
+ return null;
+ }
+ if (ret == null) {
+ log.log(LogLevel.ERROR, "Protocol '" + protocolName + "' failed to create routing policy '" + policyName +
+ "' with parameter '" + policyParam + "'.");
+ return null;
+ }
+ routingPolicyCache.put(cacheKey, ret);
+ }
+ return ret;
+ }
+
+ public final RoutingPolicy getRoutingPolicy(Utf8String protocolName, String policyName, String policyParam) {
+ return getRoutingPolicy(protocolName.toString(), policyName, policyParam);
+ }
+
+ /**
+ * Clears the internal cache of routing policies.
+ */
+ public synchronized void clearPolicyCache() {
+ for (RoutingPolicy policy : routingPolicyCache.values()) {
+ policy.destroy();
+ }
+ routingPolicyCache.clear();
+ }
+}
diff --git a/messagebus/src/main/java/com/yahoo/messagebus/RPCMessageBus.java b/messagebus/src/main/java/com/yahoo/messagebus/RPCMessageBus.java new file mode 100644 index 00000000000..d767e197b11 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/RPCMessageBus.java @@ -0,0 +1,112 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.log.LogLevel; +import com.yahoo.messagebus.network.Identity; +import com.yahoo.messagebus.network.rpc.RPCNetwork; +import com.yahoo.messagebus.network.rpc.RPCNetworkParams; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; + +/** + * The RPCMessageBus class wraps a MessageBus with an RPCNetwork and handles reconfiguration. Please note that according + * to the object shutdown order, you must shut down all sessions before shutting down this object. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RPCMessageBus { + + private static final Logger log = Logger.getLogger(RPCMessageBus.class.getName()); + private final AtomicBoolean destroyed = new AtomicBoolean(false); + private final MessageBus mbus; + private final RPCNetwork net; + private final ConfigAgent configAgent; + + /** + * Constructs a new instance of this class. + * + * @param mbusParams A complete set of message bus parameters. + * @param rpcParams A complete set of network parameters. + * @param routingCfgId The config id for message bus routing specs. + */ + public RPCMessageBus(MessageBusParams mbusParams, RPCNetworkParams rpcParams, String routingCfgId) { + net = new RPCNetwork(rpcParams); + mbus = new MessageBus(net, mbusParams); + configAgent = new ConfigAgent(routingCfgId != null ? routingCfgId : "client", mbus); + configAgent.subscribe(); + } + + /** + * This constructor requires an array of protocols that it is to support, as well as the host application's config + * identifier. That identifier is necessary so that all created sessions can be uniquely identified on the network. + * + * @param protocols An array of known protocols. + * @param rpcParams A complete set of network parameters. + * @param routingCfgId The config id for message bus routing specs. + */ + public RPCMessageBus(List<Protocol> protocols, RPCNetworkParams rpcParams, String routingCfgId) { + this(new MessageBusParams().addProtocols(protocols), rpcParams, routingCfgId); + } + + /** + * This constructor requires a single protocol that it is to support, as well as the host application's config + * identifier. + * + * @param protocol An instance of the known protocol. + * @param configId The host application's config id. This will be used to resolve the service name prefix used when + * registering with the slobrok. Using null here is allowed, but will not allow intermediate- or + * destination sessions to be routed to. + */ + public RPCMessageBus(Protocol protocol, String configId) { + this(Arrays.asList(protocol), new RPCNetworkParams().setIdentity(new Identity(configId)), null); + } + + // Overrides Object. + @Override + protected void finalize() throws Throwable { + try { + if (destroy()) { + log.log(LogLevel.WARNING, "RPCMessageBus destroyed by finalizer, please review application shutdown logic."); + } + } finally { + super.finalize(); + } + } + + /** + * Sets the destroyed flag to true. The very first time this method is called, it cleans up all its dependencies. + * Even if you retain a reference to this object, all of its content is allowed to be garbage collected. + * + * @return True if content existed and was destroyed. + */ + public boolean destroy() { + if (!destroyed.getAndSet(true)) { + configAgent.shutdown(); + mbus.destroy(); + return true; + } + return false; + } + + /** + * Returns the contained message bus object. + * + * @return Message bus. + */ + public MessageBus getMessageBus() { + return mbus; + } + + /** + * Returns the contained rpc network object. + * + * @return RPC network. + */ + public RPCNetwork getRPCNetwork() { + return net; + } + +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/RateThrottlingPolicy.java b/messagebus/src/main/java/com/yahoo/messagebus/RateThrottlingPolicy.java new file mode 100644 index 00000000000..a5f2004080e --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/RateThrottlingPolicy.java @@ -0,0 +1,61 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.concurrent.SystemTimer; +import com.yahoo.concurrent.Timer; + +import java.util.logging.Logger; + +/** + * Throttling policy that throttles sending based on a desired rate. It will + * block messages if the current rate is higher than desired, but otherwise will + * respect the static throttle policy's maximum window size. + * + * Rate is measured from at most the last 60 seconds. + */ +public class RateThrottlingPolicy extends StaticThrottlePolicy { + + public static final Logger log = Logger.getLogger(RateThrottlingPolicy.class.getName()); + + long PERIOD = 1000; + double desiredRate; + + double allotted = 0.0; + long currentPeriod = 0; + + Timer timer; + + public RateThrottlingPolicy(double desiredRate) { + this(desiredRate, SystemTimer.INSTANCE); + } + + public RateThrottlingPolicy(double desiredRate, Timer timer) { + this.desiredRate = desiredRate; + this.timer = timer; + currentPeriod = timer.milliTime() / PERIOD; + } + + public boolean canSend(Message msg, int pendingCount) { + if (!super.canSend(msg, pendingCount)) { + return false; + } + + long period = timer.milliTime() / PERIOD; + + while (currentPeriod < period) { + if (allotted > 0) { + allotted = 0.0; + } + + allotted = allotted + PERIOD * desiredRate / 1000; + currentPeriod++; + } + + if (allotted > 0.0) { + allotted -= 1; + return true; + } + + return false; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/Reply.java b/messagebus/src/main/java/com/yahoo/messagebus/Reply.java new file mode 100644 index 00000000000..c43f84714de --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/Reply.java @@ -0,0 +1,141 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +/** + * <p>A reply is a response to a message that has been sent throught the messagebus. No reply will ever exist without a + * corresponding message. There are no error-replies defined, as errors can instead piggyback any reply by the {@link + * #errors} member variable.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public abstract class Reply extends Routable { + + private double retryDelay = -1.0; + private Message msg = null; + private List<Error> errors = new ArrayList<>(); + + @Override + public void swapState(Routable rhs) { + super.swapState(rhs); + if (rhs instanceof Reply) { + Reply reply = (Reply)rhs; + + double retryDelay = this.retryDelay; + this.retryDelay = reply.retryDelay; + reply.retryDelay = retryDelay; + + Message msg = this.msg; + this.msg = reply.msg; + reply.msg = msg; + + List<Error> errors = this.errors; + this.errors = reply.errors; + reply.errors = errors; + } + } + + /** + * <p>Returns the message to which this is a reply.</p> + * + * @return The message. + */ + public Message getMessage() { + return msg; + } + + /** + * <p>Sets the message to which this is a reply. Although it might seem very bogus to allow such an accessor, it is + * necessary since we allow an empty constructor.</p> + * + * @param msg The message to which this is a reply. + */ + public void setMessage(Message msg) { + this.msg = msg; + } + + /** + * <p>Returns whether or not this reply contains any errors.</p> + * + * @return True if there are errors, false otherwise. + */ + public boolean hasErrors() { + return errors.size() > 0; + } + + /** + * <p>Returns whether or not this reply contains any fatal errors.</p> + * + * @return True if it contains fatal errors. + */ + public boolean hasFatalErrors() { + for (Error error : errors) { + if (error.getCode() >= ErrorCode.FATAL_ERROR) { + return true; + } + } + return false; + } + + /** + * <p>Returns the error at the given position.</p> + * + * @param i The index of the error to return. + * @return The error at the given index. + */ + public Error getError(int i) { + return errors.get(i); + } + + /** + * <p>Returns the number of errors that this reply contains.</p> + * + * @return The number of replies. + */ + public int getNumErrors() { + return errors.size(); + } + + /** + * <p>Add an error to this reply. This method will also trace the error as long as there is any tracing + * enabled.</p> + * + * @param error The error object to add. + */ + public void addError(Error error) { + errors.add(error); + getTrace().trace(TraceLevel.ERROR, error.toString()); + } + + /** + * <p>Returns the retry request of this reply. This can be set using {@link #setRetryDelay} and is an instruction to + * the resender logic of message bus on how to perform the retry. If this value is anything other than a negative + * number, it instructs the resender to disregard all configured resending attributes and instead act according to + * this value.</p> + * + * @return The retry request. + */ + public double getRetryDelay() { + return retryDelay; + } + + /** + * <p>Sets the retry delay request of this reply. If this is a negative number, it will use the defaults configured + * in the source session.</p> + * + * @param retryDelay The retry request. + */ + public void setRetryDelay(double retryDelay) { + this.retryDelay = retryDelay; + } + + /** + * Retrieves a (read only) stream of the errors in this reply + */ + public Stream<Error> getErrors() { + return errors.stream(); + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/ReplyHandler.java b/messagebus/src/main/java/com/yahoo/messagebus/ReplyHandler.java new file mode 100644 index 00000000000..7b51348d4aa --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/ReplyHandler.java @@ -0,0 +1,20 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +/** + * All classes that wants to handle replies that move through the messagebus need to implement this interface. As + * opposed to the {@link MessageHandler} which handles messages as they travel from the sender to the receiver, this + * interface is intended for handling replies as they return from the receiver to the sender. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface ReplyHandler { + + /** + * This function is called when a reply arrives. + * + * @param reply The reply that arrived. + */ + void handleReply(Reply reply); + +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/Result.java b/messagebus/src/main/java/com/yahoo/messagebus/Result.java new file mode 100644 index 00000000000..095398f6b5d --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/Result.java @@ -0,0 +1,69 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +/** + * <p>Information on the outcome of <i>initiating</i> a send or forward on a session. + * The result will tell if the send/forward was accepted or not. If it was accepted, + * an (asynchroneous) reply is guaranteed to be delivered at some later time. + * If it was not accepted, a <i>transient error</i> has occured. In that case, + * {@link #getError} can be used to access the exact error.</p> + * + * <p>This class is <b>immutable</b>. + * + * @author <a href="bratseth@yahoo-inc.com">Jon Bratseth</a> + */ +public class Result { + + public static final Result ACCEPTED = new Result(); + private final Error error; + + /** + * The default constructor is private so that the only error-results can be new'ed by + * the user. All accepted results should use the public "accepted" constant. + */ + private Result() { + error = null; + } + + /** + * This constructor assigns a given error to the member variable such that this result + * becomes unaccepted with a descriptive error. + * + * @param error The error to assign to this result. + */ + public Result(Error error) { + this.error = error; + } + + /** + * This constructor is a convencience function to allow simpler instantiation of a result that contains an error. + * It does nothing but proxy the {@link #Result(Error)} function with a new instance of {@link Error}. + * + * @param code The numerical code of the error. + * @param message The description of the error. + */ + public Result(int code, String message) { + this(new Error(code, message)); + } + + /** + * Returns whether this message was accepted. + * If it was accepted, a Reply is guaranteed to be produced for this message + * at some later time. If it was not accepted, getError can be called to + * investigate why. + * + * @return true if this message was accepted, false otherwise + */ + public boolean isAccepted() { + return error == null; + } + + /** + * The error resulting from this send/forward if the message was not accepted. + * + * @return The error is not accepcted, null if accepted. + */ + public Error getError() { + return error; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/Routable.java b/messagebus/src/main/java/com/yahoo/messagebus/Routable.java new file mode 100755 index 00000000000..cbc8ce8f2d2 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/Routable.java @@ -0,0 +1,126 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.text.Utf8String; + +/** + * Superclass for objects that can be either explicitly (Message) or implicitly (Reply) routed. Note that protocol + * implementors should never subclass this directly, but rather through the {@link Message} and {@link Reply} classes. + * + * A routable can be regarded as a protocol-defined value with additional message bus related state. The state is what + * differentiates two Routables that carry the same value. This includes the application context attached to the + * routable and the {@link CallStack} used to track the path of the routable within messagebus. When a routable is + * copied (if the protocol supports it) only the value part is copied. The state must be explicitly transfered by + * invoking the {@link #swapState(Routable)} method. That method is used to transfer the state from a message to the + * corresponding reply, or to a different message if the application decides to replace it. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public abstract class Routable { + + private final CallStack callStack = new CallStack(); + private final Trace trace = new Trace(); + private Object context = null; + + /** + * Discards this routable. Invoking this prevents the auto-generation of replies if you later discard the routable. + * This is a required step to ensure safe shutdown if you need destroy a message bus instance while there are still + * messages and replies alive in your application. + */ + public void discard() { + context = null; + callStack.clear(); + trace.clear(); + } + + /** + * Swaps the state that makes this routable unique to another routable. The state is what identifies a routable for + * message bus, so only one message can ever have the same state. This function must be called explicitly when + * cloning and copying messages. + * + * @param rhs The routable to swap state with. + */ + public void swapState(Routable rhs) { + Object context = this.context; + this.context = rhs.context; + rhs.context = context; + + callStack.swap(rhs.getCallStack()); + trace.swap(rhs.getTrace()); + } + + /** + * Pushes the given reply handler onto the call stack of this routable, also storing the current context. + * + * @param handler The handler to push. + */ + public void pushHandler(ReplyHandler handler) { + callStack.push(handler, context); + } + + /** + * <p>This is a convenience method for calling {@link CallStack#pop(Routable)} on the {@link CallStack} of this + * Routable. It equals calling <tt>routable.getCallStack().pop(routable)</tt>.</p> + * + * @return The handler that was popped. + * @see CallStack#pop(Routable) + */ + public ReplyHandler popHandler() { + return callStack.pop(this); + } + + /** + * Return the context of this routable. + * + * @return The context. + */ + public Object getContext() { + return context; + } + + /** + * Set a new context for this routable. Please note that the context is <u>not</u> something that is passed along a + * message, it is simply a user context for the handler currently manipulating a message. When the corresponding + * reply reaches the registered reply handler, its content will be the same as that of the outgoing message. More + * technically, this context is contained in the callstack of a routable. + * + * @param context The new context. + */ + public void setContext(Object context) { + this.context = context; + } + + /** + * Return the callstack of this routable. + * + * @return The callstack. + */ + public CallStack getCallStack() { + return callStack; + } + + /** + * Returns the trace object of this routable. + * + * @return The trace object. + */ + public Trace getTrace() { + return trace; + } + + /** + * Return the name of the protocol that defines this routable. This must be implemented by all inheriting classes, + * and should then return the result of {@link com.yahoo.messagebus.Protocol#getName} of its protocol. + * + * @return The name of the protocol defining this message. + */ + public abstract Utf8String getProtocol(); + + /** + * Obtain the type of this routable. The id '0' is reserved for the EmptyReply class. Other ids must be defined by + * the application protocol. + * + * @return The message type. + */ + public abstract int getType(); +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/SendProxy.java b/messagebus/src/main/java/com/yahoo/messagebus/SendProxy.java new file mode 100644 index 00000000000..fe894427a13 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/SendProxy.java @@ -0,0 +1,93 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus;
+
+import com.yahoo.concurrent.SystemTimer;
+import com.yahoo.messagebus.metrics.RouteMetricSet;
+import com.yahoo.messagebus.network.Network;
+import com.yahoo.messagebus.routing.Resender;
+import com.yahoo.messagebus.routing.RoutingNode;
+import com.yahoo.log.LogLevel;
+
+import java.util.logging.Logger;
+
+/**
+ * This class owns a message that is being sent by message bus. Once a reply is received, the message is attached to it
+ * and returned to the application. This also implements the discard policy of {@link RoutingNode}.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class SendProxy implements MessageHandler, ReplyHandler {
+
+ private static final Logger log = Logger.getLogger(SendProxy.class.getName());
+ private final MessageBus mbus;
+ private final Network net;
+ private final Resender resender;
+ private Message msg = null;
+ private boolean logTrace = false;
+ private long sendTime = 0;
+
+ /**
+ * Constructs a new instance of this class to maintain sending of a single message.
+ *
+ * @param mbus The message bus that owns this.
+ * @param net The network layer to transmit through.
+ * @param resender The resender to use.
+ */
+ public SendProxy(MessageBus mbus, Network net, Resender resender) {
+ this.mbus = mbus;
+ this.net = net;
+ this.resender = resender;
+ sendTime = SystemTimer.INSTANCE.milliTime();
+ }
+
+ public void handleMessage(Message msg) {
+ Trace trace = msg.getTrace();
+ if (trace.getLevel() == 0) {
+ if (log.isLoggable(LogLevel.SPAM)) {
+ trace.setLevel(9);
+ logTrace = true;
+ } else if (log.isLoggable(LogLevel.DEBUG)) {
+ trace.setLevel(6);
+ logTrace = true;
+ }
+ }
+ this.msg = msg;
+ RoutingNode root = new RoutingNode(mbus, net, resender, this, msg);
+ root.send();
+ }
+
+ public void handleReply(Reply reply) {
+ if (reply == null) {
+ msg.discard();
+ } else {
+ Trace trace = msg.getTrace();
+ if (logTrace) {
+ if (reply.hasErrors()) {
+ log.log(LogLevel.DEBUG, "Trace for reply with error(s):\n" + reply.getTrace());
+ } else if (log.isLoggable(LogLevel.SPAM)) {
+ log.log(LogLevel.SPAM, "Trace for reply:\n" + reply.getTrace());
+ }
+ Trace empty = new Trace();
+ trace.swap(empty);
+ } else if (trace.getLevel() > 0) {
+ trace.getRoot().addChild(reply.getTrace().getRoot());
+ trace.getRoot().normalize();
+ }
+ reply.swapState(msg);
+ reply.setMessage(msg);
+
+ if (msg.getRoute() != null) {
+ RouteMetricSet metrics = mbus.getMetrics().getRouteMetrics(msg.getRoute());
+ for (int i = 0; i < reply.getNumErrors(); i++) {
+ metrics.addFailure(reply.getError(i));
+ }
+ if (reply.getNumErrors() == 0) {
+ metrics.latency.addValue(msg.getTimeReceived() - sendTime);
+ }
+ }
+
+ ReplyHandler handler = reply.popHandler();
+ handler.handleReply(reply);
+ }
+ }
+}
diff --git a/messagebus/src/main/java/com/yahoo/messagebus/Sequencer.java b/messagebus/src/main/java/com/yahoo/messagebus/Sequencer.java new file mode 100644 index 00000000000..6f90bb8c994 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/Sequencer.java @@ -0,0 +1,155 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Sequencing is implemented as a message handler that is configured in a source session in that session's chain of + * linked message handlers. Each message that carries a sequencing id is queued in an internal list of messages for that + * id, and messages are only sent when they are at the front of their list. When a reply arrives, the current front of + * the list is removed and the next message, if any, is sent. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class Sequencer implements MessageHandler, ReplyHandler { + + private final AtomicBoolean destroyed = new AtomicBoolean(false); + private final MessageHandler sender; + private final Map<Long, Queue<Message>> seqMap = new HashMap<Long, Queue<Message>>(); + + /** + * Constructs a new sequencer on top of the given async sender. + * + * @param sender The underlying sender. + */ + public Sequencer(MessageHandler sender) { + this.sender = sender; + } + + /** + * Sets the destroyed flag to true. The very first time this method is called, it cleans up all its dependencies. + * Even if you retain a reference to this object, all of its content is allowed to be garbage collected. + * + * @return True if content existed and was destroyed. + */ + public boolean destroy() { + if (!destroyed.getAndSet(true)) { + synchronized (this) { + for (Queue<Message> queue : seqMap.values()) { + if (queue != null) { + for (Message msg : queue) { + msg.discard(); + } + } + } + seqMap.clear(); + } + return true; + } + return false; + } + + /** + * Filter a message against the current sequencing state. If this method returns true, the message has been cleared + * for sending and its sequencing information has been added to the state. If this method returns false, it has been + * queued for later sending due to sequencing restrictions. This method also sets the sequence id as message + * context. + * + * @param msg The message to filter. + * @return True if the message was consumed. + */ + private boolean filter(Message msg) { + long seqId = msg.getSequenceId(); + msg.setContext(seqId); + synchronized (this) { + if (seqMap.containsKey(seqId)) { + Queue<Message> queue = seqMap.get(seqId); + if (queue == null) { + queue = new LinkedList<Message>(); + seqMap.put(seqId, queue); + } + if (msg.getTrace().shouldTrace(TraceLevel.COMPONENT)) { + msg.getTrace().trace(TraceLevel.COMPONENT, + "Sequencer queued message with sequence id '" + seqId + "'."); + } + queue.add(msg); + return false; + } + seqMap.put(seqId, null); + } + return true; + } + + /** + * Internal method for forwarding a sequenced message to the underlying sender. + * + * @param msg The message to forward. + */ + private void sequencedSend(Message msg) { + if (msg.getTrace().shouldTrace(TraceLevel.COMPONENT)) { + msg.getTrace().trace(TraceLevel.COMPONENT, + "Sequencer sending message with sequence id '" + msg.getContext() + "'."); + } + msg.pushHandler(this); + sender.handleMessage(msg); + } + + /** + * All messages pass through this handler when being sent by the owning source session. In case the message has no + * sequencing-id, it is simply passed through to the next handler in the chain. Sequenced messages are sent only if + * there is no queue for their id, otherwise they are queued. + * + * @param msg The message to send. + */ + @Override + public void handleMessage(Message msg) { + if (destroyed.get()) { + msg.discard(); + return; + } + if (msg.hasSequenceId()) { + if (filter(msg)) { + sequencedSend(msg); + } + } else { + sender.handleMessage(msg); // unsequenced + } + } + + /** + * Lookup the sequencing id of an incoming reply to pop the front of the corresponding queue, and then send the next + * message in line, if any. + * + * @param reply The reply received. + */ + @Override + public void handleReply(Reply reply) { + if (destroyed.get()) { + reply.discard(); + return; + } + long seqId = (Long)reply.getContext(); // non-sequenced messages do not enter here + if (reply.getTrace().shouldTrace(TraceLevel.COMPONENT)) { + reply.getTrace().trace(TraceLevel.COMPONENT, + "Sequencer received reply with sequence id '" + seqId + "'."); + } + Message msg = null; + synchronized (this) { + Queue<Message> queue = seqMap.get(seqId); + if (queue == null || queue.isEmpty()) { + seqMap.remove(seqId); + } else { + msg = queue.remove(); + } + } + if (msg != null) { + sequencedSend(msg); + } + ReplyHandler handler = reply.popHandler(); + handler.handleReply(reply); + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/SourceSession.java b/messagebus/src/main/java/com/yahoo/messagebus/SourceSession.java new file mode 100644 index 00000000000..bef6e37476c --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/SourceSession.java @@ -0,0 +1,300 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.log.LogLevel; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.messagebus.routing.RoutingTable; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; + +/** + * <p>A session supporting sending new messages.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public final class SourceSession implements ReplyHandler { + + private static Logger log = Logger.getLogger(SourceSession.class.getName()); + private final AtomicBoolean destroyed = new AtomicBoolean(false); + private final CountDownLatch done = new CountDownLatch(1); + private final Object lock = new Object(); + private final MessageBus mbus; + private final Sequencer sequencer; + private final ReplyHandler replyHandler; + private final ThrottlePolicy throttlePolicy; + private volatile double timeout; + private volatile int pendingCount = 0; + private boolean closed = false; + + /** + * <p>The default constructor requires values for all final member variables + * of this. It expects all arguments but the {@link SourceSessionParams} to + * be proper, so no checks are performed. The constructor is declared + * package private since only {@link MessageBus} is supposed to instantiate + * it.</p> + * + * @param mbus The message bus that created this instance. + * @param params A parameter object that holds configuration parameters. + */ + SourceSession(MessageBus mbus, SourceSessionParams params) { + this.mbus = mbus; + sequencer = new Sequencer(mbus); + if (!params.hasReplyHandler()) { + throw new NullPointerException("Reply handler is null."); + } + replyHandler = params.getReplyHandler(); + throttlePolicy = params.getThrottlePolicy(); + timeout = params.getTimeout(); + } + + @Override + protected void finalize() throws Throwable { + try { + if (destroy()) { + log.log(LogLevel.WARNING, "SourceSession destroyed by finalizer, please review application shutdown logic."); + } + } finally { + super.finalize(); + } + } + + /** + * Sets the destroyed flag to true. The very first time this method is + * called, it cleans up all its dependencies. Even if you retain a + * reference to this object, all of its content is allowed to be garbage + * collected. + * + * @return true if content existed and was destroyed. + */ + public boolean destroy() { + if (destroyed.getAndSet(true)) { + return false; + } + synchronized (lock) { + closed = true; + } + sequencer.destroy(); + mbus.sync(); + return true; + } + + /** + * Reject all new messages and wait until no messages are pending. Before + * returning, this method calls {@link #destroy()}. + */ + public void close() { + synchronized (lock) { + closed = true; + } + if (pendingCount == 0) { + done.countDown(); + } + try { + done.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + destroy(); + } + + /** + * <p>Sends a new message. Calling this immediately causes one of three + * possible results:</p> + * <ul><li>A result is returned indicating that the message is accepted. In + * this case, a reply to the message is guaranteed to be produced on this + * session within a timeout limit. That reply may indicate either success or + * failure.</li> + * <li>A result is returned indicating that the message is not + * accepted. This is a <i>transient failure</i>, retrying the same operation + * after some wait period should cause it to be accepted.</li> + * <li>An exception is thrown, indicating a non-transient error which is not + * expected to be fixed before some corrective action is taken.</li> </ul> + * + * <p>A source client should typically do some equivalent of:</p> + * <code> + * do { + * Result result = sourceSession.send(message); + * if (!result.isAccepted()) + * // Do something else or wait a while + * } while (!result.isAccepted()); + * </code> + * + * @param msg the message to send + * @return The result of <i>initiating</i> sending of this message. + */ + public Result send(Message msg) { + msg.setTimeReceivedNow(); + if (msg.getTimeRemaining() <= 0) { + msg.setTimeRemaining((long)(timeout * 1000)); + } + synchronized (lock) { + if (closed) { + return new Result(ErrorCode.SEND_QUEUE_CLOSED, + "Source session is closed."); + } + if (throttlePolicy != null && !throttlePolicy.canSend(msg, pendingCount)) { + return new Result(ErrorCode.SEND_QUEUE_FULL, + "Too much pending data (" + pendingCount + " messages)."); + } + msg.pushHandler(replyHandler); + if (throttlePolicy != null) { + throttlePolicy.processMessage(msg); + } + ++pendingCount; + } + if (msg.getTrace().shouldTrace(TraceLevel.COMPONENT)) { + msg.getTrace().trace(TraceLevel.COMPONENT, + "Source session accepted a " + msg.getApproxSize() + " byte message. " + + pendingCount + " message(s) now pending."); + } + msg.pushHandler(this); + sequencer.handleMessage(msg); + return Result.ACCEPTED; + } + + /** + * <p>This is a blocking proxy to the {@link #send(Message)} method. This + * method blocks until the message is accepted by the send queue. Note that + * the message timeout does not activate by calling this method. This method + * will also return if this session is closed or the calling thread is + * interrupted.</p> + * + * @param msg The message to send. + * @return The result of initiating send. + * @throws InterruptedException Thrown if the calling thread is interrupted. + */ + public Result sendBlocking(Message msg) throws InterruptedException { + while (true) { + Result res = send(msg); + if (res.isAccepted() || res.getError().getCode() != ErrorCode.SEND_QUEUE_FULL) { + return res; + } + synchronized (lock) { + while (!closed && !throttlePolicy.canSend(msg, pendingCount)) { + lock.wait(100); + } + } + } + } + + @Override + public void handleReply(Reply reply) { + if (destroyed.get()) { + reply.discard(); + return; + } + boolean done; + synchronized (lock) { + --pendingCount; + if (throttlePolicy != null) { + throttlePolicy.processReply(reply); + } + done = (closed && pendingCount == 0); + lock.notifyAll(); + } + if (reply.getTrace().shouldTrace(TraceLevel.COMPONENT)) { + reply.getTrace().trace(TraceLevel.COMPONENT, + "Source session received reply. " + pendingCount + " message(s) now pending."); + } + ReplyHandler handler = reply.popHandler(); + handler.handleReply(reply); + if (done) { + this.done.countDown(); + } + } + + /** + * <p>This is a convenience function to assign a given route to the given + * message, and then pass it to the other {@link #send(Message)} method of + * this session.</p> + * + * @param msg The message to send. + * @param route The route to assign to the message. + * @return The immediate result of the attempt to send this message. + */ + public Result send(Message msg, Route route) { + return send(msg.setRoute(route)); + } + + /** + * <p>This is a convenience method to call {@link + * #send(Message,String,boolean)} with a <code>false</code> value for the + * 'parseIfNotFound' parameter.</p> + * + * @param msg The message to send. + * @param routeName The route to assign to the message. + * @return The immediate result of the attempt to send this message. + */ + public Result send(Message msg, String routeName) { + return send(msg, routeName, false); + } + + /** + * <p>This is a convenience function to assign a named route to the given + * message, and then pass it to the other {@link #send(Message)} method of + * this session. If the route could not be found this methods returns with + * an appropriate error, unless the 'parseIfNotFound' argument is true. In + * that case, the route name is passed through to the Route factory method + * {@link Route#parse}.</p> + * + * @param msg The message to send. + * @param routeName The route to assign to the message. + * @param parseIfNotFound Whether or not to parse routeName as a route if + * it could not be found. + * @return The immediate result of the attempt to send this message. + */ + public Result send(Message msg, String routeName, boolean parseIfNotFound) { + boolean found = false; + RoutingTable table = mbus.getRoutingTable(msg.getProtocol().toString()); + if (table != null) { + Route route = table.getRoute(routeName); + if (route != null) { + msg.setRoute(new Route(route)); + found = true; + } else if (!parseIfNotFound) { + return new Result(ErrorCode.ILLEGAL_ROUTE, + "Route '" + routeName + "' not found for protocol '" + msg.getProtocol() + "'."); + } + } else if (!parseIfNotFound) { + return new Result(ErrorCode.ILLEGAL_ROUTE, + "Protocol '" + msg.getProtocol() + "' has no routing table."); + } + if (!found) { + msg.setRoute(Route.parse(routeName)); + } + return send(msg); + } + + /** + * <p>Returns the reply handler of this session.</p> + * + * @return The reply handler. + */ + public ReplyHandler getReplyHandler() { + return replyHandler; + } + + /** + * <p>Returns the number of messages sent that have not been replied to + * yet.</p> + * + * @return The pending count. + */ + public int getPendingCount() { + return pendingCount; + } + + /** + * <p>Sets the number of seconds a message can be attempted sent until it + * times out.</p> + * + * @param timeout The numer of seconds allowed. + * @return This, to allow chaining. + */ + public SourceSession setTimeout(double timeout) { + this.timeout = timeout; + return this; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/SourceSessionParams.java b/messagebus/src/main/java/com/yahoo/messagebus/SourceSessionParams.java new file mode 100644 index 00000000000..0a57e777dbd --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/SourceSessionParams.java @@ -0,0 +1,104 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +/** + * To facilitate several configuration parameters to the {@link MessageBus#createSourceSession(ReplyHandler, + * SourceSessionParams)}, all parameters are held by this class. This class has reasonable default values for each + * parameter. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class SourceSessionParams { + + private ReplyHandler replyHandler = null; + private ThrottlePolicy throttlePolicy = new DynamicThrottlePolicy(); + private double timeout = 180.0; + + /** + * Instantiates a parameter object with default values. + */ + public SourceSessionParams() { + // empty + } + + /** + * Implements the copy constructor. + * + * @param params The object to copy. + */ + public SourceSessionParams(SourceSessionParams params) { + throttlePolicy = params.throttlePolicy; + timeout = params.timeout; + replyHandler = params.replyHandler; + } + + /** + * Returns the policy to use for throttling output. + * + * @return The policy. + */ + public ThrottlePolicy getThrottlePolicy() { + return throttlePolicy; + } + + /** + * Sets the policy to use for throttling output. + * + * @param throttlePolicy The policy to set. + * @return This, to allow chaining. + */ + public SourceSessionParams setThrottlePolicy(ThrottlePolicy throttlePolicy) { + this.throttlePolicy = throttlePolicy; + return this; + } + + /** + * Returns the number of seconds a message can spend trying to succeed. + * + * @return The timeout in seconds. + */ + public double getTimeout() { + return timeout; + } + + /** + * Sets the number of seconds a message can be attempted sent until it times out. This is the maximum allowed time + * for any message bus operation. + * + * @param timeout The numer of seconds allowed. + * @return This, to allow chaining. + */ + public SourceSessionParams setTimeout(double timeout) { + this.timeout = timeout; + return this; + } + + /** + * Returns whether or not a reply handler has been assigned to this. + * + * @return True if a handler is set. + */ + boolean hasReplyHandler() { + return replyHandler != null; + } + + /** + * Returns the handler to receive incoming replies. + * + * @return The handler. + */ + public ReplyHandler getReplyHandler() { + return replyHandler; + } + + /** + * Sets the handler to recive incoming replies. + * + * @param handler The handler to set. + * @return This, to allow chaining. + */ + public SourceSessionParams setReplyHandler(ReplyHandler handler) { + replyHandler = handler; + return this; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/StaticThrottlePolicy.java b/messagebus/src/main/java/com/yahoo/messagebus/StaticThrottlePolicy.java new file mode 100644 index 00000000000..bb1ae9e69b3 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/StaticThrottlePolicy.java @@ -0,0 +1,91 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus;
+
+/**
+ * This is an implementatin of the {@link ThrottlePolicy} that offers static limits to the amount of pending data a
+ * {@link SourceSession} is allowed to have. You may choose to set a limit to the total number of pending messages (by
+ * way of {@link #setMaxPendingCount(int)}), the total size of pending messages (by way of {@link
+ * #setMaxPendingSize(long)}), or some combination thereof.
+ *
+ * <b>NOTE:</b> By context, "pending" is refering to the number of sent messages that have not been replied to yet.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class StaticThrottlePolicy implements ThrottlePolicy {
+
+ private int maxPendingCount = 0;
+ private long maxPendingSize = 0;
+ private long pendingSize = 0;
+
+ public boolean canSend(Message msg, int pendingCount) {
+ if (maxPendingCount > 0 && pendingCount >= maxPendingCount) {
+ return false;
+ }
+ if (maxPendingSize > 0 && pendingSize >= maxPendingSize) {
+ return false;
+ }
+ return true;
+ }
+
+ public void processMessage(Message msg) {
+ int size = msg.getApproxSize();
+ msg.setContext(size);
+ pendingSize += size;
+ }
+
+ public void processReply(Reply reply) {
+ int size = (Integer)reply.getContext();
+ pendingSize -= size;
+ }
+
+ /**
+ * Returns the maximum number of pending messages allowed.
+ *
+ * @return The max limit.
+ */
+ public int getMaxPendingCount() {
+ return maxPendingCount;
+ }
+
+ /**
+ * Sets the maximum number of pending messages allowed.
+ *
+ * @param maxCount The max count.
+ * @return This, to allow chaining.
+ */
+ public StaticThrottlePolicy setMaxPendingCount(int maxCount) {
+ maxPendingCount = maxCount;
+ return this;
+ }
+
+ /**
+ * Returns the maximum total size of pending messages allowed.
+ *
+ * @return The max limit.
+ */
+ public long getMaxPendingSize() {
+ return maxPendingSize;
+ }
+
+ /**
+ * Sets the maximum total size of pending messages allowed. This size is relative to the value returned by {@link
+ * com.yahoo.messagebus.Message#getApproxSize()}.
+ *
+ * @param maxSize The max size.
+ * @return This, to allow chaining.
+ */
+ public StaticThrottlePolicy setMaxPendingSize(long maxSize) {
+ maxPendingSize = maxSize;
+ return this;
+ }
+
+ /**
+ * Returns the total size of pending messages.
+ *
+ * @return The size.
+ */
+ public long getPendingSize() {
+ return pendingSize;
+ }
+
+}
diff --git a/messagebus/src/main/java/com/yahoo/messagebus/ThrottlePolicy.java b/messagebus/src/main/java/com/yahoo/messagebus/ThrottlePolicy.java new file mode 100644 index 00000000000..611694b0c62 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/ThrottlePolicy.java @@ -0,0 +1,36 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus;
+
+/**
+ * An implementation of this interface is used by {@link SourceSession} to throttle output. Every message entering
+ * {@link SourceSession#send(Message)} needs to be accepted by this interface's {@link #canSend(Message, int)} method.
+ * All messages accepted are passed through the {@link #processMessage(Message)} method, and the corresponding replies
+ * are passed through the {@link #processReply(Reply)} method.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface ThrottlePolicy {
+
+ /**
+ * Returns whether or not the given message can be sent according to the current state of this policy.
+ *
+ * @param msg The message to evaluate.
+ * @param pendingCount The current number of pending messages.
+ * @return True to send the message.
+ */
+ public boolean canSend(Message msg, int pendingCount);
+
+ /**
+ * This method is called once for every message that was accepted by {@link #canSend(Message, int)} and sent.
+ *
+ * @param msg The message beint sent.
+ */
+ public void processMessage(Message msg);
+
+ /**
+ * This method is called once for every reply that is received.
+ *
+ * @param reply The reply received.
+ */
+ public void processReply(Reply reply);
+}
diff --git a/messagebus/src/main/java/com/yahoo/messagebus/Trace.java b/messagebus/src/main/java/com/yahoo/messagebus/Trace.java new file mode 100755 index 00000000000..00db4963dea --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/Trace.java @@ -0,0 +1,158 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import java.util.Date; + +/** + * A Trace object contains ad-hoc string notes organized in a strict-loose tree. A Trace object consists of a trace + * level indicating which trace notes should be included and a TraceTree object containing the tree structure and + * collecting the trace information. Tracing is used to collect debug information about a Routable traveling through the + * system. The trace level is in the range [0,9]. 0 means no tracing, and 9 means all tracing is enabled. A client that + * has the ability to trace information will have a predefined level attached to that information. If the level on the + * information is lower or equal to the level set in the Trace object, the information will be traced. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class Trace { + + private int level = 0; + private TraceNode root = new TraceNode(); + + /** + * Create an empty trace with level set to 0 (no tracing) + */ + public Trace() { + // empty + } + + /** + * Create an empty trace with given level. + * + * @param level Level to set. + */ + public Trace(int level) { + this.level = level; + } + + /** + * Remove all trace information and set the trace level to 0. + * + * @return This, to allow chaining. + */ + public Trace clear() { + level = 0; + root = new TraceNode(); + return this; + } + + /** + * Swap the internals of this with another. + * + * @param other The trace to swap internals with. + * @return This, to allow chaining. + */ + public Trace swap(Trace other) { + int level = this.level; + this.level = other.level; + other.level = level; + + TraceNode root = this.root; + this.root = other.root; + other.root = root; + + return this; + } + + /** + * Set the trace level. 0 means no tracing, 9 means enable all tracing. + * + * @param level The level to set. + * @return This, to allow chaining. + */ + public Trace setLevel(int level) { + this.level = Math.min(Math.max(level, 0), 9); + return this; + } + + /** + * Returns the trace level. + * + * @return The trace level. + */ + public int getLevel() { + return level; + } + + /** + * Check if information with the given level should be traced. This method is added to allow clients to check if + * something should be traced before spending time building up the trace information itself. + * + * @param level The trace level to test. + * @return True if tracing is enabled for the given level, false otherwise. + */ + public boolean shouldTrace(int level) { + return level <= this.level; + } + + /** + * Add the given note to the trace information if tracing is enabled for the given level. + * + * @param level The trace level of the note. + * @param note The note to add. + * @return True if the note was added to the trace information, false otherwise. + */ + public boolean trace(int level, String note) { + return trace(level, note, true); + } + + /** + * Add the given note to the trace information if tracing is enabled for the given level. If the addTime parameter + * is true, then the note is prefixed with the current time. This is the default behaviour when ommiting this + * parameter. + * + * @param level The trace level of the note. + * @param note The note to add. + * @param addTime Whether or not to prefix note with a timestamp. + * @return True if the note was added to the trace information, false otherwise. + */ + public boolean trace(int level, String note, boolean addTime) { + if (!shouldTrace(level)) { + return false; + } + if (addTime) { + String timeString = Long.toString(System.currentTimeMillis()); + StringBuilder buf = new StringBuilder(); + buf.append("["); + int len = timeString.length(); + // something wrong. handle it by using the input long as a string. + if (len < 3) { + buf.append(timeString); + } else { + buf.append(timeString.substring(0, len - 3)); + buf.append('.'); + buf.append(timeString.substring(len - 3)); + } + buf.append("] "); + buf.append(note); + root.addChild(buf.toString()); + } else { + root.addChild(note); + } + return true; + } + + /** + * Returns the root of the trace tree. + * + * @return The root. + */ + public TraceNode getRoot() { + return root; + } + + // Overrides Object. + @Override + public String toString() { + return root.toString(31337); + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/TraceLevel.java b/messagebus/src/main/java/com/yahoo/messagebus/TraceLevel.java new file mode 100755 index 00000000000..994eb17b434 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/TraceLevel.java @@ -0,0 +1,30 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus;
+
+/**
+ * This class defines the {@link Trace} levels used by message bus.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class TraceLevel {
+
+ /**
+ * Traces whenever an Error is added to a Reply.
+ */
+ public static final int ERROR = 1;
+
+ /**
+ * Traces sending and receiving messages and replies on network level.
+ */
+ public static final int SEND_RECEIVE = 4;
+
+ /**
+ * Traces splitting messages and merging replies.
+ */
+ public static final int SPLIT_MERGE = 5;
+
+ /**
+ * Traces information about which internal components are processing a routable.
+ */
+ public static final int COMPONENT = 6;
+}
\ No newline at end of file diff --git a/messagebus/src/main/java/com/yahoo/messagebus/TraceNode.java b/messagebus/src/main/java/com/yahoo/messagebus/TraceNode.java new file mode 100755 index 00000000000..d7f24432496 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/TraceNode.java @@ -0,0 +1,473 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.log.LogLevel; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; + +/** + * This class contains the actual trace information of a {@link Trace} object. A trace node can be encoded to, and + * decoded from a string representation to allow transport across the network. Each node contains a list of children, a + * strictness flag and an optional note. The child list is what forms the trace tree, the strictness flag dictates + * whether or not the ordering of the children is important, and the note is the actual traced data. + * + * The most important feature to notice is the {@link #normalize()} method that will compact, sort and 'rootify' the + * trace tree so that trees become well-formed (and can be compared for equality). + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class TraceNode implements Comparable<TraceNode> { + + private static final Logger log = Logger.getLogger(TraceNode.class.getName()); + private TraceNode parent = null; + private boolean strict = true; + private String note = null; + private List<TraceNode> children = new ArrayList<TraceNode>(); + + /** + * Create an empty trace tree. + */ + public TraceNode() { + // empty + } + + /** + * Create a leaf node with the given note. + * + * @param note The note to assign to this. + */ + private TraceNode(String note) { + this.note = note; + } + + /** + * Create a trace tree which is a copy of another. + * + * @param rhs The tree to copy. + */ + TraceNode(TraceNode rhs) { + strict = rhs.strict; + note = rhs.note; + addChildren(rhs.children); + } + + /** + * Swap the internals of this tree with another. + * + * @param other The tree to swap internals with. + * @return This, to allow chaining. + */ + public TraceNode swap(TraceNode other) { + TraceNode parent = this.parent; + this.parent = other.parent; + other.parent = parent; + + boolean strict = this.strict; + this.strict = other.strict; + other.strict = strict; + + String note = this.note; + this.note = other.note; + other.note = note; + + List<TraceNode> children = this.children; + this.children = other.children; + for (TraceNode child : this.children) { + child.parent = this; + } + other.children = children; + for (TraceNode child : other.children) { + child.parent = other; + } + + return this; + } + + /** + * Remove all trace information from this tree. + * + * @return This, to allow chaining. + */ + public TraceNode clear() { + parent = null; + strict = true; + note = null; + children.clear(); + return this; + } + + /** + * Sort non-strict children recursively down the tree. + * + * @return This, to allow chaining. + */ + public TraceNode sort() { + if (!isLeaf()) { + for (TraceNode child : children) { + child.sort(); + } + if (!isStrict()) { + Collections.sort(children); + } + } + return this; + } + + @Override + public int compareTo(TraceNode rhs) { + if (isLeaf() || rhs.isLeaf()) { + if (isLeaf() && rhs.isLeaf()) { + return note.compareTo(rhs.getNote()); + } else { + return isLeaf() ? -1 : 1; + } + } + if (children.size() != rhs.children.size()) { + return children.size() < rhs.children.size() ? -1 : 1; + } + for (int i = 0; i < children.size(); ++i) { + int cmp = children.get(i).compareTo(rhs.children.get(i)); + if (cmp != 0) { + return cmp; + } + } + return -1; + } + + /** + * Compact this tree. This will reduce the height of this tree as much as possible without removing information + * stored in it. + * + * @return This, to allow chaining. + */ + public TraceNode compact() { + if (isLeaf()) { + return this; + } + List<TraceNode> tmp = this.children; + this.children = new ArrayList<TraceNode>(); + for (TraceNode child : tmp) { + child.compact(); + if (child.isEmpty()) { + // ignore + } else if (child.isLeaf()) { + addChild(child); + } else if (strict == child.strict) { + addChildren(child.children); + } else if (child.getNumChildren() == 1) { + TraceNode grandChild = child.getChild(0); + if (grandChild.isEmpty()) { + // ignore + } else if (grandChild.isLeaf() || strict != grandChild.strict) { + addChild(grandChild); + } else { + addChildren(grandChild.children); + } + } else { + addChild(child); + } + } + return this; + } + + /** + * Normalize this tree. This will transform all equivalent trees into the same form. Note that this will also + * perform an implicit compaction of the tree. + * + * @return This, to allow chaining. + */ + public TraceNode normalize() { + compact(); + sort(); + if (note != null || !strict) { + TraceNode child = new TraceNode(); + child.swap(this); + addChild(child); + strict = true; + } + return this; + } + + /** + * Check whether or not this is a root node. + * + * @return True if this has no parent. + */ + public boolean isRoot() { + return parent == null; + } + + /** + * Check whether or not this is a leaf node. + * + * @return True if this has no children. + */ + public boolean isLeaf() { + return children.isEmpty(); + } + + /** + * Check whether or not this node is empty, i.e. it has no note and no children. + * + * @return True if this node is empty. + */ + public boolean isEmpty() { + return note == null && children.isEmpty(); + } + + /** + * Check whether or not the children of this node are strictly ordered. + * + * @return True if this node is strict. + */ + public boolean isStrict() { + return strict; + } + + /** + * Sets whether or not the children of this node are strictly ordered. + * + * @param strict True to order children strictly. + * @return This, to allow chaining. + */ + public TraceNode setStrict(boolean strict) { + this.strict = strict; + return this; + } + + /** + * Returns whether or not a note is assigned to this node. + * + * @return True if a note is assigned. + */ + public boolean hasNote() { + return note != null; + } + + /** + * Returns the note assigned to this node. + * + * @return The note. + */ + public String getNote() { + return note; + } + + /** + * Returns the number of child nodes of this. + * + * @return The number of children. + */ + public int getNumChildren() { + return children.size(); + } + + /** + * Returns the child trace node at the given index. + * + * @param i The index of the child to return. + * @return The child at the given index. + */ + public TraceNode getChild(int i) { + return children.get(i); + } + + /** + * Convenience method to add a child node containing a note to this. + * + * @param note The note to assign to the child. + * @return This, to allow chaining. + */ + public TraceNode addChild(String note) { + return addChild(new TraceNode(note)); + } + + /** + * Adds a child node to this. + * + * @param child The child to add. + * @return This, to allow chaining. + */ + public TraceNode addChild(TraceNode child) { + if (note != null) { + throw new IllegalStateException("Nodes with notes are leaf nodes, you can not add children to it."); + } + TraceNode node = new TraceNode(child); + node.parent = this; + children.add(node); + return this; + } + + /** + * Adds a list of child nodes to this. + * + * @param children The children to add. + * @return This, to allow chaining. + */ + public TraceNode addChildren(List<TraceNode> children) { + for (TraceNode child : children) { + addChild(child); + } + return this; + } + + // Overrides Object. + @Override + public String toString() { + return toString(Integer.MAX_VALUE); + } + + /** + * Generates a non-parseable, human-readable string representation + * of this trace node. + * + * @return generated string + * @param limit soft limit for maximum string size + **/ + public String toString(int limit) { + StringBuilder out = new StringBuilder(); + if (!writeString(out, "", limit)) { + out.append("...\n"); + } + return out.toString(); + } + + /** + * Writes a non-parseable, human-readable string representation of + * this trace node to the given string builder using the given + * indent string for every written line. + * + * @return false if written string was capped + * @param ret The string builder to write to. + * @param indent The indent to use. + * @param limit soft limit for maximum string size + */ + private boolean writeString(StringBuilder ret, String indent, int limit) { + if (ret.length() >= limit) { + return false; + } + if (note != null) { + ret.append(indent).append(note).append("\n"); + } else { + String name = isStrict() ? "trace" : "fork"; + ret.append(indent).append("<").append(name).append(">\n"); + for (TraceNode child : children) { + if (!child.writeString(ret, indent + " ", limit)) { + return false; + } + } + if (ret.length() >= limit) { + return false; + } + ret.append(indent).append("</").append(name).append(">\n"); + } + return true; + } + + /** + * Returns a parseable (using {@link #decode(String)}) string representation of this trace node. + * + * @return A string representation of this tree. + */ + public String encode() { + StringBuilder ret = new StringBuilder(); + encode(ret); + return ret.toString(); + } + + /** + * Writes a parseable string representation of this trace node to the given string builder. + * + * @param ret The string builder to write to. + */ + private void encode(StringBuilder ret) { + if (note != null) { + ret.append("["); + for (int i = 0, len = note.length(); i < len; ++i) { + char c = note.charAt(i); + if (c == '\\' || c == ']') { + ret.append('\\'); + } + ret.append(note.charAt(i)); + } + ret.append("]"); + } else { + ret.append(strict ? "(" : "{"); + for (TraceNode child : children) { + child.encode(ret); + } + ret.append(strict ? ")" : "}"); + } + } + + /** + * Build a trace tree from the given string representation (possibly encoded using {@link #encode()}). + * + * @param str The string to parse. + * @return The corresponding trace tree, or an empty node if parsing failed. + */ + public static TraceNode decode(String str) { + if (str == null || str.isEmpty()) { + return new TraceNode(); + } + TraceNode proxy = new TraceNode(); + TraceNode node = proxy; + StringBuilder note = null; + boolean inEscape = false; + for (int i = 0, len = str.length(); i < len; ++i) { + char c = str.charAt(i); + if (note != null) { + if (inEscape) { + note.append(c); + inEscape = false; + } else if (c == '\\') { + inEscape = true; + } else if (c == ']') { + node.addChild(note.toString()); + note = null; + } else { + note.append(c); + } + } else { + if (c == '[') { + note = new StringBuilder(); + } else if (c == '(' || c == '{') { + node.addChild(new TraceNode()); + node = node.getChild(node.getNumChildren() - 1); + node.setStrict(c == '('); + } else if (c == ')' || c == '}') { + if (node == null) { + log.log(LogLevel.WARNING, "Unexpected closing brace in trace '" + str + "' at position " + i + "."); + return new TraceNode(); + } + if (node.isStrict() != (c == ')')) { + log.log(LogLevel.WARNING, "Mismatched closing brace in trace '" + str + "' at position " + i + "."); + return new TraceNode(); + } + node = node.parent; + } + } + } + if (note != null) { + log.log(LogLevel.WARNING, "Unterminated note in trace '" + str + "'."); + return new TraceNode(); + } + if (node != proxy) { + log.log(LogLevel.WARNING, "Missing closing brace in trace '" + str + "'."); + return new TraceNode(); + } + if (proxy.getNumChildren() == 0) { + log.log(LogLevel.WARNING, "No nodes found in trace '" + str + "'."); + return new TraceNode(); + } + if (proxy.getNumChildren() != 1) { + return proxy; // best-effort recovery from malformed input + } + TraceNode ret = proxy.children.remove(0); + ret.parent = null; + return ret; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/metrics/AverageMetric.java b/messagebus/src/main/java/com/yahoo/messagebus/metrics/AverageMetric.java new file mode 100644 index 00000000000..96339a2f703 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/metrics/AverageMetric.java @@ -0,0 +1,52 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.metrics; + +import com.yahoo.text.XMLWriter; +import com.yahoo.text.Utf8String; + +/** + * @author thomasg + */ +public class AverageMetric extends Metric { + double sum = 0; + double min = 0; + double max = 0; + int count = 0; + + + public AverageMetric(String name, MetricSet owner) { + super(name); + owner.addMetric(this); + } + + public void addValue(double value) { + sum += value; + count++; + + if (min == 0 || value < min) { + min = value; + } + if (max == 0 || value > max) { + max = value; + } + + } + + static private final Utf8String attrValue = new Utf8String("value"); + static private final Utf8String attrCount = new Utf8String("count"); + static private final Utf8String attrMin = new Utf8String("min"); + static private final Utf8String attrMax = new Utf8String("max"); + + @Override + public void toXML(XMLWriter writer) { + renderXmlName(writer); + + if (count > 0) { + writer.attribute(attrValue, (sum / count)); + writer.attribute(attrCount, count); + writer.attribute(attrMin, min); + writer.attribute(attrMax, max); + } + writer.closeTag(); + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/metrics/CountMetric.java b/messagebus/src/main/java/com/yahoo/messagebus/metrics/CountMetric.java new file mode 100644 index 00000000000..2c2f21ddc20 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/metrics/CountMetric.java @@ -0,0 +1,18 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.metrics; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author thomasg + */ +public class CountMetric extends NumberMetric<AtomicLong> { + public CountMetric(String name, MetricSet owner) { + super(name, new AtomicLong(0), owner); + } + + public void inc(long increment) { + get().addAndGet(increment); + } + +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/metrics/MessageBusMetricSet.java b/messagebus/src/main/java/com/yahoo/messagebus/metrics/MessageBusMetricSet.java new file mode 100644 index 00000000000..28ef51d8a29 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/metrics/MessageBusMetricSet.java @@ -0,0 +1,42 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.metrics; + +import com.yahoo.concurrent.CopyOnWriteHashMap; +import com.yahoo.messagebus.Reply; +import com.yahoo.messagebus.routing.Route; + +/** + * @author thomasg + */ +public class MessageBusMetricSet extends MetricSet { + public MetricSet protocols = new MetricSet("protocols"); + + private final CopyOnWriteHashMap<String, RouteMetricSet> routeMetrics = new CopyOnWriteHashMap<String, RouteMetricSet>(); + + public MessageBusMetricSet() { + super("messagebus"); + addMetric(protocols); + } + + public RouteMetricSet getRouteMetrics(Route r) { + String route = r.toString(); + RouteMetricSet metric = routeMetrics.get(route); + if (metric == null) { + synchronized (routeMetrics) { + metric = routeMetrics.get(route); + if (metric == null) { + metric = new RouteMetricSet(route); + addMetric(metric); + routeMetrics.put(route, metric); + } + } + } + + return metric; + } + + public void updateMetrics(Reply reply, Route r) { + + } + +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/metrics/Metric.java b/messagebus/src/main/java/com/yahoo/messagebus/metrics/Metric.java new file mode 100644 index 00000000000..ac7ac1aa6cc --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/metrics/Metric.java @@ -0,0 +1,48 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.metrics; + +import com.yahoo.text.XMLWriter; +import com.yahoo.text.Utf8String; + +import java.io.Writer; + +/** + * @author thomasg + */ +public abstract class Metric { + String name; + String xmlTagName = null; + + public Metric(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public String toHTML() { + return toString(); + } + + public String getXmlTagName() { + return xmlTagName; + } + + public void setXmlTagName(String newName) { + xmlTagName = newName; + } + + static private final Utf8String attrName = new Utf8String("name"); + + public void renderXmlName(XMLWriter writer) { + if (xmlTagName != null) { + writer.openTag(xmlTagName); + writer.attribute(attrName, name); + } else { + writer.openTag(name); + } + } + + public abstract void toXML(XMLWriter writer); +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/metrics/MetricSet.java b/messagebus/src/main/java/com/yahoo/messagebus/metrics/MetricSet.java new file mode 100644 index 00000000000..adca0d1c67f --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/metrics/MetricSet.java @@ -0,0 +1,49 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.metrics; + +import com.yahoo.text.XMLWriter; + +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author thomasg + */ +public class MetricSet extends Metric { + private List<Metric> metrics = new ArrayList<Metric>(); + + public MetricSet(String name) { + super(name); + } + + public void addMetric(Metric m) { + metrics.add(m); + } + + public List<Metric> getMetrics() { + return Collections.unmodifiableList(metrics); + } + + public String toHTML() { + StringBuilder builder = new StringBuilder(); + builder.append("<ul>\n"); + for (Metric m : metrics) { + builder.append("<li>\n").append(m.toHTML()).append("\n</li>"); + } + builder.append("\n</ul>\n"); + return builder.toString(); + } + + public void toXML(XMLWriter xmlWriter) { + renderXmlName(xmlWriter); + + for (Metric m : metrics) { + m.toXML(xmlWriter); + } + + xmlWriter.closeTag(); + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/metrics/NumberMetric.java b/messagebus/src/main/java/com/yahoo/messagebus/metrics/NumberMetric.java new file mode 100644 index 00000000000..f9269a9e53d --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/metrics/NumberMetric.java @@ -0,0 +1,40 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.metrics; + +import com.yahoo.text.XMLWriter; +import com.yahoo.text.Utf8String; + +/** + * @author thomasg + */ +public abstract class NumberMetric<V extends Number> extends Metric { + private V value; + + public NumberMetric(String name, V v, MetricSet owner) { + super(name); + value = v; + owner.addMetric(this); + } + + public V get() { + return value; + } + + public void set(V value) { + this.value = value; + } + + public String toString() { + return value.toString(); + } + + static private final Utf8String attrValue = new Utf8String("value"); + + public void toXML(XMLWriter writer) { + renderXmlName(writer); + writer.attribute(attrValue, value); + writer.closeTag(); + } + + +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/metrics/RouteMetricSet.java b/messagebus/src/main/java/com/yahoo/messagebus/metrics/RouteMetricSet.java new file mode 100644 index 00000000000..ba00fb6f578 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/metrics/RouteMetricSet.java @@ -0,0 +1,45 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.metrics; + +import com.yahoo.messagebus.ErrorCode; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author thomasg + */ +public class RouteMetricSet extends MetricSet { + public MetricSet allErrors = new MetricSet("errors"); + public MetricSet failures = new MetricSet("failures"); + public AverageMetric latency = new AverageMetric("latency", this); + + private Map<Integer, CountMetric> errorMap = new HashMap<Integer, CountMetric>(); + + RouteMetricSet(String route) { + super(route); + setXmlTagName("messages"); + addMetric(allErrors); + addMetric(failures); + } + + public void addError(com.yahoo.messagebus.Error e) { + CountMetric metric = errorMap.get(e.getCode()); + if (metric == null) { + metric = new CountMetric(ErrorCode.getName(e.getCode()), allErrors); + metric.setXmlTagName("error"); + errorMap.put(e.getCode(), metric); + } + metric.inc(1); + } + + public void addFailure(com.yahoo.messagebus.Error e) { + CountMetric metric = errorMap.get(e.getCode()); + if (metric == null) { + metric = new CountMetric(ErrorCode.getName(e.getCode()), failures); + metric.setXmlTagName("failure"); + errorMap.put(e.getCode(), metric); + } + metric.inc(1); + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/metrics/ValueMetric.java b/messagebus/src/main/java/com/yahoo/messagebus/metrics/ValueMetric.java new file mode 100644 index 00000000000..f8a55661961 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/metrics/ValueMetric.java @@ -0,0 +1,14 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.metrics; + +import java.io.Writer; + +/** + * @author thomasg + */ +public class ValueMetric<V extends Number> extends NumberMetric<V> { + + public ValueMetric(String name, V v, MetricSet owner) { + super(name, v, owner); + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/metrics/package-info.java b/messagebus/src/main/java/com/yahoo/messagebus/metrics/package-info.java new file mode 100644 index 00000000000..0562757fd9d --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/metrics/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.messagebus.metrics; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/Identity.java b/messagebus/src/main/java/com/yahoo/messagebus/network/Identity.java new file mode 100644 index 00000000000..52b3d824019 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/Identity.java @@ -0,0 +1,76 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network; + +import com.yahoo.log.LogLevel; +import com.yahoo.net.LinuxInetAddress; + +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.logging.Logger; + +/** + * This class encapsulates the identity of the application that uses this instance of message bus. This identity + * contains a servicePrefix identifier, which is the configuration id of the current servicePrefix, and the canonical + * host name of the host running this. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class Identity { + + private static final Logger log = Logger.getLogger(Identity.class.getName()); + private final String hostname; + private final String servicePrefix; + + /** + * The default constructor requires a configuration identifier that it will use to subscribe to this message bus' + * identity. This identity is necessary so that the network layer is able to identify self for registration with + * Slobrok. + * + * @param configId The config identifier for the application. + */ + public Identity(String configId) { + InetAddress addr; + try { + addr = LinuxInetAddress.getLocalHost(); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + if (addr instanceof Inet6Address) { + log.log(LogLevel.WARNING, "Local host resolved to IPv6 address '" + addr.getHostAddress() + + "', this might be problematic."); + } + hostname = addr.getCanonicalHostName(); + servicePrefix = configId; + } + + /** + * Implements the copy constructor. + * + * @param identity The object to copy. + */ + public Identity(Identity identity) { + hostname = identity.hostname; + servicePrefix = identity.servicePrefix; + } + + /** + * Returns the hostname for this. This is the network name of the host on which this identity exists. It is + * retrieved on creation by InetAddress.getLocalHost().getCanonicalHostName(). + * + * @return The canonical host name. + */ + public String getHostname() { + return hostname; + } + + /** + * Returns the service prefix for this. This is what is prefixed to every session that is created on this identity's + * message bus before registered in the naming service. + * + * @return The service prefix. + */ + public String getServicePrefix() { + return servicePrefix; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/Network.java b/messagebus/src/main/java/com/yahoo/messagebus/network/Network.java new file mode 100644 index 00000000000..cd3b3286778 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/Network.java @@ -0,0 +1,100 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network; + +import com.yahoo.jrt.slobrok.api.IMirror; +import com.yahoo.messagebus.Message; +import com.yahoo.messagebus.routing.RoutingNode; + +import java.util.List; + +/** + * This interface separates the low-level network implementation from the rest of messagebus. The methods defined in + * this interface is intended to be invoked by MessageBus and not by the application. + * + * @author <a href="mailto:havardpe@yahoo-inc.com">Haavard Pettersen</a> + */ +public interface Network { + + /** + * Waits for at most the given number of seconds for all dependencies to become ready. + * + * @param seconds The timeout. + * @return True if ready. + */ + public boolean waitUntilReady(double seconds); + + /** + * Attach the network layer to the given owner + * + * @param owner owner of the network + */ + public void attach(NetworkOwner owner); + + /** + * Register a session name with the network layer. This will make the session visible to other nodes. + * + * @param session the session name + */ + public void registerSession(String session); + + /** + * Unregister a session name with the network layer. This will make the session unavailable for other nodes. + * + * @param session session name + */ + public void unregisterSession(String session); + + /** + * Resolves the service address of the recipient referenced by the given routing node. If a recipient can not be + * resolved, this method tags the node with an error. If this method succeeds, you need to invoke {@link + * #freeServiceAddress(RoutingNode)} once you are done with the service address. + * + * @param recipient The node whose service address to allocate. + * @return True if a service address was allocated. + */ + public boolean allocServiceAddress(RoutingNode recipient); + + /** + * Frees the service address from the given routing node. This allows the network layer to track and close + * connections as required. + * + * @param recipient The node whose service address to free. + */ + public void freeServiceAddress(RoutingNode recipient); + + /** + * Send a message to the given recipients. A {@link RoutingNode} contains all the necessary context for sending. + * + * @param msg The message to send. + * @param recipients A list of routing leaf nodes resolved for the message. + */ + public void send(Message msg, List<RoutingNode> recipients); + + /** + * Synchronize with internal threads. This method will handshake with all internal threads. This has the implicit + * effect of waiting for all active callbacks. Note that this method should never be invoked from a callback since + * that would make the thread wait for itself... forever. This method is typically used to untangle during session + * shutdown. + */ + public void sync(); + + /** + * Shuts down the network. This is a blocking call that waits for all scheduled tasks to complete. + */ + public void shutdown(); + + /** + * Returns a string that represents the connection specs of this network. It is in not a complete address since it + * know nothing of the sessions that run on it. + * + * @return The connection string. + */ + public String getConnectionSpec(); + + /** + * Returns a reference to a name server mirror. + * + * @return The mirror object. + */ + public IMirror getMirror(); +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/NetworkOwner.java b/messagebus/src/main/java/com/yahoo/messagebus/network/NetworkOwner.java new file mode 100644 index 00000000000..42197e086b7 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/NetworkOwner.java @@ -0,0 +1,44 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network; + +import com.yahoo.messagebus.Message; +import com.yahoo.messagebus.Protocol; +import com.yahoo.messagebus.Reply; +import com.yahoo.messagebus.ReplyHandler; +import com.yahoo.text.Utf8Array; +import com.yahoo.text.Utf8String; + +/** + * A network owner is the object that instantiates and uses a network. The API to send messages + * across the network is part of the Network interface, whereas this interface exposes the required + * functionality of a network owner to be able to decode and deliver incoming messages. + * + * @author <a href="mailto:havardpe@yahoo-inc.com">Haavard Pettersen</a> + */ +public interface NetworkOwner { + + /** + * All messages are sent across the network with its accompanying protocol name so that it can be decoded at the + * receiving end. The network queries its owner through this function to resolve the protocol from its name. + * + * @param name The name of the protocol to return. + * @return The named protocol. + */ + public Protocol getProtocol(Utf8Array name); + + /** + * All messages that arrive in the network layer is passed to its owner through this function. + * + * @param message The message that just arrived from the network. + * @param session The name of the session that is the recipient of the request. + */ + public void deliverMessage(Message message, String session); + + /** + * All replies that arrive in the network layer is passed through this to unentangle it from the network thread. + * + * @param reply The reply that just arrived from the network. + * @param handler The handler that is to receive the reply. + */ + public void deliverReply(Reply reply, ReplyHandler handler); +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/ServiceAddress.java b/messagebus/src/main/java/com/yahoo/messagebus/network/ServiceAddress.java new file mode 100644 index 00000000000..94983f3adb5 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/ServiceAddress.java @@ -0,0 +1,14 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network; + +/** + * This interface represents an abstract network service; i.e. somewhere to send messages. An instance of this is + * retrieved by calling {@link Network#allocServiceAddress(com.yahoo.messagebus.routing.RoutingNode)}. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface ServiceAddress { + // empty +} + + diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/local/LocalNetwork.java b/messagebus/src/main/java/com/yahoo/messagebus/network/local/LocalNetwork.java new file mode 100644 index 00000000000..ffcb853a0a7 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/local/LocalNetwork.java @@ -0,0 +1,198 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.local; + +import com.yahoo.component.Vtag; +import com.yahoo.jrt.slobrok.api.IMirror; +import com.yahoo.messagebus.EmptyReply; +import com.yahoo.messagebus.Error; +import com.yahoo.messagebus.Message; +import com.yahoo.messagebus.Reply; +import com.yahoo.messagebus.ReplyHandler; +import com.yahoo.messagebus.Routable; +import com.yahoo.messagebus.TraceNode; +import com.yahoo.messagebus.network.Network; +import com.yahoo.messagebus.network.NetworkOwner; +import com.yahoo.messagebus.network.ServiceAddress; +import com.yahoo.messagebus.routing.RoutingNode; +import com.yahoo.text.Utf8String; + +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import static com.yahoo.messagebus.ErrorCode.NO_ADDRESS_FOR_SERVICE; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class LocalNetwork implements Network { + + private final Executor executor = Executors.newSingleThreadExecutor(); + private final LocalWire wire; + private final String hostId; + private volatile NetworkOwner owner; + + public LocalNetwork(final LocalWire wire) { + this.wire = wire; + this.hostId = wire.newHostId(); + } + + @Override + public boolean waitUntilReady(final double seconds) { + return true; + } + + @Override + public void attach(final NetworkOwner owner) { + this.owner = owner; + } + + @Override + public void registerSession(final String session) { + wire.registerService(hostId + "/" + session, this); + } + + @Override + public void unregisterSession(final String session) { + wire.unregisterService(hostId + "/" + session); + } + + @Override + public boolean allocServiceAddress(final RoutingNode recipient) { + final String service = recipient.getRoute().getHop(0).getServiceName(); + final ServiceAddress address = wire.resolveServiceAddress(service); + if (address == null) { + recipient.setError(new Error(NO_ADDRESS_FOR_SERVICE, "No address for service '" + service + "'.")); + return false; + } + recipient.setServiceAddress(address); + return true; + } + + @Override + public void freeServiceAddress(final RoutingNode recipient) { + recipient.setServiceAddress(null); + } + + @Override + public void send(final Message msg, final List<RoutingNode> recipients) { + for (final RoutingNode recipient : recipients) { + new MessageEnvelope(this, msg, recipient).send(); + } + } + + private void receiveLater(final MessageEnvelope envelope) { + final byte[] payload = envelope.sender.encode(envelope.msg.getProtocol(), envelope.msg); + executor.execute(new Runnable() { + + @Override + public void run() { + final Message msg = decode(envelope.msg.getProtocol(), payload, Message.class); + msg.getTrace().setLevel(envelope.msg.getTrace().getLevel()); + msg.setRoute(envelope.msg.getRoute()).getRoute().removeHop(0); + msg.setRetryEnabled(envelope.msg.getRetryEnabled()); + msg.setRetry(envelope.msg.getRetry()); + msg.setTimeRemaining(envelope.msg.getTimeRemainingNow()); + msg.pushHandler(new ReplyHandler() { + + @Override + public void handleReply(final Reply reply) { + new ReplyEnvelope(LocalNetwork.this, envelope, reply).send(); + } + }); + owner.deliverMessage(msg, LocalServiceAddress.class.cast(envelope.recipient.getServiceAddress()) + .getSessionName()); + } + }); + } + + private void receiveLater(final ReplyEnvelope envelope) { + final byte[] payload = envelope.sender.encode(envelope.reply.getProtocol(), envelope.reply); + executor.execute(new Runnable() { + + @Override + public void run() { + final Reply reply = decode(envelope.reply.getProtocol(), payload, Reply.class); + reply.setRetryDelay(envelope.reply.getRetryDelay()); + reply.getTrace().getRoot().addChild(TraceNode.decode(envelope.reply.getTrace().getRoot().encode())); + for (int i = 0, len = envelope.reply.getNumErrors(); i < len; ++i) { + final Error error = envelope.reply.getError(i); + reply.addError(new Error(error.getCode(), + error.getMessage(), + error.getService() != null ? error.getService() : envelope.sender.hostId)); + } + owner.deliverReply(reply, envelope.parent.recipient); + } + }); + } + + private byte[] encode(final Utf8String protocolName, final Routable toEncode) { + if (toEncode.getType() == 0) { + return new byte[0]; + } + return owner.getProtocol(protocolName).encode(Vtag.currentVersion, toEncode); + } + + @SuppressWarnings("unchecked") + private <T extends Routable> T decode(final Utf8String protocolName, final byte[] toDecode, final Class<T> clazz) { + if (toDecode.length == 0) { + return clazz.cast(new EmptyReply()); + } + return clazz.cast(owner.getProtocol(protocolName).decode(Vtag.currentVersion, toDecode)); + } + + @Override + public void sync() { + + } + + @Override + public void shutdown() { + + } + + @Override + public String getConnectionSpec() { + return hostId; + } + + @Override + public IMirror getMirror() { + return wire; + } + + private static class MessageEnvelope { + + final LocalNetwork sender; + final Message msg; + final RoutingNode recipient; + + MessageEnvelope(final LocalNetwork sender, final Message msg, final RoutingNode recipient) { + this.sender = sender; + this.msg = msg; + this.recipient = recipient; + } + + void send() { + LocalServiceAddress.class.cast(recipient.getServiceAddress()) + .getNetwork().receiveLater(this); + } + } + + private static class ReplyEnvelope { + + final LocalNetwork sender; + final MessageEnvelope parent; + final Reply reply; + + ReplyEnvelope(final LocalNetwork sender, final MessageEnvelope parent, final Reply reply) { + this.sender = sender; + this.parent = parent; + this.reply = reply; + } + + void send() { + parent.sender.receiveLater(this); + } + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/local/LocalServiceAddress.java b/messagebus/src/main/java/com/yahoo/messagebus/network/local/LocalServiceAddress.java new file mode 100644 index 00000000000..9cc96d72e50 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/local/LocalServiceAddress.java @@ -0,0 +1,26 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.local; + +import com.yahoo.messagebus.network.ServiceAddress; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class LocalServiceAddress implements ServiceAddress { + + private final LocalNetwork network; + private final String sessionName; + + public LocalServiceAddress(final String serviceName, final LocalNetwork network) { + this.network = network; + this.sessionName = serviceName.substring(serviceName.lastIndexOf('/') + 1); + } + + public LocalNetwork getNetwork() { + return network; + } + + public String getSessionName() { + return sessionName; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/local/LocalWire.java b/messagebus/src/main/java/com/yahoo/messagebus/network/local/LocalWire.java new file mode 100644 index 00000000000..84ca8c64bc0 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/local/LocalWire.java @@ -0,0 +1,59 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.local; + +import com.yahoo.jrt.slobrok.api.IMirror; +import com.yahoo.jrt.slobrok.api.Mirror; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Pattern; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class LocalWire implements IMirror { + + private final AtomicInteger serviceId = new AtomicInteger(); + private final AtomicInteger updateCnt = new AtomicInteger(); + private final ConcurrentHashMap<String, LocalNetwork> services = new ConcurrentHashMap<>(); + + public void registerService(final String serviceName, final LocalNetwork owner) { + if (services.putIfAbsent(serviceName, owner) != null) { + throw new IllegalStateException(); + } + updateCnt.incrementAndGet(); + } + + public void unregisterService(final String serviceName) { + services.remove(serviceName); + updateCnt.incrementAndGet(); + } + + public LocalServiceAddress resolveServiceAddress(final String serviceName) { + final LocalNetwork owner = services.get(serviceName); + return owner != null ? new LocalServiceAddress(serviceName, owner) : null; + } + + public String newHostId() { + return "tcp/local:" + serviceId.getAndIncrement(); + } + + @Override + public Mirror.Entry[] lookup(final String pattern) { + final List<Mirror.Entry> out = new ArrayList<>(); + final Pattern regex = Pattern.compile(pattern.replace("*", "[a-zA-Z0-9_-]+")); + for (final String key : services.keySet()) { + if (regex.matcher(key).matches()) { + out.add(new Mirror.Entry(key, key)); + } + } + return out.toArray(new Mirror.Entry[out.size()]); + } + + @Override + public int updates() { + return updateCnt.get(); + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/package-info.java b/messagebus/src/main/java/com/yahoo/messagebus/network/package-info.java new file mode 100644 index 00000000000..2fc632f82ba --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/package-info.java @@ -0,0 +1,8 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * This package declares the API of the network layer required by the message bus. + */ +@ExportPackage +package com.yahoo.messagebus.network; + +import com.yahoo.osgi.annotation.ExportPackage;
\ No newline at end of file diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/OOSClient.java b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/OOSClient.java new file mode 100755 index 00000000000..8b16fd44cee --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/OOSClient.java @@ -0,0 +1,171 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc; + +import com.yahoo.jrt.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +/** + * This class keeps track of OOS information obtained from a single server. This class is used by the OOSManager class. + * Note that since this class is only used inside the transport thread it has no synchronization. Using it directly will + * lead to race conditions and possible crashes. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class OOSClient implements Runnable, RequestWaiter { + + private Supervisor orb; + private Target target = null; + private Request request = null; + private boolean requestDone = false; + private Spec spec; + private Task task; + private List<String> oosList = new ArrayList<String>(); + private int requestGen = 0; + private int listGen = 0; + private int dumpGen = 0; + private boolean shutdown = false; + + /** + * Create a new OOSClient polling oos information from the given server. + * + * @param orb The object used for RPC operations. + * @param spec The fnet connect spec for oos server. + */ + public OOSClient(Supervisor orb, Spec spec) { + this.orb = orb; + this.spec = spec; + + task = this.orb.transport().createTask(this); + task.scheduleNow(); + } + + /** + * Handle a server reply. + */ + private void handleReply() { + if (!request.checkReturnTypes("Si")) { + if (target != null) { + target.close(); + target = null; + } + task.schedule(1.0); + return; + } + + Values ret = request.returnValues(); + int retGen = ret.get(1).asInt32(); + if (requestGen != retGen) { + List<String> oos = new ArrayList<String>(); + oos.addAll(Arrays.asList(ret.get(0).asStringArray())); + oosList = oos; + requestGen = retGen; + listGen = retGen; + } + task.schedule(0.1); + } + + /** + * Handle server (re)connect. + */ + private void handleConnect() { + if (target == null) { + target = orb.connect(spec); + requestGen = 0; + } + } + + /** + * Handle server invocation. + */ + private void handleInvoke() { + if (target == null) { + throw new IllegalStateException("Attempting to invoke a request on a null target."); + } + request = new Request("fleet.getOOSList"); + request.parameters().add(new Int32Value(requestGen)); + request.parameters().add(new Int32Value(60000)); + target.invokeAsync(request, 70.0, this); + } + + /** + * Implements runnable. Performs overall server poll logic. + */ + public void run() { + if (shutdown) { + task.kill(); + if (target != null) { + target.close(); + } + } else if (requestDone) { + requestDone = false; + handleReply(); + } else { + handleConnect(); + handleInvoke(); + } + } + + /** + * Shut down this OOS client. Invoking this method will take down any active connections and block further activity + * from this object. + */ + public void shutdown() { + shutdown = true; + task.scheduleNow(); + } + + /** + * From FRT_IRequestWait, picks up server replies. + * + * @param request The request that has completed. + */ + public void handleRequestDone(Request request) { + if (request != this.request || requestDone) { + throw new IllegalStateException("Multiple invocations of RequestDone()."); + } + requestDone = true; + task.scheduleNow(); + } + + /** + * Obtain the connect spec of the OOS server this client is talking to. + * + * @return OOS server connect spec + */ + public Spec getSpec() { + return spec; + } + + /** + * Check if this client has changed. A client has changed if it has obtain now information after the dumpState + * method was last invoked. + * + * @return True is this client has changed. + */ + public boolean isChanged() { + return listGen != dumpGen; + } + + /** + * Returns whether or not this client has receieved any reply at all from the server it is connected to. + * + * @return True if initial request has returned. + */ + public boolean isReady() { + return listGen != 0; + } + + /** + * Dump the current oos information known by this client into the given string set. + * + * @param dst The object used to aggregate oos information. + */ + public void dumpState(Set<String> dst) { + dst.addAll(oosList); + dumpGen = listGen; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/OOSManager.java b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/OOSManager.java new file mode 100755 index 00000000000..c3c973d4e6d --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/OOSManager.java @@ -0,0 +1,169 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc; + +import com.yahoo.jrt.Spec; +import com.yahoo.jrt.Supervisor; +import com.yahoo.jrt.Task; +import com.yahoo.jrt.slobrok.api.Mirror; + +import java.util.*; + +/** + * This class keeps track of OOS information. A set of servers having OOS information are identified by looking up a + * service pattern in the slobrok. These servers are then polled for information. The information is compiled into a + * local repository for fast lookup. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class OOSManager implements Runnable { + + // An internal flag that indicates whether or not this manager is disabled. This is used to short-circuit any + // requests made when the service pattern is null. + private boolean disabled; + + // Whether or not this manager has received status information from all connected clients. + private boolean ready; + + // The JRT supervisor object. + private final Supervisor orb; + + // The JRT slobrok mirror object. + private final Mirror mirror; + + // A transport task object used for scheduling this. + private Task task; + + // The service pattern used to resolve what services registered in slobrok resolve to OOS servers. + private final String servicePattern; + + // A map of OOS clients that each poll a single OOS server. This map will contain an entry for each service that + // the service pattern resolves to. + private Map<String, OOSClient> clients = Collections.emptyMap(); + + // A set of out-of-service service names. + private volatile Set<String> oosSet; + + // The generation of the current slobrok resolve. + private int slobrokGen = 0; + + // A local copy of the services that the service pattern resolved to after the previous slobrok lookup. This is used + // to avoid updating the internal list every time slobrok's generation differs, but instead only when the service + // pattern resolves to something different. + private List<Mirror.Entry> services; + + /** + * Create a new OOSManager. The given service pattern will be looked up in the given slobrok mirror. The resulting + * set of services will be polled for oos information. + * + * @param orb The object used for RPC operations. + * @param mirror The slobrok mirror. + * @param servicePattern The service pattern for oos servers. + */ + public OOSManager(Supervisor orb, Mirror mirror, String servicePattern) { + this.orb = orb; + this.mirror = mirror; + this.servicePattern = servicePattern; + + disabled = (servicePattern == null || servicePattern.isEmpty()); + ready = disabled; + + if (!disabled) { + task = orb.transport().createTask(this); + task.scheduleNow(); + } + } + + /** + * Method invoked when this object is run as a task. This method will update the oos information held by this + * object. + */ + public void run() { + boolean changed = updateFromSlobrok(); + boolean allOk = mirror.ready(); + for (OOSClient client : clients.values()) { + if (client.isChanged()) { + changed = true; + } + if (!client.isReady()) { + allOk = false; + } + } + if (changed) { + Set<String> oos = new LinkedHashSet<String>(); + for (OOSClient client : clients.values()) { + client.dumpState(oos); + } + oosSet = oos; + } + if (allOk && !ready) { + ready = true; + } + task.schedule(ready ? 1.0 : 0.1); + } + + /** + * This method will check the local slobrok mirror to make sure that its clients are connected to the appropriate + * services. If anything changes this method returns true. + * + * @return True if anything changed. + */ + private boolean updateFromSlobrok() { + if (slobrokGen == mirror.updates()) { + return false; + } + slobrokGen = mirror.updates(); + List<Mirror.Entry> newServices = Arrays.asList(mirror.lookup(servicePattern)); + Collections.sort(newServices, new Comparator<Mirror.Entry>() { + public int compare(Mirror.Entry lhs, Mirror.Entry rhs) { + return lhs.compareTo(rhs); + } + }); + if (newServices.equals(services)) { + return false; + } + Map<String, OOSClient> newClients = new HashMap<String, OOSClient>(); + for (Mirror.Entry service : newServices) { + OOSClient client = clients.remove(service.getSpec()); + if (client == null) { + client = new OOSClient(orb, new Spec(service.getSpec())); + } + newClients.put(service.getSpec(), client); + } + for (OOSClient client : clients.values()) { + client.shutdown(); + } + services = newServices; + clients = newClients; + return true; + } + + /** + * Returns whether or not some initial state has been returned. + * + * @return True, if initial state has been found. + */ + public boolean isReady() { + return ready; + } + + /** + * Returns whether or not the given service has been marked as out of service. + * + * @param service The service to check. + * @return True if the service is out of service. + */ + @SuppressWarnings({ "RedundantIfStatement" }) + public boolean isOOS(String service) { + if (disabled) { + return false; + } + Set<String> s = oosSet; + if (s == null) { + return false; + } + if (!s.contains(service)) { + return false; + } + return true; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCNetwork.java b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCNetwork.java new file mode 100644 index 00000000000..9ab24d662bd --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCNetwork.java @@ -0,0 +1,530 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc; + +import com.yahoo.component.Version; +import com.yahoo.component.VersionSpecification; +import com.yahoo.component.Vtag; +import com.yahoo.concurrent.ThreadFactoryFactory; +import com.yahoo.jrt.*; +import com.yahoo.jrt.slobrok.api.IMirror; +import com.yahoo.jrt.slobrok.api.Mirror; +import com.yahoo.jrt.slobrok.api.Register; +import com.yahoo.log.LogLevel; +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.Error; +import com.yahoo.messagebus.ErrorCode; +import com.yahoo.messagebus.network.Identity; +import com.yahoo.messagebus.network.Network; +import com.yahoo.messagebus.network.NetworkOwner; +import com.yahoo.messagebus.routing.Hop; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.messagebus.routing.RoutingNode; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; + +/** + * An RPC implementation of the Network interface. + * + * @author <a href="mailto:havardpe@yahoo-inc.com">Haavard Pettersen</a> + */ +public class RPCNetwork implements Network, MethodHandler { + + private static final Logger log = Logger.getLogger(RPCNetwork.class.getName()); + private final AtomicBoolean destroyed = new AtomicBoolean(false); + private final Identity identity; + private final OOSManager oosManager; + private final Supervisor orb; + private final RPCTargetPool targetPool; + private final RPCServicePool servicePool; + private final Acceptor listener; + private final Mirror mirror; + private final Register register; + private final Map<VersionSpecification, RPCSendAdapter> sendAdapters = new HashMap<>(); + private NetworkOwner owner; + private final SlobrokConfigSubscriber slobroksConfig; + private final LinkedHashMap<String, Route> lruRouteMap = new LinkedHashMap<>(10000, 0.5f, true); + private final ExecutorService sendService = + new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors(), + 0L, TimeUnit.SECONDS, + new SynchronousQueue<Runnable>(false), + ThreadFactoryFactory.getDaemonThreadFactory("mbus.net"), new ThreadPoolExecutor.CallerRunsPolicy()); + + /** + * Create an RPCNetwork. The servicePrefix is combined with session names to create service names. If the service + * prefix is 'a/b' and the session name is 'c', the resulting service name that identifies the session on the + * message bus will be 'a/b/c' + * + * @param params A complete set of parameters. + * @param slobrokConfig subscriber for slobroks config + */ + public RPCNetwork(RPCNetworkParams params, SlobrokConfigSubscriber slobrokConfig) { + this.slobroksConfig = slobrokConfig; + identity = params.getIdentity(); + orb = new Supervisor(new Transport()); + orb.setMaxInputBufferSize(params.getMaxInputBufferSize()); + orb.setMaxOutputBufferSize(params.getMaxOutputBufferSize()); + targetPool = new RPCTargetPool(params.getConnectionExpireSecs()); + servicePool = new RPCServicePool(this, 4096); + + Method method = new Method("mbus.getVersion", "", "s", this); + method.methodDesc("Retrieves the message bus version."); + method.returnDesc(0, "version", "The message bus version."); + orb.addMethod(method); + + try { + listener = orb.listen(new Spec(params.getListenPort())); + } catch (ListenFailedException e) { + orb.transport().shutdown().join(); + throw new RuntimeException(e); + } + TargetPoolTask task = new TargetPoolTask(targetPool, orb); + task.jrtTask.scheduleNow(); + register = new Register(orb, slobrokConfig.getSlobroks(), identity.getHostname(), listener.port()); + mirror = new Mirror(orb, slobrokConfig.getSlobroks()); + oosManager = new OOSManager(orb, mirror, params.getOOSServerPattern()); + } + + /** + * Create an RPCNetwork. The servicePrefix is combined with session names to create service names. If the service + * prefix is 'a/b' and the session name is 'c', the resulting service name that identifies the session on the + * message bus will be 'a/b/c' + * + * @param params A complete set of parameters. + */ + public RPCNetwork(RPCNetworkParams params) { + this(params, params.getSlobroksConfig() != null ? new SlobrokConfigSubscriber(params.getSlobroksConfig()) + : new SlobrokConfigSubscriber(params.getSlobrokConfigId())); + } + + /** + * The network uses a cache of RPC targets (see {@link RPCTargetPool}) that allows it to save time by reusing open + * connections. It works by keeping a set of the most recently used targets open. Calling this method forces all + * unused connections to close immediately. + */ + protected void flushTargetPool() { + targetPool.flushTargets(true); + } + + final Route getRoute(String routeString) { + Route route = lruRouteMap.get(routeString); + if (route == null) { + route = Route.parse(routeString); + lruRouteMap.put(routeString, route); + } + return new Route(route); + } + + @Override + public boolean waitUntilReady(double seconds) { + for (int i = 0; i < seconds * 100; ++i) { + if (mirror.ready() && oosManager.isReady()) { + return true; + } + try { + Thread.sleep(10); + } catch (InterruptedException e) { + // empty + } + } + return false; + } + + @Override + public boolean allocServiceAddress(RoutingNode recipient) { + Hop hop = recipient.getRoute().getHop(0); + String service = hop.getServiceName(); + Error error = resolveServiceAddress(recipient, service); + if (error == null) { + return true; // service address resolved + } + recipient.setError(error); + return false; // service address not resolved + } + + @Override + public void freeServiceAddress(RoutingNode recipient) { + RPCTarget target = ((RPCServiceAddress)recipient.getServiceAddress()).getTarget(); + if (target != null) { + target.subRef(); + } + recipient.setServiceAddress(null); + } + + @Override + public void attach(NetworkOwner owner) { + if (this.owner != null) { + throw new IllegalStateException("Network is already attached to another owner."); + } + this.owner = owner; + + RPCSendAdapter adapter = new RPCSendV1(); + addSendAdapter(new VersionSpecification(5), adapter); + addSendAdapter(new VersionSpecification(6), adapter); + } + + @Override + public void registerSession(String session) { + register.registerName(identity.getServicePrefix() + "/" + session); + } + + @Override + public void unregisterSession(String session) { + register.unregisterName(identity.getServicePrefix() + "/" + session); + } + + @Override + public void sync() { + SyncTask sh = new SyncTask(); + orb.transport().perform(sh); + sh.await(); + } + + @Override + public void shutdown() { + destroy(); + } + + @Override + public String getConnectionSpec() { + return "tcp/" + identity.getHostname() + ":" + listener.port(); + } + + @Override + public IMirror getMirror() { + return mirror; + } + + @Override + public void invoke(Request request) { + request.returnValues().add(new StringValue(getVersion().toString())); + } + + @Override + protected void finalize() throws Throwable { + try { + if (destroy()) { + log.log(LogLevel.WARNING, "RPCNetwork destroyed by finalizer, please review application shutdown logic."); + } + } finally { + super.finalize(); + } + } + + @Override + public void send(Message msg, List<RoutingNode> recipients) { + SendContext ctx = new SendContext(this, msg, recipients); + double timeout = ctx.msg.getTimeRemainingNow() / 1000.0; + for (RoutingNode recipient : ctx.recipients) { + RPCServiceAddress address = (RPCServiceAddress)recipient.getServiceAddress(); + address.getTarget().resolveVersion(timeout, ctx); + } + } + + /** + * This method is a callback invoked after {@link #send(Message, List)} once the version of all recipients have been + * resolved. If all versions were resolved ahead of time, this method is invoked by the same thread as the former. + * If not, this method is invoked by the network thread during the version callback. + * + * @param ctx All the required send-data. + */ + private void send(SendContext ctx) { + if (destroyed.get()) { + replyError(ctx, ErrorCode.NETWORK_SHUTDOWN, + "Network layer has performed shutdown."); + } else if (ctx.hasError) { + replyError(ctx, ErrorCode.HANDSHAKE_FAILED, + "An error occured while resolving version."); + } else { + sendService.execute(new SendTask(owner.getProtocol(ctx.msg.getProtocol()), ctx)); + } + } + + /** + * Sets the destroyed flag to true. The very first time this method is called, it cleans up all its dependencies. + * Even if you retain a reference to this object, all of its content is allowed to be garbage collected. + * + * @return True if content existed and was destroyed. + */ + public boolean destroy() { + if (!destroyed.getAndSet(true)) { + if (slobroksConfig != null) { + slobroksConfig.shutdown(); + } + register.shutdown(); + mirror.shutdown(); + listener.shutdown().join(); + orb.transport().shutdown().join(); + targetPool.flushTargets(true); + sendService.shutdown(); + return true; + } + return false; + } + + /** + * Returns the version of this network. This gets called when the "mbus.getVersion" method is invoked on this + * network, and is separated into its own function so that unit tests can override it to simulate other versions + * than current. + * + * @return The version to claim to be. + */ + protected Version getVersion() { + return Vtag.currentVersion; + } + + /** + * Resolves and assigns a service address for the given recipient using the given address. This is called by the + * {@link #allocServiceAddress(RoutingNode)} method. The target allocated here is released when the routing node + * calls {@link #freeServiceAddress(RoutingNode)}. + * + * @param recipient The recipient to assign the service address to. + * @param serviceName The name of the service to resolve. + * @return Any error encountered, or null. + */ + public Error resolveServiceAddress(RoutingNode recipient, String serviceName) { + if (oosManager.isOOS(serviceName)) { + return new Error(ErrorCode.SERVICE_OOS, + "The service '" + serviceName + "' has been marked as out of service."); + } + RPCServiceAddress ret = servicePool.resolve(serviceName); + if (ret == null) { + return new Error(ErrorCode.NO_ADDRESS_FOR_SERVICE, + "The address of service '" + serviceName + "' could not be resolved. It is not currently " + + "registered with the Vespa name server. " + + "The service must be having problems, or the routing configuration is wrong."); + } + RPCTarget target = targetPool.getTarget(orb, ret); + if (target == null) { + return new Error(ErrorCode.CONNECTION_ERROR, + "Failed to connect to service '" + serviceName + "'."); + } + ret.setTarget(target); // free by freeServiceAddress() + recipient.setServiceAddress(ret); + return null; // no error + } + + /** + * Registers a send adapter for a given version. This will overwrite whatever is already registered under the same + * version. + * + * @param version The version for which to register an adapter. + * @param adapter The adapter to register. + */ + private void addSendAdapter(VersionSpecification version, RPCSendAdapter adapter) { + adapter.attach(this); + sendAdapters.put(version, adapter); + } + + /** + * Determines and returns the send adapter that is compatible with the given version. If no adapter can be found, + * this method returns null. + * + * @param version The version for which to return an adapter. + * @return The compatible adapter. + */ + private RPCSendAdapter getSendAdapter(Version version) { + for (Map.Entry<VersionSpecification, RPCSendAdapter> entry : sendAdapters.entrySet()) { + if (entry.getKey().matches(version)) { + return entry.getValue(); + } + } + return null; + } + + /** + * Deliver an error reply to the recipients of a {@link SendContext} in a way that avoids entanglement. + * + * @param ctx The send context that contains the recipient data. + * @param errCode The error code to return. + * @param errMsg The error string to return. + */ + private void replyError(SendContext ctx, int errCode, String errMsg) { + for (RoutingNode recipient : ctx.recipients) { + Reply reply = new EmptyReply(); + reply.getTrace().setLevel(ctx.traceLevel); + reply.addError(new Error(errCode, errMsg)); + owner.deliverReply(reply, recipient); + } + } + + /** + * Get the owner of this network + * + * @return network owner + */ + NetworkOwner getOwner() { + return owner; + } + + /** + * Returns the identity of this network. + * + * @return The identity. + */ + public Identity getIdentity() { + return identity; + } + + /** + * Obtain the port number this network listens to + * + * @return listening port number + */ + public int getPort() { + return listener.port(); + } + + /** + * Returns the JRT supervisor. + * + * @return The supervisor. + */ + Supervisor getSupervisor() { + return orb; + } + + /** + * Returns the oos manager object so that it can be manually queried about out-of-service services. + * + * @return The oos manager. + */ + public OOSManager getOOSManager() { + return oosManager; + } + + private class SendTask implements Runnable { + + final Protocol protocol; + final SendContext ctx; + + SendTask(Protocol protocol, SendContext ctx) { + this.protocol = protocol; + this.ctx = ctx; + } + + public void run() { + long timeRemaining = ctx.msg.getTimeRemainingNow(); + if (timeRemaining <= 0) { + replyError(ctx, ErrorCode.TIMEOUT, "Aborting transmission because zero time remains."); + return; + } + byte[] payload; + try { + payload = protocol.encode(ctx.version, ctx.msg); + } catch (Exception e) { + StringWriter out = new StringWriter(); + e.printStackTrace(new PrintWriter(out)); + replyError(ctx, ErrorCode.ENCODE_ERROR, out.toString()); + return; + } + if (payload == null || payload.length == 0) { + replyError(ctx, ErrorCode.ENCODE_ERROR, + "Protocol '" + ctx.msg.getProtocol() + "' failed to encode message."); + return; + } + RPCSendAdapter adapter = getSendAdapter(ctx.version); + if (adapter == null) { + replyError(ctx, ErrorCode.INCOMPATIBLE_VERSION, + "Can not send to version '" + ctx.version + "' recipient."); + return; + } + for (RoutingNode recipient : ctx.recipients) { + adapter.send(recipient, ctx.version, payload, timeRemaining); + } + } + } + + /** + * Implements a helper class for {@link RPCNetwork#sync()}. It provides a blocking method {@link #await()} that will + * wait until the internal state of this object is set to 'done'. By scheduling this task in the network thread and + * then calling this method, we achieve handshaking with the network thread. + */ + private static class SyncTask implements Runnable { + + final CountDownLatch latch = new CountDownLatch(1); + + @Override + public void run() { + latch.countDown(); + } + + public void await() { + try { + latch.await(); + } catch (InterruptedException e) { + // ignore + } + } + } + + /** + * Implements a helper class for {@link RPCNetwork#send(com.yahoo.messagebus.Message, java.util.List)}. It works by + * encapsulating all the data required for sending a message, but postponing the call to {@link + * RPCNetwork#send(com.yahoo.messagebus.network.rpc.RPCNetwork.SendContext)} until the version of all targets have + * been resolved. + */ + private static class SendContext implements RPCTarget.VersionHandler { + + final RPCNetwork net; + final Message msg; + final int traceLevel; + final List<RoutingNode> recipients = new LinkedList<>(); + boolean hasError = false; + int pending; + Version version; + + SendContext(RPCNetwork net, Message msg, List<RoutingNode> recipients) { + this.net = net; + this.msg = msg; + this.traceLevel = this.msg.getTrace().getLevel(); + this.recipients.addAll(recipients); + this.pending = this.recipients.size(); + this.version = this.net.getVersion(); + } + + @Override + public void handleVersion(Version version) { + boolean shouldSend = false; + synchronized (this) { + if (version == null) { + hasError = true; + } else if (version.compareTo(this.version) < 0) { + this.version = version; + } + if (--pending == 0) { + shouldSend = true; + } + } + if (shouldSend) { + net.send(this); + } + } + } + + /** + * Implements a helper class to invoke {@link RPCTargetPool#flushTargets(boolean)} once every second. This is to + * unentangle the target pool from the scheduler. + */ + private static class TargetPoolTask implements Runnable { + + final RPCTargetPool pool; + final Task jrtTask; + + TargetPoolTask(RPCTargetPool pool, Supervisor orb) { + this.pool = pool; + this.jrtTask = orb.transport().createTask(this); + this.jrtTask.schedule(1.0); + } + + @Override + public void run() { + pool.flushTargets(false); + jrtTask.schedule(1.0); + } + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCNetworkParams.java b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCNetworkParams.java new file mode 100755 index 00000000000..5f66414ef45 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCNetworkParams.java @@ -0,0 +1,210 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc; + +import com.yahoo.messagebus.network.Identity; +import com.yahoo.cloud.config.SlobroksConfig; + +/** + * To facilitate several configuration parameters to the {@link RPCNetwork} constructor, all parameters are held by this + * class. This class has reasonable default values for each parameter. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RPCNetworkParams { + + private Identity identity = new Identity(""); + private String slobrokConfigId = "admin/slobrok.0"; + private SlobroksConfig slobroksConfig = null; + private String oosServerPattern = ""; + private int listenPort = 0; + private int maxInputBufferSize = 256 * 1024; + private int maxOutputBufferSize = 256 * 1024; + private double connectionExpireSecs = 30; + + /** + * Constructs a new instance of this class with reasonable default values. + */ + public RPCNetworkParams() { + // empty + } + + /** + * Implements the copy constructor. + * + * @param params The object to copy. + */ + public RPCNetworkParams(RPCNetworkParams params) { + identity = new Identity(params.identity); + slobrokConfigId = params.slobrokConfigId; + slobroksConfig = params.slobroksConfig; + oosServerPattern = params.oosServerPattern; + listenPort = params.listenPort; + connectionExpireSecs = params.connectionExpireSecs; + maxInputBufferSize = params.maxInputBufferSize; + maxOutputBufferSize = params.maxOutputBufferSize; + } + + /** + * Returns the identity to use for the network. + * + * @return The identity. + */ + public Identity getIdentity() { + return identity; + } + + /** + * Sets the identity to use for the network. + * + * @param identity The new identity. + * @return This, to allow chaining. + */ + public RPCNetworkParams setIdentity(Identity identity) { + this.identity = identity; + return this; + } + + /** + * Returns the config id of the slobrok config. + * + * @return The config id. + */ + public String getSlobrokConfigId() { + return slobrokConfigId; + } + + /** + * Sets the config id of the slobrok config. Setting this to null string will revert to the default slobrok config + * identifier. + * + * @param slobrokConfigId The new config id. + * @return This, to allow chaining. + */ + public RPCNetworkParams setSlobrokConfigId(String slobrokConfigId) { + this.slobrokConfigId = slobrokConfigId; + return this; + } + + /** + * Returns the 'slobroks' config, if set, otherwise null. + * @return The 'slobroks' config, if set, otherwise null. + */ + public SlobroksConfig getSlobroksConfig() { + return slobroksConfig; + } + + /** + * Sets the 'slobroks' config object. Setting this to null will revert to self-subscribing using {@link #getSlobrokConfigId}. + * + * @param slobroksConfig the new slobroks config to use, or null. + * @return This, to allow chaining. + */ + public RPCNetworkParams setSlobroksConfig(SlobroksConfig slobroksConfig) { + this.slobroksConfig = slobroksConfig; + return this; + } + + /** + * Returns the config id pattern used to lookup OOS servers. + * + * @return The config id. + */ + public String getOOSServerPattern() { + return oosServerPattern; + } + + /** + * Sets the config id pattern used to lookup OOS servers. + * + * @param oosServerPattern The server pattern. + * @return This, to allow chaining. + */ + public RPCNetworkParams setOOSServerPattern(String oosServerPattern) { + this.oosServerPattern = oosServerPattern; + return this; + } + + /** + * Returns the port to listen to. + * + * @return The port. + */ + public int getListenPort() { + return listenPort; + } + + /** + * Sets the port to listen to. + * + * @param listenPort The new port. + * @return This, to allow chaining. + */ + public RPCNetworkParams setListenPort(int listenPort) { + this.listenPort = listenPort; + return this; + } + + /** + * Returns the number of seconds before an idle network connection expires. + * + * @return The number of seconds. + */ + public double getConnectionExpireSecs() { + return connectionExpireSecs; + } + + /** + * Sets the number of seconds before an idle network connection expires. + * + * @param secs The number of seconds. + * @return This, to allow chaining. + */ + public RPCNetworkParams setConnectionExpireSecs(double secs) { + this.connectionExpireSecs = secs; + return this; + } + + /** + * Returns the maximum input buffer size allowed for the underlying FNET connection. + * + * @return The maximum number of bytes. + */ + public int getMaxInputBufferSize() { + return maxInputBufferSize; + } + + /** + * Sets the maximum input buffer size allowed for the underlying FNET connection. Using the value 0 means that there + * is no limit; the connection will not free any allocated memory until it is cleaned up. This might potentially + * save alot of allocation time. + * + * @param maxInputBufferSize The maximum number of bytes. + * @return This, to allow chaining. + */ + RPCNetworkParams setMaxInputBufferSize(int maxInputBufferSize) { + this.maxInputBufferSize = maxInputBufferSize; + return this; + } + + /** + * Returns the maximum output buffer size allowed for the underlying FNET connection. + * + * @return The maximum number of bytes. + */ + int getMaxOutputBufferSize() { + return maxOutputBufferSize; + } + + /** + * Sets the maximum output buffer size allowed for the underlying FNET connection. Using the value 0 means that + * there is no limit; the connection will not free any allocated memory until it is cleaned up. This might + * potentially save alot of allocation time. + * + * @param maxOutputBufferSize The maximum number of bytes. + * @return This, to allow chaining. + */ + RPCNetworkParams setMaxOutputBufferSize(int maxOutputBufferSize) { + this.maxOutputBufferSize = maxOutputBufferSize; + return this; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCSendAdapter.java b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCSendAdapter.java new file mode 100755 index 00000000000..63fa3639cd9 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCSendAdapter.java @@ -0,0 +1,31 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc; + +import com.yahoo.component.Version; +import com.yahoo.messagebus.routing.RoutingNode; + +/** + * This interface defines the necessary methods to process incoming and send outgoing RPC requests. The {@link + * RPCNetwork} maintains a list of supported RPC signatures, and dispatches requests to the corresponding adapter. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface RPCSendAdapter { + + /** + * Attaches this adapter to the given network. + * + * @param net The network to attach to. + */ + public void attach(RPCNetwork net); + + /** + * Performs the actual sending to the given recipient. + * + * @param recipient The recipient to send to. + * @param version The version for which the payload is serialized. + * @param payload The already serialized payload of the message to send. + * @param timeRemaining The time remaining until the message expires. + */ + public void send(RoutingNode recipient, Version version, byte[] payload, long timeRemaining); +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCSendV1.java b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCSendV1.java new file mode 100755 index 00000000000..b3709869ba6 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCSendV1.java @@ -0,0 +1,321 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc; + +import com.yahoo.component.Version; +import com.yahoo.jrt.*; +import com.yahoo.jrt.StringValue; +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.Error; +import com.yahoo.messagebus.ErrorCode; +import com.yahoo.messagebus.ReplyHandler; +import com.yahoo.messagebus.routing.Hop; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.messagebus.routing.RoutingNode; +import com.yahoo.text.Utf8Array; +import com.yahoo.text.Utf8String; + +/** + * Implements the request adapter for method "mbus.send1". + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RPCSendV1 implements MethodHandler, ReplyHandler, RequestWaiter, RPCSendAdapter { + + private final String METHOD_NAME = "mbus.send1"; + private final String METHOD_PARAMS = "sssbilsxi"; + private final String METHOD_RETURN = "sdISSsxs"; + private RPCNetwork net = null; + private String clientIdent = "client"; + private String serverIdent = "server"; + + @Override + public void attach(RPCNetwork net) { + this.net = net; + String prefix = net.getIdentity().getServicePrefix(); + if (prefix != null && prefix.length() > 0) { + clientIdent = "'" + prefix + "'"; + serverIdent = clientIdent; + } + + Method method = new Method(METHOD_NAME, METHOD_PARAMS, METHOD_RETURN, this); + method.methodDesc("Send a message bus request and get a reply back."); + method.paramDesc(0, "version", "The version of the message.") + .paramDesc(1, "route", "Names of additional hops to visit.") + .paramDesc(2, "session", "The local session that should receive this message.") + .paramDesc(3, "retryEnabled", "Whether or not this message can be resent.") + .paramDesc(4, "retry", "The number of times the sending of this message has been retried.") + .paramDesc(5, "timeRemaining", "The number of milliseconds until timeout.") + .paramDesc(6, "protocol", "The name of the protocol that knows how to decode this message.") + .paramDesc(7, "payload", "The protocol specific message payload.") + .paramDesc(8, "level", "The trace level of the message."); + method.returnDesc(0, "version", "The lowest version the message was serialized as.") + .returnDesc(1, "retryDelay", "The retry request of the reply.") + .returnDesc(2, "errorCodes", "The reply error codes.") + .returnDesc(3, "errorMessages", "The reply error messages.") + .returnDesc(4, "errorServices", "The reply error service names.") + .returnDesc(5, "protocol", "The name of the protocol that knows how to decode this reply.") + .returnDesc(6, "payload", "The protocol specific reply payload.") + .returnDesc(7, "trace", "A string representation of the trace."); + net.getSupervisor().addMethod(method); + } + + @Override + public void send(RoutingNode recipient, Version version, byte[] payload, long timeRemaining) { + SendContext ctx = new SendContext(recipient, timeRemaining); + RPCServiceAddress address = (RPCServiceAddress)recipient.getServiceAddress(); + Message msg = recipient.getMessage(); + Route route = new Route(recipient.getRoute()); + Hop hop = route.removeHop(0); + + Request req = new Request(METHOD_NAME); + req.parameters().add(new StringValue(version.toString())); + req.parameters().add(new StringValue(route.toString())); + req.parameters().add(new StringValue(address.getSessionName())); + req.parameters().add(new Int8Value(msg.getRetryEnabled() ? (byte)1 : (byte)0)); + req.parameters().add(new Int32Value(msg.getRetry())); + req.parameters().add(new Int64Value(timeRemaining)); + req.parameters().add(new StringValue(msg.getProtocol())); + req.parameters().add(new DataValue(payload)); + req.parameters().add(new Int32Value(ctx.trace.getLevel())); + + if (ctx.trace.shouldTrace(TraceLevel.SEND_RECEIVE)) { + ctx.trace.trace(TraceLevel.SEND_RECEIVE, + "Sending message (version " + version + ") from " + clientIdent + " to '" + + address.getServiceName() + "' with " + ctx.timeout + " seconds timeout."); + } + + if (hop.getIgnoreResult()) { + address.getTarget().getJRTTarget().invokeVoid(req); + if (ctx.trace.shouldTrace(TraceLevel.SEND_RECEIVE)) { + ctx.trace.trace(TraceLevel.SEND_RECEIVE, + "Not waiting for a reply from '" + address.getServiceName() + "'."); + } + Reply reply = new EmptyReply(); + reply.getTrace().swap(ctx.trace); + net.getOwner().deliverReply(reply, recipient); + } else { + req.setContext(ctx); + address.getTarget().getJRTTarget().invokeAsync(req, ctx.timeout, this); + } + req.discardParameters(); // allow garbage collection of request parameters + } + + @Override + public void handleRequestDone(Request req) { + SendContext ctx = (SendContext)req.getContext(); + String serviceName = ((RPCServiceAddress)ctx.recipient.getServiceAddress()).getServiceName(); + Reply reply = null; + Error error = null; + if (!req.checkReturnTypes(METHOD_RETURN)) { + // Map all known JRT errors to the appropriate message bus error. + reply = new EmptyReply(); + switch (req.errorCode()) { + case com.yahoo.jrt.ErrorCode.TIMEOUT: + error = new Error(com.yahoo.messagebus.ErrorCode.TIMEOUT, + "A timeout occured while waiting for '" + serviceName + "' (" + + ctx.timeout + " seconds expired); " + req.errorMessage()); + break; + case com.yahoo.jrt.ErrorCode.CONNECTION: + error = new Error(com.yahoo.messagebus.ErrorCode.CONNECTION_ERROR, + "A connection error occured for '" + serviceName + "'; " + req.errorMessage()); + break; + default: + error = new Error(com.yahoo.messagebus.ErrorCode.NETWORK_ERROR, + "A network error occured for '" + serviceName + "'; " + req.errorMessage()); + } + } else { + // Retrieve all reply components from JRT request object. + Version version = new Version(req.returnValues().get(0).asUtf8Array()); + double retryDelay = req.returnValues().get(1).asDouble(); + int[] errorCodes = req.returnValues().get(2).asInt32Array(); + String[] errorMessages = req.returnValues().get(3).asStringArray(); + String[] errorServices = req.returnValues().get(4).asStringArray(); + Utf8Array protocolName = req.returnValues().get(5).asUtf8Array(); + byte[] payload = req.returnValues().get(6).asData(); + String replyTrace = req.returnValues().get(7).asString(); + + // Make sure that the owner understands the protocol. + if (payload.length > 0) { + Protocol protocol = net.getOwner().getProtocol(protocolName); + if (protocol != null) { + Routable routable = protocol.decode(version, payload); + if (routable != null) { + if (routable instanceof Reply) { + reply = (Reply)routable; + } else { + error = new Error(com.yahoo.messagebus.ErrorCode.DECODE_ERROR, + "Payload decoded to a reply when expecting a message."); + } + } else { + error = new Error(com.yahoo.messagebus.ErrorCode.DECODE_ERROR, + "Protocol '" + protocol.getName() + "' failed to decode routable."); + } + } else { + error = new Error(com.yahoo.messagebus.ErrorCode.UNKNOWN_PROTOCOL, + "Protocol '" + protocolName + "' is not known by " + serverIdent + "."); + } + } + if (reply == null) { + reply = new EmptyReply(); + } + reply.setRetryDelay(retryDelay); + for (int i = 0; i < errorCodes.length && i < errorMessages.length; i++) { + reply.addError(new Error(errorCodes[i], + errorMessages[i], + errorServices[i].length() > 0 ? errorServices[i] : serviceName)); + } + if (ctx.trace.getLevel() > 0) { + ctx.trace.getRoot().addChild(TraceNode.decode(replyTrace)); + } + } + if (ctx.trace.shouldTrace(TraceLevel.SEND_RECEIVE)) { + ctx.trace.trace(TraceLevel.SEND_RECEIVE, + "Reply (type " + reply.getType() + ") received at " + clientIdent + "."); + } + reply.getTrace().swap(ctx.trace); + if (error != null) { + reply.addError(error); + } + net.getOwner().deliverReply(reply, ctx.recipient); + } + + @Override + public void invoke(Request request) { + request.detach(); + Version version = new Version(request.parameters().get(0).asUtf8Array()); + String route = request.parameters().get(1).asString(); + String session = request.parameters().get(2).asString(); + boolean retryEnabled = (request.parameters().get(3).asInt8() != 0); + int retry = request.parameters().get(4).asInt32(); + long timeRemaining = request.parameters().get(5).asInt64(); + Utf8Array protocolName = request.parameters().get(6).asUtf8Array(); + byte[] payload = request.parameters().get(7).asData(); + int traceLevel = request.parameters().get(8).asInt32(); + + request.discardParameters(); // allow garbage collection of request parameters + + // Make sure that the owner understands the protocol. + Protocol protocol = net.getOwner().getProtocol(protocolName); + if (protocol == null) { + replyError(request, version, traceLevel, + new com.yahoo.messagebus.Error(ErrorCode.UNKNOWN_PROTOCOL, + "Protocol '" + protocolName + "' is not known by " + serverIdent + ".")); + return; + } + Routable routable = protocol.decode(version, payload); + if (routable == null) { + replyError(request, version, traceLevel, + new Error(ErrorCode.DECODE_ERROR, + "Protocol '" + protocol.getName() + "' failed to decode routable.")); + return; + } + if (routable instanceof Reply) { + replyError(request, version, traceLevel, + new Error(ErrorCode.DECODE_ERROR, + "Payload decoded to a reply when expecting a message.")); + return; + } + Message msg = (Message)routable; + if (route != null && route.length() > 0) { + msg.setRoute(net.getRoute(route)); + } + msg.setContext(new ReplyContext(request, version)); + msg.pushHandler(this); + msg.setRetryEnabled(retryEnabled); + msg.setRetry(retry); + msg.setTimeReceivedNow(); + msg.setTimeRemaining(timeRemaining); + msg.getTrace().setLevel(traceLevel); + if (msg.getTrace().shouldTrace(TraceLevel.SEND_RECEIVE)) { + msg.getTrace().trace(TraceLevel.SEND_RECEIVE, + "Message (type " + msg.getType() + ") received at " + serverIdent + " for session '" + session + "'."); + } + net.getOwner().deliverMessage(msg, session); + } + + @Override + public void handleReply(Reply reply) { + ReplyContext ctx = (ReplyContext)reply.getContext(); + reply.setContext(null); + + // Add trace information. + if (reply.getTrace().shouldTrace(TraceLevel.SEND_RECEIVE)) { + reply.getTrace().trace(TraceLevel.SEND_RECEIVE, + "Sending reply (version " + ctx.version + ") from " + serverIdent + "."); + } + + // Encode and return the reply through the RPC request. + byte[] payload = new byte[0]; + if (reply.getType() != 0) { + Protocol protocol = net.getOwner().getProtocol(reply.getProtocol()); + if (protocol != null) { + payload = protocol.encode(ctx.version, reply); + } + if (payload == null || payload.length == 0) { + reply.addError(new Error(ErrorCode.ENCODE_ERROR, + "An error occured while encoding the reply.")); + } + } + int[] eCodes = new int[reply.getNumErrors()]; + String[] eMessages = new String[reply.getNumErrors()]; + String[] eServices = new String[reply.getNumErrors()]; + for (int i = 0; i < reply.getNumErrors(); ++i) { + Error error = reply.getError(i); + eCodes[i] = error.getCode(); + eMessages[i] = error.getMessage(); + eServices[i] = error.getService() != null ? error.getService() : ""; + } + ctx.request.returnValues().add(new StringValue(ctx.version.toString())); + ctx.request.returnValues().add(new DoubleValue(reply.getRetryDelay())); + ctx.request.returnValues().add(new Int32Array(eCodes)); + ctx.request.returnValues().add(new StringArray(eMessages)); + ctx.request.returnValues().add(new StringArray(eServices)); + ctx.request.returnValues().add(new StringValue(reply.getProtocol())); + ctx.request.returnValues().add(new DataValue(payload)); + ctx.request.returnValues().add(new StringValue( + reply.getTrace().getRoot() != null ? + reply.getTrace().getRoot().encode() : + "")); + ctx.request.returnRequest(); + } + + /** + * Send an error reply for a given request. + * + * @param request The JRT request to reply to. + * @param version The version to serialize for. + * @param traceLevel The trace level to set in the reply. + * @param err The error to reply with. + */ + private void replyError(Request request, Version version, int traceLevel, Error err) { + Reply reply = new EmptyReply(); + reply.setContext(new ReplyContext(request, version)); + reply.getTrace().setLevel(traceLevel); + reply.addError(err); + handleReply(reply); + } + + private static class SendContext { + + final RoutingNode recipient; + final Trace trace; + final double timeout; + + SendContext(RoutingNode recipient, long timeRemaining) { + this.recipient = recipient; + trace = new Trace(recipient.getTrace().getLevel()); + timeout = timeRemaining * 0.001; + } + } + + private static class ReplyContext { + + final Request request; + final Version version; + + public ReplyContext(Request request, Version version) { + this.request = request; + this.version = version; + } + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCService.java b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCService.java new file mode 100644 index 00000000000..09d50452fce --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCService.java @@ -0,0 +1,71 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc; + +import com.yahoo.jrt.slobrok.api.IMirror; +import com.yahoo.jrt.slobrok.api.Mirror; + +import java.util.Random; + +/** + * An RPCService represents a set of remote sessions matching a service pattern. The sessions are monitored using the + * slobrok. If multiple sessions are available, round robin is used to balance load between them. + * + * @author <a href="mailto:havardpe@yahoo-inc.com">Haavard Pettersen</a> + */ +public class RPCService { + + private final IMirror mirror; + private final String pattern; + private int addressIdx = new Random().nextInt(Integer.MAX_VALUE); + private int addressGen = 0; + private Mirror.Entry[] addressList = null; + + /** + * Create a new RPCService backed by the given network and using the given service pattern. + * + * @param mirror The naming server to send queries to. + * @param pattern The pattern to use when querying. + */ + public RPCService(IMirror mirror, String pattern) { + this.mirror = mirror; + this.pattern = pattern; + } + + /** + * Resolve a concrete address from this service. This service may represent multiple remote sessions, so this will + * select one that is online. + * + * @return A concrete service address. + */ + public RPCServiceAddress resolve() { + if (pattern.startsWith("tcp/")) { + int pos = pattern.lastIndexOf('/'); + if (pos > 0 && pos < pattern.length() - 1) { + RPCServiceAddress ret = new RPCServiceAddress(pattern, pattern.substring(0, pos)); + if (!ret.isMalformed()) { + return ret; + } + } + } else { + if (addressGen != mirror.updates()) { + addressGen = mirror.updates(); + addressList = mirror.lookup(pattern); + } + if (addressList != null && addressList.length > 0) { + addressIdx = ++addressIdx % addressList.length; + Mirror.Entry entry = addressList[addressIdx]; + return new RPCServiceAddress(entry.getName(), entry.getSpec()); + } + } + return null; + } + + /** + * Returns the pattern used when querying for the naming server for addresses. This is given at construtor time. + * + * @return The service pattern. + */ + String getPattern() { + return pattern; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCServiceAddress.java b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCServiceAddress.java new file mode 100755 index 00000000000..c95769d6840 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCServiceAddress.java @@ -0,0 +1,116 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc; + +import com.yahoo.messagebus.network.ServiceAddress; +import com.yahoo.jrt.Spec; + +/** + * Implements the {@link ServiceAddress} interface for the RPC network. + * + * @author <a href="mailto:havardpe@yahoo-inc.com">Haavard Pettersen</a> + */ +public class RPCServiceAddress implements ServiceAddress { + + private final String serviceName; + private final String sessionName; + private final Spec connectionSpec; + private RPCTarget target; + + /** + * Constructs a service address from the given specifications. The last component of the service is stored as the + * session name. + * + * @param serviceName The full service name of the address. + * @param connectionSpec The connection specification. + */ + public RPCServiceAddress(String serviceName, String connectionSpec) { + this.serviceName = serviceName; + int pos = serviceName.lastIndexOf('/'); + if (pos > 0 && pos < serviceName.length() - 1) { + sessionName = serviceName.substring(pos + 1); + } else { + sessionName = null; + } + this.connectionSpec = new Spec(connectionSpec); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof RPCServiceAddress)) { + return false; + } + RPCServiceAddress rhs = (RPCServiceAddress)obj; + if (!serviceName.equals(rhs.serviceName)) { + return false; + } + if (!connectionSpec.host().equals(rhs.connectionSpec.host())) { + return false; + } + if (connectionSpec.port() != rhs.connectionSpec.port()) { + return false; + } + if (!sessionName.equals(rhs.sessionName)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return serviceName.hashCode() + connectionSpec.hashCode() + sessionName.hashCode(); + } + + /** + * Returns whether or not this service address is malformed. + * + * @return True if malformed. + */ + public boolean isMalformed() { + return sessionName == null || connectionSpec.malformed(); + } + + /** + * Returns the name of the remove service. + * + * @return The service name. + */ + public String getServiceName() { + return serviceName; + } + + /** + * Returns the name of the remote session. + * + * @return The session name. + */ + public String getSessionName() { + return sessionName; + } + + /** + * Returns the connection spec for the remote service. + * + * @return The connection spec. + */ + public Spec getConnectionSpec() { + return connectionSpec; + } + + /** + * Sets the RPC target to be used when communicating with the remove service. + * + * @param target The target to set. + */ + public void setTarget(RPCTarget target) { + this.target = target; + } + + /** + * Returns the RPC target to be used when communicating with the remove service. + * + * @return The target to use. + */ + public RPCTarget getTarget() { + return target; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCServicePool.java b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCServicePool.java new file mode 100755 index 00000000000..fa3b2b2b260 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCServicePool.java @@ -0,0 +1,84 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Class used to reuse services for the same address when sending messages over the rpc network. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RPCServicePool { + + private final RPCNetwork net; + private final ThreadLocalCache services = new ThreadLocalCache(); + private final int maxSize; + + /** + * Create a new service pool for the given network. + * + * @param net The underlying RPC network. + * @param maxSize The max number of services to cache. + */ + public RPCServicePool(RPCNetwork net, int maxSize) { + this.net = net; + this.maxSize = maxSize; + } + + /** + * Returns the RPCServiceAddress that corresponds to a given pattern. This reuses the RPCService object for matching + * pattern so that load balancing is possible on the network level. + * + * @param pattern The pattern for the service we require. + * @return A service address for the given pattern. + */ + public RPCServiceAddress resolve(String pattern) { + RPCService service = services.get().get(pattern); + if (service == null) { + service = new RPCService(net.getMirror(), pattern); + services.get().put(pattern, service); + } + return service.resolve(); + } + + /** + * Returns the number of services available in the pool. This number will never exceed the limit given at + * construction time. + * + * @return The current size of this pool. + */ + public int getSize() { + return services.get().size(); + } + + /** + * Returns whether or not there is a service available in the pool the corresponds to the given pattern. + * + * @param pattern The pattern to check for. + * @return True if a corresponding service is in the pool. + */ + public boolean hasService(String pattern) { + return services.get().containsKey(pattern); + } + + private class ThreadLocalCache extends ThreadLocal<ServiceLRUCache> { + + @Override + protected ServiceLRUCache initialValue() { + return new ServiceLRUCache(); + } + } + + private class ServiceLRUCache extends LinkedHashMap<String, RPCService> { + + ServiceLRUCache() { + super(16, 0.75f, true); + } + + @Override + protected boolean removeEldestEntry(Map.Entry<String, RPCService> entry) { + return size() > maxSize; + } + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCTarget.java b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCTarget.java new file mode 100755 index 00000000000..b7cd1249dda --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCTarget.java @@ -0,0 +1,173 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc; + +import com.yahoo.component.Version; +import com.yahoo.jrt.*; +import com.yahoo.log.LogLevel; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +/** + * <p>Implements a target object that encapsulates the JRT connection + * target. Instances of this class are returned by {@link RPCService}, and + * cached by {@link RPCTargetPool}.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RPCTarget implements RequestWaiter { + + private static final Logger log = Logger.getLogger(RPCTarget.class.getName()); + private final AtomicInteger ref = new AtomicInteger(1); + private final String name; + private final Target target; + private boolean targetInvoked = false; + private Version version = null; + private List<VersionHandler> versionHandlers = new LinkedList<>(); + + /** + * <p>Constructs a new instance of this class.</p> + * + * @param spec The connection spec of this target. + * @param orb The jrt supervisor to use when connecting to target. + */ + public RPCTarget(Spec spec, Supervisor orb) { + this.name = spec.toString(); + this.target = orb.connect(spec); + } + + /** + * <p>Returns the encapsulated JRT target.</p> + * + * @return The target. + */ + public Target getJRTTarget() { + return target; + } + + /** + * <p>This method is used for explicit reference counting targets to allow + * reusing open connections. An instance of this class is constructed with a + * single reference.</p> + * + * @see #subRef() + */ + public void addRef() { + ref.incrementAndGet(); + } + + /** + * <p>This method is used for explicit reference counting targets to allow + * reusing open connections. When the reference count reaches 0, the + * connection is closed.</p> + * + * @see #addRef() + */ + public void subRef() { + if (ref.decrementAndGet() == 0) { + target.close(); + } + } + + /** + * <p>Returns the current reference count of this target. If this ever + * returns 0 it means the underlying connection is closed and invalid.</p> + * + * @return The number of references in use. + */ + public int getRefCount() { + return ref.get(); + } + + /** + * <p>Requests the version of this target be passed to the given {@link + * VersionHandler}. If the version is available, the handler is called + * synchronously; if not, the handler is called by the network thread once + * the target responds to the version query.</p> + * + * @param timeout The timeout for the request in seconds. + * @param handler The handler to be called once the version is available. + */ + public void resolveVersion(double timeout, VersionHandler handler) { + boolean hasVersion = false; + boolean shouldInvoke = false; + boolean shouldLog = log.isLoggable(LogLevel.DEBUG); + synchronized (this) { + if (version != null) { + if (shouldLog) { + log.log(LogLevel.DEBUG, "Version already available for target '" + name + "' (version " + version + ")."); + } + hasVersion = true; + } else { + if (shouldLog) { + log.log(LogLevel.DEBUG, "Registering version handler '" + handler + "' for target '" + name + "'."); + } + versionHandlers.add(handler); + if (!targetInvoked) { + targetInvoked = true; + shouldInvoke = true; + } + } + } + if (hasVersion) { + handler.handleVersion(version); + } else if (shouldInvoke) { + if (shouldLog) { + log.log(LogLevel.DEBUG, "Invoking mbus.getVersion() on target '" + name + "'"); + } + Request req = new Request("mbus.getVersion"); + target.invokeAsync(req, timeout, this); + } + } + + @Override + public void handleRequestDone(Request req) { + List<VersionHandler> handlers; + boolean shouldLog = log.isLoggable(LogLevel.DEBUG); + synchronized (this) { + targetInvoked = false; + if (req.checkReturnTypes("s")) { + String str = req.returnValues().get(0).asString(); + try { + version = new Version(str); + if (shouldLog) { + log.log(LogLevel.DEBUG, "Target '" + name + "' has version " + version + "."); + } + } catch (IllegalArgumentException e) { + log.log(LogLevel.WARNING, "Failed to parse '" + str + "' as version for target '" + name + "'.", e); + } + } else { + log.log(LogLevel.INFO, "Method mbus.getVersion() failed for target '" + name + "'; " + + req.errorMessage()); + } + handlers = versionHandlers; + versionHandlers = new LinkedList<>(); + } + for (VersionHandler handler : handlers) { + handler.handleVersion(version); + } + } + + /** + * <p>Declares a version handler used when resolving the version of a + * target. An instance of this is passed to {@link + * RPCTarget#resolveVersion(double, + * com.yahoo.messagebus.network.rpc.RPCTarget.VersionHandler)}, and invoked + * either synchronously or asynchronously, depending on whether or not the + * version is already available.</p> + */ + public interface VersionHandler { + + /** + * <p>This method is invoked once the version of the corresponding + * {@link RPCTarget} becomes available. If a problem occured while + * retrieving the version, this method is invoked with a null + * argument.</p> + * + * @param ver The version of corresponding target, or null. + */ + public void handleVersion(Version ver); + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCTargetPool.java b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCTargetPool.java new file mode 100755 index 00000000000..a695060649a --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/RPCTargetPool.java @@ -0,0 +1,131 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc; + +import com.yahoo.jrt.Spec; +import com.yahoo.jrt.Supervisor; +import com.yahoo.concurrent.SystemTimer; +import com.yahoo.concurrent.Timer; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * Class used to reuse targets for the same address when sending messages over the rpc network. + * + * @author <a href="mailto:havardpe@yahoo-inc.com">Haavard Pettersen</a> + */ +public class RPCTargetPool { + + private final Map<String, Entry> targets = new HashMap<String, Entry>(); + private final Timer timer; + private final long expireMillis; + + /** + * Constructs a new instance of this class, and registers the {@link SystemTimer} for detecting and closing + * connections that have expired according to the given parameter. + * + * @param expireSecs The number of seconds until an idle connection is closed. + */ + public RPCTargetPool(double expireSecs) { + this(SystemTimer.INSTANCE, expireSecs); + } + + /** + * Constructs a new instance of this class, using the given {@link Timer} for detecting and closing connections that + * have expired according to the second paramter. + * + * @param timer The timer to use for connection expiration. + * @param expireSecs The number of seconds until an idle connection is closed. + */ + public RPCTargetPool(Timer timer, double expireSecs) { + this.timer = timer; + this.expireMillis = (long)(expireSecs * 1000); + } + + /** + * Closes all unused target connections. Unless the force argument is true, this method will allow a grace period + * for all connections after last use before it starts closing them. This allows the most recently used connections + * to stay open. + * + * @param force Whether or not to force flush. + */ + public synchronized void flushTargets(boolean force) { + Iterator<Entry> it = targets.values().iterator(); + long currentTime = timer.milliTime(); + long expireTime = currentTime - expireMillis; + while (it.hasNext()) { + Entry entry = it.next(); + RPCTarget target = entry.target; + if (target.getJRTTarget().isValid()) { + if (target.getRefCount() > 1) { + entry.lastUse = currentTime; + continue; // someone is using this + } + if (!force) { + if (entry.lastUse > expireTime) { + continue; // not sufficiently idle + } + } + } + target.subRef(); + it.remove(); + } + } + + /** + * This method will return a target for the given address. If a target does not currently exist for the given + * address, it will be created and added to the internal map. Each target is also reference counted so that an + * active target is never expired. + * + * @param orb The supervisor to use to connect to the target. + * @param address The address to resolve to a target. + * @return A target for the given address. + */ + public RPCTarget getTarget(Supervisor orb, RPCServiceAddress address) { + Spec spec = address.getConnectionSpec(); + String key = spec.toString(); + RPCTarget ret; + synchronized (this) { + Entry entry = targets.get(key); + if (entry != null) { + if (entry.target.getJRTTarget().isValid()) { + entry.target.addRef(); + entry.lastUse = timer.milliTime(); + return entry.target; + } + entry.target.subRef(); + targets.remove(key); + } + ret = new RPCTarget(spec, orb); + targets.put(key, new Entry(ret, timer.milliTime())); + } + ret.addRef(); + return ret; + } + + + /** + * Returns the number of targets currently contained in this. + * + * @return The size of the internal map. + */ + public synchronized int size() { + return targets.size(); + } + + /** + * Implements a helper class holds the necessary reference and timestamp of a target. The lastUse member is updated + * when a call to {@link RPCTargetPool#flushTargets(boolean)} iterates over an active target. + */ + private static class Entry { + + final RPCTarget target; + long lastUse = 0; + + Entry(RPCTarget target, long lastUse) { + this.target = target; + this.lastUse = lastUse; + } + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/SlobrokConfigSubscriber.java b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/SlobrokConfigSubscriber.java new file mode 100755 index 00000000000..0c7e8b4bbbd --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/SlobrokConfigSubscriber.java @@ -0,0 +1,58 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc; + +import com.yahoo.config.subscription.ConfigSubscriber; +import com.yahoo.jrt.slobrok.api.SlobrokList; +import com.yahoo.cloud.config.SlobroksConfig; + +/** + * This class implements subscription to slobrok config. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class SlobrokConfigSubscriber implements ConfigSubscriber.SingleSubscriber<SlobroksConfig>{ + + private SlobrokList slobroks = new SlobrokList(); + private ConfigSubscriber subscriber; + + /** + * Constructs a new config subscriber for a given config id. + * + * @param configId The id of the config to subscribe to. + */ + public SlobrokConfigSubscriber(String configId) { + subscriber = new ConfigSubscriber(); + subscriber.subscribe(this, SlobroksConfig.class, configId); + } + + public SlobrokConfigSubscriber(SlobroksConfig slobroksConfig) { + configure(slobroksConfig); + } + + @Override + public void configure(SlobroksConfig config) { + String[] list = new String[config.slobrok().size()]; + for(int i = 0; i < config.slobrok().size(); i++) { + list[i] = config.slobrok(i).connectionspec(); + } + slobroks.setup(list); + } + + /** + * Returns the current slobroks config as an array of connection spec strings. + * + * @return The slobroks config. + */ + public SlobrokList getSlobroks() { + return new SlobrokList(slobroks); + } + + /** + * Shuts down the config subscription by unsubscribing to the slobroks config. + */ + public void shutdown() { + if (subscriber != null) { + subscriber.close(); + } + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/package-info.java b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/package-info.java new file mode 100644 index 00000000000..ac7f349acd7 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/package-info.java @@ -0,0 +1,8 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * This package contains an RPC implementation of the Network interface declared in the com.yahoo.messagebus.network package. + */ +@ExportPackage +package com.yahoo.messagebus.network.rpc; + +import com.yahoo.osgi.annotation.ExportPackage;
\ No newline at end of file diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/test/OOSServer.java b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/test/OOSServer.java new file mode 100755 index 00000000000..ad4d21ce171 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/test/OOSServer.java @@ -0,0 +1,81 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc.test; + +import com.yahoo.jrt.*; +import com.yahoo.jrt.slobrok.api.SlobrokList; +import com.yahoo.jrt.slobrok.api.Register; +import com.yahoo.jrt.slobrok.server.Slobrok; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class OOSServer { + private int getCnt = 1; + private List<String> state = new ArrayList<String>(); + private Supervisor orb; + private Register register; + private Acceptor listener; + + public OOSServer(Slobrok slobrok, String service, OOSState state) { + orb = new Supervisor(new Transport()); + orb.addMethod(new Method("fleet.getOOSList", "ii", "Si", + new MethodHandler() { + public void invoke(Request request) { + rpc_poll(request); + } + }) + .methodDesc("Fetch OOS information.") + .paramDesc(0, "gencnt", "Generation already known by client.") + .paramDesc(1, "timeout", "How many milliseconds to wait for changes before returning if nothing has changed (max=10000).") + .returnDesc(0, "names", "List of services that are OOS (empty if generation has not changed).") + .returnDesc(1, "newgen", "Generation of the returned list.")); + try { + listener = orb.listen(new Spec(0)); + } + catch (ListenFailedException e) { + orb.transport().shutdown().join(); + throw new RuntimeException(e); + } + SlobrokList slist = new SlobrokList(); + slist.setup(new String[] { new Spec("localhost", slobrok.port()).toString() }); + register = new Register(orb, slist, "localhost", listener.port()); + register.registerName(service); + setState(state); + } + + public void shutdown() { + register.shutdown(); + listener.shutdown().join(); + orb.transport().shutdown().join(); + } + + public void setState(OOSState state) { + List<String> newState = new ArrayList<String>(); + for (String service : state.getServices()) { + if (state.isOOS(service)) { + newState.add(service); + } + } + synchronized(this) { + this.state = newState; + if (++getCnt == 0) { + getCnt = 1; + } + } + } + + private void rpc_poll(Request request) { + synchronized(this) { + request.returnValues() + .add(new StringArray(state.toArray(new String[state.size()]))) + .add(new Int32Value(getCnt)); + } + } + + public int getPort() { + return listener.port(); + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/test/OOSState.java b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/test/OOSState.java new file mode 100755 index 00000000000..912caf45691 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/test/OOSState.java @@ -0,0 +1,26 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc.test; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class OOSState { + private Map<String, Boolean> data = new LinkedHashMap<String, Boolean>(); + + public OOSState add(String service, boolean oos) { + data.put(service, oos); + return this; + } + + public Set<String> getServices() { + return data.keySet(); + } + + public boolean isOOS(String service) { + return data.containsKey(service) && data.get(service); + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/test/SlobrokState.java b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/test/SlobrokState.java new file mode 100755 index 00000000000..b0865b7618e --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/test/SlobrokState.java @@ -0,0 +1,26 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc.test; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class SlobrokState { + private Map<String, Integer> data = new LinkedHashMap<String, Integer>(); + + public SlobrokState add(String pattern, int count) { + data.put(pattern, count); + return this; + } + + public Set<String> getPatterns() { + return data.keySet(); + } + + public int getCount(String pattern) { + return data.containsKey(pattern) ? data.get(pattern) : 1; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/test/TestServer.java b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/test/TestServer.java new file mode 100644 index 00000000000..683b986bb0d --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/test/TestServer.java @@ -0,0 +1,211 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc.test; + +import com.yahoo.component.Version; +import com.yahoo.component.Vtag; +import com.yahoo.jrt.Spec; +import com.yahoo.jrt.slobrok.api.Mirror; +import com.yahoo.jrt.slobrok.server.Slobrok; +import com.yahoo.log.LogLevel; +import com.yahoo.messagebus.MessageBus; +import com.yahoo.messagebus.MessageBusParams; +import com.yahoo.messagebus.Protocol; +import com.yahoo.messagebus.network.Identity; +import com.yahoo.messagebus.network.rpc.RPCNetwork; +import com.yahoo.messagebus.network.rpc.RPCNetworkParams; +import com.yahoo.messagebus.routing.RoutingSpec; +import com.yahoo.messagebus.routing.RoutingTableSpec; +import com.yahoo.messagebus.test.SimpleProtocol; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; + +/** + * A simple test server implementation. + * + * @author <a href="mailto:havardpe@yahoo-inc.com">Haavard Pettersen</a> + */ +public class TestServer { + + private static final Logger log = Logger.getLogger(TestServer.class.getName()); + private final AtomicBoolean destroyed = new AtomicBoolean(false); + public final VersionedRPCNetwork net; + public final MessageBus mb; + + /** + * Create a new test server. + * + * @param name The service name prefix for this server. + * @param table The routing table spec to be used, may be null for no routing. + * @param slobrok The slobrok to register with (local). + * @param oosServerPattern the string pattern for oos servers, may be null for deactivate. + * @param protocol The protocol that this server should support in addition to SimpleProtocol. + */ + public TestServer(String name, RoutingTableSpec table, Slobrok slobrok, String oosServerPattern, Protocol protocol) { + this(new MessageBusParams().addProtocol(new SimpleProtocol()), + new RPCNetworkParams() + .setIdentity(new Identity(name)) + .setSlobrokConfigId(getSlobrokConfig(slobrok)) + .setOOSServerPattern(oosServerPattern)); + if (protocol != null) { + mb.putProtocol(protocol); + } + if (table != null) { + setupRouting(table); + } + } + + /** + * Create a new test server. + * + * @param mbusParams The parameters for mesasge bus. + * @param netParams The parameters for the rpc network. + */ + public TestServer(MessageBusParams mbusParams, RPCNetworkParams netParams) { + net = new VersionedRPCNetwork(netParams); + mb = new MessageBus(net, mbusParams); + } + + // Overrides Object. + @Override + protected void finalize() throws Throwable { + try { + if (destroy()) { + log.log(LogLevel.WARNING, "TestServer destroyed by finalizer, please review application shutdown logic."); + } + } finally { + super.finalize(); + } + } + + /** + * Sets the destroyed flag to true. The very first time this method is called, it cleans up all its dependencies. + * Even if you retain a reference to this object, all of its content is allowed to be garbage collected. + * + * @return True if content existed and was destroyed. + */ + public boolean destroy() { + if (!destroyed.getAndSet(true)) { + mb.destroy(); + net.destroy(); + return true; + } + return false; + } + + /** + * Returns the raw config needed to connect to the given slobrok. + * + * @param slobrok The slobrok whose connection spec to include. + * @return The raw config string. + */ + public static String getSlobrokConfig(Slobrok slobrok) { + return "raw:slobrok[1]\n" + + "slobrok[0].connectionspec \"" + new Spec("localhost", slobrok.port()).toString() + "\"\n"; + } + + /** + * Proxies the {@link MessageBus#setupRouting(RoutingSpec)} method by encapsulating the given table specification + * within the required {@link RoutingSpec}. + * + * @param table The table to configure. + */ + public void setupRouting(RoutingTableSpec table) { + mb.setupRouting(new RoutingSpec().addTable(table)); + } + + /** + * Wait for some pattern to resolve to some number of services. + * + * @param pattern Pattern to lookup in slobrok. + * @param cnt Number of services it must resolve to. + * @return Whether or not the required state was reached. + */ + public boolean waitSlobrok(String pattern, int cnt) { + return waitState(new SlobrokState().add(pattern, cnt)); + } + + /** + * Wait for a required slobrok state. + * + * @param slobrokState The state to wait for. + * @return Whether or not the required state was reached. + */ + public boolean waitState(SlobrokState slobrokState) { + for (int i = 0; i < 6000 && !Thread.currentThread().isInterrupted(); ++i) { + boolean done = true; + for (String pattern : slobrokState.getPatterns()) { + Mirror.Entry[] res = net.getMirror().lookup(pattern); + if (res.length != slobrokState.getCount(pattern)) { + done = false; + } + } + if (done) { + return true; + } + try { + Thread.sleep(10); + } + catch (InterruptedException e) { + // ignore + } + } + return false; + } + + /** + * Wait for some service to go out-of-service. + * + * @param service The service to wait for. + * @return Whether or not the service went out-of-service. + */ + public boolean waitOOS(String service) { + return waitState(new OOSState().add(service, true)); + } + + /** + * Wait for a required OOS state. + * + * @param oosState The state to wait for. + * @return Whether or not the required state was reached. + */ + public boolean waitState(OOSState oosState) { + for (int i = 0; i < 1000 && !Thread.currentThread().isInterrupted(); ++i) { + boolean done = true; + for (String service : oosState.getServices()) { + if (net.getOOSManager().isOOS(service) != oosState.isOOS(service)) { + done = false; + } + } + if (done) { + return true; + } + try { + Thread.sleep(10); + } + catch (InterruptedException e) { + // ignore + } + } + return false; + } + + public static class VersionedRPCNetwork extends RPCNetwork { + + private Version version = Vtag.currentVersion; + + public VersionedRPCNetwork(RPCNetworkParams netParams) { + super(netParams); + } + + @Override + protected Version getVersion() { + return version; + } + + public void setVersion(Version version) { + this.version = version; + flushTargetPool(); + } + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/test/package-info.java b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/test/package-info.java new file mode 100644 index 00000000000..511e1df4a8a --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/network/rpc/test/package-info.java @@ -0,0 +1,6 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * This package contains utility classes for the unit tests in the com.yahoo.messagebus.network.rpc package. + */ +@com.yahoo.api.annotations.PackageMarker +package com.yahoo.messagebus.network.rpc.test; diff --git a/messagebus/src/main/java/com/yahoo/messagebus/package-info.java b/messagebus/src/main/java/com/yahoo/messagebus/package-info.java new file mode 100644 index 00000000000..b6e05b4bd28 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/package-info.java @@ -0,0 +1,11 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * This package contains the main API of the message bus. The typical user will instantiate + * {@link com.yahoo.messagebus.RPCMessageBus}. + */ +@ExportPackage +@PublicApi +package com.yahoo.messagebus; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/ApplicationSpec.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/ApplicationSpec.java new file mode 100755 index 00000000000..47f688e0f51 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/ApplicationSpec.java @@ -0,0 +1,131 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * This class holds the specifications of an application running message bus services. It is used for ensuring that a
+ * {@link RoutingSpec} holds valid routing specifications.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ApplicationSpec {
+
+ private final HashMap<String, HashSet<String>> services = new HashMap<String, HashSet<String>>();
+
+ /**
+ * Constructs a new instance of this class.
+ */
+ public ApplicationSpec() {
+ // empty
+ }
+
+ /**
+ * Implements the copy constructor.
+ *
+ * @param obj The object to copy.
+ */
+ public ApplicationSpec(ApplicationSpec obj) {
+ add(obj);
+ }
+
+ /**
+ * Adds the content of the given application to this.
+ *
+ * @param app The application whose content to copy.
+ * @return This, to allow chaining.
+ */
+ public ApplicationSpec add(ApplicationSpec app) {
+ for (Map.Entry<String, HashSet<String>> entry : app.services.entrySet()) {
+ String protocol = entry.getKey();
+ for (String service : entry.getValue()) {
+ addService(protocol, service);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Adds a service name to the list of known services.
+ *
+ * @param protocol The protocol for which to add the service.
+ * @param name The service to add.
+ * @return This, to allow chaining.
+ */
+ public ApplicationSpec addService(String protocol, String name) {
+ if (!services.containsKey(protocol)) {
+ services.put(protocol, new HashSet<String>());
+ }
+ services.get(protocol).add(name);
+ return this;
+ }
+
+ /**
+ * Determines whether or not the given service pattern matches any of the known services.
+ *
+ * @param protocol The protocol whose services to check.
+ * @param pattern The pattern to match.
+ * @return True if at least one service was found.
+ */
+ public boolean isService(String protocol, String pattern) {
+ if (services.containsKey(protocol)) {
+ Pattern regex = toRegex(pattern);
+ for (String service : services.get(protocol)) {
+ if (regex.matcher(service).find()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Converts the given string pattern to a usable regex pattern object.
+ *
+ * @param pattern The string pattern to convert.
+ * @return The corresponding regex pattern object.
+ */
+ private static Pattern toRegex(String pattern) {
+ StringBuilder ret = new StringBuilder();
+ ret.append("^");
+ for (int i = 0; i < pattern.length(); i++) {
+ ret.append(toRegex(pattern.charAt(i)));
+ }
+ ret.append("$");
+ return Pattern.compile(ret.toString());
+ }
+
+ /**
+ * Converts a single string pattern char to a regex string. This method is invoked by {@link #toRegex(String)} once
+ * for each character in the string pattern.
+ *
+ * @param c The character to convert.
+ * @return The corresponding regex pattern string.
+ */
+ private static String toRegex(char c) {
+ switch (c) {
+ case '*':
+ return ".*";
+ case '?':
+ return ".";
+ case '^':
+ case '$':
+ case '|':
+ case '{':
+ case '}':
+ case '(':
+ case ')':
+ case '[':
+ case ']':
+ case '\\':
+ case '+':
+ case '.':
+ return "\\" + c;
+ default:
+ return "" + c;
+ }
+ }
+}
diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/ErrorDirective.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/ErrorDirective.java new file mode 100755 index 00000000000..6087a795c2b --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/ErrorDirective.java @@ -0,0 +1,63 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +/** + * This class represents an error directive within a {@link Hop}'s selector. This means to stop whatever is being + * resolved, and instead return a reply containing a specified error. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ErrorDirective implements HopDirective { + + private String msg; + + /** + * Constructs a new error directive. + * + * @param msg The error message. + */ + public ErrorDirective(String msg) { + this.msg = msg; + } + + /** + * Returns the error string that is to be assigned to the reply. + * + * @return The error string. + */ + public String getMessage() { + return msg; + } + + @Override + public boolean matches(HopDirective dir) { + return false; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ErrorDirective)) { + return false; + } + ErrorDirective rhs = (ErrorDirective)obj; + if (!msg.equals(rhs.msg)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "(" + msg + ")"; + } + + @Override + public String toDebugString() { + return "ErrorDirective(msg = '" + msg + "')"; + } + + @Override + public int hashCode() { + return msg != null ? msg.hashCode() : 0; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/Hop.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/Hop.java new file mode 100755 index 00000000000..25bcd062e36 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/Hop.java @@ -0,0 +1,291 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +import java.util.ArrayList; +import java.util.List; + +/** + * <p>Hops are the components of routes. + * They are instantiated from a {@link HopBlueprint} or + * using the factory method {@link #parse(String)}. A hop is resolved to a recipient, from + * a set of primitives, either a string primitive that is to be matched verbatim to a + * service address, or a {@link RoutingPolicy} directive.</p> + * + * @author bratseth + */ +public class Hop { + + private final List<HopDirective> selector = new ArrayList<>(); + private boolean ignoreResult = false; + private String cache = null; + + /** + * <p>Constructs an empty hop. You will need to add directives to the + * selector to make this usable.</p> + */ + public Hop() { + // empty + } + + /** + * <p>Implements the copy constructor.</p> + * + * @param hop The hop to copy. + */ + public Hop(Hop hop) { + selector.addAll(hop.selector); + ignoreResult = hop.ignoreResult; + } + + /** + * <p>Constructs a fully populated hop. This is package private and used by + * the {@link HopBlueprint#create()} method.</p> + * + * @param selector The selector to copy. + * @param ignoreResult Whether or not to ignore the result of this hop. + */ + Hop(List<HopDirective> selector, boolean ignoreResult) { + this.selector.addAll(selector); + this.ignoreResult = ignoreResult; + } + + /** + * <p>Parses the given string as a single hop. The {@link #toString()} + * method is compatible with this parser.</p> + * + * @param str The string to parse. + * @return A hop that corresponds to the string. + */ + public static Hop parse(String str) { + Route route = Route.parse(str); + if (route.getNumHops() > 1) { + return new Hop().addDirective(new ErrorDirective("Failed to completely parse '" + str + "'.")); + } + return route.getHop(0); + } + + /** + * <p>Returns whether or not there are any directives contained in this + * hop.</p> + * + * @return True if there is at least one directive. + */ + public boolean hasDirectives() { + return !selector.isEmpty(); + } + + /** + * <p>Returns the number of directives contained in this hop.</p> + * + * @return The number of directives. + */ + public int getNumDirectives() { + return selector.size(); + } + + /** + * <p>Returns the directive at the given index.</p> + * + * @param i The index of the directive to return. + * @return The item. + */ + public HopDirective getDirective(int i) { + return selector.get(i); + } + + /** + * <p>Adds a new directive to this hop.</p> + * + * @param directive The directive to add. + * @return This, to allow chaining. + */ + public Hop addDirective(HopDirective directive) { + cache = null; + selector.add(directive); + return this; + } + + /** + * <p>Sets the directive at a given index.</p> + * + * @param i The index at which to set the directive. + * @param directive The directive to set. + * @return This, to allow chaining. + */ + public Hop setDirective(int i, HopDirective directive) { + selector.set(i, directive); + return this; + } + + /** + * <p>Removes the directive at the given index.</p> + * + * @param i The index of the directive to remove. + * @return The removed directive. + */ + public HopDirective removeDirective(int i) { + cache = null; + return selector.remove(i); + } + + /** + * <p>Clears all directives from this hop.</p> + * + * @return This, to allow chaining. + */ + public Hop clearDirectives() { + cache = null; + selector.clear(); + return this; + } + + /** + * <p>Returns whether or not to ignore the result when routing through this + * hop.</p> + * + * @return True to ignore the result. + */ + public boolean getIgnoreResult() { + return ignoreResult; + } + + /** + * <p>Sets whether or not to ignore the result when routing through this + * hop.</p> + * + * @param ignoreResult Whether or not to ignore the result. + * @return This, to allow chaining. + */ + public Hop setIgnoreResult(boolean ignoreResult) { + this.ignoreResult = ignoreResult; + return this; + } + + /** + * <p>Returns true whether this hop matches another. This respects policy + * directives matching any other.</p> + * + * @param hop The hop to compare to. + * @return True if this matches the argument, false otherwise. + */ + public boolean matches(Hop hop) { + if (hop == null || hop.getNumDirectives() != selector.size()) { + return false; + } + for (int i = 0; i < hop.getNumDirectives(); ++i) { + if (!selector.get(i).matches(hop.getDirective(i))) { + return false; + } + } + return true; + } + + /** + * <p>Returns a string representation of this that can be debugged but not + * parsed.</p> + * + * @return The debug string. + */ + public String toDebugString() { + StringBuilder ret = new StringBuilder("Hop(selector = { "); + for (int i = 0; i < selector.size(); ++i) { + ret.append(selector.get(i).toDebugString()); + if (i < selector.size() - 1) { + ret.append(", "); + } + } + ret.append(" }, ignoreResult = ").append(ignoreResult).append(")"); + return ret.toString(); + } + + @Override + public String toString() { + return (ignoreResult ? "?" : "") + getServiceName(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Hop)) { + return false; + } + Hop rhs = (Hop)obj; + if (selector.size() != rhs.selector.size()) { + return false; + } + for (int i = 0; i < selector.size(); ++i) { + if (!selector.get(i).equals(rhs.selector.get(i))) { + return false; + } + } + return true; + } + + /** + * <p>Returns the service name referenced by this hop. This is the + * concatenation of all selector primitives, but with no ignore-result + * prefix.</p> + * + * @return The service name. + */ + public String getServiceName() { + if (cache == null) { + cache = toString(0, selector.size()); + } + return cache; + } + + /** + * <p>Returns a string concatenation of a subset of the selector primitives + * contained in this.</p> + * + * @param fromIncluding The index of the first primitive to include. + * @param toNotIncluding The index after the last primitive to include. + * @return The string concatenation. + */ + public String toString(int fromIncluding, int toNotIncluding) { + StringBuilder ret = new StringBuilder(); + for (int i = fromIncluding; i < toNotIncluding; ++i) { + ret.append(selector.get(i)); + if (i < toNotIncluding - 1) { + ret.append("/"); + } + } + return ret.toString(); + } + + /** + * <p>Returns the prefix of this hop's selector to, but not including, the + * given index.</p> + * + * @param toNotIncluding The index to which to generate prefix. + * @return The prefix before the index. + */ + public String getPrefix(int toNotIncluding) { + if (toNotIncluding > 0) { + return toString(0, toNotIncluding) + "/"; + } + return ""; + } + + /** + * <p>Returns the suffix of this hop's selector from, but not including, the + * given index.</p> + * + * @param fromNotIncluding The index from which to generate suffix. + * @return The suffix after the index. + */ + public String getSuffix(int fromNotIncluding) { + if (fromNotIncluding < selector.size() - 1) { + return "/" + toString(fromNotIncluding + 1, selector.size()); + } + return ""; + } + + @Override + public int hashCode() { + int result = selector != null ? selector.hashCode() : 0; + result = 31 * result + (ignoreResult ? 1 : 0); + result = 31 * result + (cache != null ? cache.hashCode() : 0); + return result; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/HopBlueprint.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/HopBlueprint.java new file mode 100755 index 00000000000..66321b5975e --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/HopBlueprint.java @@ -0,0 +1,144 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +import java.util.*; + +/** + * A hop blueprint is a stored prototype of a hop that has been created from a {@link HopSpec} object. A map of these + * are stored in a {@link RoutingTable}. + * + * @author <a href="bratseth@yahoo-inc.com">Jon Bratseth</a> + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class HopBlueprint { + + private final List<HopDirective> selector = new ArrayList<>(); + private final List<Hop> recipients = new ArrayList<>(); + private boolean ignoreResult = false; + + /** + * The default constructor requires valid arguments for all member variables. + * + * @param spec The spec of this rule. + */ + HopBlueprint(HopSpec spec) { + Hop hop = Hop.parse(spec.getSelector()); + for (int i = 0; i < hop.getNumDirectives(); ++i) { + selector.add(hop.getDirective(i)); + } + List<String> lst = new ArrayList<>(); + for (int i = 0; i < spec.getNumRecipients(); ++i) { + lst.add(spec.getRecipient(i)); + } + for (String recipient : lst) { + recipients.add(Hop.parse(recipient)); + } + ignoreResult = spec.getIgnoreResult(); + } + + /** + * Creates a hop instance from thie blueprint. + * + * @return The created hop. + */ + public Hop create() { + return new Hop(selector, ignoreResult); + } + + /** + * Returns whether or not there are any directives contained in this hop. + * + * @return True if there is at least one directive. + */ + public boolean hasDirectives() { + return !selector.isEmpty(); + } + + /** + * Returns the number of directives contained in this hop. + * + * @return The number of directives. + */ + public int getNumDirectives() { + return selector.size(); + } + + /** + * Returns the directive at the given index. + * + * @param i The index of the directive to return. + * @return The item. + */ + public HopDirective getDirective(int i) { + return selector.get(i); + } + + /** + * Returns whether or not there are any recipients that the selector can choose from. + * + * @return True if there is at least one recipient. + */ + public boolean hasRecipients() { + return !recipients.isEmpty(); + } + + /** + * Returns the number of recipients that the selector can choose from. + * + * @return The number of recipients. + */ + public int getNumRecipients() { + return recipients.size(); + } + + /** + * Returns the recipient at the given index. + * + * @param i The index of the recipient to return. + * @return The recipient at the given index. + */ + public Hop getRecipient(int i) { + return recipients.get(i); + } + + /** + * Returns whether or not to ignore the result when routing through this hop. + * + * @return True to ignore the result. + */ + public boolean getIgnoreResult() { + return ignoreResult; + } + + /** + * Sets whether or not to ignore the result when routing through this hop. + * + * @param ignoreResult Whether or not to ignore the result. + * @return This, to allow chaining. + */ + public HopBlueprint setIgnoreResult(boolean ignoreResult) { + this.ignoreResult = ignoreResult; + return this; + } + + @Override + public String toString() { + StringBuilder ret = new StringBuilder("HopBlueprint(selector = { "); + for (int i = 0; i < selector.size(); ++i) { + ret.append("'").append(selector.get(i)).append("'"); + if (i < selector.size() - 1) { + ret.append(", "); + } + } + ret.append(" }, recipients = { "); + for (int i = 0; i < recipients.size(); ++i) { + ret.append("'").append(recipients.get(i)).append("'"); + if (i < recipients.size() - 1) { + ret.append(", "); + } + } + ret.append(" }, ignoreResult = ").append(ignoreResult).append(")"); + return ret.toString(); + } +} + diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/HopDirective.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/HopDirective.java new file mode 100755 index 00000000000..9623b45ca20 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/HopDirective.java @@ -0,0 +1,26 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing;
+
+/**
+ * This class is the base class for the primitives that make up a {@link Hop}'s selector.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface HopDirective {
+
+ /**
+ * Returns true if this directive matches another.
+ *
+ * @param dir The directive to compare this to.
+ * @return True if this matches the argument.
+ */
+ public boolean matches(HopDirective dir);
+
+ /**
+ * Returns a string representation of this that can be debugged but not parsed.
+ *
+ * @return The debug string.
+ */
+ public String toDebugString();
+}
+
diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/HopSpec.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/HopSpec.java new file mode 100644 index 00000000000..cfe4ab1b6ef --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/HopSpec.java @@ -0,0 +1,353 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +import java.util.ArrayList; +import java.util.List; + +/** + * Along with the {@link RoutingSpec}, {@link RoutingTableSpec} and {@link RouteSpec}, this holds the routing + * specifications for all protocols. The only way a client can configure or alter the settings of a message bus instance + * is through these classes. + * <p> + * This class contains the spec for a single hop. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class HopSpec { + + private final String name; + private final String selector; + private final List<String> recipients = new ArrayList<>(); + private final boolean verify; + private boolean ignoreResult = false; + + /** + * Creates a new named hop specification. + * + * @param name A protocol-unique name for this hop. + * @param selector A string that represents the selector for this hop. + */ + public HopSpec(String name, String selector) { + this(name, selector, true); + } + + /** + * Creates a new named hop specification. + * + * @param name A protocol-unique name for this hop. + * @param selector A string that represents the selector for this hop. + * @param verify Whether or not this should be verified. + */ + public HopSpec(String name, String selector, boolean verify) { + this.name = name; + this.selector = selector; + this.verify = verify; + } + + /** + * Implements the copy constructor. + * + * @param obj The object to copy. + */ + public HopSpec(HopSpec obj) { + this.name = obj.name; + this.selector = obj.selector; + this.verify = obj.verify; + for (String recipient : obj.recipients) { + recipients.add(recipient); + } + this.ignoreResult = obj.ignoreResult; + } + + /** + * Returns the protocol-unique name of this hop. + * + * @return The name. + */ + public String getName() { + return name; + } + + /** + * Returns the string selector that resolves the recipients of this hop. + * + * @return The selector. + */ + public String getSelector() { + return selector; + } + + /** + * Returns whether or not there are any recipients that the selector can choose from. + * + * @return True if there is at least one recipient. + */ + public boolean hasRecipients() { + return !recipients.isEmpty(); + } + + /** + * Returns the number of recipients that the selector can choose from. + * + * @return The number of recipients. + */ + public int getNumRecipients() { + return recipients.size(); + } + + /** + * Returns the recipients at the given index. + * + * @param i The index of the recipient to return. + * @return The recipient at the given index. + */ + public String getRecipient(int i) { + return recipients.get(i); + } + + /** + * Adds the given recipient to this. + * + * @param recipient The recipient to add. + * @return This, to allow chaining. + */ + public HopSpec addRecipient(String recipient) { + recipients.add(recipient); + return this; + } + + /** + * Adds the given recipients to this. + * + * @param recipients The recipients to add. + * @return This, to allow chaining. + */ + public HopSpec addRecipients(List<String> recipients) { + this.recipients.addAll(recipients); + return this; + } + + /** + * Sets the recipient at the given index. + * + * @param i The index at which to set the recipient. + * @param recipient The recipient to set. + * @return This, to allow chaining. + */ + public HopSpec setRecipient(int i, String recipient) { + recipients.set(i, recipient); + return this; + } + + /** + * Removes the recipient at the given index. + * + * @param i The index of the recipient to remove. + * @return The removed recipient. + */ + public String removeRecipient(int i) { + return recipients.remove(i); + } + + /** + * Clears the list of recipients that the selector may choose from. + * + * @return This, to allow chaining. + */ + public HopSpec clearRecipients() { + recipients.clear(); + return this; + } + + /** + * Returns whether or not to ignore the result when routing through this hop. + * + * @return True to ignore the result. + */ + public boolean getIgnoreResult() { + return ignoreResult; + } + + /** + * Sets whether or not to ignore the result when routing through this hop. + * + * @param ignoreResult Whether or not to ignore the result. + * @return This, to allow chaining. + */ + public HopSpec setIgnoreResult(boolean ignoreResult) { + this.ignoreResult = ignoreResult; + return this; + } + + /** + * Verifies the content of this against the given application. + * + * @param app The application to verify against. + * @param table The routing table to verify against. + * @param errors The list of errors found. + * @return True if no errors where found. + */ + public boolean verify(ApplicationSpec app, RoutingTableSpec table, List<String> errors) { + if (verify) { + verify(app, table, null, recipients, selector, errors, + "hop '" + name + "' in routing table '" + table.getProtocol() + "'"); + } + return errors.isEmpty(); + } + + /** + * Verifies that the hop given by the given selector and children is valid. + * + * @param app The application to verify against. + * @param table The routing table to verify against. + * @param parent The parent hop that the selector must match. + * @param selector The selector to verify. + * @param children The children to verify, may be null or empty. + * @param errors The list of errors found. + * @param context The context to use if adding an error. + * @return True if no errors where found. + */ + static boolean verify(ApplicationSpec app, RoutingTableSpec table, Hop parent, List<String> children, + String selector, List<String> errors, String context) { + // Verify that the selector can be parsed. + Hop hop = Hop.parse(selector); + for (int i = 0, len = hop.getNumDirectives(); i < len; ++i) { + HopDirective dir = hop.getDirective(i); + if (dir instanceof ErrorDirective) { + errors.add("For " + context + "; " + ((ErrorDirective)dir).getMessage()); + return false; + } + } + + // Verify that the parent matches this, if any. + if (parent != null) { + if (parent.getNumDirectives() == 1) { + // hops that contain a single policy directive will typically be magic + } else if (!parent.matches(hop)) { + errors.add("Selector '" + parent.getServiceName() + "' does not match " + context + "."); + return false; + } + } + + // Traverse and verify the directives of the hop. + boolean verifyServiceName = true; + boolean allowRecipients = false; + for (int i = 0, len = hop.getNumDirectives(); i < len; ++i) { + HopDirective dir = hop.getDirective(i); + if (dir instanceof ErrorDirective) { + // caught above + } else if (dir instanceof PolicyDirective) { + allowRecipients = true; + verifyServiceName = false; + } else if (dir instanceof RouteDirective) { + String routeName = ((RouteDirective)dir).getName(); + if (!table.hasRoute(routeName)) { + errors.add(capitalize(context) + " references route '" + routeName + "' which does not exist."); + return false; + } + verifyServiceName = false; + } else if (dir instanceof TcpDirective) { + verifyServiceName = false; + } else if (dir instanceof VerbatimDirective) { + // will be verified below + } + } + + // Verify that the service referenced by the hop exists, if required. + if (verifyServiceName) { + String serviceName = hop.getServiceName(); + if (table.hasRoute(serviceName)) { + // all good + } else if (table.hasHop(serviceName)) { + // also good + } else if (!app.isService(table.getProtocol(), serviceName)) { + errors.add(capitalize(context) + " references '" + serviceName + "' which is neither a service," + + " a route nor another hop."); + return false; + } + } + + // Verify that recipients are allowed and that they are valid themselves. + if (children != null && children.size() > 0) { + if (!allowRecipients) { + errors.add(capitalize(context) + " has recipients but no policy directive."); + return false; + } + for (String child : children) { + verify(app, table, hop, null, child, errors, "recipient '" + child + "' in " + context); + } + } + + // All ok. + return true; + } + + /** + * Appends the content of this to the given config string builder. + * + * @param cfg The config to add to. + * @param prefix The prefix to use for each add. + */ + public void toConfig(StringBuilder cfg, String prefix) { + cfg.append(prefix).append("name ").append(RoutingSpec.toConfigString(name)).append("\n"); + cfg.append(prefix).append("selector ").append(RoutingSpec.toConfigString(selector)).append("\n"); + if (ignoreResult) { + cfg.append(prefix).append("ignoreresult true\n"); + } + int numRecipients = recipients.size(); + if (numRecipients > 0) { + cfg.append(prefix).append("recipient[").append(numRecipients).append("]\n"); + for (int i = 0; i < numRecipients; ++i) { + cfg.append(prefix).append("recipient[").append(i).append("] "); + cfg.append(RoutingSpec.toConfigString(recipients.get(i))).append("\n"); + } + } + } + + /** + * Capitalizes the given string by upper-casing its first letter. + * + * @param str The string to capitalize. + * @return The capitalized string. + */ + private static String capitalize(String str) { + return Character.toUpperCase(str.charAt(0)) + str.substring(1); + } + + // Overrides Object. + @Override + public String toString() { + StringBuilder ret = new StringBuilder(); + toConfig(ret, ""); + return ret.toString(); + } + + // Overrides Object. + @Override + public boolean equals(Object obj) { + if (!(obj instanceof HopSpec)) { + return false; + } + HopSpec rhs = (HopSpec)obj; + if (!name.equals(rhs.name)) { + return false; + } + if (!selector.equals(rhs.selector)) { + return false; + } + if (!recipients.equals(rhs.recipients)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (selector != null ? selector.hashCode() : 0); + result = 31 * result + (recipients != null ? recipients.hashCode() : 0); + result = 31 * result + (verify ? 1 : 0); + result = 31 * result + (ignoreResult ? 1 : 0); + return result; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/PolicyDirective.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/PolicyDirective.java new file mode 100755 index 00000000000..e4b88dca6e7 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/PolicyDirective.java @@ -0,0 +1,83 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing;
+
+/**
+ * This class represents a policy directive within a {@link Hop}'s selector. This means to create the named protocol
+ * using the given parameter string, and the running that protocol within the context of this directive.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class PolicyDirective implements HopDirective {
+
+ private final String name;
+ private final String param;
+
+ /**
+ * Constructs a new policy selector item.
+ *
+ * @param name The name of the policy to invoke.
+ * @param param The parameter to pass to the name constructor.
+ */
+ public PolicyDirective(String name, String param) {
+ this.name = name;
+ this.param = param;
+ }
+
+ @Override
+ public boolean matches(HopDirective dir) {
+ return true;
+ }
+
+ /**
+ * Returns the name of the policy that this item is to invoke.
+ *
+ * @return The name name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the parameter string for this policy directive.
+ *
+ * @return The parameter.
+ */
+ public String getParam() {
+ return param;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof PolicyDirective)) {
+ return false;
+ }
+ PolicyDirective rhs = (PolicyDirective)obj;
+ if (!name.equals(rhs.name)) {
+ return false;
+ }
+ if (param == null && rhs.param != null) {
+ return false;
+ }
+ if (param != null && !param.equals(rhs.param)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "[" + name + (param != null && param.length() > 0 ? ":" + param : "") + "]";
+ }
+
+ @Override
+ public String toDebugString() {
+ return "PolicyDirective(name = '" + name + "', param = '" + param + "')";
+ }
+
+ @Override
+ public int hashCode() {
+ int result = name != null ? name.hashCode() : 0;
+ result = 31 * result + (param != null ? param.hashCode() : 0);
+ return result;
+ }
+}
\ No newline at end of file diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/Resender.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/Resender.java new file mode 100755 index 00000000000..7cb42da3219 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/Resender.java @@ -0,0 +1,143 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +import com.yahoo.concurrent.SystemTimer; +import com.yahoo.messagebus.*; + +import java.util.PriorityQueue; +import java.util.LinkedList; +import java.util.List; + +/** + * The resender handles scheduling and execution of sending instances of {@link RoutingNode}. An instance of this class + * is owned by {@link com.yahoo.messagebus.MessageBus}. Because this class does not have any internal thread, it depends + * on message bus to keep polling it whenever it has time. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class Resender { + + private final PriorityQueue<Entry> queue = new PriorityQueue<Entry>(); + private final RetryPolicy retryPolicy; + + /** + * Constructs a new resender. + * + * @param retryPolicy The retry policy to use. + */ + public Resender(RetryPolicy retryPolicy) { + this.retryPolicy = retryPolicy; + } + + /** + * Returns whether or not the current {@link RetryPolicy} supports resending a {@link Reply} that contains an error + * with the given error code. + * + * @param errorCode The code to check. + * @return True if the message can be resent. + */ + public boolean canRetry(int errorCode) { + return retryPolicy.canRetry(errorCode); + } + + /** + * Returns whether or not the given reply should be retried. + * + * @param reply The reply to check. + * @return True if retry is required. + */ + public boolean shouldRetry(Reply reply) { + int numErrors = reply.getNumErrors(); + if (numErrors == 0) { + return false; + } + for (int i = 0; i < numErrors; ++i) { + if (!retryPolicy.canRetry(reply.getError(i).getCode())) { + return false; + } + } + return true; + } + + /** + * Schedules the given node for resending, if enabled. This will invoke {@link com.yahoo.messagebus.routing.RoutingNode#prepareForRetry()} + * if the node was queued. This method is NOT thread-safe, and should only be called by the messenger thread. + * + * @param node The node to resend. + * @return True if the node was queued. + */ + public boolean scheduleRetry(RoutingNode node) { + Message msg = node.getMessage(); + if (!msg.getRetryEnabled()) { + return false; + } + int retry = msg.getRetry() + 1; + double delay = node.getReply().getRetryDelay(); + if (delay < 0) { + delay = retryPolicy.getRetryDelay(retry); + } + if (msg.getTimeRemainingNow() * 0.001 - delay <= 0) { + node.addError(ErrorCode.TIMEOUT, "Timeout exceeded by resender, giving up."); + return false; + } + node.prepareForRetry(); // consumes the reply + node.getTrace().trace(TraceLevel.COMPONENT, + "Message scheduled for retry " + retry + " in " + delay + " seconds."); + msg.setRetry(retry); + queue.add(new Entry(node, SystemTimer.INSTANCE.milliTime() + (long)(delay * 1000))); + return true; + } + + /** + * Invokes {@link RoutingNode#send()} on all routing nodes that are applicable for sending at the current time. + */ + public void resendScheduled() { + if (queue.isEmpty()) return; + + List<RoutingNode> sendList = new LinkedList<RoutingNode>(); + long now = SystemTimer.INSTANCE.milliTime(); + while (!queue.isEmpty() && queue.peek().time <= now) { + sendList.add(queue.poll().node); + } + + for (RoutingNode node : sendList) { + node.getTrace().trace(TraceLevel.COMPONENT, "Resender resending message."); + node.send(); + } + } + + /** + * Discards all the routing nodes currently scheduled for resending. + */ + public void destroy() { + while (!queue.isEmpty()) { + queue.poll().node.discard(); + } + } + + /** + * This class encapsulates a routing node and some arbitrary time. This is required for the resending logic so that + * it can properly schedule resending. + */ + private static class Entry implements Comparable<Entry> { + + final RoutingNode node; + final Long time; + + /** + * The default constructor requires initial values for both members. + * + * @param node The routing node being scheduled. + * @param time The time of this schedule. + */ + public Entry(RoutingNode node, long time) { + this.node = node; + this.time = time; + } + + @Override + public int compareTo(Entry rhs) { + return time.compareTo(rhs.time); + } + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/RetryPolicy.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/RetryPolicy.java new file mode 100644 index 00000000000..19ea949e741 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/RetryPolicy.java @@ -0,0 +1,30 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing;
+
+/**
+ * When a {@link com.yahoo.messagebus.Reply} containing errors is returned to a {@link com.yahoo.messagebus.MessageBus},
+ * an object implementing this interface is consulted on whether or not to resend the corresponding {@link
+ * com.yahoo.messagebus.Message}. The policy is passed to the message bus at creation time using the {@link
+ * com.yahoo.messagebus.MessageBusParams#setRetryPolicy(RetryPolicy)} method.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface RetryPolicy {
+
+ /**
+ * Returns whether or not a {@link com.yahoo.messagebus.Reply} containing an {@link com.yahoo.messagebus.Error} with
+ * the given error code can be retried. This method is invoked once for each error in a reply.
+ *
+ * @param errorCode The code to check.
+ * @return True if the message can be resent.
+ */
+ public boolean canRetry(int errorCode);
+
+ /**
+ * Returns the number of seconds to delay resending a message.
+ *
+ * @param retry The retry attempt.
+ * @return The delay in seconds.
+ */
+ public double getRetryDelay(int retry);
+}
diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/RetryTransientErrorsPolicy.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/RetryTransientErrorsPolicy.java new file mode 100644 index 00000000000..128ca3e819d --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/RetryTransientErrorsPolicy.java @@ -0,0 +1,48 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing;
+
+import com.yahoo.messagebus.ErrorCode;
+
+/**
+ * Implements a retry policy that allows resending of any error that is not fatal. It also does progressive back-off,
+ * delaying each attempt by the given time multiplied by the retry attempt.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class RetryTransientErrorsPolicy implements RetryPolicy {
+
+ private volatile boolean enabled = true;
+ private volatile double baseDelay = 1;
+
+ /**
+ * Sets whether or not this policy should allow retries or not.
+ *
+ * @param enabled True to allow retries.
+ * @return This, to allow chaining.
+ */
+ public RetryTransientErrorsPolicy setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ return this;
+ }
+
+ /**
+ * Sets the base delay in seconds to wait between retries. This amount is multiplied by the retry number.
+ *
+ * @param baseDelay The time in seconds.
+ * @return This, to allow chaining.
+ */
+ public RetryTransientErrorsPolicy setBaseDelay(double baseDelay) {
+ this.baseDelay = baseDelay;
+ return this;
+ }
+
+ @Override
+ public boolean canRetry(int errorCode) {
+ return enabled && errorCode < ErrorCode.FATAL_ERROR;
+ }
+
+ @Override
+ public double getRetryDelay(int retry) {
+ return baseDelay * retry;
+ }
+}
diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/Route.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/Route.java new file mode 100755 index 00000000000..c9684e77a66 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/Route.java @@ -0,0 +1,206 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +import java.util.ArrayList; +import java.util.List; + +/** + * <p>A route is a list of {@link Hop hops} that are resolved from first to last + * as a routable moves from source to destination. A route may be changed at any + * time be either application logic or an invoked {@link RoutingPolicy}, so no + * guarantees on actual path can be given without the full knowledge of all that + * logic.</p> + * + * <p>To construct a route you may either use the factory method {@link + * #parse(String)} to produce a route instance from a string representation, or + * you may build one programatically through the hop accessors.</p> + * + * @author <a href="bratseth@yahoo-inc.com">Jon Bratseth</a> + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class Route { + + private final List<Hop> hops = new ArrayList<Hop>(); + private String cache = null; + + /** + * <p>Creates an empty route that contains no hops.</p> + */ + public Route() { + // empty + } + + /** + * <p>The copy constructor ignores integrity, it simply duplicates the list + * of hops in the other route. If that route is illegal, then so is + * this.</p> + * + * @param route The route to copy. + */ + public Route(Route route) { + this(route.hops); + cache = route.cache; + } + + private void setRaw(String s) { + cache = s; + } + + /** + * <p>Parses the given string as a list of space-separated hops. The {@link + * #toString()} method is compatible with this parser.</p> + * + * @param str The string to parse. + * @return A route that corresponds to the string. + */ + public static Route parse(String str) { + if (str == null || str.length() == 0) { + return new Route().addHop(new Hop().addDirective(new ErrorDirective("Failed to parse empty string."))); + } + RouteParser parser = new RouteParser(str); + Route route = parser.route(); + route.setRaw(str); + return route; + } + + /** + * <p>Constructs a route based on a list of hops.</p> + * + * @param hops The hops to be contained in this. + */ + public Route(List<Hop> hops) { + this.hops.addAll(hops); + } + + /** + * <p>Returns whether or not there are any hops in this route.</p> + * + * @return True if there is at least one hop. + */ + public boolean hasHops() { + return !hops.isEmpty(); + } + + /** + * <p>Returns the number of hops that make up this route.</p> + * + * @return The number of hops. + */ + public int getNumHops() { + return hops.size(); + } + + /** + * <p>Returns the hop at the given index.</p> + * + * @param i The index of the hop to return. + * @return The hop. + */ + public Hop getHop(int i) { + return hops.get(i); + } + + /** + * <p>Adds a hop to the list of hops that make up this route.</p> + * + * @param hop The hop to add. + * @return This, to allow chaining. + */ + public Route addHop(Hop hop) { + cache = null; + hops.add(hop); + return this; + } + + /** + * <p>Sets the hop at a given index.</p> + * + * @param i The index at which to set the hop. + * @param hop The hop to set. + * @return This, to allow chaining. + */ + public Route setHop(int i, Hop hop) { + cache = null; + hops.set(i, hop); + return this; + } + + /** + * <p>Removes the hop at a given index.</p> + * + * @param i The index of the hop to remove. + * @return The hop removed. + */ + public Hop removeHop(int i) { + cache = null; + return hops.remove(i); + } + + /** + * <p>Clears the list of hops that make up this route.</p> + * + * @return This, to allow chaining. + */ + public Route clearHops() { + cache = null; + hops.clear(); + return this; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Route)) { + return false; + } + Route rhs = (Route)obj; + if (hops.size() != rhs.hops.size()) { + return false; + } + for (int i = 0; i < hops.size(); ++i) { + if (!hops.get(i).equals(rhs.hops.get(i))) { + return false; + } + } + return true; + } + + @Override + public String toString() { + if (cache == null) { + StringBuilder ret = new StringBuilder(""); + for (int i = 0; i < hops.size(); ++i) { + ret.append(hops.get(i)); + if (i < hops.size() - 1) { + ret.append(" "); + } + } + cache = ret.toString(); + } + return cache; + } + + /** + * <p>Returns a string representation of this that can be debugged but not + * parsed.</p> + * + * @return The debug string. + */ + public String toDebugString() { + StringBuilder ret = new StringBuilder("Route(hops = { "); + for (int i = 0; i < hops.size(); ++i) { + ret.append(hops.get(i).toDebugString()); + if (i < hops.size() - 1) { + ret.append(", "); + } + } + ret.append(" })"); + return ret.toString(); + } + + @Override + public int hashCode() { + int result = hops != null ? hops.hashCode() : 0; + result = 31 * result + (cache != null ? cache.hashCode() : 0); + return result; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/RouteDirective.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/RouteDirective.java new file mode 100755 index 00000000000..6ea79a256e0 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/RouteDirective.java @@ -0,0 +1,63 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing;
+
+/**
+ * This class represents a route directive within a {@link Hop}'s selector. This will be replaced by the named route
+ * when evaluated. If the route is not present in the running protocol's routing table, routing will fail.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class RouteDirective implements HopDirective {
+
+ private final String name;
+
+ /**
+ * Constructs a new directive to insert a route.
+ *
+ * @param name The name of the route to insert.
+ */
+ public RouteDirective(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public boolean matches(HopDirective dir) {
+ return dir instanceof RouteDirective && name.equals(((RouteDirective)dir).name);
+ }
+
+ /**
+ * Returns the name of the route to insert.
+ *
+ * @return The route name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof RouteDirective)) {
+ return false;
+ }
+ RouteDirective rhs = (RouteDirective)obj;
+ if (!name.equals(rhs.name)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "route:" + name;
+ }
+
+ @Override
+ public String toDebugString() {
+ return "RouteDirective(name = '" + name + "')";
+ }
+
+ @Override
+ public int hashCode() {
+ return name != null ? name.hashCode() : 0;
+ }
+}
diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/RouteParser.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/RouteParser.java new file mode 100644 index 00000000000..a9f2ed8de3c --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/RouteParser.java @@ -0,0 +1,118 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +/** + * This replaces the incredibly slow javacc RouteParser.jj. It is a has its c++ sibling and + * the implementation is a a copy of the C++ version. + * @author <a href="mailto:balder@yahoo-inc.com">Henning Baldersheim</a> + * @since 5.2 + */ + +public class RouteParser { + private final String routeText; + RouteParser(String route) { + this.routeText = route; + } + + Route route() { + Route route = new Route(); + for (int from = 0, at = 0, depth = 0; at <= routeText.length(); ++at) { + if (at == routeText.length() || ((depth == 0) && Character.isWhitespace(routeText.charAt(at)))) { + if (from < at) { + Hop hop = createHop(routeText.substring(from, at)); + if (hop.hasDirectives() && hop.getDirective(0) instanceof ErrorDirective) { + return new Route().addHop(new Hop().addDirective(hop.getDirective(0))); + } + route.addHop(hop); + } + from = at + 1; + } else if ((routeText.charAt(at) == '(') || (routeText.charAt(at) == '[') ) { + ++depth; + } else if ((routeText.charAt(at) == ')') || (routeText.charAt(at) == ']')) { + --depth; + } + } + return route; + } + private static Hop createHop(String s) { + final int len = s.length(); + if (len == 0) { + return new Hop().addDirective(createErrorDirective("Failed to parse empty string.")); + } else if (len > 1 && (s.charAt(0) == '?')) { + return createHop(s.substring(1, len)).setIgnoreResult(true); + } else if (len > 4 && s.charAt(0) == 't' && s.charAt(1) == 'c' && s.charAt(2) == 'p' && s.charAt(3) == '/') { + HopDirective directive = createTcpDirective(s.substring(4, len)); + if (directive != null) { + return new Hop().addDirective(directive); + } + } else if (len > 6 && s.charAt(0) == 'r' && s.charAt(1) == 'o' && s.charAt(2) == 'u' + && s.charAt(3) == 't' && s.charAt(4) == 'e' && s.charAt(5) == ':') { + return new Hop().addDirective(createRouteDirective(s.substring(6, len))); + } + Hop hop = new Hop(); + for (int from = 0, at = 0, depth = 0; at <= len; ++at) { + if (at == len) { + if (depth > 0) { + return new Hop().addDirective(createErrorDirective("Unterminated '[' in '" + s + "'")); + } + hop.addDirective(createDirective(s.substring(from, at))); + from = at + 1; + } else { + char c = s.charAt(at); + if (Character.isWhitespace(c) && depth == 0) { + return new Hop().addDirective(createErrorDirective("Failed to completely parse '" + s + "'.")); + } else if ((depth == 0 && c == '/')) { + hop.addDirective(createDirective(s.substring(from, at))); + from = at + 1; + } else if (c == '[') { + ++depth; + } else if (c == ']') { + if (depth == 0) { + return new Hop().addDirective(createErrorDirective("Unexpected token ']' in '" + s + "'")); + } + --depth; + } + } + } + return hop; + } + private static HopDirective createErrorDirective(String s) { + return new ErrorDirective(s); + } + private static HopDirective createTcpDirective(String s) { + int posP = s.indexOf(':'); + if (posP <= 0) { + return null; + } + int posS = s.indexOf('/', posP+1); + if (posS <= posP + 1) { + return null; + } + return new TcpDirective(s.substring(0, posP), + Integer.valueOf(s.substring(posP + 1, posS)), + s.substring(posS + 1)); + } + private static HopDirective createRouteDirective(String s) { + return new RouteDirective(s); + } + private static HopDirective createDirective(String s) { + return (s.length() > 2 && s.charAt(0) == '[') + ? ((s.charAt(s.length() - 1) == ']') + ? createPolicyDirective(s.substring(1, s.length() - 1)) + : createErrorDirective("Unterminated '[' in '" + s + "'")) + : createVerbatimDirective(s); + + } + private static HopDirective createPolicyDirective(String s) + { + int pos = s.indexOf(':'); + return (pos == -1) + ? new PolicyDirective(s, "") + : new PolicyDirective(s.substring(0, pos), s.substring(pos + 1).trim()); + } + + private static HopDirective createVerbatimDirective(String s) { + return new VerbatimDirective(s); + } + +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/RouteSpec.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/RouteSpec.java new file mode 100644 index 00000000000..4062a0d9d0f --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/RouteSpec.java @@ -0,0 +1,219 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +import java.util.ArrayList; +import java.util.List; + +/** + * Along with the {@link RoutingSpec}, {@link RoutingTableSpec} and {@link HopSpec}, this holds the routing + * specifications for all protocols. The only way a client can configure or alter the settings of a message bus instance + * is through these classes. + * <p> + * This class contains the spec for a single route. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RouteSpec { + + private final String name; + private final List<String> hops = new ArrayList<String>(); + private final boolean verify; + + /** + * Creates a new named route specification. + * + * @param name A protocol-unique name for this route. + */ + public RouteSpec(String name) { + this(name, true); + } + + /** + * Creates a new named route specification. + * + * @param name A protocol-unique name for this route. + * @param verify Whether or not this should be verified. + */ + public RouteSpec(String name, boolean verify) { + this.name = name; + this.verify = verify; + } + + /** + * Implements the copy constructor. + * + * @param obj The object to copy. + */ + public RouteSpec(RouteSpec obj) { + this.name = obj.name; + this.verify = obj.verify; + for (String hop : obj.hops) { + hops.add(hop); + } + } + + /** + * Returns the protocol-unique name of this route. + * + * @return The name. + */ + public String getName() { + return name; + } + + /** + * Returns the hop name at the given index. + * + * @param i The index of the hop to return. + * @return The hop at the given index. + */ + public String getHop(int i) { + return hops.get(i); + } + + /** + * Returns whether or not there are any hops in this route. + * + * @return True if there is at least one hop. + */ + public boolean hasHops() { + return !hops.isEmpty(); + } + + /** + * Returns the number of hops that make up this route. + * + * @return The number of hops. + */ + public int getNumHops() { + return hops.size(); + } + + /** + * Adds the given hop name to this. + * + * @param hop The hop to add. + * @return This, to allow chaining. + */ + public RouteSpec addHop(String hop) { + hops.add(hop); + return this; + } + + /** + * Adds the given hop names to this. + * + * @param hops The hops to add. + * @return This, to allow chaining. + */ + public RouteSpec addHops(List<String> hops) { + this.hops.addAll(hops); + return this; + } + + /** + * Sets the hop name for a given index. + * + * @param i The index of the hop to set. + * @param hop The hop to set. + * @return This, to allow chaining. + */ + public RouteSpec setHop(int i, String hop) { + hops.set(i, hop); + return this; + } + + /** + * Removes the hop name at the given index. + * + * @param i The index of the hop to remove. + * @return The removed hop. + */ + public String removeHop(int i) { + return hops.remove(i); + } + + /** + * Clears the list of hops that make up this route. + * + * @return This, to allow chaining. + */ + public RouteSpec clearHops() { + hops.clear(); + return this; + } + + /** + * Verifies the content of this against the given application. + * + * @param app The application to verify against. + * @param table The routing table to verify against. + * @param errors The list of errors found. + * @return True if no errors where found. + */ + public boolean verify(ApplicationSpec app, RoutingTableSpec table, List<String> errors) { + if (verify) { + String protocol = table.getProtocol(); + int numHops = hops.size(); + if (numHops == 0) { + errors.add("Route '" + name + "' in routing table '" + protocol + "' has no hops."); + } else { + for (int i = 0; i < numHops; ++i) { + HopSpec.verify(app, table, null, null, hops.get(i), errors, + "hop " + (i + 1) + " in route '" + name + "' in routing table '" + protocol + "'"); + } + } + } + return errors.isEmpty(); + } + + /** + * Appends the content of this to the given config string builder. + * + * @param cfg The config to add to. + * @param prefix The prefix to use for each add. + */ + public void toConfig(StringBuilder cfg, String prefix) { + cfg.append(prefix).append("name ").append(RoutingSpec.toConfigString(name)).append("\n"); + int numHops = hops.size(); + if (numHops > 0) { + cfg.append(prefix).append("hop[").append(numHops).append("]\n"); + for (int i = 0; i < numHops; ++i) { + cfg.append(prefix).append("hop[").append(i).append("] "); + cfg.append(RoutingSpec.toConfigString(hops.get(i))).append("\n"); + } + } + } + + // Overrides Object. + @Override + public String toString() { + StringBuilder ret = new StringBuilder(); + toConfig(ret, ""); + return ret.toString(); + } + + // Overrides Object. + @Override + public boolean equals(Object obj) { + if (!(obj instanceof RouteSpec)) { + return false; + } + RouteSpec rhs = (RouteSpec)obj; + if (!name.equals(rhs.name)) { + return false; + } + if (!hops.equals(rhs.hops)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (hops != null ? hops.hashCode() : 0); + result = 31 * result + (verify ? 1 : 0); + return result; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingContext.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingContext.java new file mode 100755 index 00000000000..d7105a92f16 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingContext.java @@ -0,0 +1,383 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +import com.yahoo.jrt.slobrok.api.IMirror; +import com.yahoo.messagebus.Error; +import com.yahoo.messagebus.*; + +import java.util.*; + +/** + * <p>This context object is what is seen by {@link RoutingPolicy} when doing + * both select() and merge(). It contains the necessary accessors to everything + * a policy is expected to need. An instance of this is created for every {@link + * RoutingNode} that contains a policy.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RoutingContext { + + private final RoutingNode node; + private final int directive; + private final Set<Integer> consumableErrors = new HashSet<Integer>(); + private boolean selectOnRetry = true; + private Object context = null; + + /** + * <p>Constructs a new routing context for a given routing node and hop.</p> + * + * @param node The owning routing node. + * @param directive The index to the policy directive of the hop. + */ + RoutingContext(RoutingNode node, int directive) { + this.node = node; + this.directive = directive; + } + + /** + * <p>Returns whether or not this hop has any configured recipients.</p> + * + * @return True if there is at least one recipient. + */ + public boolean hasRecipients() { + return !node.getRecipients().isEmpty(); + } + + /** + * <p>Returns the number of configured recipients for this hop.</p> + * + * @return The recipient count. + */ + public int getNumRecipients() { + return node.getRecipients().size(); + } + + /** + * <p>Returns the configured recipient at the given index.</p> + * + * @param idx The index of the recipient to return. + * @return The reipient at the given index. + */ + public Route getRecipient(int idx) { + return node.getRecipients().get(idx); + } + + /** + * <p>Returns all configured recipients for this hop.</p> + * + * @return An unmodifiable list of recipients. + */ + public List<Route> getAllRecipients() { + return Collections.unmodifiableList(node.getRecipients()); + } + + /** + * <p>Returns a list of all configured recipients whose first hop matches + * this.</p> + * + * @return A modifiable list of recipients. + */ + public List<Route> getMatchedRecipients() { + List<Route> ret = new ArrayList<Route>(); + Set<String> done = new HashSet<String>(); + Hop hop = getHop(); + for (Route route : node.getRecipients()) { + if (route.hasHops() && hop.matches(route.getHop(0))) { + HopDirective dir = route.getHop(0).getDirective(directive); + String key = dir.toString(); + if (!done.contains(key)) { + ret.add(new Route(route).setHop(0, new Hop(hop).setDirective(directive, dir))); + done.add(key); + } + } + } + return ret; + } + + /** + * <p>Returns whether or not the policy is required to reselect if resending + * occurs.</p> + * + * @return True to invoke {@link RoutingPolicy#select(RoutingContext)} on + * resend. + */ + public boolean getSelectOnRetry() { + return selectOnRetry; + } + + /** + * <p>Sets whether or not the policy is required to reselect if resending + * occurs.</p> + * + * @param selectOnRetry The value to set. + * @return This, to allow chaining. + */ + public RoutingContext setSelectOnRetry(boolean selectOnRetry) { + this.selectOnRetry = selectOnRetry; + return this; + } + + /** + * <p>Returns the route that contains the routing policy that spawned + * this.</p> + * + * @return The route. + */ + public Route getRoute() { + return node.getRoute(); + } + + /** + * <p>Returns the hop that contains the routing policy that spawned + * this.</p> + * + * @return The hop. + */ + public Hop getHop() { + return node.getRoute().getHop(0); + } + + /** + * <p>Returns the index of the hop directive that spawned this.</p> + * + * @return The directive index. + */ + public int getDirectiveIndex() { + return directive; + } + + /** + * <p>Returns the policy directive that spawned this.</p> + * + * @return The directive object. + */ + public PolicyDirective getDirective() { + return (PolicyDirective)getHop().getDirective(directive); + } + + /** + * <p>Returns the part of the route string that precedes the active policy + * directive. This is the same as calling {@link #getHop()}.getPrefix({@link + * #getDirectiveIndex()}).</p> + * + * @return The hop prefix. + */ + public String getHopPrefix() { + return getHop().getPrefix(directive); + } + + /** + * <p>Returns the remainder of the route string immediately following the + * active policy directive. This is the same as calling {@link + * #getHop()}.getSuffix({@link #getDirectiveIndex()}).</p> + * + * @return The hop suffix. + */ + public String getHopSuffix() { + return getHop().getSuffix(directive); + } + + /** + * <p>Returns the policy specific context object.</p> + * + * @return The context. + */ + public Object getContext() { + return context; + } + + /** + * <p>Sets a policy specific context object that will be available at + * merge().</p> + * + * @param context An arbitrary object. + * @return This, to allow chaining. + */ + public RoutingContext setContext(Object context) { + this.context = context; + return this; + } + + /** + * <p>Returns the message being routed.</p> + * + * @return The message. + */ + public Message getMessage() { + return node.getMessage(); + } + + /** + * <p>Adds a string to the trace of the message being routed.</p> + * + * @param level The level of the trace note. + * @param note The note to add. + */ + public void trace(int level, String note) { + node.getTrace().trace(level, note); + } + + /** + * Indicates if tracing is enabled at this level. + * @param level the level + * @return true if tracing is enabled at this level + */ + public boolean shouldTrace(int level) { + return node.getTrace().shouldTrace(level); + } + + /** + * <p>Returns whether or not a reply is available.</p> + * + * @return True if a reply is set. + */ + public boolean hasReply() { + return node.hasReply(); + } + + /** + * <p>Returns the reply generated by the associated routing policy.</p> + * + * @return The reply. + */ + public Reply getReply() { + return node.getReply(); + } + + /** + * <p>Sets the reply generated by the associated routing policy.</p> + * + * @param reply The reply to set. + * @return This, to allow chaining. + */ + public RoutingContext setReply(Reply reply) { + node.setReply(reply); + return this; + } + + /** + * <p>This is a convenience method to call {@link #setError(Error)}.</p> + * + * @param code The code of the error to set. + * @param msg The message of the error to set. + * @return This, to allow chaining. + */ + public RoutingContext setError(int code, String msg) { + node.setError(code, msg); + return this; + } + + /** + * <p>This is a convenience method to assign an {@link EmptyReply} + * containing a single error to this. This also fiddles with the trace + * object so that the error gets written to it.</p> + * + * @param err The error to set. + * @return This, to allow chaining. + * @see #setReply(Reply) + */ + public RoutingContext setError(Error err) { + node.setError(err); + return this; + } + + /** + * <p>Returns the message bus instance on which this is running.</p> + * + * @return The message bus. + */ + public MessageBus getMessageBus() { + return node.getMessageBus(); + } + + /** + * <p>Returns whether or not the owning routing node has any child + * nodes.</p> + * + * @return True if there is at least one child. + */ + public boolean hasChildren() { + return !node.getChildren().isEmpty(); + } + + /** + * <p>Returns the number of children the owning routing node has.</p> + * + * @return The child count. + */ + public int getNumChildren() { + return node.getChildren().size(); + } + + /** + * <p>Returns an iterator for the child routing nodes of the owning + * node.</p> + * + * @return The iterator. + */ + public RoutingNodeIterator getChildIterator() { + return new RoutingNodeIterator(node.getChildren()); + } + + /** + * <p>Adds a child routing context to this based on a given route. This is + * the typical entry point a policy will use to select recipients during a + * {@link RoutingPolicy#select(RoutingContext)} invokation.</p> + * + * @param route The route to contain in the child context. + */ + public void addChild(Route route) { + node.addChild(route); + } + + /** + * <p>This is a convenience method to more easily add a list of children to + * this. It will simply call the {@link #addChild} method for each element + * in the list.</p> + * + * @param routes A list of routes to add as children. + */ + public void addChildren(List<Route> routes) { + if (routes != null) { + for (Route route : routes) { + addChild(route); + } + } + } + + /** + * <p>Returns the local mirror of the system's name server.</p> + * + * @return The mirror api. + */ + public IMirror getMirror() { + return node.getNetwork().getMirror(); + } + + /** + * <p>Adds the given error code to the list of codes that the associated + * routing policy <u>may</u> consume. This is used to verify whether or not + * a resolved routing tree can succeed if sent. Because verification is only + * done before sending, the error types that must be added here are only + * those that can be generated by message bus itself.</p> + * + * @param errorCode The code that might be consumed. + * @see RoutingNode#hasUnconsumedErrors() + * @see com.yahoo.messagebus.ErrorCode + */ + public void addConsumableError(int errorCode) { + consumableErrors.add(errorCode); + } + + /** + * <p>Returns whether or not the given error code <u>may</u> be consumed by + * the associated routing policy.</p> + * + * @param errorCode The code to check. + * @return True if the code may be consumed. + * @see #addConsumableError(int) + */ + public boolean isConsumableError(int errorCode) { + return consumableErrors.contains(errorCode); + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingNode.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingNode.java new file mode 100755 index 00000000000..e394b133bca --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingNode.java @@ -0,0 +1,802 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.Error; +import com.yahoo.messagebus.metrics.RouteMetricSet; +import com.yahoo.messagebus.network.Network; +import com.yahoo.messagebus.network.ServiceAddress; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This class represents a node in the routing tree that is created when a route is resolved. There will be one node per + * modification of the route. For every {@link RoutingPolicy} there will be an instance of this that has its policy and + * {@link RoutingContext} member set. A policy is oblivious to this class, it can only access the context object. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RoutingNode implements ReplyHandler { + + private final MessageBus mbus; + private final Network net; + private final Resender resender; + private final RoutingNode parent; + private final List<Route> recipients = new ArrayList<>(); + private final List<RoutingNode> children = new ArrayList<>(); + private final ReplyHandler handler; + private final Trace trace; + private final AtomicInteger pending = new AtomicInteger(0); + private final Message msg; + private Reply reply = null; + private Route route = null; + private RoutingPolicy policy = null; + private RoutingContext routingContext = null; + private ServiceAddress serviceAddress = null; + private boolean isActive = true; + private boolean shouldRetry = false; + private RouteMetricSet routeMetrics; + + /** + * Constructs a new instance of this class. This is the root node constructor, and will be used by the different + * sessions for sending messages. Note that the {@link #discard()} functionality of this class is implemented so + * that it passes a null reply to the handler to notify the discard. + * + * @param mbus The message bus on which we are running. + * @param net The network layer we are to transmit through. + * @param resender The resender to schedule with. + * @param handler The handler to receive the final reply. + * @param msg The message being sent. + */ + public RoutingNode(MessageBus mbus, Network net, Resender resender, ReplyHandler handler, Message msg) { + this.mbus = mbus; + this.net = net; + this.resender = resender; + this.handler = handler; + this.msg = msg; + this.trace = new Trace(msg.getTrace().getLevel()); + this.route = msg.getRoute(); + this.parent = null; + + if (route != null) { + routeMetrics = mbus.getMetrics().getRouteMetrics(route); + } + } + + /** + * Constructs a new instance of this class. This is the child node constructor, and is the constructor used when + * building the routing tree. + * + * @param parent The parent routing node. + * @param route The route to assign to this. + */ + private RoutingNode(RoutingNode parent, Route route) { + mbus = parent.mbus; + net = parent.net; + resender = parent.resender; + handler = null; + msg = parent.msg; + trace = new Trace(parent.trace.getLevel()); + this.route = new Route(route); + this.parent = parent; + recipients.addAll(parent.recipients); + } + + /** + * Discards this routing node. Invoking this will notify the parent {@link SendProxy} to ensure that the + * corresponding message is discarded. This is a required step to ensure safe shutdown if you need to destroy a + * message bus instance while there are still routing nodes alive in your application. + */ + public void discard() { + if (handler != null) { + handler.handleReply(null); + } else if (parent != null) { + parent.discard(); + } + } + + /** + * This is the single entry-point for sending a message along a route. This can only be invoked on the root node of + * a routing tree. It runs all the necessary selection, verification and transmission logic. Once this has been + * called, it guarantees that a reply is returned to the registered reply handler. + */ + public void send() { + if (!resolve(0)) { + notifyAbort("Route resolution failed."); + } else if (hasUnconsumedErrors()) { + notifyAbort("Errors found while resolving route."); + } else { + notifyTransmit(); + } + } + + /** + * This method assigns an error reply to all unsent leaf nodes, and invokes {@link #notifyParent()} on them. This + * has the effect of ensuring that a reply will return to sender. + * + * @param msg The error message to assign. + */ + public void notifyAbort(String msg) { + Stack<RoutingNode> stack = new Stack<>(); + stack.push(this); + while (!stack.isEmpty()) { + RoutingNode node = stack.pop(); + if (!node.isActive) { + // reply not pending + } else if (node.reply != null) { + node.notifyParent(); + } else if (node.children.isEmpty()) { + node.setError(ErrorCode.SEND_ABORTED, msg); + node.notifyParent(); + } else { + for (RoutingNode child : node.children) { + stack.push(child); + } + } + } + } + + /** + * This method collects all unsent leaf nodes and passes them to {@link Network#send(com.yahoo.messagebus.Message, + * java.util.List)}. This is orthogonal to {@link #notifyAbort(String)} in that it ensures that a reply will return + * to sender. + */ + private void notifyTransmit() { + List<RoutingNode> sendTo = new ArrayList<>(); + Deque<RoutingNode> stack = new ArrayDeque<>(); + stack.push(this); + while (!stack.isEmpty()) { + RoutingNode node = stack.pop(); + if (node.isActive) { + if (node.children.isEmpty()) { + if (node.reply != null) { + node.notifyParent(); + } else { + sendTo.add(node); + } + } else { + for (RoutingNode child : node.children) { + stack.push(child); + } + } + } + } + if (!sendTo.isEmpty()) { + net.send(msg, sendTo); + } + } + + /** + * This method may only be invoked on a root node, as it passes the current reply to the member {@link + * ReplyHandler}. + */ + private void notifySender() { + reply.getTrace().swap(trace); + handler.handleReply(reply); + reply = null; + } + + /** + * This method mergs this node as ready for merge. If it has a parent routing node, its pending member is + * decremented. If this causes the parent's pending count to reach zero, its {@link #notifyMerge()} method is + * invoked. A special flag is used to make sure that failed resending avoids notifying parents of previously + * resolved branches of the tree. + */ + private void notifyParent() { + if (serviceAddress != null) { + net.freeServiceAddress(this); + } + tryIgnoreResult(); + if (parent != null) { + parent.notifyMerge(); + return; + } + if (shouldRetry && resender.scheduleRetry(this)) { + return; + } + notifySender(); + } + + /** + * This method merges the content of all its children, and invokes itself on the parent node. If not all children + * are ready for merg, this method does nothing. The rationale for this is that the last child to receive a reply + * will propagate the merge upwards. Once this method reaches the root node, the reply is either scheduled for + * resending or passed to the owning reply handler. + */ + private void notifyMerge() { + if (pending.decrementAndGet() != 0) { + return; // not done yet + } + + // Merges the trace information from all children into this. This method takes care not to spend cycles + // manipulating the trace in case tracing is disabled. + if (trace.getLevel() > 0) { + TraceNode tail = new TraceNode(); + for (RoutingNode child : children) { + TraceNode root = child.trace.getRoot(); + tail.addChild(root); + root.clear(); + } + tail.setStrict(false); + trace.getRoot().addChild(tail); + } + + // Execute the {@link RoutingPolicy#notifyMerge(RoutingContext)} method of the current routing policy. If a + // policy fails to produce a reply, this attaches an error reply to this node. + PolicyDirective dir = routingContext.getDirective(); + if (trace.shouldTrace(TraceLevel.SPLIT_MERGE)) { + trace.trace(TraceLevel.SPLIT_MERGE, "Routing policy '" + dir.getName() + "' merging replies."); + } + try { + policy.merge(routingContext); + } catch (RuntimeException e) { + setError(ErrorCode.POLICY_ERROR, + "Policy '" + dir.getName() + "' threw an exception; " + e.toString()); + } + if (reply == null) { + setError(ErrorCode.APP_FATAL_ERROR, + "Routing policy '" + routingContext.getDirective().getName() + "' failed to merge replies."); + } + + // Notifies the parent node. + notifyParent(); + } + + /** + * This is a helper method to call {@link Hop#getIgnoreResult()} on the first Hop of the current Route. + * + * @return True to ignore the result. + */ + private boolean shouldIgnoreResult() { + return route != null && route.getNumHops() > 0 && route.getHop(0).getIgnoreResult(); + } + + /** + * If a reply has been set containing an error, and {@link #shouldIgnoreResult()} returns <tt>true</tt>, this method + * replaces that reply with one that has no error. + * + * @return Whether or not the reply was replaced. + */ + private boolean tryIgnoreResult() { + if (!shouldIgnoreResult()) { + return false; + } + if (reply == null || !reply.hasErrors()) { + return false; + } + setReply(new EmptyReply()); + trace.trace(TraceLevel.SPLIT_MERGE, "Ignoring errors in reply."); + return true; + } + + /** + * This method is used to reset the internal state of routing nodes that will be resent. If a routing policy sets + * {@link RoutingContext#setSelectOnRetry(boolean)} to true, this method will reroute everything from that node + * onwards. If that flag is not set, scheduling recurses into any child that got a reply with only transient errors. + * Finally, if neither this node or none of its children were scheduled for resending, force reroute from this. + */ + void prepareForRetry() { + shouldRetry = false; + reply = null; + if (routingContext != null && routingContext.getSelectOnRetry()) { + children.clear(); + } else if (!children.isEmpty()) { + boolean retryingSome = false; + for (RoutingNode child : children) { + if (child.shouldRetry || child.reply == null) { + child.prepareForRetry(); + retryingSome = true; + } + } + if (!retryingSome) { + // Entering here means we have no children that should be resent even though this node reports a transient + // error. The only thing we can do is to reselect from this. + children.clear(); + } + } + } + + /** + * Returns whether or not transmitting along this routing tree can possibly succeed. This evaluates to false if + * either a) there are no leaf nodes to send to, or b) some leaf node contains a fatal error that is not masked by a + * routing policy above it in the tree. If only transient errors would reach this, the resend flag is set to true. + * + * @return True if no error reaches this. + */ + private boolean hasUnconsumedErrors() { + boolean hasError = false; + + Deque<RoutingNode> stack = new ArrayDeque<>(); + stack.push(this); + while (!stack.isEmpty()) { + RoutingNode node = stack.pop(); + if (node.reply != null) { + for (int i = 0; i < node.reply.getNumErrors(); ++i) { + int errorCode = node.reply.getError(i).getCode(); + RoutingNode it = node; + while (it != null) { + if (it.routingContext != null && it.routingContext.isConsumableError(errorCode)) { + errorCode = ErrorCode.NONE; + break; + } + it = it.parent; + } + if (errorCode != ErrorCode.NONE) { + shouldRetry = resender != null && resender.canRetry(errorCode); + if (!shouldRetry) { + return true; // no need to continue + } + hasError = true; + } + } + } else { + for (RoutingNode child : node.children) { + stack.push(child); + } + } + } + + return hasError; + } + + /** + * This method performs the necessary selection logic to resolve the next step of the current route. There is a hard + * limit to how deep the routing tree may resolve to, and if that depth is ever exceeded, this method returns false. + * This should only really happen if routing has been misconfigured. + * + * @param depth The current depth. + * @return False if selection failed. + */ + private boolean resolve(int depth) { + if (route == null || !route.hasHops()) { + setError(ErrorCode.ILLEGAL_ROUTE, "Route has no hops."); + return false; + } + if (!children.isEmpty()) { + return resolveChildren(depth + 1); + } + while (lookupHop() || lookupRoute()) { + if (++depth > 64) { + break; + } + } + if (depth > 64) { + setError(ErrorCode.ILLEGAL_ROUTE, "Depth limit exceeded."); + return false; + } + if (findErrorDirective()) { + return false; + } + if (findPolicyDirective()) { + if (executePolicySelect()) { + return resolveChildren(depth + 1); + } + return reply != null; + } + net.allocServiceAddress(this); + return serviceAddress != null || reply != null; + } + + /** + * This method checks to see whether the string representation of the current hop is actually the name of another. + * If a hop is found, the first hop of the current route is replaced by this. + * + * @return True if a hop was found and added. + */ + private boolean lookupHop() { + RoutingTable table = mbus.getRoutingTable(msg.getProtocol()); + if (table != null) { + String name = route.getHop(0).getServiceName(); + if (table.hasHop(name)) { + HopBlueprint hop = table.getHop(name); + configureFromBlueprint(hop); + if (trace.shouldTrace(TraceLevel.SPLIT_MERGE)) { + trace.trace(TraceLevel.SPLIT_MERGE, "Recognized '" + name + "' as " + hop + "."); + } + return true; + } + } + return false; + } + + /** + * This method checks to see whether the current hop contains a {@link RouteDirective}, or if its string + * representation is actually the name of a configured route. If a route is found, the first hop of the current + * route is replaced by expanding the named route. If a route directive requests a non-existant route, this method + * creates an error-reply for this node. + * + * @return True if a route was found and added. + * @see #insertRoute(Route) + */ + private boolean lookupRoute() { + RoutingTable table = mbus.getRoutingTable(msg.getProtocol()); + Hop hop = route.getHop(0); + if (hop.getDirective(0) instanceof RouteDirective) { + RouteDirective dir = (RouteDirective)hop.getDirective(0); + if (table == null || !table.hasRoute(dir.getName())) { + setError(ErrorCode.ILLEGAL_ROUTE, "Route '" + dir.getName() + "' does not exist."); + return false; + } + insertRoute(table.getRoute(dir.getName())); + if (trace.shouldTrace(TraceLevel.SPLIT_MERGE)) { + trace.trace(TraceLevel.SPLIT_MERGE, + "Route '" + dir.getName() + "' retrieved by directive; new route is '" + route + "'."); + } + return true; + } + if (table != null) { + String name = hop.getServiceName(); + if (table.hasRoute(name)) { + insertRoute(table.getRoute(name)); + if (trace.shouldTrace(TraceLevel.SPLIT_MERGE)) { + trace.trace(TraceLevel.SPLIT_MERGE, "Recognized '" + name + "' as route '" + route + "'."); + } + return true; + } + } + return false; + } + + /** + * This method replaces the first hop of the current route with the given route. + * + * @param ins The route to insert. + */ + private void insertRoute(Route ins) { + Route route = new Route(ins); + if (shouldIgnoreResult()) { + route.getHop(0).setIgnoreResult(true); + } + for (int i = 1; i < this.route.getNumHops(); ++i) { + route.addHop(this.route.getHop(i)); + } + this.route = route; + } + + /** + * This method traverses the current hop looking for an isntance of {@link ErrorDirective}. If one is found, this + * method assigns a corresponding error reply to this node. + * + * @return True if an error was found. + */ + private boolean findErrorDirective() { + Hop hop = route.getHop(0); + for (int i = 0; i < hop.getNumDirectives(); ++i) { + HopDirective dir = hop.getDirective(i); + if (dir instanceof ErrorDirective) { + setError(ErrorCode.ILLEGAL_ROUTE, ((ErrorDirective)dir).getMessage()); + return true; + } + } + return false; + } + + /** + * This method traverses the current hop looking for an instance of {@link PolicyDirective}. If one is found, this + * method creates and assigns a routing context to this. + * + * @return True if a policy was found. + */ + private boolean findPolicyDirective() { + Hop hop = route.getHop(0); + for (int i = 0; i < hop.getNumDirectives(); ++i) { + HopDirective dir = hop.getDirective(i); + if (dir instanceof PolicyDirective) { + routingContext = new RoutingContext(this, i); + return true; + } + } + return false; + } + + private String exceptionMessageWithTrace(Exception e) { + StringWriter sw = new StringWriter(); + try (PrintWriter pw = new PrintWriter(sw)) { + e.printStackTrace(pw); + pw.flush(); + } + return sw.toString(); + } + + /** + * Creates the {@link RoutingPolicy} referenced by the current routing context, and executes its {@link + * RoutingPolicy#select(RoutingContext)} method. + * + * @return True if at least one child was added. + */ + private boolean executePolicySelect() { + PolicyDirective dir = routingContext.getDirective(); + policy = mbus.getRoutingPolicy(msg.getProtocol(), dir.getName(), dir.getParam()); + if (policy == null) { + setError(ErrorCode.UNKNOWN_POLICY, + "Protocol '" + msg.getProtocol() + "' could not create routing policy '" + + dir.getName() + "' with parameter '" + dir.getParam() + "'."); + return false; + } + if (trace.shouldTrace(TraceLevel.SPLIT_MERGE)) { + trace.trace(TraceLevel.SPLIT_MERGE, "Running routing policy '" + dir.getName() + "'."); + } + try { + policy.select(routingContext); + } catch (RuntimeException e) { + setError(ErrorCode.POLICY_ERROR, + "Policy '" + dir.getName() + "' threw an exception; " + exceptionMessageWithTrace(e)); + return false; + } + if (children.isEmpty()) { + if (reply == null) { + setError(ErrorCode.NO_SERVICES_FOR_ROUTE, + "Policy '" + dir.getName() + "' selected no recipients for route '" + route + "'."); + } else { + if (trace.shouldTrace(TraceLevel.SPLIT_MERGE)) { + trace.trace(TraceLevel.SPLIT_MERGE, + "Policy '" + dir.getName() + "' assigned a reply to this branch."); + } + } + return false; + } + for (RoutingNode child : children) { + if (child.trace.shouldTrace(TraceLevel.SPLIT_MERGE)) { + Hop hop = child.route.getHop(0); + child.trace.trace(TraceLevel.SPLIT_MERGE, + "Component '" + hop + "' selected by policy '" + dir.getName() + "'."); + } + } + return true; + } + + /** + * This method invokes the {@link #resolve(int)} method of all the child nodes of this. If any of these exceed the + * depth limit, this method returns false. + * + * @param childDepth The depth of the children. + * @return False if depth limit was exceeded. + */ + private boolean resolveChildren(int childDepth) { + int numActiveChildren = 0; + boolean ret = true; + for (RoutingNode child : children) { + if (child.trace.shouldTrace(TraceLevel.SPLIT_MERGE)) { + child.trace.trace(TraceLevel.SPLIT_MERGE, "Resolving '" + child.route + "'."); + } + child.isActive = (child.reply == null); + if (child.isActive) { + ++numActiveChildren; + if (!child.resolve(childDepth)) { + ret = false; + break; + } + } else { + if (child.trace.shouldTrace(TraceLevel.SPLIT_MERGE)) { + child.trace.trace(TraceLevel.SPLIT_MERGE, "Already completed."); + } + } + } + pending.set(numActiveChildren); + return ret; + } + + /** + * Adds a child routing node to this based on a route. This is package private because client code should only + * access it through a {@link RoutingPolicy} and its {@link RoutingContext#addChild(Route)} method. + * + * @param route The route to store in the child node. + */ + void addChild(Route route) { + RoutingNode child = new RoutingNode(this, route); + if (shouldIgnoreResult()) { + child.route.getHop(0).setIgnoreResult(true); + } + children.add(child); + } + + /** + * Configures this node based on a hop blueprint. For each recipient in the blueprint it creates a copy of the + * current route, and sets the first hop of that route to be the configured recipient hop. In effect, this replaces + * the current hop and retains the rest of the route. + * + * @param hop The blueprint to use for configuration. + */ + private void configureFromBlueprint(HopBlueprint hop) { + boolean ignoreResult = shouldIgnoreResult(); + route.setHop(0, hop.create()); + if (ignoreResult) { + route.getHop(0).setIgnoreResult(true); + } + recipients.clear(); + for (int r = 0; r < hop.getNumRecipients(); ++r) { + Route recipient = new Route(); + recipient.addHop(hop.getRecipient(r)); + for (int h = 1; h < route.getNumHops(); ++h) { + recipient.addHop(route.getHop(h)); + } + recipients.add(recipient); + } + } + + /** + * This is a convenience method to call {@link #setError(Error)}. + * + * @param code The code of the error to set. + * @param msg The message of the error to set. + */ + public void setError(int code, String msg) { + setError(new Error(code, msg)); + } + + /** + * This is a convenience method to assign an {@link EmptyReply} containing a single error to this. This also fiddles + * with the trace object so that the error gets written to it. + * + * @param err The error to set. + * @see #setReply(Reply) + */ + public void setError(Error err) { + Reply reply = new EmptyReply(); + reply.getTrace().setLevel(trace.getLevel()); + reply.addError(err); + setReply(reply); + } + + /** + * This is a convenience method to call {@link #addError(Error)}. + * + * @param code The code of the error to add. + * @param msg The message of the error to add. + */ + public void addError(int code, String msg) { + addError(new Error(code, msg)); + } + + /** + * This is a convenience method to add an error to this. If a reply has already been set, this method will add the + * error to it. If no reply is set, this method calls {@link #setError(Error)}. This method also fiddles with the + * trace object so that the error gets written to it. + * + * @param err The error to add. + */ + public void addError(Error err) { + if (reply != null) { + reply.getTrace().swap(trace); + reply.addError(err); + reply.getTrace().swap(trace); + } else { + setError(err); + } + } + + /** + * Returns the message bus being used to send the message. + * + * @return The message bus. + */ + MessageBus getMessageBus() { + return mbus; + } + + /** + * Returns the network being used to send the message. + * + * @return The network layer. + */ + Network getNetwork() { + return net; + } + + /** + * Returns the message being routed. You should NEVER modify a message that is retrieved from a routing node or + * context, as the result of doing so is undefined. + * + * @return The message being routed. + */ + public Message getMessage() { + return msg; + } + + /** + * Returns the trace object for this node. Each node has a separate trace object so that merging can be done + * correctly. + * + * @return The trace object. + */ + public Trace getTrace() { + return trace; + } + + /** + * Returns the route object as it exists at this point of the tree. + * + * @return The route at this point. + */ + public Route getRoute() { + return route; + } + + /** + * Returns whether or not this node contains a reply. + * + * @return True if this node has a reply. + */ + boolean hasReply() { + return reply != null; + } + + /** + * Returns the reply of this node. + * + * @return The reply assigned to this node. + */ + Reply getReply() { + return reply; + } + + /** + * Sets the reply of this routing node. This method also updates the internal state of this node; it is tagged for + * resending if the reply has only transient errors, and the reply's {@link Trace} is copied. This method <u>does + * not</u> call the parent node's {@link #notifyMerge()}. + * + * @param reply The reply to set. + */ + public void setReply(Reply reply) { + if (reply != null) { + shouldRetry = resender != null && resender.shouldRetry(reply); + trace.getRoot().addChild(reply.getTrace().getRoot()); + reply.getTrace().clear(); + } + this.reply = reply; + } + + /** + * Returns the list of configured recipient {@link Route routes}. This is accessed by client code through a more + * strict api in {@link RoutingContext}. + * + * @return The list of recipients. + */ + List<Route> getRecipients() { + return recipients; + } + + /** + * Returns the list of current child nodes. This is accessed by client code through a more strict api in {@link + * RoutingContext}. + * + * @return The list of children. + */ + List<RoutingNode> getChildren() { + return children; + } + + /** + * Returns the service address of this node. This is attached by the network layer, and should only ever be present + * in leaf nodes. + * + * @return The recipient address. + */ + public ServiceAddress getServiceAddress() { + return serviceAddress; + } + + /** + * Sets the service address of this node. This is called by the network layer as this calls its {@link + * Network#allocServiceAddress(RoutingNode)} method. + * + * @param serviceAddress The recipient address. + */ + public void setServiceAddress(ServiceAddress serviceAddress) { + this.serviceAddress = serviceAddress; + } + + @Override + public void handleReply(Reply reply) { + setReply(reply); + if (routeMetrics != null) { + for (int i = 0; i < reply.getNumErrors(); i++) { + routeMetrics.addError(reply.getError(i)); + } + } + notifyParent(); + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingNodeIterator.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingNodeIterator.java new file mode 100755 index 00000000000..dbf152fdfcf --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingNodeIterator.java @@ -0,0 +1,107 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing;
+
+import com.yahoo.messagebus.Reply;
+
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Implements an iterator for routing nodes. Use {@link RoutingContext#getChildIterator()} to retrieve an instance of
+ * this.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class RoutingNodeIterator {
+
+ // The underlying iterator.
+ private Iterator<RoutingNode> it;
+
+ // The current child entry.
+ private RoutingNode entry;
+
+ /**
+ * Constructs a new iterator based on a given list.
+ *
+ * @param children The list to iterate through.
+ */
+ public RoutingNodeIterator(List<RoutingNode> children) {
+ it = children.iterator();
+ next();
+ }
+
+ /**
+ * Steps to the next child in the map.
+ *
+ * @return This, to allow chaining.
+ */
+ public RoutingNodeIterator next() {
+ entry = it.hasNext() ? it.next() : null;
+ return this;
+ }
+
+ /**
+ * Skips the given number of children.
+ *
+ * @param num The number of children to skip.
+ * @return This, to allow chaining.
+ */
+ public RoutingNodeIterator skip(int num) {
+ for (int i = 0; i < num && isValid(); ++i) {
+ next();
+ }
+ return this;
+ }
+
+ /**
+ * Returns whether or not this iterator is valid.
+ *
+ * @return True if we are still pointing to a valid entry.
+ */
+ public boolean isValid() {
+ return entry != null;
+ }
+
+ /**
+ * Returns the route of the current child.
+ *
+ * @return The route.
+ */
+ public Route getRoute() {
+ return entry.getRoute();
+ }
+
+ /**
+ * Returns whether or not a reply is set in the current child.
+ *
+ * @return True if a reply is available.
+ */
+ public boolean hasReply() {
+ return entry.hasReply();
+ }
+
+ /**
+ * Removes and returns the reply of the current child. This is the correct way of reusing a reply of a child node,
+ * the {@link #getReplyRef()} should be used when just inspecting a child reply.
+ *
+ * @return The reply.
+ */
+ public Reply removeReply() {
+ Reply ret = entry.getReply();
+ ret.getTrace().setLevel(entry.getTrace().getLevel());
+ ret.getTrace().swap(entry.getTrace());
+ entry.setReply(null);
+ return ret;
+ }
+
+ /**
+ * Returns the reply of the current child. It is VERY important that the reply returned by this function is not
+ * reused anywhere. This is a reference to another node's reply, do NOT use it for anything but inspection. If you
+ * want to retrieve and reuse it, call {@link #removeReply()} instead.
+ *
+ * @return The reply.
+ */
+ public Reply getReplyRef() {
+ return entry.getReply();
+ }
+}
diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingPolicy.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingPolicy.java new file mode 100644 index 00000000000..b8fa37b0542 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingPolicy.java @@ -0,0 +1,48 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +/** + * Decides how to choose between candidate recipients of a hop template point. + * <p> + * The routing policy will be given an address and a set of recipients matching this address. It is responsible for + * choosing one of the given recipients each time {@link #select} is called. The routing policy chooses which recipient + * to return each time at its sole discretion, using information in the RoutingContext argument to choose or not as required + * to implement the policy. + * <p> + * Example: + * <ul> + * <li>The given <i>address</i> is <code>a/b/?</code> + * <li>The given <i>recipients</i> are <code>a/b/c, a/b/d and a/b/e</code> - one of these three must be returned on every call to <code>choose</code> + * </ul> + * <p> + * This class is pluggable per template point in the address of a hop. + * + * @author <a href="bratseth@yahoo-inc.com">Jon Bratseth</a> + * @author <a href="bratseth@yahoo-inc.com">Simon Thoresen</a> + */ +public interface RoutingPolicy { + + /** + * This function must choose a set of services that is to receive the given message from a list of possible + * recipients. This is done by adding child routing contexts to the argument object. These children can then be + * iterated and manipulated even before selection pass is concluded. + * + * @param context the complete context for the invocation of this policy. Contains all available data. + */ + public void select(RoutingContext context); + + /** + * This function is called when all replies have arrived for some message. The implementation is responsible for + * merging multiple replies into a single sensible reply. The replies is contained in the child context objects of + * the argument context, and then response must be set in that context. + * + * @param context the complete context for the invocation of this policy. Contains all available data. + */ + public void merge(RoutingContext context); + + /** + * Destroys this factory and frees up any resources it has held. Making further calls on a destroyed + * factory causes a runtime exception. + */ + public void destroy(); +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingSpec.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingSpec.java new file mode 100644 index 00000000000..c9469e8e368 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingSpec.java @@ -0,0 +1,225 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Along with the {@link RoutingTableSpec}, {@link RouteSpec} and {@link HopSpec}, this holds the routing specifications + * for all protocols. The only way a client can configure or alter the settings of a message bus instance is through + * these classes. + * <p> + * This class is the root spec class for configuring message bus routing. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RoutingSpec { + + private final List<RoutingTableSpec> tables = new ArrayList<>(); + private final boolean verify; + + /** + * Creates an empty specification. + */ + public RoutingSpec() { + this(true); + } + + /** + * Creates an empty specification. + * + * @param verify Whether or not this should be verified. + */ + public RoutingSpec(boolean verify) { + this.verify = verify; + } + + /** + * Implements the copy constructor. + * + * @param spec the spec to copy. + */ + public RoutingSpec(RoutingSpec spec) { + verify = spec.verify; + for (RoutingTableSpec table : spec.tables) { + tables.add(new RoutingTableSpec(table)); + } + } + + /** + * Returns whether or not there are routing table specs contained in this. + * + * @return True if there is at least one table. + */ + public boolean hasTables() { + return !tables.isEmpty(); + } + + /** + * Returns the number of routing table specs that are contained in this. + * + * @return The number of routing tables. + */ + public int getNumTables() { + return tables.size(); + } + + /** + * Returns the routing table spec at the given index. + * + * @param i The index of the routing table to return. + * @return The routing table at the given index. + */ + public RoutingTableSpec getTable(int i) { + return tables.get(i); + } + + /** + * Sets the routing table spec at the given index. + * + * @param i The index at which to set the routing table. + * @param table The routing table to set. + * @return This, to allow chaining. + */ + public RoutingSpec setTable(int i, RoutingTableSpec table) { + tables.set(i, table); + return this; + } + + /** + * Adds a routing table spec to the list of tables. + * + * @param table The routing table to add. + * @return This, to allow chaining. + */ + public RoutingSpec addTable(RoutingTableSpec table) { + tables.add(table); + return this; + } + + /** + * Returns the routing table spec at the given index. + * + * @param i The index of the routing table to remove. + * @return The removed routing table. + */ + public RoutingTableSpec removeTable(int i) { + return tables.remove(i); + } + + /** + * Clears the list of routing table specs contained in this. + * + * @return This, to allow chaining. + */ + public RoutingSpec clearTables() { + tables.clear(); + return this; + } + + /** + * Verifies the content of this against the given application. + * + * @param app The application to verify against. + * @param errors The list of errors found. + * @return True if no errors where found. + */ + public boolean verify(ApplicationSpec app, List<String> errors) { + if (verify) { + Map<String, Integer> tableNames = new HashMap<>(); + for (RoutingTableSpec table : tables) { + String name = table.getProtocol(); + + int count = tableNames.containsKey(name) ? tableNames.get(name) : 0; + tableNames.put(name, count + 1); + table.verify(app, errors); + } + for (Map.Entry<String, Integer> entry : tableNames.entrySet()) { + int count = entry.getValue(); + if (count > 1) { + errors.add("Routing table '" + entry.getKey() + "' is defined " + count + " times."); + } + } + } + return errors.isEmpty(); + } + + /** + * Appends the content of this to the given config string builder. + * + * @param cfg The config to add to. + * @param prefix The prefix to use for each add. + */ + public void toConfig(StringBuilder cfg, String prefix) { + int numTables = tables.size(); + if (numTables > 0) { + cfg.append(prefix).append("routingtable[").append(numTables).append("]\n"); + for (int i = 0; i < numTables; ++i) { + tables.get(i).toConfig(cfg, prefix + "routingtable[" + i + "]."); + } + } + } + + /** + * Convert a string value to a quoted value suitable for use in a config string. + * <p> + * Adds double quotes before and after, and adds backslash-escapes to any double quotes that was contained in the + * string. A null pointer will produce the special unquoted string null that the config library will convert back + * to a null pointer. + * + * @param input the String to be escaped + * @return an escaped String + */ + static String toConfigString(String input) { + if (input == null) { + return "null"; + } + StringBuilder ret = new StringBuilder(2 + input.length()); + ret.append("\""); + for (int i = 0, len = input.length(); i < len; ++i) { + if (input.charAt(i) == '\\') { + ret.append("\\\\"); + } else if (input.charAt(i) == '"') { + ret.append("\\\""); + } else if (input.charAt(i) == '\n') { + ret.append("\\n"); + } else if (input.charAt(i) == 0) { + ret.append("\\x00"); + } else { + ret.append(input.charAt(i)); + } + } + ret.append("\""); + return ret.toString(); + } + + // Overrides Object. + @Override + public String toString() { + StringBuilder ret = new StringBuilder(); + toConfig(ret, ""); + return ret.toString(); + } + + // Overrides Object. + @Override + public boolean equals(Object obj) { + if (!(obj instanceof RoutingSpec)) { + return false; + } + RoutingSpec rhs = (RoutingSpec)obj; + if (!tables.equals(rhs.tables)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int result = tables != null ? tables.hashCode() : 0; + result = 31 * result + (verify ? 1 : 0); + return result; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingTable.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingTable.java new file mode 100644 index 00000000000..724dbc2a642 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingTable.java @@ -0,0 +1,264 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * At any time there may only ever be zero or one routing table registered in message bus for each protocol. This class + * contains a list of named hops and routes that may be used to substitute references to these during route resolving. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RoutingTable { + + private final Map<String, HopBlueprint> hops = new LinkedHashMap<String, HopBlueprint>(); + private final Map<String, Route> routes = new LinkedHashMap<String, Route>(); + + /** + * Creates a new routing table based on a given specification. This also verifies the integrity of the table. + * + * @param spec The specification to use. + */ + public RoutingTable(RoutingTableSpec spec) { + for (int i = 0; i < spec.getNumHops(); ++i) { + HopSpec hopSpec = spec.getHop(i); + hops.put(hopSpec.getName(), new HopBlueprint(hopSpec)); + } + for (int i = 0; i < spec.getNumRoutes(); ++i) { + RouteSpec routeSpec = spec.getRoute(i); + Route route = new Route(); + for (int j = 0; j < routeSpec.getNumHops(); ++j) { + route.addHop(Hop.parse(routeSpec.getHop(j))); + } + routes.put(routeSpec.getName(), route); + } + } + + /** + * Returns whether or not there are any hops in this routing table. + * + * @return True if there is at least one hop. + */ + public boolean hasHops() { + return !hops.isEmpty(); + } + + /** + * Returns the number of hops that are contained in this. + * + * @return The number of hops. + */ + public int getNumHops() { + return hops.size(); + } + + /** + * Returns an iterator for the hops of this table. + * + * @return An iterator. + */ + public HopIterator getHopIterator() { + return new HopIterator(hops); + } + + /** + * Returns an iterator for the routes of this table. + * + * @return An iterator. + */ + public RouteIterator getRouteIterator() { + return new RouteIterator(routes); + } + + /** + * Returns whether or not there are any routes in this routing table. + * + * @return True if there is at least one route. + */ + public boolean hasRoutes() { + return !routes.isEmpty(); + } + + /** + * Returns the number of routes that are contained in this. + * + * @return The number of routes. + */ + public int getNumRoutes() { + return routes.size(); + } + + /** + * Returns whether or not there exists a named hop in this. + * + * @param name The name of the hop to look for. + * @return True if the named hop exists. + */ + public boolean hasHop(String name) { + return hops.containsKey(name); + } + + /** + * Returns the named hop, may be null. + * + * @param name The name of the hop to return. + * @return The hop implementation object. + */ + public HopBlueprint getHop(String name) { + return hops.get(name); + } + + /** + * Returns whether or not there exists a named route in this. + * + * @param name The name of the route to look for. + * @return True if the named route exists. + */ + public boolean hasRoute(String name) { + return routes.containsKey(name); + } + + /** + * Returns the named route, may be null. + * + * @param name The name of the route to return. + * @return The route implementation object. + */ + public Route getRoute(String name) { + return routes.get(name); + } + + @Override + public String toString() { + StringBuilder ret = new StringBuilder("RoutingTable(hops = { "); + int i = 0; + for (String name : hops.keySet()) { + ret.append("'").append(name).append("' : ").append(hops.get(name)); + if (i++ < hops.size() - 1) { + ret.append(", "); + } + } + ret.append(" }, routes = { "); + i = 0; + for (String name : routes.keySet()) { + ret.append("'").append(name).append("' : ").append(routes.get(name)); + if (i++ < routes.size()) { + ret.append(", "); + } + } + ret.append(" })"); + return ret.toString(); + } + + /** + * Implements an iterator for the hops of this. Use {@link RoutingTable#getHopIterator()} + * to retrieve an instance of this. + */ + public static class HopIterator { + + private Iterator<Map.Entry<String, HopBlueprint>> it; + private Map.Entry<String, HopBlueprint> entry; + + /** + * Constructs a new iterator based on a given map. This is private so that only a {@link RoutingTable} can + * create one. + * + * @param hops The map to iterate through. + */ + private HopIterator(Map<String, HopBlueprint> hops) { + it = hops.entrySet().iterator(); + next(); + } + + /** + * Steps to the next hop in the map. + */ + public void next() { + entry = it.hasNext() ? it.next() : null; + } + + /** + * Returns whether or not this iterator is valid. + * + * @return True if valid. + */ + public boolean isValid() { + return entry != null; + } + + /** + * Returns the name of the current hop. + * + * @return The name. + */ + public String getName() { + return entry.getKey(); + } + + /** + * Returns the current hop. + * + * @return The hop. + */ + public HopBlueprint getHop() { + return entry.getValue(); + } + } + + /** + * Implements an iterator for the routes of this. Use {@link RoutingTable#getRouteIterator()} + * to retrieve an instance of this. + */ + public static class RouteIterator { + + private Iterator<Map.Entry<String, Route>> it; + private Map.Entry<String, Route> entry; + + /** + * Constructs a new iterator based on a given map. This is private so that only a {@link RoutingTable} can + * create one. + * + * @param routes The map to iterate through. + */ + private RouteIterator(Map<String, Route> routes) { + it = routes.entrySet().iterator(); + next(); + } + + /** + * Steps to the next route in the map. + */ + public void next() { + entry = it.hasNext() ? it.next() : null; + } + + /** + * Returns whether or not this iterator is valid. + * + * @return True if valid. + */ + public boolean isValid() { + return entry != null; + } + + /** + * Returns the name of the current route. + * + * @return The name. + */ + public String getName() { + return entry.getKey(); + } + + /** + * Returns the current route. + * + * @return The route. + */ + public Route getRoute() { + return entry.getValue(); + } + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingTableSpec.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingTableSpec.java new file mode 100644 index 00000000000..c2b3f736c93 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/RoutingTableSpec.java @@ -0,0 +1,391 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +import com.yahoo.text.Utf8String; + +import java.util.*; + +/** + * Along with the {@link RoutingSpec}, {@link RouteSpec} and {@link HopSpec}, this holds the routing specifications for + * all protocols. The only way a client can configure or alter the settings of a message bus instance is through these + * classes. + * <p> + * This class contains the spec for a single routing table, which corresponds to exactly one protocol. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RoutingTableSpec { + + private final String protocol; + private final List<HopSpec> hops = new ArrayList<>(); + private final List<RouteSpec> routes = new ArrayList<>(); + private final boolean verify; + + /** + * Creates a new routing table specification for a named protocol. + * + * @param protocol The name of the protocol that this belongs to. + */ + public RoutingTableSpec(String protocol) { + this(protocol, true); + } + /** + * Creates a new routing table specification for a named protocol. + * + * @param protocol The name of the protocol that this belongs to. + */ + public RoutingTableSpec(Utf8String protocol) { + this(protocol.toString(), true); + } + + /** + * Creates a new routing table specification for a named protocol. + * + * @param protocol The name of the protocol that this belongs to. + * @param verify Whether or not this should be verified. + */ + public RoutingTableSpec(String protocol, boolean verify) { + this.protocol = protocol; + this.verify = verify; + } + + /** + * Implements the copy constructor. + * + * @param obj The object to copy. + */ + public RoutingTableSpec(RoutingTableSpec obj) { + this.protocol = obj.protocol; + this.verify = obj.verify; + for (HopSpec hop : obj.hops) { + hops.add(new HopSpec(hop)); + } + for (RouteSpec route : obj.routes) { + routes.add(new RouteSpec(route)); + } + } + + /** + * Returns the name of the protocol that this is the routing table for. + * + * @return The protocol name. + */ + public String getProtocol() { + return protocol; + } + + /** + * Returns whether or not there are any hop specs contained in this. + * + * @return True if there is at least one hop. + */ + public boolean hasHops() { + return !hops.isEmpty(); + } + + /** + * Returns whether or not there is a named hop spec contained in this. + * + * @param hopName The hop name to check for. + * @return True if the hop exists. + */ + public boolean hasHop(String hopName) { + for (HopSpec hop : hops) { + if (hop.getName().equals(hopName)) { + return true; + } + } + return false; + } + + /** + * Returns the number of hops that are contained in this table. + * + * @return The number of hops. + */ + public int getNumHops() { + return hops.size(); + } + + /** + * Returns the hop spec at the given index. + * + * @param i The index of the hop to return. + * @return The hop at the given position. + */ + public HopSpec getHop(int i) { + return hops.get(i); + } + + /** + * Adds the given hop spec to this. + * + * @param hop The hop to add. + * @return This, to allow chaining. + */ + public RoutingTableSpec addHop(HopSpec hop) { + hops.add(hop); + return this; + } + + /** + * Sets the hop spec at the given index. + * + * @param i The index at which to set the hop. + * @param hop The hop to set. + * @return This, to allow chaining. + */ + public RoutingTableSpec setHop(int i, HopSpec hop) { + hops.set(i, hop); + return this; + } + + /** + * Removes the hop spec at the given index. + * + * @param i The index of the hop to remove. + * @return The removed hop. + */ + public HopSpec removeHop(int i) { + return hops.remove(i); + } + + /** + * Clears the list of hop specs contained in this. + * + * @return This, to allow chaining. + */ + public RoutingTableSpec clearHops() { + hops.clear(); + return this; + } + + /** + * Returns whether or not there are any route specs contained in this. + * + * @return True if there is at least one route. + */ + public boolean hasRoutes() { + return !routes.isEmpty(); + } + + /** + * Returns whether or not there is a named route spec contained in this. + * + * @param routeName The hop name to check for. + * @return True if the hop exists. + */ + public boolean hasRoute(String routeName) { + for (RouteSpec route : routes) { + if (route.getName().equals(routeName)) { + return true; + } + } + return false; + } + + /** + * Returns the number of route specs contained in this. + * + * @return The number of routes. + */ + public int getNumRoutes() { + return routes.size(); + } + + /** + * Returns the route spec at the given index. + * + * @param i The index of the route to return. + * @return The route at the given index. + */ + public RouteSpec getRoute(int i) { + return routes.get(i); + } + + /** + * Adds a route spec to this. + * + * @param route The route to add. + * @return This, to allow chaining. + */ + public RoutingTableSpec addRoute(RouteSpec route) { + routes.add(route); + return this; + } + + /** + * Sets the route spec at the given index. + * + * @param i The index at which to set the route. + * @param route The route to set. + * @return This, to allow chaining. + */ + public RoutingTableSpec setRoute(int i, RouteSpec route) { + routes.set(i, route); + return this; + } + + /** + * Removes a route spec at a given index. + * + * @param i The index of the route to remove. + * @return The removed route. + */ + public RouteSpec removeRoute(int i) { + return routes.remove(i); + } + + /** + * Clears the list of routes that are contained in this. + * + * @return This, to allow chaining. + */ + public RoutingTableSpec clearRoutes() { + routes.clear(); + return this; + } + + /** + * A convenience function to add a new hop to this routing table. + * + * @param name A protocol-unique name for this hop. + * @param selector A string that represents the selector for this hop. + * @param recipients A list of recipients for this hop. + * @return This, to allow chaining. + */ + public RoutingTableSpec addHop(String name, String selector, List<String> recipients) { + return addHop(new HopSpec(name, selector).addRecipients(recipients)); + } + + /** + * A convenience function to add a new route to this routing table. + * + * @param name A protocol-unique name for this route. + * @param hops A list of hops for this route. + * @return This, to allow chaining. + */ + public RoutingTableSpec addRoute(String name, List<String> hops) { + return addRoute(new RouteSpec(name).addHops(hops)); + } + + /** + * Verifies the content of this against the given application. + * + * @param app The application to verify against. + * @param errors The list of errors found. + * @return True if no errors where found. + */ + public boolean verify(ApplicationSpec app, List<String> errors) { + if (verify) { + // Verify and count hops. + Map<String, Integer> hopNames = new HashMap<String, Integer>(); + for (HopSpec hop : hops) { + String name = hop.getName(); + int count = hopNames.containsKey(name) ? hopNames.get(name) : 0; + hopNames.put(name, count + 1); + hop.verify(app, this, errors); + } + for (Map.Entry<String, Integer> entry : hopNames.entrySet()) { + int count = entry.getValue(); + if (count > 1) { + errors.add("Hop '" + entry.getKey() + "' in routing table '" + protocol + "' is defined " + + count + " times."); + } + } + + // Verify and count routes. + Map<String, Integer> routeNames = new HashMap<String, Integer>(); + for (RouteSpec route : routes) { + String name = route.getName(); + int count = routeNames.containsKey(name) ? routeNames.get(name) : 0; + routeNames.put(name, count + 1); + route.verify(app, this, errors); + } + for (Map.Entry<String, Integer> entry : routeNames.entrySet()) { + int count = entry.getValue(); + if (count > 1) { + errors.add("Route '" + entry.getKey() + "' in routing table '" + protocol + "' is defined " + + count + " times."); + } + } + } + return errors.isEmpty(); + } + + /** + * Sorts the hops and routes of this table by name. This is useful for generating a stable config for testing. + */ + public void sort() { + Collections.sort(hops, new Comparator<HopSpec>() { + public int compare(HopSpec lhs, HopSpec rhs) { + return lhs.getName().compareTo(rhs.getName()); + } + }); + Collections.sort(routes, new Comparator<RouteSpec>() { + public int compare(RouteSpec lhs, RouteSpec rhs) { + return lhs.getName().compareTo(rhs.getName()); + } + }); + } + + /** + * Appends the content of this to the given config string builder. + * + * @param cfg The config to add to. + * @param prefix The prefix to use for each add. + */ + public void toConfig(StringBuilder cfg, String prefix) { + cfg.append(prefix).append("protocol ").append(RoutingSpec.toConfigString(protocol)).append("\n"); + int numHops = hops.size(); + if (numHops > 0) { + cfg.append(prefix).append("hop[").append(numHops).append("]\n"); + for (int i = 0; i < numHops; ++i) { + hops.get(i).toConfig(cfg, prefix + "hop[" + i + "]."); + } + } + int numRoutes = routes.size(); + if (numRoutes > 0) { + cfg.append(prefix).append("route[").append(numRoutes).append("]\n"); + for (int i = 0; i < numRoutes; ++i) { + routes.get(i).toConfig(cfg, prefix + "route[" + i + "]."); + } + } + } + + // Overrides Object. + @Override + public String toString() { + StringBuilder ret = new StringBuilder(); + toConfig(ret, ""); + return ret.toString(); + } + + // Overrides Object. + @Override + public boolean equals(Object obj) { + if (!(obj instanceof RoutingTableSpec)) { + return false; + } + RoutingTableSpec rhs = (RoutingTableSpec)obj; + if (!protocol.equals(rhs.protocol)) { + return false; + } + if (!hops.equals(rhs.hops)) { + return false; + } + if (!routes.equals(rhs.routes)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int result = protocol != null ? protocol.hashCode() : 0; + result = 31 * result + (hops != null ? hops.hashCode() : 0); + result = 31 * result + (routes != null ? routes.hashCode() : 0); + result = 31 * result + (verify ? 1 : 0); + return result; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/TcpDirective.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/TcpDirective.java new file mode 100755 index 00000000000..dd0f6a47596 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/TcpDirective.java @@ -0,0 +1,100 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing;
+
+/**
+ * This class represents a tcp directive within a {@link Hop}'s selector. This is a connection string used to establish
+ * a direct connection to a host, bypassing service lookups through Slobrok.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class TcpDirective implements HopDirective {
+
+ private final String host;
+ private final int port;
+ private final String session;
+
+ /**
+ * Constructs a new directive to route directly to a tcp address.
+ *
+ * @param host The host name to connect to.
+ * @param port The port to connect to.
+ * @param session The session to route to.
+ */
+ public TcpDirective(String host, int port, String session) {
+ this.host = host;
+ this.port = port;
+ this.session = session;
+ }
+
+ @Override
+ public boolean matches(HopDirective dir) {
+ if (!(dir instanceof TcpDirective)) {
+ return false;
+ }
+ TcpDirective rhs = (TcpDirective)dir;
+ return host.equals(rhs.host) && port == rhs.port && session.equals(rhs.session);
+ }
+
+ /**
+ * Returns the host to connect to. This may be an ip address or a name.
+ *
+ * @return The host.
+ */
+ public String getHost() {
+ return host;
+ }
+
+ /**
+ * Returns the port to connect to on the remove host.
+ *
+ * @return The port number.
+ */
+ public int getPort() {
+ return port;
+ }
+
+ /**
+ * Returns the name of the session to route to.
+ *
+ * @return The session name.
+ */
+ public String getSession() {
+ return session;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof TcpDirective)) {
+ return false;
+ }
+ TcpDirective rhs = (TcpDirective)obj;
+ if (!host.equals(rhs.host)) {
+ return false;
+ }
+ if (port != rhs.port) {
+ return false;
+ }
+ if (!session.equals(rhs.session)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "tcp/" + host + ":" + port + "/" + session;
+ }
+
+ @Override
+ public String toDebugString() {
+ return "TcpDirective(host = '" + host + "', port = " + port + ", session = '" + session + "')";
+ }
+
+ @Override
+ public int hashCode() {
+ int result = host != null ? host.hashCode() : 0;
+ result = 31 * result + port;
+ result = 31 * result + (session != null ? session.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/VerbatimDirective.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/VerbatimDirective.java new file mode 100755 index 00000000000..f2f95fa53f8 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/VerbatimDirective.java @@ -0,0 +1,65 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing;
+
+/**
+ * This class represents a verbatim match within a {@link Hop}'s selector. This is nothing more than a string that will
+ * be used as-is when performing service name lookups.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class VerbatimDirective implements HopDirective {
+
+ private final String image;
+
+ /**
+ * Constructs a new verbatim selector item for a given image.
+ *
+ * @param image The image to assign to this.
+ */
+ public VerbatimDirective(String image) {
+ this.image = image;
+ }
+
+ @Override
+ public boolean matches(HopDirective dir) {
+
+ return dir instanceof VerbatimDirective && image.equals(((VerbatimDirective)dir).image);
+ }
+
+ /**
+ * Returns the image to which this is a verbatim match.
+ *
+ * @return The image.
+ */
+ public String getImage() {
+ return image;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof VerbatimDirective)) {
+ return false;
+ }
+ VerbatimDirective rhs = (VerbatimDirective)obj;
+ if (!image.equals(rhs.image)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return image != null ? image.hashCode() : 0;
+ }
+
+ @Override
+ public String toString() {
+ return image;
+ }
+
+ @Override
+ public String toDebugString() {
+ return "VerbatimDirective(image = '" + image + "')";
+ }
+}
+
diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/package-info.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/package-info.java new file mode 100644 index 00000000000..bedb9d96f26 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/package-info.java @@ -0,0 +1,8 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * This package contains all classes and interfaces that concern routing over message bus. + */ +@ExportPackage +package com.yahoo.messagebus.routing; + +import com.yahoo.osgi.annotation.ExportPackage;
\ No newline at end of file diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/test/CustomPolicy.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/test/CustomPolicy.java new file mode 100755 index 00000000000..1627679234a --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/test/CustomPolicy.java @@ -0,0 +1,57 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing.test; + +import com.yahoo.messagebus.EmptyReply; +import com.yahoo.messagebus.Reply; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.messagebus.routing.RoutingContext; +import com.yahoo.messagebus.routing.RoutingNodeIterator; +import com.yahoo.messagebus.routing.RoutingPolicy; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class CustomPolicy implements RoutingPolicy { + + private boolean selectOnRetry; + private final List<Integer> consumableErrors = new ArrayList<Integer>(); + private final List<Route> routes = new ArrayList<Route>(); + + public CustomPolicy(boolean selectOnRetry, List<Integer> consumableErrors, List<Route> routes) { + this.selectOnRetry = selectOnRetry; + this.consumableErrors.addAll(consumableErrors); + this.routes.addAll(routes); + } + + public void select(RoutingContext context) { + context.trace(1, "Selecting " + routes + "."); + context.setSelectOnRetry(selectOnRetry); + for (int e : consumableErrors) { + context.addConsumableError(e); + } + context.addChildren(routes); + } + + public void merge(RoutingContext context) { + List<String> lst = new ArrayList<String>(); + Reply ret = new EmptyReply(); + for (RoutingNodeIterator it = context.getChildIterator(); + it.isValid(); it.next()) + { + lst.add(it.getRoute().toString()); + Reply reply = it.getReplyRef(); + for (int i = 0; i < reply.getNumErrors(); ++i) { + ret.addError(reply.getError(i)); + } + } + context.setReply(ret); + context.trace(1, "Merged " + lst + "."); + } + + @Override + public void destroy() { + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/routing/test/CustomPolicyFactory.java b/messagebus/src/main/java/com/yahoo/messagebus/routing/test/CustomPolicyFactory.java new file mode 100755 index 00000000000..60f20b55f8d --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/routing/test/CustomPolicyFactory.java @@ -0,0 +1,52 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing.test;
+
+import com.yahoo.messagebus.routing.Route;
+import com.yahoo.messagebus.routing.RoutingPolicy;
+import com.yahoo.messagebus.test.SimpleProtocol;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class CustomPolicyFactory implements SimpleProtocol.PolicyFactory {
+
+ private boolean selectOnRetry;
+ private final List<Integer> consumableErrors = new ArrayList<Integer>();
+
+ public CustomPolicyFactory() {
+ this(true);
+ }
+
+ public CustomPolicyFactory(boolean selectOnRetry) {
+ this(selectOnRetry, new ArrayList<Integer>());
+ }
+
+ public CustomPolicyFactory(boolean selectOnRetry, int consumableError) {
+ this(selectOnRetry, Arrays.asList(consumableError));
+ }
+
+ public CustomPolicyFactory(boolean selectOnRetry, List<Integer> consumableErrors) {
+ this.selectOnRetry = selectOnRetry;
+ this.consumableErrors.addAll(consumableErrors);
+ }
+
+ public RoutingPolicy create(String param) {
+ return new CustomPolicy(selectOnRetry, consumableErrors, parseRoutes(param));
+ }
+
+ public static List<Route> parseRoutes(String routes) {
+ List<Route> ret = new ArrayList<Route>();
+ if (routes != null && !routes.isEmpty()) {
+ for (String route : routes.split(",")) {
+ Route r = Route.parse(route);
+ assert(route.equals(r.toString()));
+ ret.add(r);
+ }
+ }
+ return ret;
+ }
+}
diff --git a/messagebus/src/main/java/com/yahoo/messagebus/test/QueueAdapter.java b/messagebus/src/main/java/com/yahoo/messagebus/test/QueueAdapter.java new file mode 100644 index 00000000000..25ea51e05dc --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/test/QueueAdapter.java @@ -0,0 +1,56 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.test; + +import com.yahoo.messagebus.*; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author <a href="mailto:havardpe@yahoo-inc.com">Haavard Pettersen</a> + */ +public class QueueAdapter implements MessageHandler, ReplyHandler { + + private final Queue<Routable> queue = new ConcurrentLinkedQueue<>(); + + @Override + public void handleMessage(Message message) { + queue.offer(message); + } + + @Override + public void handleReply(Reply reply) { + queue.offer(reply); + } + + public Routable dequeue() { + return queue.poll(); + } + + public boolean isEmpty() { + return queue.isEmpty(); + } + + public int size() { + return queue.size(); + } + + public boolean waitSize(int size, int seconds) { + long timeout = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(seconds); + while (true) { + if (size() == size) { + return true; + } + if (System.currentTimeMillis() > timeout) { + return false; + } + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/test/Receptor.java b/messagebus/src/main/java/com/yahoo/messagebus/test/Receptor.java new file mode 100644 index 00000000000..f7b3d5ee9c0 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/test/Receptor.java @@ -0,0 +1,51 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.test; + +import com.yahoo.messagebus.Message; +import com.yahoo.messagebus.MessageHandler; +import com.yahoo.messagebus.Reply; +import com.yahoo.messagebus.ReplyHandler; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * @author <a href="mailto:havardpe@yahoo-inc.com">Haavard Pettersen</a> + */ +public class Receptor implements MessageHandler, ReplyHandler { + + private final BlockingQueue<Message> msg = new LinkedBlockingQueue<>(); + private final BlockingQueue<Reply> reply = new LinkedBlockingQueue<>(); + + public void reset() { + msg.clear(); + reply.clear(); + } + + public void handleMessage(Message msg) { + this.msg.add(msg); + } + + public void handleReply(Reply reply) { + this.reply.add(reply); + } + + public Message getMessage(int seconds) { + try { + return msg.poll(seconds, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } + } + + public Reply getReply(int seconds) { + try { + return reply.poll(seconds, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/test/SimpleMessage.java b/messagebus/src/main/java/com/yahoo/messagebus/test/SimpleMessage.java new file mode 100644 index 00000000000..17e4375fbca --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/test/SimpleMessage.java @@ -0,0 +1,40 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.test; + +import com.yahoo.messagebus.Message; +import com.yahoo.text.Utf8String; + +/** + * @author <a href="mailto:havardpe@yahoo-inc.com">Haavard Pettersen</a> + */ +public class SimpleMessage extends Message { + + private String value; + + public SimpleMessage(String value) { + this.value = value; + } + + @Override + public int getType() { + return SimpleProtocol.MESSAGE; + } + + @Override + public Utf8String getProtocol() { + return SimpleProtocol.NAME; + } + + @Override + public int getApproxSize() { + return value.length(); + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/test/SimpleProtocol.java b/messagebus/src/main/java/com/yahoo/messagebus/test/SimpleProtocol.java new file mode 100644 index 00000000000..868662f9634 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/test/SimpleProtocol.java @@ -0,0 +1,100 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.test; + +import com.yahoo.component.Version; +import com.yahoo.messagebus.EmptyReply; +import com.yahoo.messagebus.Protocol; +import com.yahoo.messagebus.Reply; +import com.yahoo.messagebus.Routable; +import com.yahoo.messagebus.metrics.MetricSet; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.messagebus.routing.RoutingContext; +import com.yahoo.messagebus.routing.RoutingNodeIterator; +import com.yahoo.messagebus.routing.RoutingPolicy; +import com.yahoo.text.Utf8; +import com.yahoo.text.Utf8String; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class SimpleProtocol implements Protocol { + + public static final Utf8String NAME = new Utf8String("Simple"); + public static final int MESSAGE = 1; + public static final int REPLY = 2; + private final Map<String, PolicyFactory> policies = new HashMap<String, PolicyFactory>(); + + @Override + public String getName() { + return NAME.toString(); + } + + @Override + public RoutingPolicy createPolicy(String name, String param) { + if (policies.containsKey(name)) { + return policies.get(name).create(param); + } + return null; + } + + @Override + public Routable decode(Version version, byte[] data) { + String str = Utf8.toString(data); + if (str.length() < 1) { + return null; + } + char c = str.charAt(0); + if (c == 'M') { + return new SimpleMessage(str.substring(1)); + } + if (c == 'R') { + return new SimpleReply(str.substring(1)); + } + return null; + } + + @Override + public byte[] encode(Version version, Routable routable) { + if (routable.getType() == MESSAGE) { + return Utf8.toBytes("M" + ((SimpleMessage)routable).getValue()); + } else if (routable.getType() == REPLY) { + return Utf8.toBytes("R" + ((SimpleReply)routable).getValue()); + } else { + return null; + } + } + + @Override + public MetricSet getMetrics() { + return null; + } + + /** + * Registers a policy factory with this protocol under a given name. Whenever a policy is requested that matches + * this name, the factory is invoked. + * + * @param name The name of the policy. + * @param factory The policy factory. + */ + public void addPolicyFactory(String name, PolicyFactory factory) { + policies.put(name, factory); + } + + /** + * Defines a policy factory interface that tests can use to register arbitrary policies with this protocol. + */ + public interface PolicyFactory { + + /** + * Creates a new instance of the routing policy that this factory encapsulates. + * + * @param param The param for the policy constructor. + * @return The routing policy created. + */ + public RoutingPolicy create(String param); + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/test/SimpleReply.java b/messagebus/src/main/java/com/yahoo/messagebus/test/SimpleReply.java new file mode 100644 index 00000000000..47c0a6e48a6 --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/test/SimpleReply.java @@ -0,0 +1,33 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.test; + +import com.yahoo.messagebus.Reply; +import com.yahoo.text.Utf8String; + +/** + * @author <a href="mailto:havardpe@yahoo-inc.com">Haavard Pettersen</a> + */ +public class SimpleReply extends Reply { + + private String value; + + public SimpleReply(String value) { + this.value = value; + } + + public int getType() { + return SimpleProtocol.REPLY; + } + + public Utf8String getProtocol() { + return SimpleProtocol.NAME; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/messagebus/src/main/java/com/yahoo/messagebus/test/package-info.java b/messagebus/src/main/java/com/yahoo/messagebus/test/package-info.java new file mode 100644 index 00000000000..7f17252865a --- /dev/null +++ b/messagebus/src/main/java/com/yahoo/messagebus/test/package-info.java @@ -0,0 +1,6 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * This package contains utility classes for the unit tests in the com.yahoo.messagebus package. + */ +@com.yahoo.api.annotations.PackageMarker +package com.yahoo.messagebus.test; diff --git a/messagebus/src/test/files/.gitignore b/messagebus/src/test/files/.gitignore new file mode 100644 index 00000000000..b1f2d513ab4 --- /dev/null +++ b/messagebus/src/test/files/.gitignore @@ -0,0 +1 @@ +test.cfg diff --git a/messagebus/src/test/java/com/yahoo/messagebus/ChokeTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/ChokeTestCase.java new file mode 100755 index 00000000000..cde801d81f2 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/ChokeTestCase.java @@ -0,0 +1,169 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.jrt.ListenFailedException; +import com.yahoo.jrt.slobrok.server.Slobrok; +import com.yahoo.messagebus.network.Identity; +import com.yahoo.messagebus.network.rpc.RPCNetworkParams; +import com.yahoo.messagebus.network.rpc.test.TestServer; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.messagebus.test.Receptor; +import com.yahoo.messagebus.test.SimpleMessage; +import com.yahoo.messagebus.test.SimpleProtocol; +import junit.framework.TestCase; + +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ChokeTestCase extends TestCase { + + //////////////////////////////////////////////////////////////////////////////// + // + // Setup + // + //////////////////////////////////////////////////////////////////////////////// + + Slobrok slobrok; + TestServer srcServer, dstServer; + SourceSession srcSession; + DestinationSession dstSession; + + @Override + public void setUp() throws ListenFailedException, UnknownHostException { + slobrok = new Slobrok(); + dstServer = new TestServer(new MessageBusParams().addProtocol(new SimpleProtocol()), + new RPCNetworkParams().setIdentity(new Identity("dst")).setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok))); + dstSession = dstServer.mb.createDestinationSession(new DestinationSessionParams().setName("session").setMessageHandler(new Receptor())); + srcServer = new TestServer(new MessageBusParams().setRetryPolicy(null).addProtocol(new SimpleProtocol()), + new RPCNetworkParams().setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok))); + srcSession = srcServer.mb.createSourceSession( + new SourceSessionParams().setTimeout(600.0).setThrottlePolicy(null).setReplyHandler(new Receptor())); + assertTrue(srcServer.waitSlobrok("dst/session", 1)); + } + + @Override + public void tearDown() { + slobrok.stop(); + dstSession.destroy(); + dstServer.destroy(); + srcSession.destroy(); + srcServer.destroy(); + } + + //////////////////////////////////////////////////////////////////////////////// + // + // Tests + // + //////////////////////////////////////////////////////////////////////////////// + + public void testMaxCount() { + int max = 10; + dstServer.mb.setMaxPendingCount(max); + List<Message> lst = new ArrayList<>(); + for (int i = 0; i < max * 2; ++i) { + if (i < max) { + assertEquals(i, dstServer.mb.getPendingCount()); + } else { + assertEquals(max, dstServer.mb.getPendingCount()); + } + assertTrue(srcSession.send(createMessage("msg"), Route.parse("dst/session")).isAccepted()); + if (i < max) { + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + lst.add(msg); + } else { + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.SESSION_BUSY, reply.getError(0).getCode()); + } + } + for (int i = 0; i < 5; ++i) { + Message msg = lst.remove(0); + dstSession.acknowledge(msg); + + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + assertFalse(reply.hasErrors()); + assertNotNull(msg = reply.getMessage()); + assertTrue(srcSession.send(msg, Route.parse("dst/session")).isAccepted()); + + assertNotNull(msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60)); + lst.add(msg); + } + while (!lst.isEmpty()) { + assertEquals(lst.size(), dstServer.mb.getPendingCount()); + Message msg = lst.remove(0); + dstSession.acknowledge(msg); + + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + assertFalse(reply.hasErrors()); + } + assertEquals(0, dstServer.mb.getPendingCount()); + } + + public void testMaxSize() { + int size = createMessage("msg").getApproxSize(); + int max = size * 10; + dstServer.mb.setMaxPendingSize(max); + List<Message> lst = new ArrayList<>(); + for (int i = 0; i < max * 2; i += size) { + if (i < max) { + assertEquals(i, dstServer.mb.getPendingSize()); + } else { + assertEquals(max, dstServer.mb.getPendingSize()); + } + assertTrue(srcSession.send(createMessage("msg"), Route.parse("dst/session")).isAccepted()); + if (i < max) { + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + lst.add(msg); + } else { + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.SESSION_BUSY, reply.getError(0).getCode()); + } + } + for (int i = 0; i < 5; ++i) { + Message msg = lst.remove(0); + dstSession.acknowledge(msg); + + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + assertFalse(reply.hasErrors()); + assertNotNull(msg = reply.getMessage()); + assertTrue(srcSession.send(msg, Route.parse("dst/session")).isAccepted()); + + assertNotNull(msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60)); + lst.add(msg); + } + while (!lst.isEmpty()) { + assertEquals(size * lst.size(), dstServer.mb.getPendingSize()); + Message msg = lst.remove(0); + dstSession.acknowledge(msg); + + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + assertFalse(reply.hasErrors()); + } + assertEquals(0, dstServer.mb.getPendingSize()); + } + + //////////////////////////////////////////////////////////////////////////////// + // + // Utilities + // + //////////////////////////////////////////////////////////////////////////////// + + private static Message createMessage(String msg) { + Message ret = new SimpleMessage(msg); + ret.getTrace().setLevel(9); + return ret; + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/ConfigAgentTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/ConfigAgentTestCase.java new file mode 100755 index 00000000000..f6b21e26030 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/ConfigAgentTestCase.java @@ -0,0 +1,199 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.config.subscription.ConfigSet; +import com.yahoo.config.subscription.ConfigURI; +import com.yahoo.messagebus.routing.HopSpec; +import com.yahoo.messagebus.routing.RouteSpec; +import com.yahoo.messagebus.routing.RoutingSpec; +import com.yahoo.messagebus.routing.RoutingTableSpec; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ConfigAgentTestCase { + + @Rule + public TemporaryFolder tmpFolder = new TemporaryFolder(); + + @Test + public void testRoutingConfig() throws InterruptedException, IOException { + LocalHandler handler = new LocalHandler(); + assertFalse(testHalf(handler.spec)); + assertFalse(testFull(handler.spec)); + + ConfigSet set = new ConfigSet(); + set.addBuilder("test", writeFull()); + + ConfigAgent agent = new ConfigAgent(ConfigURI.createFromIdAndSource("test", set), handler); + assertFalse(testHalf(handler.spec)); + assertFalse(testFull(handler.spec)); + agent.subscribe(); + assertFalse(testHalf(handler.spec)); + assertTrue(testFull(handler.spec)); + + handler.reset(); + set.addBuilder("test", writeHalf()); + assertTrue(handler.await(120, TimeUnit.SECONDS)); + assertTrue(testHalf(handler.spec)); + assertFalse(testFull(handler.spec)); + + handler.reset(); + set.addBuilder("test", writeFull()); + assertTrue(handler.await(120, TimeUnit.SECONDS)); + assertTrue(testFull(handler.spec)); + assertFalse(testHalf(handler.spec)); + } + + private boolean testHalf(RoutingSpec spec) { + if (spec.getNumTables() != 1) { + return false; + } + assertTables(spec, 1); + return true; + } + + private boolean testFull(RoutingSpec spec) { + if (spec.getNumTables() != 2) { + return false; + } + assertTables(spec, 2); + return true; + } + + private void assertTables(RoutingSpec spec, int numTables) { + assertEquals(numTables, spec.getNumTables()); + if (numTables > 0) { + assertEquals("foo", spec.getTable(0).getProtocol()); + assertEquals(2, spec.getTable(0).getNumHops()); + assertEquals("foo-h1", spec.getTable(0).getHop(0).getName()); + assertEquals("foo-h1-sel", spec.getTable(0).getHop(0).getSelector()); + assertEquals(2, spec.getTable(0).getHop(0).getNumRecipients()); + assertEquals("foo-h1-r1", spec.getTable(0).getHop(0).getRecipient(0)); + assertEquals("foo-h1-r2", spec.getTable(0).getHop(0).getRecipient(1)); + assertEquals(true, spec.getTable(0).getHop(0).getIgnoreResult()); + assertEquals("foo-h2", spec.getTable(0).getHop(1).getName()); + assertEquals("foo-h2-sel", spec.getTable(0).getHop(1).getSelector()); + assertEquals(2, spec.getTable(0).getHop(1).getNumRecipients()); + assertEquals("foo-h2-r1", spec.getTable(0).getHop(1).getRecipient(0)); + assertEquals("foo-h2-r2", spec.getTable(0).getHop(1).getRecipient(1)); + assertEquals(2, spec.getTable(0).getNumRoutes()); + assertEquals("foo-r1", spec.getTable(0).getRoute(0).getName()); + assertEquals(2, spec.getTable(0).getRoute(0).getNumHops()); + assertEquals("foo-h1", spec.getTable(0).getRoute(0).getHop(0)); + assertEquals("foo-h2", spec.getTable(0).getRoute(0).getHop(1)); + assertEquals("foo-r2", spec.getTable(0).getRoute(1).getName()); + assertEquals(2, spec.getTable(0).getRoute(1).getNumHops()); + assertEquals("foo-h2", spec.getTable(0).getRoute(1).getHop(0)); + assertEquals("foo-h1", spec.getTable(0).getRoute(1).getHop(1)); + } + if (numTables > 1) { + assertEquals("bar", spec.getTable(1).getProtocol()); + assertEquals(2, spec.getTable(1).getNumHops()); + assertEquals("bar-h1", spec.getTable(1).getHop(0).getName()); + assertEquals("bar-h1-sel", spec.getTable(1).getHop(0).getSelector()); + assertEquals(2, spec.getTable(1).getHop(0).getNumRecipients()); + assertEquals("bar-h1-r1", spec.getTable(1).getHop(0).getRecipient(0)); + assertEquals("bar-h1-r2", spec.getTable(1).getHop(0).getRecipient(1)); + assertEquals("bar-h2", spec.getTable(1).getHop(1).getName()); + assertEquals("bar-h2-sel", spec.getTable(1).getHop(1).getSelector()); + assertEquals(2, spec.getTable(1).getHop(1).getNumRecipients()); + assertEquals("bar-h2-r1", spec.getTable(1).getHop(1).getRecipient(0)); + assertEquals("bar-h2-r2", spec.getTable(1).getHop(1).getRecipient(1)); + assertEquals(2, spec.getTable(1).getNumRoutes()); + assertEquals("bar-r1", spec.getTable(1).getRoute(0).getName()); + assertEquals(2, spec.getTable(1).getRoute(0).getNumHops()); + assertEquals("bar-h1", spec.getTable(1).getRoute(0).getHop(0)); + assertEquals("bar-h2", spec.getTable(1).getRoute(0).getHop(1)); + assertEquals("bar-r2", spec.getTable(1).getRoute(1).getName()); + assertEquals(2, spec.getTable(1).getRoute(1).getNumHops()); + assertEquals("bar-h2", spec.getTable(1).getRoute(1).getHop(0)); + assertEquals("bar-h1", spec.getTable(1).getRoute(1).getHop(1)); + } + } + + private static MessagebusConfig.Builder writeHalf() { + return writeTables(1); + } + + private static MessagebusConfig.Builder writeFull() { + return writeTables(2); + } + + private static MessagebusConfig.Builder writeTables(int numTables) { + MessagebusConfig.Builder builder = new MessagebusConfig.Builder(); + if (numTables > 0) { + MessagebusConfig.Routingtable.Builder table = new MessagebusConfig.Routingtable.Builder(); + table.protocol("foo"); + table.hop(getHop("foo-h1", "foo-h1-sel", "foo-h1-r1", "foo-h1-r2", true)); + table.hop(getHop("foo-h2", "foo-h2-sel", "foo-h2-r1", "foo-h2-r2", false)); + table.route(getRoute("foo-r1", "foo-h1", "foo-h2")); + table.route(getRoute("foo-r2", "foo-h2", "foo-h1")); + builder.routingtable(table); + } + if (numTables > 1) { + MessagebusConfig.Routingtable.Builder table = new MessagebusConfig.Routingtable.Builder(); + table.protocol("bar"); + table.hop(getHop("bar-h1", "bar-h1-sel", "bar-h1-r1", "bar-h1-r2", false)); + table.hop(getHop("bar-h2", "bar-h2-sel", "bar-h2-r1", "bar-h2-r2", false)); + table.route(getRoute("bar-r1", "bar-h1", "bar-h2")); + table.route(getRoute("bar-r2", "bar-h2", "bar-h1")); + builder.routingtable(table); + } + return builder; + } + + private static MessagebusConfig.Routingtable.Route.Builder getRoute(String name, String hop1, String hop2) { + MessagebusConfig.Routingtable.Route.Builder route = new MessagebusConfig.Routingtable.Route.Builder(); + route.name(name); + route.hop(hop1); + route.hop(hop2); + return route; + } + + private static MessagebusConfig.Routingtable.Hop.Builder getHop(String name, String selector, String recipient1, String recipient2, boolean ignoreresult) { + MessagebusConfig.Routingtable.Hop.Builder hop = new MessagebusConfig.Routingtable.Hop.Builder(); + hop.name(name); + hop.selector(selector); + hop.recipient(recipient1); + hop.recipient(recipient2); + hop.ignoreresult(ignoreresult); + return hop; + } + + private static class LocalHandler implements ConfigHandler { + + volatile RoutingSpec spec = new RoutingSpec(); + + public void setupRouting(RoutingSpec spec) { + this.spec = spec; + } + + public void reset() { + spec = null; + } + + public boolean await(int timeout, TimeUnit unit) throws InterruptedException { + long millis = System.currentTimeMillis() + unit.toMillis(timeout); + while (spec == null) { + long now = System.currentTimeMillis(); + if (now >= millis) { + return false; + } + Thread.sleep(1000); + } + return true; + } + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/CustomTimer.java b/messagebus/src/test/java/com/yahoo/messagebus/CustomTimer.java new file mode 100644 index 00000000000..12749ca9bc4 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/CustomTimer.java @@ -0,0 +1,17 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.concurrent.Timer; + +/** + * @author <a href="mailto:thomasg@yahoo-inc.com">Thomas Gundersen</a> + */ +class CustomTimer implements Timer { + + long millis = 0; + + @Override + public long milliTime() { + return millis; + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/ErrorTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/ErrorTestCase.java new file mode 100755 index 00000000000..e67fc5030ed --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/ErrorTestCase.java @@ -0,0 +1,92 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus;
+
+import com.yahoo.jrt.slobrok.server.Slobrok;
+import com.yahoo.messagebus.network.rpc.test.TestServer;
+import com.yahoo.messagebus.routing.RoutingTableSpec;
+import com.yahoo.messagebus.test.Receptor;
+import com.yahoo.messagebus.test.SimpleMessage;
+import com.yahoo.messagebus.test.SimpleProtocol;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ErrorTestCase {
+
+ @Test
+ public void requireThatAccessorsWork() {
+ Error err = new Error(69, "foo");
+ assertEquals(69, err.getCode());
+ assertEquals("foo", err.getMessage());
+
+ assertFalse(new Error(ErrorCode.TRANSIENT_ERROR, "foo").isFatal());
+ assertFalse(new Error(ErrorCode.TRANSIENT_ERROR + 1, "foo").isFatal());
+ assertTrue(new Error(ErrorCode.FATAL_ERROR, "foo").isFatal());
+ assertTrue(new Error(ErrorCode.FATAL_ERROR + 1, "foo").isFatal());
+ }
+
+ @Test
+ public void requireThatErrorIsPropagated() throws Exception {
+ RoutingTableSpec table = new RoutingTableSpec(SimpleProtocol.NAME);
+ table.addHop("itr", "test/itr/session", Arrays.asList("test/itr/session"));
+ table.addHop("dst", "test/dst/session", Arrays.asList("test/dst/session"));
+ table.addRoute("test", Arrays.asList("itr", "dst"));
+
+ Slobrok slobrok = new Slobrok();
+ TestServer src = new TestServer("test/src", table, slobrok, null, null);
+ TestServer itr = new TestServer("test/itr", table, slobrok, null, null);
+ TestServer dst = new TestServer("test/dst", table, slobrok, null, null);
+
+ Receptor ss_rr = new Receptor();
+ SourceSession ss = src.mb.createSourceSession(ss_rr);
+
+ Receptor is_mr = new Receptor();
+ Receptor is_rr = new Receptor();
+ IntermediateSession is = itr.mb.createIntermediateSession("session", true, is_mr, is_rr);
+
+ Receptor ds_mr = new Receptor();
+ DestinationSession ds = dst.mb.createDestinationSession("session", true, ds_mr);
+
+ src.waitSlobrok("test/itr/session", 1);
+ src.waitSlobrok("test/dst/session", 1);
+ itr.waitSlobrok("test/dst/session", 1);
+
+ for (int i = 0; i < 5; i++) {
+ assertTrue(ss.send(new SimpleMessage("msg"), "test").isAccepted());
+ Message msg = is_mr.getMessage(60);
+ assertNotNull(msg);
+ is.forward(msg);
+
+ assertNotNull(msg = ds_mr.getMessage(60));
+ Reply reply = new EmptyReply();
+ msg.swapState(reply);
+ reply.addError(new Error(ErrorCode.APP_FATAL_ERROR, "fatality"));
+ ds.reply(reply);
+
+ assertNotNull(reply = is_rr.getReply(60));
+ assertEquals(reply.getNumErrors(), 1);
+ assertEquals(reply.getError(0).getService(), "test/dst/session");
+ reply.addError(new Error(ErrorCode.APP_FATAL_ERROR, "fatality"));
+ is.forward(reply);
+
+ assertNotNull(reply = ss_rr.getReply(60));
+ assertEquals(reply.getNumErrors(), 2);
+ assertEquals(reply.getError(0).getService(), "test/dst/session");
+ assertEquals(reply.getError(1).getService(), "test/itr/session");
+ }
+
+ ss.destroy();
+ is.destroy();
+ ds.destroy();
+
+ dst.destroy();
+ itr.destroy();
+ src.destroy();
+ slobrok.stop();
+ }
+}
diff --git a/messagebus/src/test/java/com/yahoo/messagebus/MessageBusTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/MessageBusTestCase.java new file mode 100644 index 00000000000..95b4e3663e8 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/MessageBusTestCase.java @@ -0,0 +1,144 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.jrt.ListenFailedException; +import com.yahoo.jrt.Spec; +import com.yahoo.jrt.slobrok.server.Slobrok; +import com.yahoo.messagebus.network.rpc.RPCNetwork; +import com.yahoo.messagebus.network.rpc.RPCNetworkParams; +import com.yahoo.messagebus.network.rpc.test.TestServer; +import com.yahoo.messagebus.routing.RetryTransientErrorsPolicy; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.messagebus.routing.RoutingPolicy; +import com.yahoo.messagebus.routing.test.CustomPolicyFactory; +import com.yahoo.messagebus.test.Receptor; +import com.yahoo.messagebus.test.SimpleMessage; +import com.yahoo.messagebus.test.SimpleProtocol; +import org.junit.Test; + +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + +public class MessageBusTestCase { + + @Test + public void requireThatBucketSequencingWithResenderEnabledCausesError() throws ListenFailedException { + Slobrok slobrok = new Slobrok(); + TestServer server = new TestServer(new MessageBusParams() + .addProtocol(new SimpleProtocol()) + .setRetryPolicy(new RetryTransientErrorsPolicy()), + new RPCNetworkParams() + .setSlobrokConfigId(slobrok.configId())); + Receptor receptor = new Receptor(); + SourceSession session = server.mb.createSourceSession( + new SourceSessionParams().setTimeout(600.0).setReplyHandler(receptor)); + assertTrue(session.send(new SimpleMessage("foo") { + @Override + public boolean hasBucketSequence() { + return true; + } + }.setRoute(Route.parse("bar"))).isAccepted()); + Reply reply = receptor.getReply(60); + assertNotNull(reply); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.SEQUENCE_ERROR, reply.getError(0).getCode()); + session.destroy(); + server.destroy(); + slobrok.stop(); + } + + @Test + public void testConnectionSpec() throws ListenFailedException, UnknownHostException { + // Setup servers and sessions. + Slobrok slobrok = new Slobrok(); + List<TestServer> servers = new ArrayList<>(); + + TestServer srcServer = new TestServer("feeder", null, slobrok, null, null); + servers.add(srcServer); + SourceSession src = servers.get(0).mb.createSourceSession(new Receptor()); + + List<IntermediateSession> sessions = new ArrayList<>(); + for (int i = 0; i < 10; ++i) { + TestServer server = new TestServer("intermediate/" + i, null, slobrok, null, null); + servers.add(server); + sessions.add(server.mb.createIntermediateSession("session", true, new Receptor(), new Receptor())); + } + + TestServer dstServer = new TestServer("destination", null, slobrok, null, null); + DestinationSession dst = dstServer.mb.createDestinationSession("session", true, new Receptor()); + + assertTrue(srcServer.waitSlobrok("intermediate/*/session", sessions.size())); + assertTrue(srcServer.waitSlobrok("destination/session", 1)); + + StringBuilder route = new StringBuilder(); + for (int i = 0; i < sessions.size(); i++) { + route.append("intermediate/").append(i).append("/session "); + route.append(sessions.get(i).getConnectionSpec()).append(" "); + } + route.append(dst.getConnectionSpec()); + + Message msg = new SimpleMessage("empty"); + assertTrue(src.send(msg, Route.parse(route.toString())).isAccepted()); + for (IntermediateSession itr : sessions) { + // Received using session name. + assertNotNull(msg = ((Receptor)itr.getMessageHandler()).getMessage(60)); + itr.forward(msg); + + // Received using connection spec. + assertNotNull(msg = ((Receptor)itr.getMessageHandler()).getMessage(60)); + itr.forward(msg); + } + assertNotNull(msg = ((Receptor)dst.getMessageHandler()).getMessage(60)); + dst.acknowledge(msg); + for (int i = sessions.size(); --i >= 0;) { + IntermediateSession itr = sessions.get(i); + + // Received for connection spec. + Reply reply = ((Receptor)itr.getReplyHandler()).getReply(60); + assertNotNull(reply); + itr.forward(reply); + + // Received for session name. + assertNotNull(reply = ((Receptor)itr.getReplyHandler()).getReply(60)); + itr.forward(reply); + } + assertNotNull(((Receptor)src.getReplyHandler()).getReply(60)); + + // Cleanup. + for (IntermediateSession session : sessions) { + session.destroy(); + } + for (TestServer server : servers) { + server.destroy(); + } + slobrok.stop(); + } + + @Test + public void testRoutingPolicyCache() throws ListenFailedException, UnknownHostException { + Slobrok slobrok = new Slobrok(); + String config = "slobrok[1]\nslobrok[0].connectionspec \"" + new Spec("localhost", slobrok.port()).toString() + "\"\n"; + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new CustomPolicyFactory()); + MessageBus bus = new MessageBus(new RPCNetwork(new RPCNetworkParams().setSlobrokConfigId("raw:" + config)), + new MessageBusParams().addProtocol(protocol)); + + RoutingPolicy all = bus.getRoutingPolicy(SimpleProtocol.NAME, "Custom", null); + assertNotNull(all); + + RoutingPolicy ref = bus.getRoutingPolicy(SimpleProtocol.NAME, "Custom", null); + assertNotNull(ref); + assertSame(all, ref); + + RoutingPolicy allArg = bus.getRoutingPolicy(SimpleProtocol.NAME, "Custom", "Arg"); + assertNotNull(allArg); + assertNotSame(all, allArg); + + RoutingPolicy refArg = bus.getRoutingPolicy(SimpleProtocol.NAME, "Custom", "Arg"); + assertNotNull(refArg); + assertSame(allArg, refArg); + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/MessengerTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/MessengerTestCase.java new file mode 100644 index 00000000000..37a99381cd1 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/MessengerTestCase.java @@ -0,0 +1,124 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class MessengerTestCase { + + @Test + public void requireThatSyncWithSelfDoesNotCauseDeadLock() throws InterruptedException { + final Messenger msn = new Messenger(); + msn.start(); + + final CountDownLatch latch = new CountDownLatch(1); + msn.enqueue(new Messenger.Task() { + + @Override + public void run() { + msn.sync(); + } + + @Override + public void destroy() { + latch.countDown(); + } + }); + assertTrue(latch.await(60, TimeUnit.SECONDS)); + } + + @Test + public void requireThatTaskIsExecuted() throws InterruptedException { + Messenger msn = new Messenger(); + msn.start(); + assertTrue(tryMessenger(msn)); + } + + @Test + public void requireThatRunExceptionIsCaught() throws InterruptedException { + Messenger msn = new Messenger(); + msn.start(); + msn.enqueue(new Messenger.Task() { + @Override + public void run() { + throw new RuntimeException(); + } + + @Override + public void destroy() { + + } + }); + assertTrue(tryMessenger(msn)); + } + + @Test + public void requireThatDestroyExceptionIsCaught() throws InterruptedException { + Messenger msn = new Messenger(); + msn.start(); + msn.enqueue(new Messenger.Task() { + @Override + public void run() { + + } + + @Override + public void destroy() { + throw new RuntimeException(); + } + }); + assertTrue(tryMessenger(msn)); + } + + @Test + public void requireThatRunAndDestroyExceptionsAreCaught() throws InterruptedException { + Messenger msn = new Messenger(); + msn.start(); + msn.enqueue(new Messenger.Task() { + @Override + public void run() { + throw new RuntimeException(); + } + + @Override + public void destroy() { + throw new RuntimeException(); + } + }); + assertTrue(tryMessenger(msn)); + } + + private static boolean tryMessenger(Messenger msn) { + MyTask task = new MyTask(); + msn.enqueue(task); + try { + return task.runLatch.await(60, TimeUnit.SECONDS) && + task.destroyLatch.await(60, TimeUnit.SECONDS); + } catch (InterruptedException e) { + return false; + } + } + + private static class MyTask implements Messenger.Task { + + final CountDownLatch runLatch = new CountDownLatch(1); + final CountDownLatch destroyLatch = new CountDownLatch(1); + + @Override + public void run() { + runLatch.countDown(); + } + + @Override + public void destroy() { + destroyLatch.countDown(); + } + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/ProtocolRepositoryTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/ProtocolRepositoryTestCase.java new file mode 100644 index 00000000000..ddb21baadab --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/ProtocolRepositoryTestCase.java @@ -0,0 +1,103 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.messagebus.routing.RoutingContext; +import com.yahoo.messagebus.routing.RoutingPolicy; +import com.yahoo.messagebus.test.SimpleProtocol; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ProtocolRepositoryTestCase { + + @Test + public void requireThatPolicyCanBeNull() { + ProtocolRepository repo = new ProtocolRepository(); + SimpleProtocol protocol = new SimpleProtocol(); + repo.putProtocol(protocol); + assertNull(repo.getRoutingPolicy(SimpleProtocol.NAME, "Custom", null)); + } + + @Test + public void requireThatPolicyCanBeCreated() { + ProtocolRepository repo = new ProtocolRepository(); + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new MyFactory()); + repo.putProtocol(protocol); + assertNotNull(repo.getRoutingPolicy(SimpleProtocol.NAME, "Custom", null)); + } + + @Test + public void requireThatPolicyIsCached() { + ProtocolRepository repo = new ProtocolRepository(); + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new MyFactory()); + repo.putProtocol(protocol); + + RoutingPolicy prev = repo.getRoutingPolicy(SimpleProtocol.NAME, "Custom", null); + assertNotNull(prev); + + RoutingPolicy next = repo.getRoutingPolicy(SimpleProtocol.NAME, "Custom", null); + assertNotNull(next); + assertSame(prev, next); + } + + @Test + public void requireThatPolicyParamIsPartOfCacheKey() { + ProtocolRepository repo = new ProtocolRepository(); + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new MyFactory()); + repo.putProtocol(protocol); + + RoutingPolicy prev = repo.getRoutingPolicy(SimpleProtocol.NAME, "Custom", "foo"); + assertNotNull(prev); + + RoutingPolicy next = repo.getRoutingPolicy(SimpleProtocol.NAME, "Custom", "bar"); + assertNotNull(next); + assertNotSame(prev, next); + } + + @Test + public void requireThatCreatePolicyExceptionIsCaught() { + ProtocolRepository repo = new ProtocolRepository(); + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new SimpleProtocol.PolicyFactory() { + + @Override + public RoutingPolicy create(String param) { + throw new RuntimeException(); + } + }); + repo.putProtocol(protocol); + assertNull(repo.getRoutingPolicy(SimpleProtocol.NAME, "Custom", null)); + } + + private static class MyFactory implements SimpleProtocol.PolicyFactory { + + @Override + public RoutingPolicy create(String param) { + return new MyPolicy(); + } + } + + private static class MyPolicy implements RoutingPolicy { + + @Override + public void select(RoutingContext context) { + + } + + @Override + public void merge(RoutingContext context) { + + } + + @Override + public void destroy() { + + } + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/RateThrottlingTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/RateThrottlingTestCase.java new file mode 100644 index 00000000000..dc4beb5cf9f --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/RateThrottlingTestCase.java @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.messagebus.test.SimpleMessage; +import junit.framework.TestCase; + +public class RateThrottlingTestCase extends TestCase { + + public void testPending() { + CustomTimer timer = new CustomTimer(); + RateThrottlingPolicy policy = new RateThrottlingPolicy(5.0, timer); + policy.setMaxPendingCount(200); + + // Check that we obey the max still. + assertFalse(policy.canSend(new SimpleMessage("test"), 300)); + } + + public int getActualRate(double desiredRate) { + CustomTimer timer = new CustomTimer(); + RateThrottlingPolicy policy = new RateThrottlingPolicy(desiredRate, timer); + + int ok = 0; + for (int i = 0; i < 10000; ++i) { + if (policy.canSend(new SimpleMessage("test"), 0)) { + ok++; + } + timer.millis += 10; + } + + return ok; + } + + public void testRates() { + assertEquals(10, getActualRate(0.1), 1); + assertEquals(1000, getActualRate(10), 100); + assertEquals(500, getActualRate(5), 50); + assertEquals(100, getActualRate(1), 10); + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/RoutableTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/RoutableTestCase.java new file mode 100755 index 00000000000..4e76ab86889 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/RoutableTestCase.java @@ -0,0 +1,114 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.jrt.ListenFailedException; +import com.yahoo.jrt.slobrok.server.Slobrok; +import com.yahoo.messagebus.network.rpc.test.TestServer; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.messagebus.test.Receptor; +import com.yahoo.messagebus.test.SimpleMessage; +import com.yahoo.messagebus.test.SimpleReply; + +import java.net.UnknownHostException; + +public class RoutableTestCase extends junit.framework.TestCase { + + public void testMessageContext() throws ListenFailedException, UnknownHostException { + Slobrok slobrok = new Slobrok(); + TestServer srcServer = new TestServer("src", null, slobrok, null, null); + TestServer dstServer = new TestServer("dst", null, slobrok, null, null); + SourceSession srcSession = srcServer.mb.createSourceSession( + new Receptor(), + new SourceSessionParams().setTimeout(600.0)); + DestinationSession dstSession = dstServer.mb.createDestinationSession("session", true, new Receptor()); + + assertTrue(srcServer.waitSlobrok("dst/session", 1)); + + Object context = new Object(); + Message msg = new SimpleMessage("msg"); + msg.setContext(context); + assertTrue(srcSession.send(msg, "dst/session", true).isAccepted()); + + assertNotNull(msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60)); + dstSession.acknowledge(msg); + + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + assertSame(reply.getContext(), context); + + srcSession.destroy(); + srcServer.destroy(); + dstSession.destroy(); + dstServer.destroy(); + slobrok.stop(); + } + + public void testMessageSwapState() { + Message foo = new SimpleMessage("foo"); + Route fooRoute = Route.parse("foo"); + foo.setRoute(fooRoute); + foo.setRetry(1); + foo.setTimeReceivedNow(); + foo.setTimeRemaining(2); + + Message bar = new SimpleMessage("bar"); + Route barRoute = Route.parse("bar"); + bar.setRoute(barRoute); + bar.setRetry(3); + bar.setTimeReceivedNow(); + bar.setTimeRemaining(4); + + foo.swapState(bar); + assertEquals(barRoute, foo.getRoute()); + assertEquals(fooRoute, bar.getRoute()); + assertEquals(3, foo.getRetry()); + assertEquals(1, bar.getRetry()); + assertTrue(foo.getTimeReceived() >= bar.getTimeReceived()); + assertEquals(4, foo.getTimeRemaining()); + assertEquals(2, bar.getTimeRemaining()); + } + + public void testReplySwapState() { + Reply foo = new SimpleReply("foo"); + Message fooMsg = new SimpleMessage("foo"); + foo.setMessage(fooMsg); + foo.setRetryDelay(1); + foo.addError(new Error(ErrorCode.APP_FATAL_ERROR, "fatal")); + foo.addError(new Error(ErrorCode.APP_TRANSIENT_ERROR, "transient")); + + Reply bar = new SimpleReply("bar"); + Message barMsg = new SimpleMessage("bar"); + bar.setMessage(barMsg); + bar.setRetryDelay(2); + bar.addError(new Error(ErrorCode.ERROR_LIMIT, "err")); + + foo.swapState(bar); + assertEquals(barMsg, foo.getMessage()); + assertEquals(fooMsg, bar.getMessage()); + assertEquals(2.0, foo.getRetryDelay()); + assertEquals(1.0, bar.getRetryDelay()); + assertEquals(1, foo.getNumErrors()); + assertEquals(2, bar.getNumErrors()); + } + + public void testMessageDiscard() { + Receptor handler = new Receptor(); + Message msg = new SimpleMessage("foo"); + msg.pushHandler(handler); + msg.discard(); + + assertNull(handler.getReply(0)); + } + + public void testReplyDiscard() { + Receptor handler = new Receptor(); + Message msg = new SimpleMessage("foo"); + msg.pushHandler(handler); + + Reply reply = new SimpleReply("bar"); + reply.swapState(msg); + reply.discard(); + + assertNull(handler.getReply(0)); + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/SendProxyTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/SendProxyTestCase.java new file mode 100644 index 00000000000..6eb7257328b --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/SendProxyTestCase.java @@ -0,0 +1,169 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus;
+
+import com.yahoo.component.Vtag;
+import com.yahoo.jrt.ListenFailedException;
+import com.yahoo.jrt.slobrok.server.Slobrok;
+import com.yahoo.log.LogLevel;
+import com.yahoo.messagebus.network.Identity;
+import com.yahoo.messagebus.network.rpc.RPCNetworkParams;
+import com.yahoo.messagebus.network.rpc.test.TestServer;
+import com.yahoo.messagebus.routing.Route;
+import com.yahoo.messagebus.test.Receptor;
+import com.yahoo.messagebus.test.SimpleMessage;
+import com.yahoo.messagebus.test.SimpleProtocol;
+import com.yahoo.messagebus.test.SimpleReply;
+import junit.framework.TestCase;
+
+import java.net.UnknownHostException;
+import java.util.logging.Handler;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class SendProxyTestCase extends TestCase {
+
+ Slobrok slobrok;
+ TestServer srcServer, dstServer;
+ SourceSession srcSession;
+ DestinationSession dstSession;
+
+ @Override
+ public void setUp() throws UnknownHostException, ListenFailedException {
+ slobrok = new Slobrok();
+ dstServer = new TestServer(new MessageBusParams().addProtocol(new SimpleProtocol()),
+ new RPCNetworkParams().setIdentity(new Identity("dst")).setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok)));
+ dstSession = dstServer.mb.createDestinationSession(new DestinationSessionParams().setName("session").setMessageHandler(new Receptor()));
+ srcServer = new TestServer(new MessageBusParams().addProtocol(new SimpleProtocol()),
+ new RPCNetworkParams().setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok)));
+ srcSession = srcServer.mb.createSourceSession(
+ new SourceSessionParams().setTimeout(600.0).setThrottlePolicy(null).setReplyHandler(new Receptor()));
+ assertTrue(srcServer.waitSlobrok("dst/session", 1));
+ }
+
+ @Override
+ public void tearDown() {
+ slobrok.stop();
+ dstSession.destroy();
+ dstServer.destroy();
+ srcSession.destroy();
+ srcServer.destroy();
+ }
+
+ public void testTraceByLogLevel() {
+ Logger log = Logger.getLogger(SendProxy.class.getName());
+ LogHandler logHandler = new LogHandler();
+ log.addHandler(logHandler);
+
+ log.setLevel(LogLevel.INFO);
+ sendMessage(0, null);
+ assertNull(logHandler.trace);
+
+ log.setLevel(LogLevel.DEBUG);
+ sendMessage(0, null);
+ assertNull(logHandler.trace);
+
+ sendMessage(1, new Error(ErrorCode.FATAL_ERROR, "err"));
+ assertNull(logHandler.trace);
+
+ sendMessage(0, new Error(ErrorCode.FATAL_ERROR, "err"));
+ assertEquals("Trace for reply with error(s):\n" +
+ "<trace>\n" +
+ " <trace>\n" +
+ " Sending message (version ${VERSION}) from client to 'dst/session' with x seconds timeout.\n" +
+ " <trace>\n" +
+ " Message (type 1) received at 'dst' for session 'session'.\n" +
+ " [FATAL_ERROR @ localhost]: err\n" +
+ " Sending reply (version ${VERSION}) from 'dst'.\n" +
+ " </trace>\n" +
+ " Reply (type 2) received at client.\n" +
+ " </trace>\n" +
+ "</trace>\n", logHandler.trace);
+ logHandler.trace = null;
+
+ log.setLevel(LogLevel.SPAM);
+ sendMessage(1, null);
+ assertNull(logHandler.trace);
+
+ sendMessage(0, null);
+ assertEquals("Trace for reply:\n" +
+ "<trace>\n" +
+ " <trace>\n" +
+ " Sending message (version ${VERSION}) from client to 'dst/session' with x seconds timeout.\n" +
+ " <trace>\n" +
+ " Message (type 1) received at 'dst' for session 'session'.\n" +
+ " Sending reply (version ${VERSION}) from 'dst'.\n" +
+ " </trace>\n" +
+ " Reply (type 0) received at client.\n" +
+ " </trace>\n" +
+ "</trace>\n", logHandler.trace);
+ logHandler.trace = null;
+
+ sendMessage(1, new Error(ErrorCode.FATAL_ERROR, "err"));
+ assertNull(logHandler.trace);
+
+ sendMessage(0, new Error(ErrorCode.FATAL_ERROR, "err"));
+ assertEquals("Trace for reply with error(s):\n" +
+ "<trace>\n" +
+ " <trace>\n" +
+ " Sending message (version ${VERSION}) from client to 'dst/session' with x seconds timeout.\n" +
+ " <trace>\n" +
+ " Message (type 1) received at 'dst' for session 'session'.\n" +
+ " [FATAL_ERROR @ localhost]: err\n" +
+ " Sending reply (version ${VERSION}) from 'dst'.\n" +
+ " </trace>\n" +
+ " Reply (type 2) received at client.\n" +
+ " </trace>\n" +
+ "</trace>\n", logHandler.trace);
+ logHandler.trace = null;
+ }
+
+ private void sendMessage(int traceLevel, Error err) {
+ Message msg = new SimpleMessage("foo");
+ msg.getTrace().setLevel(traceLevel);
+ assertTrue(srcSession.send(msg, Route.parse("dst/session")).isAccepted());
+ assertNotNull(msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60));
+ if (err != null) {
+ Reply reply = new SimpleReply("bar");
+ reply.swapState(msg);
+ reply.addError(err);
+ dstSession.reply(reply);
+ } else {
+ dstSession.acknowledge(msg);
+ }
+ Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60);
+ assertNotNull(reply);
+ }
+
+ private static class LogHandler extends Handler {
+
+ String trace = null;
+
+ @Override
+ public void publish(LogRecord record) {
+ String msg = record.getMessage();
+ if (msg.startsWith("Trace ")) {
+ msg = msg.replaceAll("\\[.*\\] ", "");
+ msg = msg.replaceAll("[0-9]+\\.[0-9]+ seconds", "x seconds");
+
+ String ver = Vtag.currentVersion.toString();
+ for (int i = msg.indexOf(ver); i >= 0; i = msg.indexOf(ver, i)) {
+ msg = msg.substring(0, i) + "${VERSION}" + msg.substring(i + ver.length());
+ }
+ trace = msg;
+ }
+ }
+
+ @Override
+ public void flush() {
+ // empty
+ }
+
+ @Override
+ public void close() throws SecurityException {
+ // empty
+ }
+ }
+}
diff --git a/messagebus/src/test/java/com/yahoo/messagebus/SequencerTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/SequencerTestCase.java new file mode 100644 index 00000000000..bd053e5ad63 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/SequencerTestCase.java @@ -0,0 +1,179 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.messagebus.test.SimpleMessage; + +import java.util.LinkedList; +import java.util.Queue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class SequencerTestCase extends junit.framework.TestCase { + + public void testSyncNone() { + TestQueue src = new TestQueue(); + TestQueue dst = new TestQueue(); + QueueSender sender = new QueueSender(dst); + Sequencer seq = new Sequencer(sender); + + seq.handleMessage(src.createMessage(false, 0)); + seq.handleMessage(src.createMessage(false, 0)); + seq.handleMessage(src.createMessage(false, 0)); + seq.handleMessage(src.createMessage(false, 0)); + seq.handleMessage(src.createMessage(false, 0)); + assertEquals(0, src.size()); + assertEquals(5, dst.size()); + + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + assertEquals(5, src.size()); + assertEquals(0, dst.size()); + + src.checkReply(false, 0); + src.checkReply(false, 0); + src.checkReply(false, 0); + src.checkReply(false, 0); + src.checkReply(false, 0); + assertEquals(0, src.size()); + assertEquals(0, dst.size()); + } + + public void testSyncId() { + TestQueue src = new TestQueue(); + TestQueue dst = new TestQueue(); + QueueSender sender = new QueueSender(dst); + Sequencer seq = new Sequencer(sender); + + seq.handleMessage(src.createMessage(true, 1L)); + seq.handleMessage(src.createMessage(true, 2L)); + seq.handleMessage(src.createMessage(true, 3L)); + seq.handleMessage(src.createMessage(true, 4L)); + seq.handleMessage(src.createMessage(true, 5L)); + assertEquals(0, src.size()); + assertEquals(5, dst.size()); + + seq.handleMessage(src.createMessage(true, 1L)); + seq.handleMessage(src.createMessage(true, 5L)); + seq.handleMessage(src.createMessage(true, 2L)); + seq.handleMessage(src.createMessage(true, 10L)); + seq.handleMessage(src.createMessage(true, 4L)); + seq.handleMessage(src.createMessage(true, 3L)); + assertEquals(0, src.size()); + assertEquals(6, dst.size()); + + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + assertEquals(5, src.size()); + assertEquals(6, dst.size()); + + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + assertEquals(11, src.size()); + assertEquals(0, dst.size()); + + src.checkReply(true, 1); + src.checkReply(true, 2); + src.checkReply(true, 3); + src.checkReply(true, 4); + src.checkReply(true, 5); + src.checkReply(true, 10); + src.checkReply(true, 1); + src.checkReply(true, 2); + src.checkReply(true, 3); + src.checkReply(true, 4); + src.checkReply(true, 5); + assertEquals(0, src.size()); + assertEquals(0, dst.size()); + } + + @SuppressWarnings("serial") + private static class TestQueue extends LinkedList<Routable> implements ReplyHandler { + + void checkReply(boolean hasSeqId, long seqId) { + if (size() == 0) { + throw new IllegalStateException("No routable in queue."); + } + Routable obj = remove(); + assertTrue(obj instanceof Reply); + + Reply reply = (Reply)obj; + Message msg = reply.getMessage(); + assertNotNull(msg); + + assertEquals(hasSeqId, msg.hasSequenceId()); + if (hasSeqId) { + assertEquals(seqId, msg.getSequenceId()); + } + } + + public void handleReply(Reply reply) { + add(reply); + } + + void replyNext() { + Routable obj = remove(); + assertTrue(obj instanceof Message); + Message msg = (Message)obj; + + Reply reply = new EmptyReply(); + reply.swapState(msg); + reply.setMessage(msg); + ReplyHandler handler = reply.popHandler(); + handler.handleReply(reply); + } + + Message createMessage(final boolean hasSeqId, final long seqId) { + Message ret = new MyMessage(hasSeqId, seqId); + ret.pushHandler(this); + return ret; + } + } + + private static class QueueSender implements MessageHandler { + + Queue<Routable> queue; + + QueueSender(Queue<Routable> queue) { + this.queue = queue; + } + + @Override + public void handleMessage(Message msg) { + queue.offer(msg); + } + } + + private static class MyMessage extends SimpleMessage { + + final boolean hasSeqId; + final long seqId; + + MyMessage(boolean hasSeqId, long seqId) { + super("foo"); + this.hasSeqId = hasSeqId; + this.seqId = seqId; + } + + @Override + public boolean hasSequenceId() { + return hasSeqId; + } + + @Override + public long getSequenceId() { + return seqId; + } + } +} + diff --git a/messagebus/src/test/java/com/yahoo/messagebus/SimpleTripTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/SimpleTripTestCase.java new file mode 100755 index 00000000000..2795758d922 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/SimpleTripTestCase.java @@ -0,0 +1,53 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus;
+
+import com.yahoo.jrt.ListenFailedException;
+import com.yahoo.jrt.slobrok.server.Slobrok;
+import com.yahoo.messagebus.network.Identity;
+import com.yahoo.messagebus.network.rpc.RPCNetworkParams;
+import com.yahoo.messagebus.network.rpc.test.TestServer;
+import com.yahoo.messagebus.routing.Route;
+import com.yahoo.messagebus.test.Receptor;
+import com.yahoo.messagebus.test.SimpleMessage;
+import com.yahoo.messagebus.test.SimpleProtocol;
+import com.yahoo.messagebus.test.SimpleReply;
+
+import java.net.UnknownHostException;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class SimpleTripTestCase extends junit.framework.TestCase {
+
+ public void testSimpleTrip() throws ListenFailedException, UnknownHostException {
+ Slobrok slobrok = new Slobrok();
+ TestServer server = new TestServer(new MessageBusParams().addProtocol(new SimpleProtocol()),
+ new RPCNetworkParams()
+ .setIdentity(new Identity("srv"))
+ .setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok)));
+ DestinationSession dst = server.mb.createDestinationSession(new DestinationSessionParams().setName("session").setMessageHandler(new Receptor()));
+ SourceSession src = server.mb.createSourceSession(
+ new SourceSessionParams().setTimeout(600.0).setReplyHandler(new Receptor()));
+ assertTrue(server.waitSlobrok("srv/session", 1));
+
+ assertTrue(src.send(new SimpleMessage("msg"), Route.parse("srv/session")).isAccepted());
+ Message msg = ((Receptor)dst.getMessageHandler()).getMessage(60);
+ assertNotNull(msg);
+ assertEquals(SimpleProtocol.NAME, msg.getProtocol());
+ assertEquals(SimpleProtocol.MESSAGE, msg.getType());
+ assertEquals("msg", ((SimpleMessage)msg).getValue());
+
+ Reply reply = new SimpleReply("reply");
+ reply.swapState(msg);
+ dst.reply(reply);
+
+ assertNotNull(reply = ((Receptor)src.getReplyHandler()).getReply(60));
+ assertEquals(SimpleProtocol.NAME, reply.getProtocol());
+ assertEquals(SimpleProtocol.REPLY, reply.getType());
+ assertEquals("reply", ((SimpleReply)reply).getValue());
+
+ src.destroy();
+ dst.destroy();
+ server.destroy();
+ }
+}
diff --git a/messagebus/src/test/java/com/yahoo/messagebus/ThrottlerTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/ThrottlerTestCase.java new file mode 100644 index 00000000000..405aa1c0b96 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/ThrottlerTestCase.java @@ -0,0 +1,230 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.jrt.ListenFailedException; +import com.yahoo.jrt.slobrok.server.Slobrok; +import com.yahoo.messagebus.network.rpc.test.TestServer; +import com.yahoo.messagebus.routing.RoutingTableSpec; +import com.yahoo.messagebus.test.*; + +import java.net.UnknownHostException; +import java.util.Arrays; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ThrottlerTestCase extends junit.framework.TestCase { + + //////////////////////////////////////////////////////////////////////////////// + // + // Setup + // + //////////////////////////////////////////////////////////////////////////////// + + Slobrok slobrok; + TestServer src, dst; + + public void setUp() throws ListenFailedException, UnknownHostException { + RoutingTableSpec table = new RoutingTableSpec(SimpleProtocol.NAME); + table.addHop("dst", "test/dst/session", Arrays.asList("test/dst/session")); + table.addRoute("test", Arrays.asList("dst")); + slobrok = new Slobrok(); + src = new TestServer("test/src", table, slobrok, null, null); + dst = new TestServer("test/dst", table, slobrok, null, null); + } + + public void tearDown() { + dst.destroy(); + src.destroy(); + slobrok.stop(); + } + + //////////////////////////////////////////////////////////////////////////////// + // + // Tests + // + //////////////////////////////////////////////////////////////////////////////// + + public void testMaxCount() { + // Prepare a source session with throttle enabled. + SourceSessionParams params = new SourceSessionParams().setTimeout(600.0); + StaticThrottlePolicy policy = new StaticThrottlePolicy(); + policy.setMaxPendingCount(10); + params.setThrottlePolicy(policy); + + Receptor src_rr = new Receptor(); + SourceSession src_s = src.mb.createSourceSession(src_rr, params); + + // Prepare a destination session to acknowledge messages. + QueueAdapter dst_q = new QueueAdapter(); + DestinationSession dst_s = dst.mb.createDestinationSession("session", true, dst_q); + src.waitSlobrok("test/dst/session", 1); + + // Send until throttler rejects a message. + for (int i = 0; i < policy.getMaxPendingCount(); i++) { + assertTrue(src_s.send(new SimpleMessage("msg"), "test").isAccepted()); + } + assertFalse(src_s.send(new SimpleMessage("msg"), "test").isAccepted()); + + // Acknowledge one message at a time, then attempt to send two more. + for (int i = 0; i < 10; i++) { + assertTrue(dst_q.waitSize(policy.getMaxPendingCount(), 60)); + dst_s.acknowledge((Message)dst_q.dequeue()); + + assertNotNull(src_rr.getReply(60)); + assertTrue(src_s.send(new SimpleMessage("msg"), "test").isAccepted()); + assertFalse(src_s.send(new SimpleMessage("msg"), "test").isAccepted()); + } + + assertTrue(dst_q.waitSize(policy.getMaxPendingCount(), 60)); + while (!dst_q.isEmpty()) { + dst_s.acknowledge((Message)dst_q.dequeue()); + } + + src_s.close(); + dst_s.destroy(); + } + + public void testMaxSize() { + // Prepare a source session with throttle enabled. + SourceSessionParams params = new SourceSessionParams().setTimeout(600.0); + StaticThrottlePolicy policy = new StaticThrottlePolicy(); + policy.setMaxPendingCount(1000); + policy.setMaxPendingSize(2); + params.setThrottlePolicy(policy); + + Receptor src_rr = new Receptor(); + SourceSession src_s = src.mb.createSourceSession(src_rr, params); + + // Prepare a destination session to acknowledge messages. + QueueAdapter dst_q = new QueueAdapter(); + DestinationSession dst_s = dst.mb.createDestinationSession("session", true, dst_q); + src.waitSlobrok("test/dst/session", 1); + + assertTrue(src_s.send(new SimpleMessage("1"), "test").isAccepted()); + assertTrue(dst_q.waitSize(1, 60)); + assertTrue(src_s.send(new SimpleMessage("12"), "test").isAccepted()); + assertTrue(dst_q.waitSize(2, 60)); + + assertFalse(src_s.send(new SimpleMessage("1"), "test").isAccepted()); + dst_s.acknowledge((Message)dst_q.dequeue()); + assertNotNull(src_rr.getReply(60)); + + assertFalse(src_s.send(new SimpleMessage("1"), "test").isAccepted()); + dst_s.acknowledge((Message)dst_q.dequeue()); + assertNotNull(src_rr.getReply(60)); + + assertTrue(src_s.send(new SimpleMessage("12"), "test").isAccepted()); + assertTrue(dst_q.waitSize(1, 60)); + assertFalse(src_s.send(new SimpleMessage("1"), "test").isAccepted()); + dst_s.acknowledge((Message)dst_q.dequeue()); + assertNotNull(src_rr.getReply(60)); + + // Close sessions. + src_s.close(); + dst_s.destroy(); + } + + public void testDynamicWindowSize() { + CustomTimer timer = new CustomTimer(); + DynamicThrottlePolicy policy = new DynamicThrottlePolicy(timer); + + policy.setWindowSizeIncrement(5); + policy.setResizeRate(1); + + double windowSize = getWindowSize(policy, timer, 100); + assertTrue(windowSize >= 90 && windowSize <= 110); + + windowSize = getWindowSize(policy, timer, 200); + assertTrue(windowSize >= 90 && windowSize <= 210); + + windowSize = getWindowSize(policy, timer, 50); + assertTrue(windowSize >= 9 && windowSize <= 55); + + windowSize = getWindowSize(policy, timer, 500); + assertTrue(windowSize >= 90 && windowSize <= 505); + + windowSize = getWindowSize(policy, timer, 100); + assertTrue(windowSize >= 90 && windowSize <= 115); + } + + public void testIdleTimePeriod() { + CustomTimer timer = new CustomTimer(); + DynamicThrottlePolicy policy = new DynamicThrottlePolicy(timer); + + policy.setWindowSizeIncrement(5); + policy.setResizeRate(1); + + double windowSize = getWindowSize(policy, timer, 100); + assertTrue(windowSize >= 90 && windowSize <= 110); + + Message msg = new SimpleMessage("foo"); + timer.millis += 30 * 1000; + assertTrue(policy.canSend(msg, 0)); + assertTrue(windowSize >= 90 && windowSize <= 110); + + timer.millis += 60 * 1000 + 1; + assertTrue(policy.canSend(msg, 50)); + assertEquals(55, policy.getMaxPendingCount()); + + timer.millis += 60 * 1000 + 1; + assertTrue(policy.canSend(msg, 0)); + assertEquals(5, policy.getMaxPendingCount()); + + } + + public void testMinWindowSize() { + CustomTimer timer = new CustomTimer(); + DynamicThrottlePolicy policy = new DynamicThrottlePolicy(timer); + + policy.setWindowSizeIncrement(5); + policy.setResizeRate(1); + policy.setMinWindowSize(150); + + double windowSize = getWindowSize(policy, timer, 200); + assertTrue(windowSize >= 150 && windowSize <= 210); + } + + public void testMaxWindowSize() { + CustomTimer timer = new CustomTimer(); + DynamicThrottlePolicy policy = new DynamicThrottlePolicy(timer); + + policy.setWindowSizeIncrement(5); + policy.setResizeRate(1); + policy.setMaxWindowSize(50); + + double windowSize = getWindowSize(policy, timer, 100); + assertTrue(windowSize >= 40 && windowSize <= 50); + } + + + //////////////////////////////////////////////////////////////////////////////// + // + // Utilities + // + //////////////////////////////////////////////////////////////////////////////// + + private int getWindowSize(DynamicThrottlePolicy policy, CustomTimer timer, int maxPending) { + Message msg = new SimpleMessage("foo"); + Reply reply = new SimpleReply("bar"); + reply.setContext(1); + for (int i = 0; i < 999; ++i) { + int numPending = 0; + while (policy.canSend(msg, numPending)) { + policy.processMessage(msg); + ++numPending; + } + + long tripTime = (numPending < maxPending) ? 1000 : 1000 + (numPending - maxPending) * 1000; + timer.millis += tripTime; + + while (--numPending >= 0) { + policy.processReply(reply); + } + } + int ret = policy.getMaxPendingCount(); + System.out.println("getWindowSize() = " + ret); + return ret; + } + +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/TimeoutTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/TimeoutTestCase.java new file mode 100755 index 00000000000..05a14bf8d16 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/TimeoutTestCase.java @@ -0,0 +1,102 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.jrt.ListenFailedException; +import com.yahoo.jrt.slobrok.server.Slobrok; +import com.yahoo.messagebus.network.Identity; +import com.yahoo.messagebus.network.rpc.RPCNetworkParams; +import com.yahoo.messagebus.network.rpc.test.TestServer; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.messagebus.test.Receptor; +import com.yahoo.messagebus.test.SimpleMessage; +import com.yahoo.messagebus.test.SimpleProtocol; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.net.UnknownHostException; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class TimeoutTestCase { + + private final Slobrok slobrok; + private final TestServer srcServer, dstServer; + private final SourceSession srcSession; + private final DestinationSession dstSession; + + public TimeoutTestCase() throws ListenFailedException, UnknownHostException { + slobrok = new Slobrok(); + dstServer = new TestServer(new MessageBusParams().addProtocol(new SimpleProtocol()), + new RPCNetworkParams().setIdentity(new Identity("dst")) + .setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok))); + dstSession = dstServer.mb.createDestinationSession(new DestinationSessionParams() + .setName("session") + .setMessageHandler(new Receptor())); + srcServer = new TestServer(new MessageBusParams().addProtocol(new SimpleProtocol()), + new RPCNetworkParams().setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok))); + srcSession = srcServer.mb.createSourceSession( + new SourceSessionParams().setTimeout(600.0).setReplyHandler(new Receptor())); + } + + @Before + public void waitForSlobrokRegistration() { + assertTrue(srcServer.waitSlobrok("dst/session", 1)); + } + + @After + public void destroyResources() { + slobrok.stop(); + dstSession.destroy(); + dstServer.destroy(); + srcSession.destroy(); + srcServer.destroy(); + + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(0); + if (msg != null) { + msg.discard(); + } + } + + @Test + public void requireThatMessageCanTimeout() throws ListenFailedException, UnknownHostException { + srcSession.setTimeout(1); + assertSend(srcSession, newMessage(), "dst/session"); + assertTimeout(((Receptor)srcSession.getReplyHandler()).getReply(60)); + } + + @Test + public void requireThatZeroTimeoutMeansImmediateTimeout() throws ListenFailedException, UnknownHostException { + srcSession.setTimeout(0); + assertSend(srcSession, newMessage(), "dst/session"); + assertTimeout(((Receptor)srcSession.getReplyHandler()).getReply(60)); + } + + private static void assertSend(SourceSession session, Message msg, String route) { + assertTrue(session.send(msg, Route.parse(route)).isAccepted()); + } + + private static void assertTimeout(Reply reply) { + assertNotNull(reply); + assertTrue(reply.getTrace().toString(), hasError(reply, ErrorCode.TIMEOUT)); + } + + private static Message newMessage() { + Message msg = new SimpleMessage("msg"); + msg.getTrace().setLevel(9); + return msg; + } + + private static boolean hasError(Reply reply, int errorCode) { + for (int i = 0; i < reply.getNumErrors(); ++i) { + if (reply.getError(i).getCode() == errorCode) { + return true; + } + } + return false; + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/TraceTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/TraceTestCase.java new file mode 100755 index 00000000000..c6b67087b7f --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/TraceTestCase.java @@ -0,0 +1,286 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class TraceTestCase extends junit.framework.TestCase { + + public void testEncodeDecode() { + assertEquals("()", TraceNode.decode("").encode()); + assertEquals("()", TraceNode.decode("[xyz").encode()); + assertEquals("([xyz][])", TraceNode.decode("[xyz][]").encode()); + assertEquals("[xyz]", TraceNode.decode("[xyz]").encode()); + assertEquals("()", TraceNode.decode("{()").encode()); + assertEquals("({()}{})", TraceNode.decode("{()}{}").encode()); + assertEquals("{()}", TraceNode.decode("{()}").encode()); + assertEquals("()", TraceNode.decode("({}").encode()); + assertEquals("(({})())", TraceNode.decode("({})()").encode()); + assertEquals("([])", TraceNode.decode("([])").encode()); + + assertTrue(TraceNode.decode("").isEmpty()); + assertTrue(!TraceNode.decode("([note])").isEmpty()); + + String str = + "([[17/Jun/2009:09:02:30 +0200\\] Message (type 1) received at 'dst' for session 'session'.]" + + "[[17/Jun/2009:09:02:30 +0200\\] [APP_TRANSIENT_ERROR @ localhost\\]: err1]" + + "[[17/Jun/2009:09:02:30 +0200\\] Sending reply (version 4.2) from 'dst'.])"; + System.out.println(TraceNode.decode(str).toString()); + assertEquals(str, TraceNode.decode(str).encode()); + + str = "([Note 0][Note 1]{[Note 2]}{([Note 3])({[Note 4]})})"; + TraceNode t = TraceNode.decode(str); + assertEquals(str, t.encode()); + + assertTrue(t.isRoot()); + assertTrue(t.isStrict()); + assertTrue(!t.isLeaf()); + assertEquals(4, t.getNumChildren()); + + { + TraceNode c = t.getChild(0); + assertTrue(c.isLeaf()); + assertEquals("Note 0", c.getNote()); + } + { + TraceNode c = t.getChild(1); + assertTrue(c.isLeaf()); + assertEquals("Note 1", c.getNote()); + } + { + TraceNode c = t.getChild(2); + assertTrue(!c.isLeaf()); + assertTrue(!c.isStrict()); + assertEquals(1, c.getNumChildren()); + { + TraceNode d = c.getChild(0); + assertTrue(d.isLeaf()); + assertEquals("Note 2", d.getNote()); + } + } + { + TraceNode c = t.getChild(3); + assertTrue(!c.isStrict()); + assertEquals(2, c.getNumChildren()); + { + TraceNode d = c.getChild(0); + assertTrue(d.isStrict()); + assertTrue(!d.isLeaf()); + assertEquals(1, d.getNumChildren()); + { + TraceNode e = d.getChild(0); + assertTrue(e.isLeaf()); + assertEquals("Note 3", e.getNote()); + } + } + { + TraceNode d = c.getChild(1); + assertTrue(d.isStrict()); + assertEquals(1, d.getNumChildren()); + { + TraceNode e = d.getChild(0); + assertTrue(!e.isStrict()); + assertEquals(1, e.getNumChildren()); + { + TraceNode f = e.getChild(0); + assertTrue(f.isLeaf()); + assertEquals("Note 4", f.getNote()); + } + } + } + } + } + + public void testReservedChars() { + TraceNode t = new TraceNode(); + t.addChild("abc(){}[]\\xyz"); + assertEquals("abc(){}[]\\xyz", t.getChild(0).getNote()); + assertEquals("([abc(){}[\\]\\\\xyz])", t.encode()); + { + // test swap/clear/empty here + TraceNode t2 = new TraceNode(); + assertTrue(t2.isEmpty()); + t2.swap(t); + assertTrue(!t2.isEmpty()); + assertEquals("abc(){}[]\\xyz", t2.getChild(0).getNote()); + assertEquals("([abc(){}[\\]\\\\xyz])", t2.encode()); + t2.clear(); + assertTrue(t2.isEmpty()); + } + } + + public void testAdd() { + TraceNode t1 = TraceNode.decode("([x])"); + TraceNode t2 = TraceNode.decode("([y])"); + TraceNode t3 = TraceNode.decode("([z])"); + + t1.addChild(t2); + assertEquals("([x]([y]))", t1.encode()); + assertTrue(t1.getChild(1).isStrict()); + t1.addChild("txt"); + assertTrue(t1.getChild(2).isLeaf()); + assertEquals("([x]([y])[txt])", t1.encode()); + t3.addChild(t1); + assertEquals("([z]([x]([y])[txt]))", t3.encode()); + + // crazy but possible (everything is by value) + t2.addChild(t2).addChild(t2); + assertEquals("([y]([y])([y]([y])))", t2.encode()); + } + + public void testStrict() { + assertEquals("{}", TraceNode.decode("()").setStrict(false).encode()); + assertEquals("{[x]}", TraceNode.decode("([x])").setStrict(false).encode()); + assertEquals("{[x][y]}", TraceNode.decode("([x][y])").setStrict(false).encode()); + } + + public void testTraceLevel() { + Trace t = new Trace(); + t.setLevel(4); + assertEquals(4, t.getLevel()); + t.trace(9, "no"); + assertEquals(0, t.getRoot().getNumChildren()); + t.trace(8, "no"); + assertEquals(0, t.getRoot().getNumChildren()); + t.trace(7, "no"); + assertEquals(0, t.getRoot().getNumChildren()); + t.trace(6, "no"); + assertEquals(0, t.getRoot().getNumChildren()); + t.trace(5, "no"); + assertEquals(0, t.getRoot().getNumChildren()); + t.trace(4, "yes"); + assertEquals(1, t.getRoot().getNumChildren()); + t.trace(3, "yes"); + assertEquals(2, t.getRoot().getNumChildren()); + t.trace(2, "yes"); + assertEquals(3, t.getRoot().getNumChildren()); + t.trace(1, "yes"); + assertEquals(4, t.getRoot().getNumChildren()); + t.trace(0, "yes"); + assertEquals(5, t.getRoot().getNumChildren()); + } + + public void testCompact() { + assertEquals("()", TraceNode.decode("()").compact().encode()); + assertEquals("()", TraceNode.decode("(())").compact().encode()); + assertEquals("()", TraceNode.decode("(()())").compact().encode()); + assertEquals("()", TraceNode.decode("({})").compact().encode()); + assertEquals("()", TraceNode.decode("({}{})").compact().encode()); + assertEquals("()", TraceNode.decode("({{}{}})").compact().encode()); + + assertEquals("([x])", TraceNode.decode("([x])").compact().encode()); + assertEquals("([x])", TraceNode.decode("(([x]))").compact().encode()); + assertEquals("([x][y])", TraceNode.decode("(([x])([y]))").compact().encode()); + assertEquals("([x])", TraceNode.decode("({[x]})").compact().encode()); + assertEquals("([x][y])", TraceNode.decode("({[x]}{[y]})").compact().encode()); + assertEquals("({[x][y]})", TraceNode.decode("({{[x]}{[y]}})").compact().encode()); + + assertEquals("([a][b][c][d])", TraceNode.decode("(([a][b])([c][d]))").compact().encode()); + assertEquals("({[a][b]}{[c][d]})", TraceNode.decode("({[a][b]}{[c][d]})").compact().encode()); + assertEquals("({[a][b][c][d]})", TraceNode.decode("({{[a][b]}{[c][d]}})").compact().encode()); + assertEquals("({([a][b])([c][d])})", TraceNode.decode("({([a][b])([c][d])})").compact().encode()); + + assertEquals("({{}{(({()}({}){()(){}}){})}})", TraceNode.decode("({{}{(({()}({}){()(){}}){})}})").encode()); + assertEquals("()", TraceNode.decode("({{}{(({()}({}){()(){}}){})}})").compact().encode()); + assertEquals("([x])", TraceNode.decode("({{}{([x]({()}({}){()(){}}){})}})").compact().encode()); + assertEquals("([x])", TraceNode.decode("({{}{(({()}({[x]}){()(){}}){})}})").compact().encode()); + assertEquals("([x])", TraceNode.decode("({{}{(({()}({}){()(){}})[x]{})}})").compact().encode()); + + assertEquals("({[a][b][c][d][e][f]})", TraceNode.decode("({({[a][b]})({[c][d]})({[e][f]})})").compact().encode()); + } + + public void testSort() { + assertEquals("([b][a][c])", TraceNode.decode("([b][a][c])").sort().encode()); + assertEquals("({[a][b][c]})", TraceNode.decode("({[b][a][c]})").sort().encode()); + assertEquals("(([c][a])([b]))", TraceNode.decode("(([c][a])([b]))").sort().encode()); + assertEquals("({[b]([c][a])})", TraceNode.decode("({([c][a])[b]})").sort().encode()); + assertEquals("({[a][c]}[b])", TraceNode.decode("({[c][a]}[b])").sort().encode()); + assertEquals("({([b]){[a][c]}})", TraceNode.decode("({{[c][a]}([b])})").sort().encode()); + } + + public void testNormalize() { + TraceNode t1 = TraceNode.decode("({([a][b]{[x][y]([p][q])})([c][d])([e][f])})"); + TraceNode t2 = TraceNode.decode("({([a][b]{[y][x]([p][q])})([c][d])([e][f])})"); + TraceNode t3 = TraceNode.decode("({([a][b]{[y]([p][q])[x]})([c][d])([e][f])})"); + TraceNode t4 = TraceNode.decode("({([e][f])([a][b]{[y]([p][q])[x]})([c][d])})"); + TraceNode t5 = TraceNode.decode("({([e][f])([c][d])([a][b]{([p][q])[y][x]})})"); + + TraceNode tx = TraceNode.decode("({([b][a]{[x][y]([p][q])})([c][d])([e][f])})"); + TraceNode ty = TraceNode.decode("({([a][b]{[x][y]([p][q])})([d][c])([e][f])})"); + TraceNode tz = TraceNode.decode("({([a][b]{[x][y]([q][p])})([c][d])([e][f])})"); + + assertEquals("({([a][b]{[x][y]([p][q])})([c][d])([e][f])})", t1.compact().encode()); + + assertTrue(!t1.compact().encode().equals(t2.compact().encode())); + assertTrue(!t1.compact().encode().equals(t3.compact().encode())); + assertTrue(!t1.compact().encode().equals(t4.compact().encode())); + assertTrue(!t1.compact().encode().equals(t5.compact().encode())); + assertTrue(!t1.compact().encode().equals(tx.compact().encode())); + assertTrue(!t1.compact().encode().equals(ty.compact().encode())); + assertTrue(!t1.compact().encode().equals(tz.compact().encode())); + + System.out.println("1: " + t1.normalize().encode()); + System.out.println("2: " + t2.normalize().encode()); + System.out.println("3: " + t3.normalize().encode()); + System.out.println("4: " + t4.normalize().encode()); + System.out.println("5: " + t5.normalize().encode()); + System.out.println("x: " + tx.normalize().encode()); + System.out.println("y: " + ty.normalize().encode()); + System.out.println("z: " + tz.normalize().encode()); + assertTrue(t1.normalize().encode().equals(t2.normalize().encode())); + assertTrue(t1.normalize().encode().equals(t3.normalize().encode())); + assertTrue(t1.normalize().encode().equals(t4.normalize().encode())); + assertTrue(t1.normalize().encode().equals(t5.normalize().encode())); + assertTrue(!t1.normalize().encode().equals(tx.normalize().encode())); + assertTrue(!t1.normalize().encode().equals(ty.normalize().encode())); + assertTrue(!t1.normalize().encode().equals(tz.normalize().encode())); + + assertEquals("({([c][d])([e][f])([a][b]{[x][y]([p][q])})})", t1.normalize().encode()); + } + + public void testTraceDump() { + { + Trace big = new Trace(); + TraceNode b1 = new TraceNode(); + TraceNode b2 = new TraceNode(); + for (int i = 0; i < 100; ++i) { + b2.addChild("test"); + } + for (int i = 0; i < 10; ++i) { + b1.addChild(b2); + } + for (int i = 0; i < 10; ++i) { + big.getRoot().addChild(b1); + } + String normal = big.toString(); + String full = big.getRoot().toString(); + assertTrue(normal.length() > 30000); + assertTrue(normal.length() < 32000); + assertTrue(full.length() > 50000); + assertEquals(normal.substring(0, 30000), full.substring(0, 30000)); + } + { + TraceNode s1 = new TraceNode(); + TraceNode s2 = new TraceNode(); + s2.addChild("test"); + s2.addChild("test"); + s1.addChild(s2); + s1.addChild(s2); + assertEquals("...\n", s1.toString(0)); + assertEquals("<trace>\n...\n", s1.toString(1)); + assertEquals("<trace>\n" + // 8 8 + " <trace>\n" + // 12 20 + " test\n" + // 13 33 + "...\n", s1.toString(33)); + assertEquals("<trace>\n" + // 8 8 + " test\n" + // 9 17 + " test\n" + // 9 26 + "...\n", s2.toString(26)); + assertEquals("<trace>\n" + // 8 8 + " test\n" + // 9 17 + " test\n" + // 9 26 + "</trace>\n", s2.toString(27)); + assertEquals(s2.toString(27), s2.toString()); + } + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/TraceTripTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/TraceTripTestCase.java new file mode 100755 index 00000000000..e675a8ef98a --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/TraceTripTestCase.java @@ -0,0 +1,116 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus; + +import com.yahoo.jrt.ListenFailedException; +import com.yahoo.jrt.slobrok.server.Slobrok; +import com.yahoo.messagebus.network.rpc.test.TestServer; +import com.yahoo.messagebus.routing.RoutingTableSpec; +import com.yahoo.messagebus.test.Receptor; +import com.yahoo.messagebus.test.SimpleMessage; +import com.yahoo.messagebus.test.SimpleProtocol; + +import java.net.UnknownHostException; +import java.util.Arrays; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class TraceTripTestCase extends junit.framework.TestCase { + + Slobrok slobrok; + TestServer src; + TestServer pxy; + TestServer dst; + + public TraceTripTestCase(String message) { + super(message); + } + + public void setUp() throws ListenFailedException, UnknownHostException { + RoutingTableSpec table = new RoutingTableSpec(SimpleProtocol.NAME) + .addHop("pxy", "test/pxy/session", Arrays.asList("test/pxy/session")) + .addHop("dst", "test/dst/session", Arrays.asList("test/dst/session")) + .addRoute("test", Arrays.asList("pxy", "dst")); + + slobrok = new Slobrok(); + src = new TestServer("test/src", table, slobrok, null, null); + pxy = new TestServer("test/pxy", table, slobrok, null, null); + dst = new TestServer("test/dst", table, slobrok, null, null); + } + + public void tearDown() { + dst.destroy(); + pxy.destroy(); + src.destroy(); + slobrok.stop(); + } + + public void testTrip() { + Receptor src_rr = new Receptor(); + SourceSession src_s = src.mb.createSourceSession(src_rr); + + new Proxy(pxy.mb); + assertTrue(src.waitSlobrok("test/pxy/session", 1)); + + new Server(dst.mb); + assertTrue(src.waitSlobrok("test/dst/session", 1)); + assertTrue(pxy.waitSlobrok("test/dst/session", 1)); + + Message msg = new SimpleMessage(""); + msg.getTrace().setLevel(1); + msg.getTrace().trace(1, "Client message", false); + src_s.send(msg, "test"); + Reply reply = src_rr.getReply(60); + reply.getTrace().trace(1, "Client reply", false); + assertTrue(reply.getNumErrors() == 0); + + TraceNode t = new TraceNode() + .addChild("Client message") + .addChild("Proxy message") + .addChild("Server message") + .addChild("Server reply") + .addChild("Proxy reply") + .addChild("Client reply"); + System.out.println("reply: " + reply.getTrace().getRoot().encode()); + System.out.println("want : " + t.encode()); + assertTrue(reply.getTrace().getRoot().encode().equals(t.encode())); + } + + private static class Proxy implements MessageHandler, ReplyHandler { + private IntermediateSession session; + + public Proxy(MessageBus bus) { + session = bus.createIntermediateSession("session", true, this, this); + } + + public void handleMessage(Message msg) { + msg.getTrace().trace(1, "Proxy message", false); + System.out.println(msg.getTrace().getRoot().encode()); + session.forward(msg); + } + + public void handleReply(Reply reply) { + reply.getTrace().trace(1, "Proxy reply", false); + System.out.println(reply.getTrace().getRoot().encode()); + session.forward(reply); + } + } + + private static class Server implements MessageHandler { + private DestinationSession session; + + public Server(MessageBus bus) { + session = bus.createDestinationSession("session", true, this); + } + + public void handleMessage(Message msg) { + msg.getTrace().trace(1, "Server message", false); + System.out.println(msg.getTrace().getRoot().encode()); + Reply reply = new EmptyReply(); + msg.swapState(reply); + reply.getTrace().trace(1, "Server reply", false); + System.out.println(reply.getTrace().getRoot().encode()); + session.reply(reply); + } + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/network/IdentityTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/network/IdentityTestCase.java new file mode 100644 index 00000000000..2191b2750bf --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/network/IdentityTestCase.java @@ -0,0 +1,28 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class IdentityTestCase { + + @Test + public void requireThatAccessorsWork() { + Identity id = new Identity("foo"); + assertNotNull(id.getHostname()); + assertEquals("foo", id.getServicePrefix()); + } + + @Test + public void requireThatCopyConstructorWorks() { + Identity lhs = new Identity("foo"); + Identity rhs = new Identity(lhs); + assertEquals(lhs.getHostname(), rhs.getHostname()); + assertEquals(lhs.getServicePrefix(), rhs.getServicePrefix()); + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/network/local/LocalNetworkTest.java b/messagebus/src/test/java/com/yahoo/messagebus/network/local/LocalNetworkTest.java new file mode 100644 index 00000000000..296e724a502 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/network/local/LocalNetworkTest.java @@ -0,0 +1,132 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.local; + +import com.yahoo.messagebus.DestinationSession; +import com.yahoo.messagebus.DestinationSessionParams; +import com.yahoo.messagebus.EmptyReply; +import com.yahoo.messagebus.IntermediateSession; +import com.yahoo.messagebus.IntermediateSessionParams; +import com.yahoo.messagebus.Message; +import com.yahoo.messagebus.MessageBus; +import com.yahoo.messagebus.MessageBusParams; +import com.yahoo.messagebus.MessageHandler; +import com.yahoo.messagebus.Reply; +import com.yahoo.messagebus.ReplyHandler; +import com.yahoo.messagebus.SourceSession; +import com.yahoo.messagebus.SourceSessionParams; +import com.yahoo.messagebus.routing.Hop; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.messagebus.test.SimpleMessage; +import com.yahoo.messagebus.test.SimpleProtocol; +import com.yahoo.messagebus.test.SimpleReply; +import org.junit.Test; + +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class LocalNetworkTest { + + @Test + public void requireThatLocalNetworkCanSendAndReceive() throws InterruptedException { + final LocalWire wire = new LocalWire(); + + final Server serverA = new Server(wire); + final SourceSession source = serverA.newSourceSession(); + + final Server serverB = new Server(wire); + final IntermediateSession intermediate = serverB.newIntermediateSession(); + + final Server serverC = new Server(wire); + final DestinationSession destination = serverC.newDestinationSession(); + + Message msg = new SimpleMessage("foo"); + msg.setRoute(new Route().addHop(Hop.parse(intermediate.getConnectionSpec())) + .addHop(Hop.parse(destination.getConnectionSpec()))); + assertThat(source.send(msg).isAccepted(), is(true)); + + msg = serverB.messages.poll(60, TimeUnit.SECONDS); + assertThat(msg, instanceOf(SimpleMessage.class)); + assertThat(((SimpleMessage)msg).getValue(), is("foo")); + intermediate.forward(msg); + + msg = serverC.messages.poll(60, TimeUnit.SECONDS); + assertThat(msg, instanceOf(SimpleMessage.class)); + assertThat(((SimpleMessage)msg).getValue(), is("foo")); + Reply reply = new SimpleReply("bar"); + reply.swapState(msg); + destination.reply(reply); + + reply = serverB.replies.poll(60, TimeUnit.SECONDS); + assertThat(reply, instanceOf(SimpleReply.class)); + assertThat(((SimpleReply)reply).getValue(), is("bar")); + intermediate.forward(reply); + + reply = serverA.replies.poll(60, TimeUnit.SECONDS); + assertThat(reply, instanceOf(SimpleReply.class)); + assertThat(((SimpleReply)reply).getValue(), is("bar")); + + serverA.mbus.destroy(); + serverB.mbus.destroy(); + serverC.mbus.destroy(); + } + + @Test + public void requireThatUnknownServiceRepliesWithNoAddressForService() throws InterruptedException { + final Server server = new Server(new LocalWire()); + final SourceSession source = server.newSourceSession(); + + final Message msg = new SimpleMessage("foo").setRoute(Route.parse("bar")); + assertThat(source.send(msg).isAccepted(), is(true)); + final Reply reply = server.replies.poll(60, TimeUnit.SECONDS); + assertThat(reply, instanceOf(EmptyReply.class)); + + server.mbus.destroy(); + } + + private static class Server implements MessageHandler, ReplyHandler { + + final MessageBus mbus; + final BlockingDeque<Message> messages = new LinkedBlockingDeque<>(); + final BlockingDeque<Reply> replies = new LinkedBlockingDeque<>(); + + Server(final LocalWire wire) { + mbus = new MessageBus(new LocalNetwork(wire), + new MessageBusParams().addProtocol(new SimpleProtocol()) + .setRetryPolicy(null)); + } + + SourceSession newSourceSession() { + return mbus.createSourceSession( + new SourceSessionParams().setTimeout(600.0).setReplyHandler(this)); + } + + IntermediateSession newIntermediateSession() { + return mbus.createIntermediateSession(new IntermediateSessionParams() + .setMessageHandler(this) + .setReplyHandler(this)); + } + + DestinationSession newDestinationSession() { + return mbus.createDestinationSession(new DestinationSessionParams() + .setMessageHandler(this)); + } + + @Override + public void handleMessage(final Message msg) { + messages.addLast(msg); + } + + @Override + public void handleReply(final Reply reply) { + replies.addLast(reply); + } + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/BasicNetworkTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/BasicNetworkTestCase.java new file mode 100644 index 00000000000..7fe481a2196 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/BasicNetworkTestCase.java @@ -0,0 +1,152 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc; + + +import com.yahoo.concurrent.SystemTimer; +import com.yahoo.jrt.ListenFailedException; +import com.yahoo.jrt.slobrok.server.Slobrok; +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.network.rpc.test.TestServer; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.messagebus.routing.RoutingTableSpec; +import com.yahoo.messagebus.test.Receptor; +import com.yahoo.messagebus.test.SimpleMessage; +import com.yahoo.messagebus.test.SimpleProtocol; +import com.yahoo.messagebus.test.SimpleReply; + +import java.net.UnknownHostException; +import java.util.Arrays; + + +/** + * @author <a href="mailto:havardpe@yahoo-inc.com">Haavard Pettersen</a> + */ +public class BasicNetworkTestCase extends junit.framework.TestCase { + + Slobrok slobrok; + TestServer src; + TestServer pxy; + TestServer dst; + + public void setUp() throws ListenFailedException, UnknownHostException { + RoutingTableSpec table = new RoutingTableSpec(SimpleProtocol.NAME); + table.addHop("pxy", "test/pxy/session", Arrays.asList("test/pxy/session")); + table.addHop("dst", "test/dst/session", Arrays.asList("test/dst/session")); + table.addRoute("test", Arrays.asList("pxy", "dst")); + slobrok = new Slobrok(); + src = new TestServer("test/src", table, slobrok, null, null); + pxy = new TestServer("test/pxy", table, slobrok, null, null); + dst = new TestServer("test/dst", table, slobrok, null, null); + } + + public void tearDown() { + dst.destroy(); + pxy.destroy(); + src.destroy(); + slobrok.stop(); + } + + public void testNetwork() { + // set up receptors + Receptor src_rr = new Receptor(); + Receptor pxy_mr = new Receptor(); + Receptor pxy_rr = new Receptor(); + Receptor dst_mr = new Receptor(); + + // set up sessions + SourceSessionParams sp = new SourceSessionParams(); + sp.setTimeout(30.0); + + SourceSession ss = src.mb.createSourceSession(src_rr, sp); + IntermediateSession is = pxy.mb.createIntermediateSession("session", true, pxy_mr, pxy_rr); + DestinationSession ds = dst.mb.createDestinationSession("session", true, dst_mr); + + // wait for slobrok registration + assertTrue(src.waitSlobrok("test/pxy/session", 1)); + assertTrue(src.waitSlobrok("test/dst/session", 1)); + assertTrue(pxy.waitSlobrok("test/dst/session", 1)); + + // send message on client + ss.send(new SimpleMessage("test message"), "test"); + + // check message on proxy + Message msg = pxy_mr.getMessage(60); + assertTrue(msg != null); + assertEquals(SimpleProtocol.MESSAGE, msg.getType()); + SimpleMessage sm = (SimpleMessage) msg; + assertEquals("test message", sm.getValue()); + + // forward message on proxy + sm.setValue(sm.getValue() + " pxy"); + is.forward(sm); + + // check message on server + msg = dst_mr.getMessage(60); + assertTrue(msg != null); + assertEquals(SimpleProtocol.MESSAGE, msg.getType()); + sm = (SimpleMessage) msg; + assertEquals("test message pxy", sm.getValue()); + + // send reply on server + SimpleReply sr = new SimpleReply("test reply"); + sm.swapState(sr); + ds.reply(sr); + + // check reply on proxy + Reply reply = pxy_rr.getReply(60); + assertTrue(reply != null); + assertEquals(SimpleProtocol.REPLY, reply.getType()); + sr = (SimpleReply) reply; + assertEquals("test reply", sr.getValue()); + + // forward reply on proxy + sr.setValue(sr.getValue() + " pxy"); + is.forward(sr); + + // check reply on client + reply = src_rr.getReply(60); + assertTrue(reply != null); + assertEquals(SimpleProtocol.REPLY, reply.getType()); + sr = (SimpleReply) reply; + assertEquals("test reply pxy", sr.getValue()); + + ss.destroy(); + is.destroy(); + ds.destroy(); + } + + public void testTimeoutsFollowMessage() { + SourceSessionParams params = new SourceSessionParams().setTimeout(600.0); + SourceSession ss = src.mb.createSourceSession(new Receptor(), params); + DestinationSession ds = dst.mb.createDestinationSession("session", true, new Receptor()); + assertTrue(src.waitSlobrok("test/dst/session", 1)); + + // Test default timeouts being set. + Message msg = new SimpleMessage("msg"); + msg.getTrace().setLevel(9); + long now = SystemTimer.INSTANCE.milliTime(); + assertTrue(ss.send(msg, Route.parse("dst")).isAccepted()); + + assertNotNull(msg = ((Receptor)ds.getMessageHandler()).getMessage(60)); + assertTrue(msg.getTimeReceived() >= now); + assertTrue(params.getTimeout() * 1000 >= msg.getTimeRemaining()); + ds.acknowledge(msg); + + assertNotNull(((Receptor)ss.getReplyHandler()).getReply(60)); + + // Test default timeouts being overwritten. + msg = new SimpleMessage("msg"); + msg.getTrace().setLevel(9); + msg.setTimeRemaining(2 * (long)(params.getTimeout() * 1000)); + assertTrue(ss.send(msg, Route.parse("dst")).isAccepted()); + + assertNotNull(msg = ((Receptor)ds.getMessageHandler()).getMessage(60)); + assertTrue(params.getTimeout() * 1000 < msg.getTimeRemaining()); + ds.acknowledge(msg); + + assertNotNull(((Receptor)ss.getReplyHandler()).getReply(60)); + + ss.destroy(); + ds.destroy(); + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/LoadBalanceTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/LoadBalanceTestCase.java new file mode 100644 index 00000000000..3f6c449679e --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/LoadBalanceTestCase.java @@ -0,0 +1,96 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc; + +import com.yahoo.jrt.ListenFailedException; +import com.yahoo.jrt.slobrok.server.Slobrok; +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.network.Identity; +import com.yahoo.messagebus.network.rpc.test.TestServer; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.messagebus.test.QueueAdapter; +import com.yahoo.messagebus.test.SimpleMessage; + +import java.net.UnknownHostException; + +/** + * @author <a href="mailto:havardpe@yahoo-inc.com">Haavard Pettersen</a> + */ +public class LoadBalanceTestCase extends junit.framework.TestCase { + + public void testLoadBalance() throws ListenFailedException, UnknownHostException { + Slobrok slobrok = new Slobrok(); + TestServer src = new TestServer("src", null, slobrok, null, null); + TestServer dst1 = new TestServer("dst/1", null, slobrok, null, null); + TestServer dst2 = new TestServer("dst/2", null, slobrok, null, null); + TestServer dst3 = new TestServer("dst/3", null, slobrok, null, null); + + // set up handlers + final QueueAdapter sq = new QueueAdapter(); + SourceSession ss = src.mb.createSourceSession(new SourceSessionParams().setTimeout(600.0).setThrottlePolicy(null) + .setReplyHandler(new ReplyHandler() { + @Override + public void handleReply(Reply reply) { + System.out.println(Thread.currentThread().getName() + ": Reply '" + + ((SimpleMessage)reply.getMessage()).getValue() + "' received at source."); + sq.handleReply(reply); + } + })); + SimpleDestination h1 = new SimpleDestination(dst1.mb, dst1.net.getIdentity()); + SimpleDestination h2 = new SimpleDestination(dst2.mb, dst2.net.getIdentity()); + SimpleDestination h3 = new SimpleDestination(dst3.mb, dst3.net.getIdentity()); + assertTrue(src.waitSlobrok("dst/*/session", 3)); + + // send messages + int msgCnt = 30; // should be divisible by 3 + for (int i = 0; i < msgCnt; ++i) { + ss.send(new SimpleMessage("msg" + i), Route.parse("dst/*/session")); + } + + // wait for replies + assertTrue(sq.waitSize(msgCnt, 60)); + + // check handler message distribution + assertEquals(msgCnt / 3, h1.getCount()); + assertEquals(msgCnt / 3, h2.getCount()); + assertEquals(msgCnt / 3, h3.getCount()); + + ss.destroy(); + h1.session.destroy(); + h2.session.destroy(); + h3.session.destroy(); + + dst3.destroy(); + dst2.destroy(); + dst1.destroy(); + src.destroy(); + slobrok.stop(); + } + + /** + * Implements a simple destination that counts and acknowledges all messages received. + */ + private static class SimpleDestination implements MessageHandler { + + final DestinationSession session; + final String ident; + int cnt = 0; + + SimpleDestination(MessageBus mb, Identity ident) { + this.session = mb.createDestinationSession("session", true, this); + this.ident = ident.getServicePrefix(); + } + + @Override + public synchronized void handleMessage(Message msg) { + System.out.println( + Thread.currentThread().getName() + ": " + + "Message '" + ((SimpleMessage)msg).getValue() + "' received at '" + ident + "'."); + session.acknowledge(msg); + ++cnt; + } + + public synchronized int getCount() { + return cnt; + } + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/OOSTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/OOSTestCase.java new file mode 100755 index 00000000000..e65621e6a10 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/OOSTestCase.java @@ -0,0 +1,200 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc; + +import com.yahoo.jrt.ListenFailedException; +import com.yahoo.jrt.slobrok.server.Slobrok; +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.network.Identity; +import com.yahoo.messagebus.network.rpc.test.OOSServer; +import com.yahoo.messagebus.network.rpc.test.OOSState; +import com.yahoo.messagebus.network.rpc.test.TestServer; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.messagebus.test.Receptor; +import com.yahoo.messagebus.test.SimpleMessage; +import com.yahoo.messagebus.test.SimpleProtocol; + +import java.net.UnknownHostException; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class OOSTestCase extends junit.framework.TestCase { + + private static class MyServer extends TestServer implements MessageHandler { + DestinationSession session; + + public MyServer(String name, Slobrok slobrok, String oosServerPattern) + throws ListenFailedException, UnknownHostException + { + super(new MessageBusParams().setRetryPolicy(null).addProtocol(new SimpleProtocol()), + new RPCNetworkParams() + .setIdentity(new Identity(name)) + .setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok)) + .setOOSServerPattern(oosServerPattern)); + session = mb.createDestinationSession("session", true, this); + } + + public boolean destroy() { + session.destroy(); + return super.destroy(); + } + + public void handleMessage(Message msg) { + session.acknowledge(msg); + } + } + + private static void assertError(SourceSession src, String dst, int error) { + Message msg = new SimpleMessage("msg"); + msg.getTrace().setLevel(9); + assertTrue(src.send(msg, Route.parse(dst)).isAccepted()); + Reply reply = ((Receptor) src.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + if (error == ErrorCode.NONE) { + assertFalse(reply.hasErrors()); + } else { + assertTrue(reply.hasErrors()); + assertEquals(error, reply.getError(0).getCode()); + } + } + + public void testOOS() throws ListenFailedException, UnknownHostException { + Slobrok slobrok = new Slobrok(); + TestServer srcServer = new TestServer("src", null, slobrok, "oos/*", null); + SourceSession srcSession = srcServer.mb.createSourceSession(new Receptor()); + + MyServer dst1 = new MyServer("dst1", slobrok, null); + MyServer dst2 = new MyServer("dst2", slobrok, null); + MyServer dst3 = new MyServer("dst3", slobrok, null); + MyServer dst4 = new MyServer("dst4", slobrok, null); + MyServer dst5 = new MyServer("dst5", slobrok, null); + assertTrue(srcServer.waitSlobrok("*/session", 5)); + + // Ensure that normal sending is ok. + assertError(srcSession, "dst1/session", ErrorCode.NONE); + assertError(srcSession, "dst2/session", ErrorCode.NONE); + assertError(srcSession, "dst3/session", ErrorCode.NONE); + assertError(srcSession, "dst4/session", ErrorCode.NONE); + assertError(srcSession, "dst5/session", ErrorCode.NONE); + + // Ensure that 2 OOS services report properly. + OOSServer oosServer = new OOSServer(slobrok, "oos/1", new OOSState() + .add("dst2/session", true) + .add("dst3/session", true)); + assertTrue(srcServer.waitSlobrok("oos/*", 1)); + assertTrue(srcServer.waitState(new OOSState() + .add("dst2/session", true) + .add("dst3/session", true))); + assertError(srcSession, "dst1/session", ErrorCode.NONE); + assertError(srcSession, "dst2/session", ErrorCode.SERVICE_OOS); + assertError(srcSession, "dst3/session", ErrorCode.SERVICE_OOS); + assertError(srcSession, "dst4/session", ErrorCode.NONE); + assertError(srcSession, "dst5/session", ErrorCode.NONE); + + // Ensure that 1 OOS service may come up while other stays down. + oosServer.setState(new OOSState().add("dst2/session", true)); + assertTrue(srcServer.waitState(new OOSState() + .add("dst2/session", true) + .add("dst3/session", false))); + assertError(srcSession, "dst1/session", ErrorCode.NONE); + assertError(srcSession, "dst2/session", ErrorCode.SERVICE_OOS); + assertError(srcSession, "dst3/session", ErrorCode.NONE); + assertError(srcSession, "dst4/session", ErrorCode.NONE); + assertError(srcSession, "dst5/session", ErrorCode.NONE); + + // Add another OOS server and make sure that it works properly. + OOSServer oosServer2 = new OOSServer(slobrok, "oos/2", new OOSState() + .add("dst4/session", true) + .add("dst5/session", true)); + assertTrue(srcServer.waitSlobrok("oos/*", 2)); + assertTrue(srcServer.waitState(new OOSState() + .add("dst2/session", true) + .add("dst4/session", true) + .add("dst5/session", true))); + assertError(srcSession, "dst1/session", ErrorCode.NONE); + assertError(srcSession, "dst2/session", ErrorCode.SERVICE_OOS); + assertError(srcSession, "dst3/session", ErrorCode.NONE); + assertError(srcSession, "dst4/session", ErrorCode.SERVICE_OOS); + assertError(srcSession, "dst5/session", ErrorCode.SERVICE_OOS); + oosServer2.shutdown(); + + // Ensure that shutting down one OOS server will properly propagate. + assertTrue(srcServer.waitSlobrok("oos/*", 1)); + assertTrue(srcServer.waitState(new OOSState() + .add("dst1/session", false) + .add("dst2/session", true) + .add("dst3/session", false) + .add("dst4/session", false) + .add("dst5/session", false))); + assertError(srcSession, "dst1/session", ErrorCode.NONE); + assertError(srcSession, "dst2/session", ErrorCode.SERVICE_OOS); + assertError(srcSession, "dst3/session", ErrorCode.NONE); + assertError(srcSession, "dst4/session", ErrorCode.NONE); + assertError(srcSession, "dst5/session", ErrorCode.NONE); + + // Now add two new OOS servers and make sure that works too. + OOSServer oosServer3 = new OOSServer(slobrok, "oos/3", new OOSState() + .add("dst2/session", true) + .add("dst4/session", true)); + OOSServer oosServer4 = new OOSServer(slobrok, "oos/4", new OOSState() + .add("dst2/session", true) + .add("dst3/session", true) + .add("dst5/session", true)); + assertTrue(srcServer.waitSlobrok("oos/*", 3)); + assertTrue(srcServer.waitState(new OOSState() + .add("dst2/session", true) + .add("dst3/session", true) + .add("dst4/session", true) + .add("dst5/session", true))); + assertError(srcSession, "dst1/session", ErrorCode.NONE); + assertError(srcSession, "dst2/session", ErrorCode.SERVICE_OOS); + assertError(srcSession, "dst3/session", ErrorCode.SERVICE_OOS); + assertError(srcSession, "dst4/session", ErrorCode.SERVICE_OOS); + assertError(srcSession, "dst5/session", ErrorCode.SERVICE_OOS); + + // Modify the state of the two new servers and make sure it propagates. + oosServer3.setState(new OOSState() + .add("dst2/session", true)); + oosServer4.setState(new OOSState() + .add("dst1/session", true)); + assertTrue(srcServer.waitState(new OOSState() + .add("dst1/session", true) + .add("dst2/session", true) + .add("dst3/session", false) + .add("dst4/session", false) + .add("dst5/session", false))); + assertError(srcSession, "dst1/session", ErrorCode.SERVICE_OOS); + assertError(srcSession, "dst2/session", ErrorCode.SERVICE_OOS); + assertError(srcSession, "dst3/session", ErrorCode.NONE); + assertError(srcSession, "dst4/session", ErrorCode.NONE); + assertError(srcSession, "dst5/session", ErrorCode.NONE); + oosServer3.shutdown(); + oosServer4.shutdown(); + + // Ensure that shutting down the two latest OOS servers works properly. + assertTrue(srcServer.waitSlobrok("oos/*", 1)); + assertTrue(srcServer.waitState(new OOSState() + .add("dst1/session", false) + .add("dst2/session", true) + .add("dst3/session", false) + .add("dst4/session", false) + .add("dst5/session", false))); + assertError(srcSession, "dst1/session", ErrorCode.NONE); + assertError(srcSession, "dst2/session", ErrorCode.SERVICE_OOS); + assertError(srcSession, "dst3/session", ErrorCode.NONE); + assertError(srcSession, "dst4/session", ErrorCode.NONE); + assertError(srcSession, "dst5/session", ErrorCode.NONE); + + dst2.destroy(); + assertTrue(srcServer.waitSlobrok("*/session", 4)); + assertError(srcSession, "dst2/session", ErrorCode.SERVICE_OOS); + + srcSession.destroy(); + dst1.destroy(); + dst2.destroy(); + dst3.destroy(); + dst4.destroy(); + dst5.destroy(); + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/RPCNetworkTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/RPCNetworkTestCase.java new file mode 100644 index 00000000000..b2927611248 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/RPCNetworkTestCase.java @@ -0,0 +1,100 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc; + +import com.yahoo.component.Version; +import com.yahoo.jrt.slobrok.server.Slobrok; +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.metrics.MetricSet; +import com.yahoo.messagebus.network.rpc.test.TestServer; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.messagebus.routing.RoutingPolicy; +import com.yahoo.messagebus.test.Receptor; +import com.yahoo.text.Utf8String; +import org.junit.Test; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import static org.junit.Assert.*; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class RPCNetworkTestCase { + + @Test + public void requireThatProtocolEncodeExceptionIsCaught() throws Exception { + RuntimeException e = new RuntimeException(); + + Slobrok slobrok = new Slobrok(); + TestServer server = new TestServer(new MessageBusParams().addProtocol(MyProtocol.newEncodeException(e)), + new RPCNetworkParams().setSlobrokConfigId(slobrok.configId())); + Receptor receptor = new Receptor(); + SourceSession src = server.mb.createSourceSession( + new SourceSessionParams().setTimeout(600.0).setReplyHandler(receptor)); + DestinationSession dst = server.mb.createDestinationSession(new DestinationSessionParams()); + assertTrue(src.send(new MyMessage().setRoute(Route.parse(dst.getConnectionSpec()))).isAccepted()); + + Reply reply = receptor.getReply(60); + assertNotNull(reply); + assertEquals(1, reply.getNumErrors()); + + StringWriter expected = new StringWriter(); + e.printStackTrace(new PrintWriter(expected)); + + String actual = reply.getError(0).toString(); + assertTrue(actual, actual.contains(expected.toString())); + } + + private static class MyMessage extends Message { + + @Override + public Utf8String getProtocol() { + return new Utf8String(MyProtocol.NAME); + } + + @Override + public int getType() { + return 0; + } + } + + private static class MyProtocol implements Protocol { + + final static String NAME = "myProtocol"; + final RuntimeException encodeException; + + MyProtocol(RuntimeException encodeException) { + this.encodeException = encodeException; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public byte[] encode(Version version, Routable routable) { + throw encodeException; + } + + @Override + public Routable decode(Version version, byte[] payload) { + return null; + } + + @Override + public RoutingPolicy createPolicy(String name, String param) { + return null; + } + + @Override + public MetricSet getMetrics() { + return null; + } + + static MyProtocol newEncodeException(RuntimeException e) { + return new MyProtocol(e); + } + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/SendAdapterTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/SendAdapterTestCase.java new file mode 100755 index 00000000000..d296d7c7058 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/SendAdapterTestCase.java @@ -0,0 +1,157 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc;
+
+import com.yahoo.component.Version;
+import com.yahoo.jrt.ListenFailedException;
+import com.yahoo.jrt.slobrok.server.Slobrok;
+import com.yahoo.messagebus.*;
+import com.yahoo.messagebus.network.Identity;
+import com.yahoo.messagebus.network.rpc.test.TestServer;
+import com.yahoo.messagebus.routing.Route;
+import com.yahoo.messagebus.test.Receptor;
+import com.yahoo.messagebus.test.SimpleMessage;
+import com.yahoo.messagebus.test.SimpleProtocol;
+import com.yahoo.messagebus.test.SimpleReply;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class SendAdapterTestCase {
+
+ ////////////////////////////////////////////////////////////////////////////////
+ //
+ // Setup
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ Slobrok slobrok;
+ TestServer srcServer, itrServer, dstServer;
+ SourceSession srcSession;
+ IntermediateSession itrSession;
+ DestinationSession dstSession;
+ TestProtocol srcProtocol, itrProtocol, dstProtocol;
+
+ @Before
+ public void setUp() throws ListenFailedException, UnknownHostException {
+ slobrok = new Slobrok();
+ dstServer = new TestServer(
+ new MessageBusParams().addProtocol(dstProtocol = new TestProtocol()),
+ new RPCNetworkParams().setIdentity(new Identity("dst")).setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok)));
+ dstSession = dstServer.mb.createDestinationSession(
+ new DestinationSessionParams().setName("session").setMessageHandler(new Receptor()));
+ itrServer = new TestServer(
+ new MessageBusParams().addProtocol(itrProtocol = new TestProtocol()),
+ new RPCNetworkParams().setIdentity(new Identity("itr")).setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok)));
+ itrSession = itrServer.mb.createIntermediateSession(
+ new IntermediateSessionParams().setName("session").setMessageHandler(new Receptor()).setReplyHandler(new Receptor()));
+ srcServer = new TestServer(
+ new MessageBusParams().addProtocol(srcProtocol = new TestProtocol()),
+ new RPCNetworkParams().setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok)));
+ srcSession = srcServer.mb.createSourceSession(
+ new SourceSessionParams().setTimeout(600.0).setReplyHandler(new Receptor()));
+ assertTrue(srcServer.waitSlobrok("*/session", 2));
+ }
+
+ @After
+ public void tearDown() {
+ slobrok.stop();
+ dstSession.destroy();
+ dstServer.destroy();
+ itrSession.destroy();
+ itrServer.destroy();
+ srcSession.destroy();
+ srcServer.destroy();
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////
+ //
+ // Tests
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ @Test
+ public void requireThatMessagesCanBeSentAcrossAllSupportedVersions() throws Exception {
+ List<Version> versions = Arrays.asList(new Version(5, 0), new Version(5, 1));
+ for (Version srcVersion : versions) {
+ for (Version itrVersion : versions) {
+ for (Version dstVersion : versions) {
+ assertVersionedSend(srcVersion, itrVersion, dstVersion);
+ }
+ }
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////
+ //
+ // Utilities
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ private void assertVersionedSend(Version srcVersion, Version itrVersion, Version dstVersion) {
+ System.out.println("Sending from " + srcVersion + " through " + itrVersion + " to " + dstVersion + ":");
+ srcServer.net.setVersion(srcVersion);
+ itrServer.net.setVersion(itrVersion);
+ dstServer.net.setVersion(dstVersion);
+
+ Message msg = new SimpleMessage("foo");
+ msg.getTrace().setLevel(9);
+ assertTrue(srcSession.send(msg, Route.parse("itr/session dst/session")).isAccepted());
+ assertNotNull(msg = ((Receptor)itrSession.getMessageHandler()).getMessage(300));
+ System.out.println("\tMessage version " + srcProtocol.lastVersion + " serialized at source.");
+ Version minVersion = srcVersion.compareTo(itrVersion) < 0 ? srcVersion : itrVersion;
+ assertEquals(minVersion, srcProtocol.lastVersion);
+
+ System.out.println("\tMessage version " + itrProtocol.lastVersion + " reached intermediate.");
+ assertEquals(minVersion, itrProtocol.lastVersion);
+ itrSession.forward(msg);
+ assertNotNull(msg = ((Receptor)dstSession.getMessageHandler()).getMessage(300));
+ System.out.println("\tMessage version " + itrProtocol.lastVersion + " serialized at intermediate.");
+ minVersion = itrVersion.compareTo(dstVersion) < 0 ? itrVersion : dstVersion;
+ assertEquals(minVersion, itrProtocol.lastVersion);
+
+ System.out.println("\tMessage version " + dstProtocol.lastVersion + " reached destination.");
+ assertEquals(minVersion, dstProtocol.lastVersion);
+ Reply reply = new SimpleReply("bar");
+ reply.swapState(msg);
+ dstSession.reply(reply);
+ assertNotNull(reply = ((Receptor)itrSession.getReplyHandler()).getReply(300));
+ System.out.println("\tReply version " + dstProtocol.lastVersion + " serialized at destination.");
+ assertEquals(minVersion, dstProtocol.lastVersion);
+
+ System.out.println("\tReply version " + itrProtocol.lastVersion + " reached intermediate.");
+ assertEquals(minVersion, itrProtocol.lastVersion);
+ itrSession.forward(reply);
+ assertNotNull(((Receptor)srcSession.getReplyHandler()).getReply(300));
+ System.out.println("\tReply version " + itrProtocol.lastVersion + " serialized at intermediate.");
+ minVersion = srcVersion.compareTo(itrVersion) < 0 ? srcVersion : itrVersion;
+ assertEquals(minVersion, itrProtocol.lastVersion);
+
+ System.out.println("\tReply version " + srcProtocol.lastVersion + " reached source.");
+ assertEquals(minVersion, srcProtocol.lastVersion);
+ }
+
+ private static class TestProtocol extends SimpleProtocol {
+
+ Version lastVersion;
+
+ @Override
+ public byte[] encode(Version version, Routable routable) {
+ lastVersion = version;
+ return super.encode(version, routable);
+ }
+
+ public Routable decode(Version version, byte[] payload) {
+ lastVersion = version;
+ return super.decode(version, payload);
+ }
+ }
+}
diff --git a/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/ServiceAddressTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/ServiceAddressTestCase.java new file mode 100755 index 00000000000..e69896e2bcf --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/ServiceAddressTestCase.java @@ -0,0 +1,90 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc;
+
+import com.yahoo.jrt.ListenFailedException;
+import com.yahoo.jrt.Spec;
+import com.yahoo.jrt.slobrok.api.Mirror;
+import com.yahoo.jrt.slobrok.server.Slobrok;
+import com.yahoo.messagebus.network.Identity;
+
+import java.net.UnknownHostException;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ServiceAddressTestCase extends junit.framework.TestCase {
+
+ private Slobrok slobrok;
+ private RPCNetwork network;
+
+ public ServiceAddressTestCase(String msg) {
+ super(msg);
+ }
+
+ public void setUp() throws ListenFailedException, UnknownHostException {
+ slobrok = new Slobrok();
+ network = new RPCNetwork(new RPCNetworkParams()
+ .setIdentity(new Identity("foo"))
+ .setSlobrokConfigId("raw:slobrok[1]\nslobrok[0].connectionspec \"" +
+ new Spec("localhost", slobrok.port()).toString() + "\"\n"));
+ }
+
+ public void tearDown() {
+ network.shutdown();
+ slobrok.stop();
+ }
+
+ public void testAddrServiceAddress() {
+ assertNullAddress("tcp");
+ assertNullAddress("tcp/");
+ assertNullAddress("tcp/localhost");
+ assertNullAddress("tcp/localhost:");
+ assertNullAddress("tcp/localhost:1977");
+ assertNullAddress("tcp/localhost:1977/");
+ assertAddress("tcp/localhost:1977/session", "tcp/localhost:1977", "session");
+ assertNullAddress("tcp/localhost:/session");
+ //assertNullAddress("tcp/:1977/session");
+ assertNullAddress("tcp/:/session");
+ }
+
+ public void testNameServiceAddress() {
+ network.unregisterSession("session");
+ assertTrue(waitSlobrok("foo/session", 0));
+ assertNullAddress("foo/session");
+
+ network.registerSession("session");
+ assertTrue(waitSlobrok("foo/session", 1));
+ assertAddress("foo/session", network.getConnectionSpec(), "session");
+ }
+
+ private boolean waitSlobrok(String pattern, int num) {
+ for (int i = 0; i < 1000 && !Thread.currentThread().isInterrupted(); ++i) {
+ Mirror.Entry[] res = network.getMirror().lookup(pattern);
+ if (res.length == num) {
+ return true;
+ }
+ try {
+ Thread.sleep(10);
+ }
+ catch (InterruptedException e) {
+ // ignore
+ }
+ }
+ return false;
+ }
+
+ private void assertNullAddress(String pattern) {
+ assertNull(new RPCService(network.getMirror(), pattern).resolve());
+ }
+
+ private void assertAddress(String pattern, String expectedSpec, String expectedSession) {
+ RPCService service = new RPCService(network.getMirror(), pattern);
+ RPCServiceAddress obj = service.resolve();
+ assertNotNull(obj);
+ assertNotNull(obj.getConnectionSpec());
+ assertEquals(expectedSpec, obj.getConnectionSpec().toString());
+ if (expectedSession != null) {
+ assertEquals(expectedSession, obj.getSessionName());
+ }
+ }
+}
diff --git a/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/ServicePoolTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/ServicePoolTestCase.java new file mode 100644 index 00000000000..42580b963a5 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/ServicePoolTestCase.java @@ -0,0 +1,57 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc;
+
+import com.yahoo.jrt.ListenFailedException;
+import com.yahoo.jrt.slobrok.server.Slobrok;
+import com.yahoo.messagebus.network.rpc.test.TestServer;
+import junit.framework.TestCase;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ServicePoolTestCase extends TestCase {
+
+ public void testMaxSize() throws ListenFailedException {
+ Slobrok slobrok = new Slobrok();
+ RPCNetwork net = new RPCNetwork(new RPCNetworkParams().setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok)));
+ RPCServicePool pool = new RPCServicePool(net, 2);
+
+ pool.resolve("foo");
+ assertEquals(1, pool.getSize());
+ assertTrue(pool.hasService("foo"));
+ assertTrue(!pool.hasService("bar"));
+ assertTrue(!pool.hasService("baz"));
+
+ pool.resolve("foo");
+ assertEquals(1, pool.getSize());
+ assertTrue(pool.hasService("foo"));
+ assertTrue(!pool.hasService("bar"));
+ assertTrue(!pool.hasService("baz"));
+
+ pool.resolve("bar");
+ assertEquals(2, pool.getSize());
+ assertTrue(pool.hasService("foo"));
+ assertTrue(pool.hasService("bar"));
+ assertTrue(!pool.hasService("baz"));
+
+ pool.resolve("baz");
+ assertEquals(2, pool.getSize());
+ assertTrue(!pool.hasService("foo"));
+ assertTrue(pool.hasService("bar"));
+ assertTrue(pool.hasService("baz"));
+
+ pool.resolve("bar");
+ assertEquals(2, pool.getSize());
+ assertTrue(!pool.hasService("foo"));
+ assertTrue(pool.hasService("bar"));
+ assertTrue(pool.hasService("baz"));
+
+ pool.resolve("foo");
+ assertEquals(2, pool.getSize());
+ assertTrue(pool.hasService("foo"));
+ assertTrue(pool.hasService("bar"));
+ assertTrue(!pool.hasService("baz"));
+
+ slobrok.stop();
+ }
+}
\ No newline at end of file diff --git a/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/SlobrokTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/SlobrokTestCase.java new file mode 100644 index 00000000000..3f502d4da2f --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/SlobrokTestCase.java @@ -0,0 +1,151 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc; + + +import com.yahoo.jrt.ListenFailedException; +import com.yahoo.jrt.Spec; +import com.yahoo.jrt.slobrok.api.Mirror; +import com.yahoo.jrt.slobrok.server.Slobrok; +import com.yahoo.messagebus.network.Identity; + +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + + +/** + * @author <a href="mailto:havardpe@yahoo-inc.com">Haavard Pettersen</a> + */ +public class SlobrokTestCase extends junit.framework.TestCase { + + private static class Res { + private List<Mirror.Entry> lst = new ArrayList<>(); + public Res add(String fullName, String spec) { + lst.add(new Mirror.Entry(fullName, spec)); + return this; + } + public Mirror.Entry[] toArray() { + return lst.toArray(new Mirror.Entry[lst.size()]); + } + } + + + public SlobrokTestCase(String message) { + super(message); + } + + Slobrok slobrok; + RPCNetwork net1; + RPCNetwork net2; + RPCNetwork net3; + int port1; + int port2; + int port3; + + void check(RPCNetwork net, String pattern, Mirror.Entry[] expect) { + Comparator<Mirror.Entry> cmp = new Comparator<Mirror.Entry>() { + public int compare(Mirror.Entry a, Mirror.Entry b) { + return a.compareTo(b); + } + }; + Arrays.sort(expect, cmp); + Mirror.Entry[] actual = null; + for (int i = 0; i < 1000; i++) { + actual = net.getMirror().lookup(pattern); + Arrays.sort(actual, cmp); + if (Arrays.equals(actual, expect)) { + System.out.printf("lookup successful for pattern: %s\n", pattern); + return; + } + try { Thread.sleep(10); } catch (InterruptedException e) { + // + } + } + System.out.printf("lookup failed for pattern: %s\n", pattern); + System.out.printf("actual values:\n"); + if (actual == null || actual.length == 0) { + System.out.printf(" { EMPTY }\n"); + } else { + for (Mirror.Entry entry : actual) { + System.out.printf(" { %s, %s }\n", entry.getName(), entry.getSpec()); + } + } + System.out.printf("expected values:\n"); + if (expect.length == 0) { + System.out.printf(" { EMPTY }\n"); + } else { + for (Mirror.Entry entry : expect) { + System.out.printf(" { %s, %s }\n", entry.getName(), entry.getSpec()); + } + } + assertTrue(false); + } + + public void setUp() throws ListenFailedException, UnknownHostException { + slobrok = new Slobrok(); + String slobrokCfgId = "raw:slobrok[1]\nslobrok[0].connectionspec \"" + new Spec("localhost", slobrok.port()).toString() + "\"\n"; + net1 = new RPCNetwork(new RPCNetworkParams().setIdentity(new Identity("net/a")).setSlobrokConfigId(slobrokCfgId)); + net2 = new RPCNetwork(new RPCNetworkParams().setIdentity(new Identity("net/b")).setSlobrokConfigId(slobrokCfgId)); + net3 = new RPCNetwork(new RPCNetworkParams().setIdentity(new Identity("net/c")).setSlobrokConfigId(slobrokCfgId)); + port1 = net1.getPort(); + port2 = net2.getPort(); + port3 = net3.getPort(); + } + + public void tearDown() { + net3.shutdown(); + net2.shutdown(); + net1.shutdown(); + slobrok.stop(); + } + + public void testSlobrok() { + net1.registerSession("foo"); + net2.registerSession("foo"); + net2.registerSession("bar"); + net3.registerSession("foo"); + net3.registerSession("bar"); + net3.registerSession("baz"); + + check(net1, "*/*/*", new Res() + .add("net/a/foo", net1.getConnectionSpec()) + .add("net/b/foo", net2.getConnectionSpec()) + .add("net/b/bar", net2.getConnectionSpec()) + .add("net/c/foo", net3.getConnectionSpec()) + .add("net/c/bar", net3.getConnectionSpec()) + .add("net/c/baz", net3.getConnectionSpec()).toArray()); + check(net2, "*/*/*", new Res() + .add("net/a/foo", net1.getConnectionSpec()) + .add("net/b/foo", net2.getConnectionSpec()) + .add("net/b/bar", net2.getConnectionSpec()) + .add("net/c/foo", net3.getConnectionSpec()) + .add("net/c/bar", net3.getConnectionSpec()) + .add("net/c/baz", net3.getConnectionSpec()).toArray()); + check(net3, "*/*/*", new Res() + .add("net/a/foo", net1.getConnectionSpec()) + .add("net/b/foo", net2.getConnectionSpec()) + .add("net/b/bar", net2.getConnectionSpec()) + .add("net/c/foo", net3.getConnectionSpec()) + .add("net/c/bar", net3.getConnectionSpec()) + .add("net/c/baz", net3.getConnectionSpec()).toArray()); + + net2.unregisterSession("bar"); + net3.unregisterSession("bar"); + net3.unregisterSession("baz"); + + check(net1, "*/*/*", new Res() + .add("net/a/foo", net1.getConnectionSpec()) + .add("net/b/foo", net2.getConnectionSpec()) + .add("net/c/foo", net3.getConnectionSpec()).toArray()); + check(net2, "*/*/*", new Res() + .add("net/a/foo", net1.getConnectionSpec()) + .add("net/b/foo", net2.getConnectionSpec()) + .add("net/c/foo", net3.getConnectionSpec()).toArray()); + check(net3, "*/*/*", new Res() + .add("net/a/foo", net1.getConnectionSpec()) + .add("net/b/foo", net2.getConnectionSpec()) + .add("net/c/foo", net3.getConnectionSpec()).toArray()); + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/TargetPoolTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/TargetPoolTestCase.java new file mode 100755 index 00000000000..8d58b4dd89f --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/network/rpc/TargetPoolTestCase.java @@ -0,0 +1,112 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.network.rpc;
+
+import com.yahoo.jrt.ListenFailedException;
+import com.yahoo.jrt.Supervisor;
+import com.yahoo.jrt.Transport;
+import com.yahoo.jrt.slobrok.server.Slobrok;
+import com.yahoo.concurrent.Timer;
+import com.yahoo.messagebus.network.rpc.test.TestServer;
+
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class TargetPoolTestCase extends junit.framework.TestCase {
+
+ private Slobrok slobrok;
+ private List<TestServer> servers;
+ private Supervisor orb;
+
+ @Override
+ public void setUp() throws ListenFailedException {
+ slobrok = new Slobrok();
+ servers = new ArrayList<>();
+ orb = new Supervisor(new Transport());
+ }
+
+ @Override
+ public void tearDown() {
+ slobrok.stop();
+ for (TestServer server : servers) {
+ server.destroy();
+ }
+ orb.transport().shutdown().join();
+ }
+
+ public void testConnectionExpire() throws ListenFailedException, UnknownHostException {
+ // Necessary setup to be able to resolve targets.
+ RPCServiceAddress adr1 = registerServer();
+ RPCServiceAddress adr2 = registerServer();
+ RPCServiceAddress adr3 = registerServer();
+
+ PoolTimer timer = new PoolTimer();
+ RPCTargetPool pool = new RPCTargetPool(timer, 0.666);
+
+ // Assert that all connections expire.
+ RPCTarget target;
+ assertNotNull(target = pool.getTarget(orb, adr1)); target.subRef();
+ assertNotNull(target = pool.getTarget(orb, adr2)); target.subRef();
+ assertNotNull(target = pool.getTarget(orb, adr3)); target.subRef();
+ assertEquals(3, pool.size());
+ for (int i = 0; i < 10; ++i) {
+ pool.flushTargets(false);
+ assertEquals(3, pool.size());
+ }
+ timer.millis += 999;
+ pool.flushTargets(false);
+ assertEquals(0, pool.size());
+
+ // Assert that only idle connections expire.
+ assertNotNull(target = pool.getTarget(orb, adr1)); target.subRef();
+ assertNotNull(target = pool.getTarget(orb, adr2)); target.subRef();
+ assertNotNull(target = pool.getTarget(orb, adr3)); target.subRef();
+ assertEquals(3, pool.size());
+ timer.millis += 444;
+ pool.flushTargets(false);
+ assertEquals(3, pool.size());
+ assertNotNull(target = pool.getTarget(orb, adr2)); target.subRef();
+ assertNotNull(target = pool.getTarget(orb, adr3)); target.subRef();
+ assertEquals(3, pool.size());
+ timer.millis += 444;
+ pool.flushTargets(false);
+ assertEquals(2, pool.size());
+ assertNotNull(target = pool.getTarget(orb, adr3)); target.subRef();
+ timer.millis += 444;
+ pool.flushTargets(false);
+ assertEquals(1, pool.size());
+ timer.millis += 444;
+ pool.flushTargets(false);
+ assertEquals(0, pool.size());
+
+ // Assert that connections never expire while they are referenced.
+ assertNotNull(target = pool.getTarget(orb, adr1));
+ assertEquals(1, pool.size());
+ for (int i = 0; i < 10; ++i) {
+ timer.millis += 999;
+ pool.flushTargets(false);
+ assertEquals(1, pool.size());
+ }
+ target.subRef();
+ timer.millis += 999;
+ pool.flushTargets(false);
+ assertEquals(0, pool.size());
+ }
+
+ private RPCServiceAddress registerServer() throws ListenFailedException, UnknownHostException {
+ servers.add(new TestServer("srv" + servers.size(), null, slobrok, null, null));
+ return new RPCServiceAddress("foo/bar", servers.get(servers.size() - 1).mb.getConnectionSpec());
+ }
+
+ private static class PoolTimer implements Timer {
+ long millis = 0;
+
+ @Override
+ public long milliTime() {
+ return millis;
+ }
+ }
+}
\ No newline at end of file diff --git a/messagebus/src/test/java/com/yahoo/messagebus/routing/AdvancedRoutingTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/routing/AdvancedRoutingTestCase.java new file mode 100755 index 00000000000..62418c2766b --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/routing/AdvancedRoutingTestCase.java @@ -0,0 +1,119 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +import com.yahoo.jrt.ListenFailedException; +import com.yahoo.jrt.slobrok.server.Slobrok; +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.Error; +import com.yahoo.messagebus.network.Identity; +import com.yahoo.messagebus.network.rpc.RPCNetworkParams; +import com.yahoo.messagebus.network.rpc.test.TestServer; +import com.yahoo.messagebus.routing.test.CustomPolicyFactory; +import com.yahoo.messagebus.test.Receptor; +import com.yahoo.messagebus.test.SimpleMessage; +import com.yahoo.messagebus.test.SimpleProtocol; + +import java.net.UnknownHostException; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class AdvancedRoutingTestCase extends junit.framework.TestCase { + + //////////////////////////////////////////////////////////////////////////////// + // + // Setup + // + //////////////////////////////////////////////////////////////////////////////// + + Slobrok slobrok; + TestServer srcServer, dstServer; + SourceSession srcSession; + DestinationSession dstFoo, dstBar, dstBaz; + + @Override + public void setUp() throws ListenFailedException, UnknownHostException { + slobrok = new Slobrok(); + dstServer = new TestServer(new MessageBusParams().addProtocol(new SimpleProtocol()), + new RPCNetworkParams().setIdentity(new Identity("dst")).setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok))); + dstFoo = dstServer.mb.createDestinationSession(new DestinationSessionParams().setName("foo").setMessageHandler(new Receptor())); + dstBar = dstServer.mb.createDestinationSession(new DestinationSessionParams().setName("bar").setMessageHandler(new Receptor())); + dstBaz = dstServer.mb.createDestinationSession(new DestinationSessionParams().setName("baz").setMessageHandler(new Receptor())); + srcServer = new TestServer(new MessageBusParams().setRetryPolicy(new RetryTransientErrorsPolicy().setBaseDelay(0)).addProtocol(new SimpleProtocol()), + new RPCNetworkParams().setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok))); + srcSession = srcServer.mb.createSourceSession( + new SourceSessionParams().setTimeout(600.0).setReplyHandler(new Receptor())); + assertTrue(srcServer.waitSlobrok("dst/*", 3)); + } + + @Override + public void tearDown() { + slobrok.stop(); + dstFoo.destroy(); + dstBar.destroy(); + dstBaz.destroy(); + dstServer.destroy(); + srcSession.destroy(); + srcServer.destroy(); + } + + //////////////////////////////////////////////////////////////////////////////// + // + // Tests + // + //////////////////////////////////////////////////////////////////////////////// + + public void testAdvanced() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new CustomPolicyFactory(false, ErrorCode.NO_ADDRESS_FOR_SERVICE)); + srcServer.mb.putProtocol(protocol); + srcServer.setupRouting(new RoutingTableSpec(SimpleProtocol.NAME) + .addHop(new HopSpec("bar", "dst/bar")) + .addHop(new HopSpec("baz", "dst/baz")) + .addRoute(new RouteSpec("baz").addHop("baz"))); + Message msg = new SimpleMessage("msg"); + msg.getTrace().setLevel(9); + assertTrue(srcSession.send(msg, Route.parse("[Custom:" + dstFoo.getConnectionSpec() + ",bar,route:baz,dst/cox,?dst/unknown]")).isAccepted()); + + // Initial send. + assertNotNull(msg = ((Receptor)dstFoo.getMessageHandler()).getMessage(60)); + dstFoo.acknowledge(msg); + assertNotNull(msg = ((Receptor)dstBar.getMessageHandler()).getMessage(60)); + Reply reply = new EmptyReply(); + reply.swapState(msg); + reply.addError(new Error(ErrorCode.TRANSIENT_ERROR, "bar")); + dstBar.reply(reply); + assertNotNull(msg = ((Receptor)dstBaz.getMessageHandler()).getMessage(60)); + reply = new EmptyReply(); + reply.swapState(msg); + reply.addError(new Error(ErrorCode.TRANSIENT_ERROR, "baz1")); + dstBaz.reply(reply); + + // First retry. + assertNull(((Receptor)dstFoo.getMessageHandler()).getMessage(0)); + assertNotNull(msg = ((Receptor)dstBar.getMessageHandler()).getMessage(60)); + dstBar.acknowledge(msg); + assertNotNull(msg = ((Receptor)dstBaz.getMessageHandler()).getMessage(60)); + reply = new EmptyReply(); + reply.swapState(msg); + reply.addError(new Error(ErrorCode.TRANSIENT_ERROR, "baz2")); + dstBaz.reply(reply); + + // Second retry. + assertNull(((Receptor)dstFoo.getMessageHandler()).getMessage(0)); + assertNull(((Receptor)dstBar.getMessageHandler()).getMessage(0)); + assertNotNull(msg = ((Receptor)dstBaz.getMessageHandler()).getMessage(60)); + reply = new EmptyReply(); + reply.swapState(msg); + reply.addError(new Error(ErrorCode.FATAL_ERROR, "baz3")); + dstBaz.reply(reply); + + // Done. + reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertEquals(2, reply.getNumErrors()); + assertEquals(ErrorCode.FATAL_ERROR, reply.getError(0).getCode()); + assertEquals(ErrorCode.NO_ADDRESS_FOR_SERVICE, reply.getError(1).getCode()); + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/routing/ResenderTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/routing/ResenderTestCase.java new file mode 100755 index 00000000000..48993343f38 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/routing/ResenderTestCase.java @@ -0,0 +1,200 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +import com.yahoo.jrt.ListenFailedException; +import com.yahoo.jrt.slobrok.server.Slobrok; +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.Error; +import com.yahoo.messagebus.network.Identity; +import com.yahoo.messagebus.network.rpc.RPCNetworkParams; +import com.yahoo.messagebus.network.rpc.test.TestServer; +import com.yahoo.messagebus.test.Receptor; +import com.yahoo.messagebus.test.SimpleMessage; +import com.yahoo.messagebus.test.SimpleProtocol; + +import java.net.UnknownHostException; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class ResenderTestCase extends junit.framework.TestCase { + + //////////////////////////////////////////////////////////////////////////////// + // + // Setup + // + //////////////////////////////////////////////////////////////////////////////// + + Slobrok slobrok; + TestServer srcServer, dstServer; + SourceSession srcSession; + DestinationSession dstSession; + RetryTransientErrorsPolicy retryPolicy; + + @Override + public void setUp() throws ListenFailedException, UnknownHostException { + slobrok = new Slobrok(); + dstServer = new TestServer(new MessageBusParams().addProtocol(new SimpleProtocol()), + new RPCNetworkParams().setIdentity(new Identity("dst")).setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok))); + dstSession = dstServer.mb.createDestinationSession(new DestinationSessionParams().setName("session").setMessageHandler(new Receptor())); + retryPolicy = new RetryTransientErrorsPolicy(); + retryPolicy.setBaseDelay(0); + srcServer = new TestServer(new MessageBusParams().setRetryPolicy(retryPolicy).addProtocol(new SimpleProtocol()), + new RPCNetworkParams().setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok))); + srcSession = srcServer.mb.createSourceSession( + new SourceSessionParams().setTimeout(600.0).setReplyHandler(new Receptor())); + assertTrue(srcServer.waitSlobrok("dst/session", 1)); + } + + @Override + public void tearDown() { + slobrok.stop(); + dstSession.destroy(); + dstServer.destroy(); + srcSession.destroy(); + srcServer.destroy(); + } + + //////////////////////////////////////////////////////////////////////////////// + // + // Tests + // + //////////////////////////////////////////////////////////////////////////////// + + public void testRetryTag() { + assertTrue(srcSession.send(createMessage("msg"), Route.parse("dst/session")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + for (int i = 0; i < 5; ++i) { + assertEquals(i, msg.getRetry()); + assertEquals(true, msg.getRetryEnabled()); + replyFromDestination(msg, ErrorCode.APP_TRANSIENT_ERROR, 0); + assertNotNull(msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60)); + } + dstSession.acknowledge(msg); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + assertFalse(reply.hasErrors()); + assertNull(((Receptor)dstSession.getMessageHandler()).getMessage(0)); + System.out.println(reply.getTrace()); + } + + public void testRetryEnabledTag() { + Message msg = createMessage("msg"); + msg.setRetryEnabled(false); + assertTrue(srcSession.send(msg, Route.parse("dst/session")).isAccepted()); + assertNotNull(msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60)); + assertEquals(false, msg.getRetryEnabled()); + replyFromDestination(msg, ErrorCode.APP_TRANSIENT_ERROR, 0); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + assertTrue(reply.hasErrors()); + assertNull(((Receptor)dstSession.getMessageHandler()).getMessage(0)); + System.out.println(reply.getTrace()); + } + + public void testTransientError() { + assertTrue(srcSession.send(createMessage("msg"), Route.parse("dst/session")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + replyFromDestination(msg, ErrorCode.APP_TRANSIENT_ERROR, 0); + assertNotNull(msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60)); + replyFromDestination(msg, ErrorCode.APP_FATAL_ERROR, 0); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + assertTrue(reply.hasFatalErrors()); + assertNull(((Receptor)dstSession.getMessageHandler()).getMessage(0)); + } + + public void testFatalError() { + assertTrue(srcSession.send(createMessage("msg"), Route.parse("dst/session")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + replyFromDestination(msg, ErrorCode.APP_FATAL_ERROR, 0); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + assertTrue(reply.hasFatalErrors()); + assertNull(((Receptor)dstSession.getMessageHandler()).getMessage(0)); + } + + public void testDisableRetry() { + retryPolicy.setEnabled(false); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("dst/session")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + replyFromDestination(msg, ErrorCode.APP_TRANSIENT_ERROR, 0); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + assertTrue(reply.hasErrors()); + assertTrue(!reply.hasFatalErrors()); + assertNull(((Receptor)dstSession.getMessageHandler()).getMessage(0)); + } + + public void testRetryDelay() { + retryPolicy.setBaseDelay(0.01); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("dst/session")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + for (int i = 0; i < 5; ++i) { + replyFromDestination(msg, ErrorCode.APP_TRANSIENT_ERROR, -1); + assertNotNull(msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60)); + } + replyFromDestination(msg, ErrorCode.APP_FATAL_ERROR, 0); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + assertTrue(reply.hasFatalErrors()); + assertNull(((Receptor)dstSession.getMessageHandler()).getMessage(0)); + + String trace = reply.getTrace().toString(); + assertTrue(trace.contains("retry 1 in 0.01")); + assertTrue(trace.contains("retry 2 in 0.02")); + assertTrue(trace.contains("retry 3 in 0.03")); + assertTrue(trace.contains("retry 4 in 0.04")); + assertTrue(trace.contains("retry 5 in 0.05")); + } + + public void testRequestRetryDelay() { + assertTrue(srcSession.send(createMessage("msg"), Route.parse("dst/session")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + for (int i = 0; i < 5; ++i) { + replyFromDestination(msg, ErrorCode.APP_TRANSIENT_ERROR, i / 50.0); + assertNotNull(msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60)); + } + replyFromDestination(msg, ErrorCode.APP_FATAL_ERROR, 0); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + assertTrue(reply.hasFatalErrors()); + assertNull(((Receptor)dstSession.getMessageHandler()).getMessage(0)); + + String trace = reply.getTrace().toString(); + System.out.println(trace); + assertTrue(trace.contains("retry 1 in 0")); + assertTrue(trace.contains("retry 2 in 0.02")); + assertTrue(trace.contains("retry 3 in 0.04")); + assertTrue(trace.contains("retry 4 in 0.06")); + assertTrue(trace.contains("retry 5 in 0.08")); + } + + //////////////////////////////////////////////////////////////////////////////// + // + // Utilities + // + //////////////////////////////////////////////////////////////////////////////// + + private static Message createMessage(String msg) { + SimpleMessage ret = new SimpleMessage(msg); + ret.getTrace().setLevel(9); + return ret; + } + + private void replyFromDestination(Message msg, int errorCode, double retryDelay) { + Reply reply = new EmptyReply(); + reply.swapState(msg); + if (errorCode != ErrorCode.NONE) { + reply.addError(new Error(errorCode, "err")); + } + reply.setRetryDelay(retryDelay); + dstSession.reply(reply); + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/routing/RetryPolicyTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/routing/RetryPolicyTestCase.java new file mode 100644 index 00000000000..ad13f3325c7 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/routing/RetryPolicyTestCase.java @@ -0,0 +1,32 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing;
+
+import com.yahoo.messagebus.ErrorCode;
+import junit.framework.TestCase;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class RetryPolicyTestCase extends TestCase {
+
+ public void testSimpleRetryPolicy() {
+ RetryTransientErrorsPolicy policy = new RetryTransientErrorsPolicy();
+ for (int i = 0; i < 5; ++i) {
+ double delay = i / 3.0;
+ policy.setBaseDelay(delay);
+ for (int j = 0; j < 5; ++j) {
+ assertEquals((int)(j * delay), (int)policy.getRetryDelay(j));
+ }
+ for (int j = ErrorCode.NONE; j < ErrorCode.ERROR_LIMIT; ++j) {
+ policy.setEnabled(true);
+ if (j < ErrorCode.FATAL_ERROR) {
+ assertTrue(policy.canRetry(j));
+ } else {
+ assertFalse(policy.canRetry(j));
+ }
+ policy.setEnabled(false);
+ assertFalse(policy.canRetry(j));
+ }
+ }
+ }
+}
diff --git a/messagebus/src/test/java/com/yahoo/messagebus/routing/RouteParserTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/routing/RouteParserTestCase.java new file mode 100755 index 00000000000..e4d120271bc --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/routing/RouteParserTestCase.java @@ -0,0 +1,168 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RouteParserTestCase extends junit.framework.TestCase { + + public void testHopParser() { + Hop hop = Hop.parse("foo"); + assertNotNull(hop); + assertEquals(1, hop.getNumDirectives()); + assertVerbatimDirective(hop.getDirective(0), "foo"); + + assertNotNull(hop = Hop.parse("foo/bar")); + assertEquals(2, hop.getNumDirectives()); + assertVerbatimDirective(hop.getDirective(0), "foo"); + assertVerbatimDirective(hop.getDirective(1), "bar"); + + assertNotNull(hop = Hop.parse("tcp/foo:666/bar")); + assertEquals(1, hop.getNumDirectives()); + assertTcpDirective(hop.getDirective(0), "foo", 666, "bar"); + + assertNotNull(hop = Hop.parse("route:foo")); + assertEquals(1, hop.getNumDirectives()); + assertRouteDirective(hop.getDirective(0), "foo"); + + assertNotNull(hop = Hop.parse("[Extern:tcp/localhost:3619;foo/bar]")); + assertEquals(1, hop.getNumDirectives()); + assertPolicyDirective(hop.getDirective(0), "Extern","tcp/localhost:3619;foo/bar"); + + assertNotNull(hop = Hop.parse("[AND:foo bar]")); + assertEquals(1, hop.getNumDirectives()); + assertPolicyDirective(hop.getDirective(0), "AND","foo bar"); + + assertNotNull(hop = Hop.parse("[DocumentRouteSelector:raw:route[2]\n" + + "route[0].name \"foo\"\n" + + "route[0].selector \"testdoc\"\n" + + "route[0].feed \"myfeed\"\n" + + "route[1].name \"bar\"\n" + + "route[1].selector \"other\"\n" + + "route[1].feed \"myfeed\"\n" + + "]")); + assertEquals(1, hop.getNumDirectives()); + assertPolicyDirective(hop.getDirective(0), "DocumentRouteSelector", + "raw:route[2]\n" + + "route[0].name \"foo\"\n" + + "route[0].selector \"testdoc\"\n" + + "route[0].feed \"myfeed\"\n" + + "route[1].name \"bar\"\n" + + "route[1].selector \"other\"\n" + + "route[1].feed \"myfeed\""); + + assertNotNull(hop = Hop.parse("[DocumentRouteSelector:raw:route[1]\n" + + "route[0].name \"docproc/cluster.foo\"\n" + + "route[0].selector \"testdoc\"\n" + + "route[0].feed \"myfeed\"" + + "]")); + assertEquals(1, hop.getNumDirectives()); + assertPolicyDirective(hop.getDirective(0), "DocumentRouteSelector", + "raw:route[1]\n" + + "route[0].name \"docproc/cluster.foo\"\n" + + "route[0].selector \"testdoc\"\n" + + "route[0].feed \"myfeed\""); + } + + public void testHopParserErrors() { + assertError(Hop.parse(""), "Failed to parse empty string."); + assertError(Hop.parse("[foo"), "Unterminated '[' in '[foo'"); + assertError(Hop.parse("foo/[bar]]"), "Unexpected token ']' in 'foo/[bar]]'"); + assertError(Hop.parse("foo bar"), "Failed to completely parse 'foo bar'."); + } + + public void testShortRoute() { + Route shortRoute = Route.parse("c"); + assertNotNull(shortRoute); + assertEquals(1, shortRoute.getNumHops()); + Hop hop = shortRoute.getHop(0); + assertNotNull(hop); + assertEquals(1, hop.getNumDirectives()); + assertVerbatimDirective(hop.getDirective(0), "c"); + } + + public void testShortHops() { + Route shortRoute = Route.parse("a b c"); + assertNotNull(shortRoute); + assertEquals(3, shortRoute.getNumHops()); + Hop hop = shortRoute.getHop(0); + assertNotNull(hop); + assertEquals(1, hop.getNumDirectives()); + assertVerbatimDirective(hop.getDirective(0), "a"); + } + + public void testRouteParser() { + Route route = Route.parse("foo bar/baz"); + assertNotNull(route); + assertEquals(2, route.getNumHops()); + Hop hop = route.getHop(0); + assertNotNull(hop); + assertEquals(1, hop.getNumDirectives()); + assertVerbatimDirective(hop.getDirective(0), "foo"); + assertNotNull(hop = route.getHop(1)); + assertEquals(2, hop.getNumDirectives()); + assertVerbatimDirective(hop.getDirective(0), "bar"); + assertVerbatimDirective(hop.getDirective(1), "baz"); + + assertNotNull(route = Route.parse("[Extern:tcp/localhost:3633;itr/session] default")); + assertEquals(2, route.getNumHops()); + assertNotNull(hop = route.getHop(0)); + assertEquals(1, hop.getNumDirectives()); + assertPolicyDirective(hop.getDirective(0), "Extern", "tcp/localhost:3633;itr/session"); + assertNotNull(hop = route.getHop(1)); + assertEquals(1, hop.getNumDirectives()); + assertVerbatimDirective(hop.getDirective(0), "default"); + } + + public void testRouteParserErrors() { + assertError(Route.parse(""), "Failed to parse empty string."); + assertError(Route.parse("foo [bar"), "Unterminated '[' in '[bar'"); + assertError(Route.parse("foo bar/[baz]]"), "Unexpected token ']' in 'bar/[baz]]'"); + } + + private static void assertError(Route route, String msg) { + assertNotNull(route); + assertEquals(1, route.getNumHops()); + assertError(route.getHop(0), msg); + } + + private static void assertError(Hop hop, String msg) { + assertNotNull(hop); + System.out.println(hop.toDebugString()); + assertEquals(1, hop.getNumDirectives()); + assertErrorDirective(hop.getDirective(0), msg); + } + + private static void assertErrorDirective(HopDirective dir, String msg) { + assertNotNull(dir); + assertTrue(dir instanceof ErrorDirective); + assertEquals(msg, ((ErrorDirective)dir).getMessage()); + } + + private static void assertPolicyDirective(HopDirective dir, String name, String param) { + assertNotNull(dir); + assertTrue(dir instanceof PolicyDirective); + assertEquals(name, ((PolicyDirective)dir).getName()); + assertEquals(param, ((PolicyDirective)dir).getParam()); + } + + private static void assertRouteDirective(HopDirective dir, String name) { + assertNotNull(dir); + assertTrue(dir instanceof RouteDirective); + assertEquals(name, ((RouteDirective)dir).getName()); + } + + private static void assertTcpDirective(HopDirective dir, String host, int port, String session) { + assertNotNull(dir); + assertTrue(dir instanceof TcpDirective); + assertEquals(host, ((TcpDirective)dir).getHost()); + assertEquals(port, ((TcpDirective)dir).getPort()); + assertEquals(session, ((TcpDirective)dir).getSession()); + } + + private static void assertVerbatimDirective(HopDirective dir, String image) { + assertNotNull(dir); + assertTrue(dir instanceof VerbatimDirective); + assertEquals(image, ((VerbatimDirective)dir).getImage()); + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/routing/RoutingContextTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/routing/RoutingContextTestCase.java new file mode 100755 index 00000000000..ea217af5b9a --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/routing/RoutingContextTestCase.java @@ -0,0 +1,258 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +import com.yahoo.jrt.ListenFailedException; +import com.yahoo.jrt.slobrok.server.Slobrok; +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.Error; +import com.yahoo.messagebus.network.Identity; +import com.yahoo.messagebus.network.rpc.RPCNetworkParams; +import com.yahoo.messagebus.network.rpc.test.TestServer; +import com.yahoo.messagebus.test.Receptor; +import com.yahoo.messagebus.test.SimpleMessage; +import com.yahoo.messagebus.test.SimpleProtocol; + +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.List; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RoutingContextTestCase extends junit.framework.TestCase { + public static final int TIMEOUT_SECS = 120; + + //////////////////////////////////////////////////////////////////////////////// + // + // Setup + // + //////////////////////////////////////////////////////////////////////////////// + + Slobrok slobrok; + TestServer srcServer, dstServer; + SourceSession srcSession; + DestinationSession dstSession; + + @Override + public void setUp() throws ListenFailedException, UnknownHostException { + slobrok = new Slobrok(); + dstServer = new TestServer(new MessageBusParams().addProtocol(new SimpleProtocol()), + new RPCNetworkParams().setIdentity(new Identity("dst")).setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok))); + dstSession = dstServer.mb.createDestinationSession(new DestinationSessionParams().setName("session").setMessageHandler(new Receptor())); + srcServer = new TestServer(new MessageBusParams().setRetryPolicy(new RetryTransientErrorsPolicy().setBaseDelay(0)).addProtocol(new SimpleProtocol()), + new RPCNetworkParams().setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok))); + srcSession = srcServer.mb.createSourceSession( + new SourceSessionParams().setTimeout(600.0).setReplyHandler(new Receptor())); + assertTrue(srcServer.waitSlobrok("dst/session", 1)); + } + + @Override + public void tearDown() { + slobrok.stop(); + dstSession.destroy(); + dstServer.destroy(); + srcSession.destroy(); + srcServer.destroy(); + } + + //////////////////////////////////////////////////////////////////////////////// + // + // Tests + // + //////////////////////////////////////////////////////////////////////////////// + + public void testSingleDirective() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new CustomPolicyFactory( + false, + Arrays.asList("foo", "bar", "baz/cox"), + Arrays.asList("foo", "bar"))); + srcServer.mb.putProtocol(protocol); + srcServer.setupRouting(new RoutingTableSpec(SimpleProtocol.NAME) + .addRoute(new RouteSpec("myroute").addHop("myhop")) + .addHop(new HopSpec("myhop", "[Custom]") + .addRecipient("foo").addRecipient("bar").addRecipient("baz/cox"))); + for (int i = 0; i < 2; ++i) { + assertTrue(srcSession.send(createMessage("msg"), "myroute").isAccepted()); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(TIMEOUT_SECS); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertFalse(reply.hasErrors()); + } + } + + public void testMoreDirectives() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new CustomPolicyFactory( + false, + Arrays.asList("foo", "foo/bar", "foo/bar0/baz", "foo/bar1/baz", "foo/bar/baz/cox"), + Arrays.asList("foo/bar0/baz", "foo/bar1/baz"))); + srcServer.mb.putProtocol(protocol); + srcServer.setupRouting(new RoutingTableSpec(SimpleProtocol.NAME) + .addRoute(new RouteSpec("myroute").addHop("myhop")) + .addHop(new HopSpec("myhop", "foo/[Custom]/baz") + .addRecipient("foo").addRecipient("foo/bar") + .addRecipient("foo/bar0/baz").addRecipient("foo/bar1/baz") + .addRecipient("foo/bar/baz/cox"))); + for (int i = 0; i < 2; ++i) { + assertTrue(srcSession.send(createMessage("msg"), "myroute").isAccepted()); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(TIMEOUT_SECS); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertFalse(reply.hasErrors()); + } + } + + public void testRecipientsRemain() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("First", new CustomPolicyFactory(true, Arrays.asList("foo/bar"), Arrays.asList("foo/[Second]"))); + protocol.addPolicyFactory("Second", new CustomPolicyFactory(false, Arrays.asList("foo/bar"), Arrays.asList("foo/bar"))); + srcServer.mb.putProtocol(protocol); + srcServer.setupRouting(new RoutingTableSpec(SimpleProtocol.NAME) + .addRoute(new RouteSpec("myroute").addHop("myhop")) + .addHop(new HopSpec("myhop", "[First]/[Second]") + .addRecipient("foo/bar"))); + for (int i = 0; i < 2; ++i) { + assertTrue(srcSession.send(createMessage("msg"), "myroute").isAccepted()); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(TIMEOUT_SECS); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertFalse(reply.hasErrors()); + } + } + + public void testConstRoute() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("DocumentRouteSelector", + new CustomPolicyFactory(true, Arrays.asList("dst"), Arrays.asList("dst"))); + srcServer.mb.putProtocol(protocol); + srcServer.setupRouting(new RoutingTableSpec(SimpleProtocol.NAME) + .addRoute(new RouteSpec("default").addHop("indexing")) + .addHop(new HopSpec("indexing", "[DocumentRouteSelector]").addRecipient("dst")) + .addHop(new HopSpec("dst", "dst/session"))); + for (int i = 0; i < 2; ++i) { + assertTrue(srcSession.send(createMessage("msg"), Route.parse("route:default")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(TIMEOUT_SECS); + assertNotNull(msg); + dstSession.acknowledge(msg); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(TIMEOUT_SECS); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertFalse(reply.hasErrors()); + } + } + + //////////////////////////////////////////////////////////////////////////////// + // + // Utilities + // + //////////////////////////////////////////////////////////////////////////////// + + private Message createMessage(String msg) { + Message ret = new SimpleMessage(msg); + ret.getTrace().setLevel(9); + return ret; + } + + private static class CustomPolicyFactory implements SimpleProtocol.PolicyFactory { + + final boolean forward; + final List<String> expectedAll; + final List<String> expectedMatched; + + public CustomPolicyFactory(boolean forward, List<String> all, List<String> matched) { + this.forward = forward; + this.expectedAll = all; + this.expectedMatched = matched; + } + + public RoutingPolicy create(String param) { + return new CustomPolicy(this); + } + } + + private static class CustomPolicy implements RoutingPolicy { + + CustomPolicyFactory factory; + + public CustomPolicy(CustomPolicyFactory factory) { + this.factory = factory; + } + + public void select(RoutingContext ctx) { + Reply reply = new EmptyReply(); + reply.getTrace().setLevel(9); + + List<Route> recipients = ctx.getAllRecipients(); + if (factory.expectedAll.size() == recipients.size()) { + ctx.trace(1, "Got " + recipients.size() + " expected recipients."); + for (Route route : recipients) { + if (factory.expectedAll.contains(route.toString())) { + ctx.trace(1, "Got expected recipient '" + route + "'."); + } else { + reply.addError(new Error(ErrorCode.APP_FATAL_ERROR, + "Recipient '" + route + "' not expected.")); + } + } + } else { + reply.addError(new Error(ErrorCode.APP_FATAL_ERROR, + "Expected " + factory.expectedAll.size() + " recipients, got " + recipients.size() + ".")); + } + + if (ctx.getNumRecipients() == recipients.size()) { + for (int i = 0; i < recipients.size(); ++i) { + if (recipients.get(i) == ctx.getRecipient(i)) { + ctx.trace(1, "getRecipient(" + i + ") matches getAllRecipients().get(" + i + ")"); + } else { + reply.addError(new Error(ErrorCode.APP_FATAL_ERROR, + "getRecipient(" + i + ") differs from getAllRecipients().get(" + i + ")")); + } + } + } else { + reply.addError(new Error(ErrorCode.APP_FATAL_ERROR, + "getNumRecipients() differs from getAllRecipients().size()")); + } + + recipients = ctx.getMatchedRecipients(); + if (factory.expectedMatched.size() == recipients.size()) { + ctx.trace(1, "Got " + recipients.size() + " matched recipients."); + for (Route route : recipients) { + if (factory.expectedMatched.contains(route.toString())) { + ctx.trace(1, "Got matched recipient '" + route + "'."); + } else { + reply.addError(new Error(ErrorCode.APP_FATAL_ERROR, + "Matched recipient '" + route + "' not expected.")); + } + } + } else { + reply.addError(new Error(ErrorCode.APP_FATAL_ERROR, + "Expected " + factory.expectedAll.size() + " matched recipients, got " + recipients.size() + ".")); + } + + if (!reply.hasErrors() && factory.forward) { + for (Route route : recipients) { + ctx.addChild(route); + } + } else { + ctx.setReply(reply); + } + } + + public void merge(RoutingContext ctx) { + Reply ret = new EmptyReply(); + for (RoutingNodeIterator it = ctx.getChildIterator(); + it.isValid(); it.next()) { + Reply reply = it.getReplyRef(); + for (int i = 0; i < reply.getNumErrors(); ++i) { + ret.addError(reply.getError(i)); + } + } + ctx.setReply(ret); + } + + @Override + public void destroy() { + } + } + +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/routing/RoutingSpecTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/routing/RoutingSpecTestCase.java new file mode 100755 index 00000000000..3cbb9d86e44 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/routing/RoutingSpecTestCase.java @@ -0,0 +1,336 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing;
+
+import com.yahoo.messagebus.ConfigAgent;
+import com.yahoo.messagebus.ConfigHandler;
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class RoutingSpecTestCase extends TestCase {
+
+ ////////////////////////////////////////////////////////////////////////////////
+ //
+ // Tests
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ public void testConfig() {
+ assertConfig(new RoutingSpec());
+ assertConfig(new RoutingSpec().addTable(new RoutingTableSpec("mytable1")));
+ assertConfig(new RoutingSpec().addTable(new RoutingTableSpec("mytable1")
+ .addHop(new HopSpec("myhop1", "myselector1"))));
+ assertConfig(new RoutingSpec().addTable(new RoutingTableSpec("mytable1")
+ .addHop(new HopSpec("myhop1", "myselector1"))
+ .addRoute(new RouteSpec("myroute1").addHop("myhop1"))));
+ assertConfig(new RoutingSpec().addTable(new RoutingTableSpec("mytable1")
+ .addHop(new HopSpec("myhop1", "myselector1"))
+ .addHop(new HopSpec("myhop2", "myselector2"))
+ .addRoute(new RouteSpec("myroute1").addHop("myhop1"))
+ .addRoute(new RouteSpec("myroute2").addHop("myhop2"))
+ .addRoute(new RouteSpec("myroute12").addHop("myhop1").addHop("myhop2"))));
+ assertConfig(new RoutingSpec().addTable(new RoutingTableSpec("mytable1")
+ .addHop(new HopSpec("myhop1", "myselector1"))
+ .addHop(new HopSpec("myhop2", "myselector2"))
+ .addRoute(new RouteSpec("myroute1").addHop("myhop1"))
+ .addRoute(new RouteSpec("myroute2").addHop("myhop2"))
+ .addRoute(new RouteSpec("myroute12").addHop("myhop1").addHop("myhop2")))
+ .addTable(new RoutingTableSpec("mytable2")));
+ assertEquals("routingtable[2]\n" +
+ "routingtable[0].protocol \"mytable1\"\n" +
+ "routingtable[1].protocol \"mytable2\"\n" +
+ "routingtable[1].hop[3]\n" +
+ "routingtable[1].hop[0].name \"myhop1\"\n" +
+ "routingtable[1].hop[0].selector \"myselector1\"\n" +
+ "routingtable[1].hop[1].name \"myhop2\"\n" +
+ "routingtable[1].hop[1].selector \"myselector2\"\n" +
+ "routingtable[1].hop[1].ignoreresult true\n" +
+ "routingtable[1].hop[2].name \"myhop1\"\n" +
+ "routingtable[1].hop[2].selector \"myselector3\"\n" +
+ "routingtable[1].hop[2].recipient[2]\n" +
+ "routingtable[1].hop[2].recipient[0] \"myrecipient1\"\n" +
+ "routingtable[1].hop[2].recipient[1] \"myrecipient2\"\n" +
+ "routingtable[1].route[1]\n" +
+ "routingtable[1].route[0].name \"myroute1\"\n" +
+ "routingtable[1].route[0].hop[1]\n" +
+ "routingtable[1].route[0].hop[0] \"myhop1\"\n",
+ new RoutingSpec()
+ .addTable(new RoutingTableSpec("mytable1"))
+ .addTable(new RoutingTableSpec("mytable2")
+ .addHop(new HopSpec("myhop1", "myselector1"))
+ .addHop(new HopSpec("myhop2", "myselector2").setIgnoreResult(true))
+ .addHop(new HopSpec("myhop1", "myselector3")
+ .addRecipient("myrecipient1")
+ .addRecipient("myrecipient2"))
+ .addRoute(new RouteSpec("myroute1").addHop("myhop1"))).toString());
+ }
+
+ public void testApplicationSpec() {
+ assertApplicationSpec(Arrays.asList("foo"),
+ Arrays.asList("foo",
+ "*"));
+ assertApplicationSpec(Arrays.asList("foo/bar"),
+ Arrays.asList("foo/bar",
+ "foo/*",
+ "*/bar",
+ "*/*"));
+ assertApplicationSpec(Arrays.asList("foo/0/baz",
+ "foo/1/baz",
+ "foo/2/baz"),
+ Arrays.asList("foo/0/baz",
+ "foo/1/baz",
+ "foo/2/baz",
+ "foo/0/*",
+ "foo/1/*",
+ "foo/2/*",
+ "foo/*/baz",
+ "*/0/baz",
+ "*/1/baz",
+ "*/2/baz",
+ "foo/*/*",
+ "*/0/*",
+ "*/1/*",
+ "*/2/*",
+ "*/*/baz",
+ "*/*/*"));
+ }
+
+ public void testVeriyfOk() {
+ assertVerifyOk(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addHop(new HopSpec("hop1", "myservice1"))),
+ new ApplicationSpec().addService("mytable", "myservice1"));
+ assertVerifyOk(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addRoute(new RouteSpec("route1").addHop("myservice1"))),
+ new ApplicationSpec().addService("mytable", "myservice1"));
+ assertVerifyOk(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addHop(new HopSpec("hop1", "myservice1"))
+ .addRoute(new RouteSpec("route1").addHop("hop1"))),
+ new ApplicationSpec().addService("mytable", "myservice1"));
+ assertVerifyOk(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addHop(new HopSpec("hop1", "route:route2"))
+ .addHop(new HopSpec("hop2", "myservice1"))
+ .addRoute(new RouteSpec("route1").addHop("hop1"))
+ .addRoute(new RouteSpec("route2").addHop("hop2"))),
+ new ApplicationSpec().addService("mytable", "myservice1"));
+ assertVerifyOk(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addHop(new HopSpec("myhop1", "foo/[bar]/baz").addRecipient("foo/0/baz").addRecipient("foo/1/baz"))),
+ new ApplicationSpec()
+ .addService("mytable", "foo/0/baz")
+ .addService("mytable", "foo/1/baz"));
+ }
+
+ public void testVerifyToggle() {
+ assertVerifyOk(new RoutingSpec(false)
+ .addTable(new RoutingTableSpec("mytable"))
+ .addTable(new RoutingTableSpec("mytable")),
+ new ApplicationSpec());
+ assertVerifyOk(new RoutingSpec().addTable(new RoutingTableSpec("mytable", false)
+ .addHop(new HopSpec("foo", "bar"))
+ .addHop(new HopSpec("foo", "baz"))),
+ new ApplicationSpec());
+ assertVerifyOk(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addHop(new HopSpec("foo", "", false))),
+ new ApplicationSpec());
+ assertVerifyOk(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addRoute(new RouteSpec("foo", false))),
+ new ApplicationSpec());
+ }
+
+ public void testVerifyFail() {
+ // Duplicate table.
+ assertVerifyFail(new RoutingSpec()
+ .addTable(new RoutingTableSpec("mytable"))
+ .addTable(new RoutingTableSpec("mytable")),
+ new ApplicationSpec(),
+ Arrays.asList("Routing table 'mytable' is defined 2 times."));
+
+ // Duplicate hop.
+ assertVerifyFail(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addHop(new HopSpec("foo", "bar"))
+ .addHop(new HopSpec("foo", "baz"))),
+ new ApplicationSpec()
+ .addService("mytable", "bar")
+ .addService("mytable", "baz"),
+ Arrays.asList("Hop 'foo' in routing table 'mytable' is defined 2 times."));
+
+ // Duplicate route.
+ assertVerifyFail(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addRoute(new RouteSpec("foo").addHop("bar"))
+ .addRoute(new RouteSpec("foo").addHop("baz"))),
+ new ApplicationSpec()
+ .addService("mytable", "bar")
+ .addService("mytable", "baz"),
+ Arrays.asList("Route 'foo' in routing table 'mytable' is defined 2 times."));
+
+ // Empty hop.
+ assertVerifyFail(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addHop(new HopSpec("foo", ""))),
+ new ApplicationSpec(),
+ Arrays.asList("For hop 'foo' in routing table 'mytable'; Failed to parse empty string."));
+
+ // Empty route.
+ assertVerifyFail(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addRoute(new RouteSpec("foo"))),
+ new ApplicationSpec(),
+ Arrays.asList("Route 'foo' in routing table 'mytable' has no hops."));
+
+ // Hop error.
+ assertVerifyFail(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addHop(new HopSpec("foo", "bar/baz cox"))),
+ new ApplicationSpec(),
+ Arrays.asList("For hop 'foo' in routing table 'mytable'; Failed to completely parse 'bar/baz cox'."));
+
+ // Hop error in recipient.
+ assertVerifyFail(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addHop(new HopSpec("foo", "[bar]").addRecipient("bar/baz cox"))),
+ new ApplicationSpec(),
+ Arrays.asList("For recipient 'bar/baz cox' in hop 'foo' in routing table 'mytable'; Failed to completely parse 'bar/baz cox'."));
+
+ // Hop error in route.
+ assertVerifyFail(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addRoute(new RouteSpec("foo").addHop("bar/baz cox"))),
+ new ApplicationSpec(),
+ Arrays.asList("For hop 1 in route 'foo' in routing table 'mytable'; Failed to completely parse 'bar/baz cox'."));
+
+ // Hop not found.
+ assertVerifyFail(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addRoute(new RouteSpec("foo").addHop("bar"))),
+ new ApplicationSpec(),
+ Arrays.asList("Hop 1 in route 'foo' in routing table 'mytable' references 'bar' which is neither a service, a route nor another hop."));
+
+ // Mismatched recipient.
+ assertVerifyFail(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addHop(new HopSpec("foo", "bar/[baz]/cox").addRecipient("cox/0/bar"))),
+ new ApplicationSpec(),
+ Arrays.asList("Selector 'bar/[baz]/cox' does not match recipient 'cox/0/bar' in hop 'foo' in routing table 'mytable'."));
+
+ // Route not found.
+ assertVerifyFail(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addHop(new HopSpec("foo", "route:bar"))),
+ new ApplicationSpec(),
+ Arrays.asList("Hop 'foo' in routing table 'mytable' references route 'bar' which does not exist."));
+
+ // Route not found in route.
+ assertVerifyFail(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addRoute(new RouteSpec("foo").addHop("route:bar"))),
+ new ApplicationSpec(),
+ Arrays.asList("Hop 1 in route 'foo' in routing table 'mytable' references route 'bar' which does not exist."));
+
+ // Service not found.
+ assertVerifyFail(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addHop(new HopSpec("foo", "bar/baz"))),
+ new ApplicationSpec(),
+ Arrays.asList("Hop 'foo' in routing table 'mytable' references 'bar/baz' which is neither a service, a route nor another hop."));
+
+ // Unexpected recipient.
+ assertVerifyFail(new RoutingSpec().addTable(new RoutingTableSpec("mytable")
+ .addHop(new HopSpec("foo", "bar").addRecipient("baz"))),
+ new ApplicationSpec()
+ .addService("mytable", "bar")
+ .addService("mytable", "baz"),
+ Arrays.asList("Hop 'foo' in routing table 'mytable' has recipients but no policy directive."));
+
+ // Multiple errors.
+ assertVerifyFail(new RoutingSpec()
+ .addTable(new RoutingTableSpec("mytable"))
+ .addTable(new RoutingTableSpec("mytable")
+ .addHop(new HopSpec("hop1", "bar"))
+ .addHop(new HopSpec("hop1", "baz"))
+ .addHop(new HopSpec("hop2", ""))
+ .addHop(new HopSpec("hop3", "bar/baz cox"))
+ .addHop(new HopSpec("hop4", "[bar]").addRecipient("bar/baz cox"))
+ .addHop(new HopSpec("hop5", "bar/[baz]/cox").addRecipient("cox/0/bar"))
+ .addHop(new HopSpec("hop6", "route:route69"))
+ .addHop(new HopSpec("hop7", "bar/baz"))
+ .addHop(new HopSpec("hop8", "bar").addRecipient("baz"))
+ .addRoute(new RouteSpec("route1").addHop("bar"))
+ .addRoute(new RouteSpec("route1").addHop("baz"))
+ .addRoute(new RouteSpec("route2").addHop(""))
+ .addRoute(new RouteSpec("route3").addHop("bar/baz cox"))
+ .addRoute(new RouteSpec("route4").addHop("hop69"))
+ .addRoute(new RouteSpec("route5").addHop("route:route69"))),
+ new ApplicationSpec()
+ .addService("mytable", "bar")
+ .addService("mytable", "baz"),
+ Arrays.asList("Routing table 'mytable' is defined 2 times.",
+ "For hop 'hop2' in routing table 'mytable'; Failed to parse empty string.",
+ "For hop 'hop3' in routing table 'mytable'; Failed to completely parse 'bar/baz cox'.",
+ "For hop 1 in route 'route2' in routing table 'mytable'; Failed to parse empty string.",
+ "For hop 1 in route 'route3' in routing table 'mytable'; Failed to completely parse 'bar/baz cox'.",
+ "For recipient 'bar/baz cox' in hop 'hop4' in routing table 'mytable'; Failed to completely parse 'bar/baz cox'.",
+ "Hop 'hop1' in routing table 'mytable' is defined 2 times.",
+ "Hop 'hop6' in routing table 'mytable' references route 'route69' which does not exist.",
+ "Hop 'hop7' in routing table 'mytable' references 'bar/baz' which is neither a service, a route nor another hop.",
+ "Hop 'hop8' in routing table 'mytable' has recipients but no policy directive.",
+ "Hop 1 in route 'route4' in routing table 'mytable' references 'hop69' which is neither a service, a route nor another hop.",
+ "Hop 1 in route 'route5' in routing table 'mytable' references route 'route69' which does not exist.",
+ "Route 'route1' in routing table 'mytable' is defined 2 times.",
+ "Selector 'bar/[baz]/cox' does not match recipient 'cox/0/bar' in hop 'hop5' in routing table 'mytable'."));
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////
+ //
+ // Utilities
+ //
+ ////////////////////////////////////////////////////////////////////////////////
+
+ private static void assertVerifyOk(RoutingSpec routing, ApplicationSpec app) {
+ assertVerifyFail(routing, app, new ArrayList<String>());
+ }
+
+ private static void assertVerifyFail(RoutingSpec routing, ApplicationSpec app, List<String> expectedErrors) {
+ List<String> errors = new ArrayList<>();
+ routing.verify(app, errors);
+
+ Collections.sort(errors);
+ Collections.sort(expectedErrors);
+ assertEquals(expectedErrors.toString(), errors.toString());
+ }
+
+ private static void assertConfig(RoutingSpec routing) {
+ assertEquals(routing, routing);
+ assertEquals(routing, new RoutingSpec(routing));
+
+ ConfigStore store = new ConfigStore();
+ ConfigAgent subscriber = new ConfigAgent("raw:" + routing.toString(), store);
+ subscriber.subscribe();
+ assertTrue(store.routing.equals(routing));
+ }
+
+ private static void assertApplicationSpec(List<String> services, List<String> patterns) {
+ ApplicationSpec app = new ApplicationSpec();
+ for (String pattern : patterns) {
+ assertFalse(app.isService("foo", pattern));
+ assertFalse(app.isService("bar", pattern));
+ }
+ for (String service : services) {
+ app.addService("foo", service);
+ }
+ for (String pattern : patterns) {
+ assertTrue(app.isService("foo", pattern));
+ assertFalse(app.isService("bar", pattern));
+ }
+ for (String service : services) {
+ app.addService("bar", service);
+ }
+ for (String pattern : patterns) {
+ assertTrue(app.isService("foo", pattern));
+ assertTrue(app.isService("bar", pattern));
+ }
+ }
+
+ private static class ConfigStore implements ConfigHandler {
+
+ RoutingSpec routing = null;
+
+ public void setupRouting(RoutingSpec routing) {
+ this.routing = routing;
+ }
+ }
+}
diff --git a/messagebus/src/test/java/com/yahoo/messagebus/routing/RoutingTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/routing/RoutingTestCase.java new file mode 100644 index 00000000000..911350d3cc9 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/routing/RoutingTestCase.java @@ -0,0 +1,1144 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.routing; + +import com.yahoo.component.Vtag; +import com.yahoo.jrt.ListenFailedException; +import com.yahoo.jrt.slobrok.server.Slobrok; +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.Error; +import com.yahoo.messagebus.network.Identity; +import com.yahoo.messagebus.network.rpc.RPCNetworkParams; +import com.yahoo.messagebus.network.rpc.test.TestServer; +import com.yahoo.messagebus.routing.test.CustomPolicy; +import com.yahoo.messagebus.routing.test.CustomPolicyFactory; +import com.yahoo.messagebus.test.Receptor; +import com.yahoo.messagebus.test.SimpleMessage; +import com.yahoo.messagebus.test.SimpleProtocol; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * @author <a href="mailto:havardpe@yahoo-inc.com">Haavard Pettersen</a> + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RoutingTestCase { + + //////////////////////////////////////////////////////////////////////////////// + // + // Setup + // + //////////////////////////////////////////////////////////////////////////////// + + Slobrok slobrok; + TestServer srcServer, dstServer; + SourceSession srcSession; + DestinationSession dstSession; + RetryTransientErrorsPolicy retryPolicy; + + @Before + public void setUp() throws ListenFailedException, UnknownHostException { + slobrok = new Slobrok(); + dstServer = new TestServer(new MessageBusParams().addProtocol(new SimpleProtocol()), + new RPCNetworkParams().setIdentity(new Identity("dst")).setSlobrokConfigId( + TestServer.getSlobrokConfig(slobrok))); + dstSession = dstServer.mb.createDestinationSession( + new DestinationSessionParams().setName("session").setMessageHandler(new Receptor())); + retryPolicy = new RetryTransientErrorsPolicy(); + retryPolicy.setBaseDelay(0); + srcServer = new TestServer(new MessageBusParams().setRetryPolicy(retryPolicy).addProtocol(new SimpleProtocol()), + new RPCNetworkParams().setSlobrokConfigId(TestServer.getSlobrokConfig(slobrok))); + srcSession = srcServer.mb.createSourceSession( + new SourceSessionParams().setTimeout(600.0).setThrottlePolicy(null).setReplyHandler(new Receptor())); + assertTrue(srcServer.waitSlobrok("dst/session", 1)); + } + + @After + public void tearDown() { + slobrok.stop(); + dstSession.destroy(); + dstServer.destroy(); + srcSession.destroy(); + srcServer.destroy(); + } + + //////////////////////////////////////////////////////////////////////////////// + // + // Tests + // + //////////////////////////////////////////////////////////////////////////////// + + @Test + public void requireThatNullRouteIsCaught() { + assertTrue(srcSession.send(createMessage("msg")).isAccepted()); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.ILLEGAL_ROUTE, reply.getError(0).getCode()); + } + + @Test + public void requireThatEmptyRouteIsCaught() { + assertTrue(srcSession.send(createMessage("msg"), new Route()).isAccepted()); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.ILLEGAL_ROUTE, reply.getError(0).getCode()); + } + + @Test + public void requireThatHopNameIsExpanded() { + srcServer.setupRouting(new RoutingTableSpec(SimpleProtocol.NAME) + .addHop(new HopSpec("dst", "dst/session"))); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("dst")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + dstSession.acknowledge(msg); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertFalse(reply.hasErrors()); + } + + @Test + public void requireThatRouteDirectiveWorks() { + srcServer.setupRouting(new RoutingTableSpec(SimpleProtocol.NAME) + .addRoute(new RouteSpec("dst").addHop("dst/session")) + .addHop(new HopSpec("dir", "route:dst"))); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("dir")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + dstSession.acknowledge(msg); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertFalse(reply.hasErrors()); + } + + @Test + public void requireThatRouteNameIsExpanded() { + srcServer.setupRouting(new RoutingTableSpec(SimpleProtocol.NAME) + .addRoute(new RouteSpec("dst").addHop("dst/session"))); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("dst")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + dstSession.acknowledge(msg); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertFalse(reply.hasErrors()); + } + + @Test + public void requireThatHopResolutionOverflowIsCaught() { + srcServer.setupRouting(new RoutingTableSpec(SimpleProtocol.NAME) + .addHop(new HopSpec("foo", "bar")) + .addHop(new HopSpec("bar", "foo"))); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("foo")).isAccepted()); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.ILLEGAL_ROUTE, reply.getError(0).getCode()); + } + + @Test + public void requireThatRouteResolutionOverflowIsCaught() { + srcServer.setupRouting(new RoutingTableSpec(SimpleProtocol.NAME) + .addRoute(new RouteSpec("foo").addHop("route:foo"))); + assertTrue(srcSession.send(createMessage("msg"), "foo").isAccepted()); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.ILLEGAL_ROUTE, reply.getError(0).getCode()); + } + + @Test + public void requireThatRouteExpansionOnlyReplacesFirstHop() { + srcServer.setupRouting(new RoutingTableSpec(SimpleProtocol.NAME) + .addRoute(new RouteSpec("foo").addHop("dst/session").addHop("bar"))); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("route:foo baz")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + assertEquals(2, msg.getRoute().getNumHops()); + assertEquals("bar", msg.getRoute().getHop(0).toString()); + assertEquals("baz", msg.getRoute().getHop(1).toString()); + dstSession.acknowledge(msg); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertFalse(reply.hasErrors()); + } + + @Test + public void requireThatErrorDirectiveWorks() { + Route route = Route.parse("foo/bar/baz"); + route.getHop(0).setDirective(1, new ErrorDirective("err")); + assertTrue(srcSession.send(createMessage("msg"), route).isAccepted()); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.ILLEGAL_ROUTE, reply.getError(0).getCode()); + assertEquals("err", reply.getError(0).getMessage()); + } + + @Test + public void requireThatIllegalSelectIsCaught() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new CustomPolicyFactory()); + srcServer.mb.putProtocol(protocol); + Route route = Route.parse("[Custom: ]"); + assertNotNull(route); + assertTrue(srcSession.send(createMessage("msg"), route).isAccepted()); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.NO_SERVICES_FOR_ROUTE, reply.getError(0).getCode()); + } + + @Test + public void requireThatEmptySelectIsCaught() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new CustomPolicyFactory()); + srcServer.mb.putProtocol(protocol); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("[Custom]")).isAccepted()); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.NO_SERVICES_FOR_ROUTE, reply.getError(0).getCode()); + } + + @Test + public void requireThatPolicySelectWorks() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new CustomPolicyFactory()); + srcServer.mb.putProtocol(protocol); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("[Custom:dst/session]")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + dstSession.acknowledge(msg); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertFalse(reply.hasErrors()); + } + + @Test + public void requireThatTransientErrorsAreRetried() { + assertTrue(srcSession.send(createMessage("msg"), Route.parse("dst/session")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + Reply reply = new EmptyReply(); + reply.swapState(msg); + reply.addError(new Error(ErrorCode.APP_TRANSIENT_ERROR, "err1")); + dstSession.reply(reply); + assertNotNull(msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60)); + reply = new EmptyReply(); + reply.swapState(msg); + reply.addError(new Error(ErrorCode.APP_TRANSIENT_ERROR, "err2")); + dstSession.reply(reply); + assertNotNull(msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60)); + dstSession.acknowledge(msg); + assertNotNull(reply = ((Receptor)srcSession.getReplyHandler()).getReply(60)); + System.out.println(reply.getTrace()); + assertFalse(reply.hasErrors()); + assertTrace(Arrays.asList("[APP_TRANSIENT_ERROR @ localhost]: err1", + "-[APP_TRANSIENT_ERROR @ localhost]: err1", + "[APP_TRANSIENT_ERROR @ localhost]: err2", + "-[APP_TRANSIENT_ERROR @ localhost]: err2"), + reply.getTrace()); + } + + @Test + public void requireThatTransientErrorsAreRetriedWithPolicy() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new CustomPolicyFactory()); + srcServer.mb.putProtocol(protocol); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("[Custom:dst/session]")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + Reply reply = new EmptyReply(); + reply.swapState(msg); + reply.addError(new Error(ErrorCode.APP_TRANSIENT_ERROR, "err1")); + dstSession.reply(reply); + assertNotNull(msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60)); + reply = new EmptyReply(); + reply.swapState(msg); + reply.addError(new Error(ErrorCode.APP_TRANSIENT_ERROR, "err2")); + dstSession.reply(reply); + assertNotNull(msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60)); + dstSession.acknowledge(msg); + assertNotNull(reply = ((Receptor)srcSession.getReplyHandler()).getReply(60)); + System.out.println(reply.getTrace()); + assertFalse(reply.hasErrors()); + assertTrace(Arrays.asList("Source session accepted a 3 byte message. 1 message(s) now pending.", + "Running routing policy 'Custom'.", + "Selecting [dst/session].", + "Component 'dst/session' selected by policy 'Custom'.", + "Resolving 'dst/session'.", + "Sending message (version ${VERSION}) from client to 'dst/session'", + "Message (type 1) received at 'dst' for session 'session'.", + "[APP_TRANSIENT_ERROR @ localhost]: err1", + "Sending reply (version ${VERSION}) from 'dst'.", + "Reply (type 0) received at client.", + "Routing policy 'Custom' merging replies.", + "Merged [dst/session].", + "Message scheduled for retry 1 in 0.0 seconds.", + "Resender resending message.", + "Running routing policy 'Custom'.", + "Selecting [dst/session].", + "Component 'dst/session' selected by policy 'Custom'.", + "Resolving 'dst/session'.", + "Sending message (version ${VERSION}) from client to 'dst/session'", + "Message (type 1) received at 'dst' for session 'session'.", + "[APP_TRANSIENT_ERROR @ localhost]: err2", + "Sending reply (version ${VERSION}) from 'dst'.", + "Reply (type 0) received at client.", + "Routing policy 'Custom' merging replies.", + "Merged [dst/session].", + "Message scheduled for retry 2 in 0.0 seconds.", + "Resender resending message.", + "Running routing policy 'Custom'.", + "Selecting [dst/session].", + "Component 'dst/session' selected by policy 'Custom'.", + "Resolving 'dst/session'.", + "Sending message (version ${VERSION}) from client to 'dst/session'", + "Message (type 1) received at 'dst' for session 'session'.", + "Sending reply (version ${VERSION}) from 'dst'.", + "Reply (type 0) received at client.", + "Routing policy 'Custom' merging replies.", + "Merged [dst/session].", + "Source session received reply. 0 message(s) now pending."), + reply.getTrace()); + } + + @Test + public void requireThatRetryCanBeDisabled() { + retryPolicy.setEnabled(false); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("dst/session")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + Reply reply = new EmptyReply(); + reply.swapState(msg); + reply.addError(new Error(ErrorCode.APP_TRANSIENT_ERROR, "err")); + dstSession.reply(reply); + assertNotNull(reply = ((Receptor)srcSession.getReplyHandler()).getReply(60)); + System.out.println(reply.getTrace()); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.APP_TRANSIENT_ERROR, reply.getError(0).getCode()); + } + + @Test + public void requireThatRetryCallsSelect() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new CustomPolicyFactory()); + srcServer.mb.putProtocol(protocol); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("[Custom:dst/session]")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + Reply reply = new EmptyReply(); + reply.swapState(msg); + reply.addError(new Error(ErrorCode.APP_TRANSIENT_ERROR, "err")); + dstSession.reply(reply); + assertNotNull(msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60)); + dstSession.acknowledge(msg); + assertNotNull(reply = ((Receptor)srcSession.getReplyHandler()).getReply(60)); + System.out.println(reply.getTrace()); + assertFalse(reply.hasErrors()); + assertTrace(Arrays.asList("Selecting [dst/session].", + "[APP_TRANSIENT_ERROR @ localhost]", + "-[APP_TRANSIENT_ERROR @ localhost]", + "Merged [dst/session].", + "Selecting [dst/session].", + "Sending reply", + "Merged [dst/session]."), + reply.getTrace()); + } + + @Test + public void requireThatPolicyCanDisableReselectOnRetry() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new CustomPolicyFactory(false)); + srcServer.mb.putProtocol(protocol); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("[Custom:dst/session]")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + Reply reply = new EmptyReply(); + reply.swapState(msg); + reply.addError(new Error(ErrorCode.APP_TRANSIENT_ERROR, "err")); + dstSession.reply(reply); + assertNotNull(msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60)); + dstSession.acknowledge(msg); + assertNotNull(reply = ((Receptor)srcSession.getReplyHandler()).getReply(60)); + System.out.println(reply.getTrace()); + assertFalse(reply.hasErrors()); + assertTrace(Arrays.asList("Selecting [dst/session].", + "[APP_TRANSIENT_ERROR @ localhost]", + "-[APP_TRANSIENT_ERROR @ localhost]", + "Merged [dst/session].", + "-Selecting [dst/session].", + "Sending reply", + "Merged [dst/session]."), + reply.getTrace()); + } + + @Test + public void requireThatPolicyCanConsumeErrors() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new CustomPolicyFactory(true, ErrorCode.NO_ADDRESS_FOR_SERVICE)); + srcServer.mb.putProtocol(protocol); + retryPolicy.setEnabled(false); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("[Custom:dst/session,dst/unknown]")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + dstSession.acknowledge(msg); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.NO_ADDRESS_FOR_SERVICE, reply.getError(0).getCode()); + assertTrace(Arrays.asList("Selecting [dst/session, dst/unknown].", + "[NO_ADDRESS_FOR_SERVICE @ localhost]", + "Sending reply", + "Merged [dst/session, dst/unknown]."), + reply.getTrace()); + } + + @Test + public void requireThatPolicyOnlyConsumesDeclaredErrors() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new CustomPolicyFactory()); + srcServer.mb.putProtocol(protocol); + retryPolicy.setEnabled(false); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("[Custom:dst/unknown]")).isAccepted()); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.NO_ADDRESS_FOR_SERVICE, reply.getError(0).getCode()); + assertTrace(Arrays.asList("Selecting [dst/unknown].", + "[NO_ADDRESS_FOR_SERVICE @ localhost]", + "Merged [dst/unknown]."), + reply.getTrace()); + } + + @Test + public void requireThatPolicyCanExpandToPolicy() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new CustomPolicyFactory(true, ErrorCode.NO_ADDRESS_FOR_SERVICE)); + srcServer.mb.putProtocol(protocol); + retryPolicy.setEnabled(false); + assertTrue(srcSession.send(createMessage("msg"), + Route.parse("[Custom:[Custom:dst/session],[Custom:dst/unknown]]")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + dstSession.acknowledge(msg); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.NO_ADDRESS_FOR_SERVICE, reply.getError(0).getCode()); + } + + @Test + public void requireThatReplyCanBeRemovedFromChildNodes() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new SimpleProtocol.PolicyFactory() { + + @Override + public RoutingPolicy create(String param) { + return new RemoveReplyPolicy(true, + Arrays.asList(ErrorCode.NO_ADDRESS_FOR_SERVICE), + CustomPolicyFactory.parseRoutes(param), + 0); + } + }); + srcServer.mb.putProtocol(protocol); + retryPolicy.setEnabled(false); + assertTrue(srcSession.send(createMessage("msg"), + Route.parse("[Custom:[Custom:dst/session],[Custom:dst/unknown]]")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + dstSession.acknowledge(msg); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertFalse(reply.hasErrors()); + assertTrace(Arrays.asList("[NO_ADDRESS_FOR_SERVICE @ localhost]", + "-[NO_ADDRESS_FOR_SERVICE @ localhost]", + "Sending message", + "-Sending message"), + reply.getTrace()); + } + + @Test + public void requireThatSetReplyWorks() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Select", new CustomPolicyFactory(true, ErrorCode.APP_FATAL_ERROR)); + protocol.addPolicyFactory("SetReply", new SimpleProtocol.PolicyFactory() { + + @Override + public RoutingPolicy create(String param) { + return new SetReplyPolicy(true, Arrays.asList(ErrorCode.APP_FATAL_ERROR), param); + } + }); + srcServer.mb.putProtocol(protocol); + retryPolicy.setEnabled(false); + assertTrue( + srcSession.send(createMessage("msg"), Route.parse("[Select:[SetReply:foo],dst/session]")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + dstSession.acknowledge(msg); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.APP_FATAL_ERROR, reply.getError(0).getCode()); + assertEquals("foo", reply.getError(0).getMessage()); + } + + @Test + public void requireThatReplyCanBeReusedOnRetry() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("ReuseReply", new SimpleProtocol.PolicyFactory() { + + @Override + public RoutingPolicy create(String param) { + return new ReuseReplyPolicy(false, + Arrays.asList(ErrorCode.APP_FATAL_ERROR), + CustomPolicyFactory.parseRoutes(param)); + } + }); + protocol.addPolicyFactory("SetReply", new SimpleProtocol.PolicyFactory() { + + @Override + public RoutingPolicy create(String param) { + return new SetReplyPolicy(false, + Arrays.asList(ErrorCode.APP_FATAL_ERROR), + param); + } + }); + srcServer.mb.putProtocol(protocol); + assertTrue(srcSession.send(createMessage("msg"), + Route.parse("[ReuseReply:[SetReply:foo],dst/session]")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + Reply reply = new EmptyReply(); + reply.swapState(msg); + reply.addError(new Error(ErrorCode.APP_TRANSIENT_ERROR, "dst")); + dstSession.reply(reply); + assertNotNull(msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60)); + dstSession.acknowledge(msg); + assertNotNull(reply = ((Receptor)srcSession.getReplyHandler()).getReply(60)); + System.out.println(reply.getTrace()); + assertFalse(reply.hasErrors()); + } + + @Test + public void requireThatReplyCanBeRemovedAndRetried() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("RemoveReply", new SimpleProtocol.PolicyFactory() { + + @Override + public RoutingPolicy create(String param) { + return new RemoveReplyPolicy(false, + Arrays.asList(ErrorCode.APP_TRANSIENT_ERROR), + CustomPolicyFactory.parseRoutes(param), + 0); + } + }); + protocol.addPolicyFactory("SetReply", new SimpleProtocol.PolicyFactory() { + + @Override + public RoutingPolicy create(String param) { + return new SetReplyPolicy(false, + Arrays.asList(ErrorCode.APP_TRANSIENT_ERROR, ErrorCode.APP_FATAL_ERROR), + param); + } + }); + srcServer.mb.putProtocol(protocol); + assertTrue(srcSession + .send(createMessage("msg"), Route.parse("[RemoveReply:[SetReply:foo],dst/session]")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + dstSession.acknowledge(msg); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.APP_FATAL_ERROR, reply.getError(0).getCode()); + assertEquals("foo", reply.getError(0).getMessage()); + assertTrace(Arrays.asList("Resolving '[SetReply:foo]'.", + "Resolving 'dst/session'.", + "Resender resending message.", + "Resolving 'dst/session'.", + "Resolving '[SetReply:foo]'."), + reply.getTrace()); + } + + @Test + public void requireThatIgnoreResultWorks() { + assertTrue(srcSession.send(createMessage("msg"), Route.parse("?dst/session")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + Reply reply = new EmptyReply(); + reply.swapState(msg); + reply.addError(new Error(ErrorCode.APP_FATAL_ERROR, "dst")); + dstSession.reply(reply); + assertNotNull(reply = ((Receptor)srcSession.getReplyHandler()).getReply(60)); + System.out.println(reply.getTrace()); + assertFalse(reply.hasErrors()); + assertTrace(Arrays.asList("Not waiting for a reply from 'dst/session'."), + reply.getTrace()); + } + + @Test + public void requireThatIgnoreResultCanBeSetInHopBlueprint() { + srcServer.setupRouting(new RoutingTableSpec(SimpleProtocol.NAME) + .addHop(new HopSpec("foo", "dst/session").setIgnoreResult(true))); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("foo")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + Reply reply = new EmptyReply(); + reply.swapState(msg); + reply.addError(new Error(ErrorCode.APP_FATAL_ERROR, "dst")); + dstSession.reply(reply); + assertNotNull(reply = ((Receptor)srcSession.getReplyHandler()).getReply(60)); + System.out.println(reply.getTrace()); + assertFalse(reply.hasErrors()); + assertTrace(Arrays.asList("Not waiting for a reply from 'dst/session'."), + reply.getTrace()); + } + + @Test + public void requireThatIgnoreFlagPersistsThroughHopLookup() { + setupRouting(new RoutingTableSpec(SimpleProtocol.NAME).addHop(new HopSpec("foo", "dst/unknown"))); + assertSend("?foo"); + assertTrace("Ignoring errors in reply."); + } + + @Test + public void requireThatIgnoreFlagPersistsThroughRouteLookup() { + setupRouting(new RoutingTableSpec(SimpleProtocol.NAME).addRoute(new RouteSpec("foo").addHop("dst/unknown"))); + assertSend("?foo"); + assertTrace("Ignoring errors in reply."); + } + + @Test + public void requireThatIgnoreFlagPersistsThroughPolicySelect() { + setupPolicy("Custom", MyPolicy.newSelectAndMerge("dst/unknown")); + assertSend("?[Custom]"); + assertTrace("Ignoring errors in reply."); + } + + @Test + public void requireThatIgnoreFlagIsSerializedWithMessage() { + assertSend("dst/session foo ?bar"); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + Route route = msg.getRoute(); + assertEquals(2, route.getNumHops()); + Hop hop = route.getHop(0); + assertEquals("foo", hop.toString()); + assertFalse(hop.getIgnoreResult()); + hop = route.getHop(1); + assertEquals("?bar", hop.toString()); + assertTrue(hop.getIgnoreResult()); + dstSession.acknowledge(msg); + assertTrace("-Ignoring errors in reply."); + } + + @Test + public void requireThatIgnoreFlagDoesNotInterfere() { + setupPolicy("Custom", MyPolicy.newSelectAndMerge("dst/session")); + assertSend("?[Custom]"); + assertTrace("-Ignoring errors in reply."); + } + + @Test + public void requireThatEmptySelectionCanBeIgnored() { + setupPolicy("Custom", MyPolicy.newEmptySelection()); + assertSend("?[Custom]"); + assertTrace("Ignoring errors in reply."); + } + + @Test + public void requireThatSelectErrorCanBeIgnored() { + setupPolicy("Custom", MyPolicy.newSelectError(ErrorCode.APP_FATAL_ERROR, "foo")); + assertSend("?[Custom]"); + assertTrace("Ignoring errors in reply."); + } + + @Test + public void requireThatSelectExceptionCanBeIgnored() { + setupPolicy("Custom", MyPolicy.newSelectException(new RuntimeException())); + assertSend("?[Custom]"); + assertTrace("Ignoring errors in reply."); + } + + @Test + public void requireThatSelectAndThrowCanBeIgnored() { + setupPolicy("Custom", MyPolicy.newSelectAndThrow("dst/session", new RuntimeException())); + assertSend("?[Custom]"); + assertTrace("Ignoring errors in reply."); + } + + @Test + public void requireThatEmptyMergeCanBeIgnored() { + setupPolicy("Custom", MyPolicy.newEmptyMerge("dst/session")); + assertSend("?[Custom]"); + assertAcknowledge(); + assertTrace("Ignoring errors in reply."); + } + + @Test + public void requireThatMergeErrorCanBeIgnored() { + setupPolicy("Custom", MyPolicy.newMergeError("dst/session", ErrorCode.APP_FATAL_ERROR, "foo")); + assertSend("?[Custom]"); + assertAcknowledge(); + assertTrace("Ignoring errors in reply."); + } + + @Test + public void requireThatMergeExceptionCanBeIgnored() { + setupPolicy("Custom", MyPolicy.newMergeException("dst/session", new RuntimeException())); + assertSend("?[Custom]"); + assertAcknowledge(); + assertTrace("Ignoring errors in reply."); + } + + @Test + public void requireThatMergeAndThrowCanBeIgnored() { + setupPolicy("Custom", MyPolicy.newMergeAndThrow("dst/session", new RuntimeException())); + assertSend("?[Custom]"); + assertAcknowledge(); + assertTrace("Ignoring errors in reply."); + } + + @Test + public void requireThatAllocServiceAddressCanBeIgnored() { + assertSend("?dst/unknown"); + assertTrace("Ignoring errors in reply."); + } + + @Test + public void requireThatDepthLimitCanBeIgnored() { + setupPolicy("Custom", MyPolicy.newSelectAndMerge("[Custom]")); + assertSend("?[Custom]"); + assertTrace("Ignoring errors in reply."); + } + + @Test + public void requireThatRouteCanBeEmptyInDestination() { + assertTrue(srcSession.send(createMessage("msg"), Route.parse("dst/session")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + assertNull(msg.getRoute()); + dstSession.acknowledge(msg); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + } + + @Test + public void requireThatOnlyActiveNodesAreAborted() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new CustomPolicyFactory(false)); + protocol.addPolicyFactory("SetReply", new SimpleProtocol.PolicyFactory() { + + @Override + public RoutingPolicy create(String param) { + return new SetReplyPolicy(false, + Arrays.asList(ErrorCode.APP_TRANSIENT_ERROR, + ErrorCode.APP_TRANSIENT_ERROR, + ErrorCode.APP_FATAL_ERROR), + param); + } + }); + srcServer.mb.putProtocol(protocol); + assertTrue(srcSession.send(createMessage("msg"), + Route.parse("[Custom:[SetReply:foo],?bar,dst/session]")).isAccepted()); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertEquals(2, reply.getNumErrors()); + assertEquals(ErrorCode.APP_FATAL_ERROR, reply.getError(0).getCode()); + assertEquals(ErrorCode.SEND_ABORTED, reply.getError(1).getCode()); + } + + @Test + public void requireThatTimeoutWorks() { + retryPolicy.setBaseDelay(0.01); + srcSession.setTimeout(0.5); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("dst/unknown")).isAccepted()); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertEquals(2, reply.getNumErrors()); + assertEquals(ErrorCode.NO_ADDRESS_FOR_SERVICE, reply.getError(0).getCode()); + assertEquals(ErrorCode.TIMEOUT, reply.getError(1).getCode()); + } + + @Test + public void requireThatUnknownPolicyIsCaught() { + assertTrue(srcSession.send(createMessage("msg"), Route.parse("[Unknown]")).isAccepted()); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.UNKNOWN_POLICY, reply.getError(0).getCode()); + } + + private SimpleProtocol.PolicyFactory exceptionOnSelectThrowingMockFactory() { + return new SimpleProtocol.PolicyFactory() { + + @Override + public RoutingPolicy create(String param) { + return new RoutingPolicy() { + + @Override + public void select(RoutingContext context) { + throw new RuntimeException("69"); + } + + @Override + public void merge(RoutingContext context) { + } + + @Override + public void destroy() { + } + }; + } + }; + } + + @Test + public void requireThatSelectExceptionIsCaught() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", exceptionOnSelectThrowingMockFactory()); + srcServer.mb.putProtocol(protocol); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("[Custom]")).isAccepted()); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.POLICY_ERROR, reply.getError(0).getCode()); + assertTrue(reply.getError(0).getMessage().contains("69")); + } + + @Test + public void selectExceptionIncludesStackTraceInMessage() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", exceptionOnSelectThrowingMockFactory()); + srcServer.mb.putProtocol(protocol); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("[Custom]")).isAccepted()); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertEquals(ErrorCode.POLICY_ERROR, reply.getError(0).getCode()); + // Attempting any sort of full matching of the stack trace is brittle, so + // simplify by assuming any message which mentions the source file of the + // originating exception is good to go. + assertTrue(reply.getError(0).getMessage().contains("RoutingTestCase")); + } + + @Test + public void requireThatMergeExceptionIsCaught() { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("Custom", new SimpleProtocol.PolicyFactory() { + + @Override + public RoutingPolicy create(String param) { + return new RoutingPolicy() { + + @Override + public void select(RoutingContext context) { + context.addChild(Route.parse("dst/session")); + } + + @Override + public void merge(RoutingContext context) { + throw new RuntimeException("69"); + } + + @Override + public void destroy() { + + } + }; + } + }); + srcServer.mb.putProtocol(protocol); + assertTrue(srcSession.send(createMessage("msg"), Route.parse("[Custom]")).isAccepted()); + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + dstSession.acknowledge(msg); + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertEquals(1, reply.getNumErrors()); + assertEquals(ErrorCode.POLICY_ERROR, reply.getError(0).getCode()); + assertTrue(reply.getError(0).getMessage().contains("69")); + } + + //////////////////////////////////////////////////////////////////////////////// + // + // Utilities + // + //////////////////////////////////////////////////////////////////////////////// + + private static Message createMessage(String msg) { + SimpleMessage ret = new SimpleMessage(msg); + ret.getTrace().setLevel(9); + return ret; + } + + private void setupRouting(RoutingTableSpec spec) { + srcServer.setupRouting(spec); + } + + private void setupPolicy(String policyName, SimpleProtocol.PolicyFactory policyFactory) { + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory(policyName, policyFactory); + srcServer.mb.putProtocol(protocol); + } + + private void assertSend(String route) { + assertTrue(srcSession.send(createMessage("msg").setRoute(Route.parse(route))).isAccepted()); + } + + private void assertAcknowledge() { + Message msg = ((Receptor)dstSession.getMessageHandler()).getMessage(60); + assertNotNull(msg); + dstSession.acknowledge(msg); + } + + private void assertTrace(String... expectedTrace) { + Reply reply = ((Receptor)srcSession.getReplyHandler()).getReply(60); + assertNotNull(reply); + System.out.println(reply.getTrace()); + assertFalse(reply.hasErrors()); + assertTrace(Arrays.asList(expectedTrace), reply.getTrace()); + } + + public static void assertTrace(List<String> expected, Trace trace) { + String actual = trace.toString(); + for (int i = 0, pos = -1; i < expected.size(); ++i) { + String line = expected.get(i).replaceFirst("\\$\\{VERSION\\}", Vtag.currentVersion.toString()); + if (line.charAt(0) == '-') { + String str = line.substring(1); + assertTrue("Line " + i + " '" + str + "' not expected.", + actual.indexOf(str, pos + 1) < 0); + } else { + pos = actual.indexOf(line, pos + 1); + assertTrue("Line " + i + " '" + line + "' missing.", pos >= 0); + } + } + } + + private static class RemoveReplyPolicy extends CustomPolicy { + + final int idxRemove; + + public RemoveReplyPolicy(boolean selectOnRetry, List<Integer> consumableErrors, List<Route> routes, + int idxRemove) { + super(selectOnRetry, consumableErrors, routes); + this.idxRemove = idxRemove; + } + + public void merge(RoutingContext ctx) { + ctx.setReply(ctx.getChildIterator().skip(idxRemove).removeReply()); + } + + @Override + public void destroy() { + } + } + + private static class ReuseReplyPolicy extends CustomPolicy { + + final List<Integer> errorMask = new ArrayList<>(); + + public ReuseReplyPolicy(boolean selectOnRetry, List<Integer> errorMask, + List<Route> routes) { + super(selectOnRetry, errorMask, routes); + this.errorMask.addAll(errorMask); + } + + public void merge(RoutingContext ctx) { + Reply ret = new EmptyReply(); + int idx = 0; + int idxFirstOk = -1; + for (RoutingNodeIterator it = ctx.getChildIterator(); + it.isValid(); it.next(), ++idx) { + Reply ref = it.getReplyRef(); + if (!ref.hasErrors()) { + if (idxFirstOk < 0) { + idxFirstOk = idx; + } + } else { + for (int i = 0; i < ref.getNumErrors(); ++i) { + Error err = ref.getError(i); + if (!errorMask.contains(err.getCode())) { + ret.addError(err); + } + } + } + } + if (ret.hasErrors()) { + ctx.setReply(ret); + } else { + ctx.setReply(ctx.getChildIterator().skip(idxFirstOk).removeReply()); + } + } + + @Override + public void destroy() { + } + } + + private static class SetReplyPolicy implements RoutingPolicy { + + final boolean selectOnRetry; + final List<Integer> errors = new ArrayList<>(); + final String param; + int idx = 0; + + public SetReplyPolicy(boolean selectOnRetry, List<Integer> errors, String param) { + this.selectOnRetry = selectOnRetry; + this.errors.addAll(errors); + this.param = param; + } + + public void select(RoutingContext ctx) { + int idx = this.idx++; + int err = errors.get(idx < errors.size() ? idx : errors.size() - 1); + if (err != ErrorCode.NONE) { + ctx.setError(err, param); + } else { + ctx.setReply(new EmptyReply()); + } + ctx.setSelectOnRetry(selectOnRetry); + } + + public void merge(RoutingContext ctx) { + Reply reply = new EmptyReply(); + reply.addError(new Error(ErrorCode.FATAL_ERROR, + "Merge should not be called when select() sets a reply.")); + ctx.setReply(reply); + } + + public void destroy() { + } + } + + private static class MyPolicy implements SimpleProtocol.PolicyFactory { + + final Route selectRoute; + final Reply selectReply; + final Reply mergeReply; + final RuntimeException selectException; + final RuntimeException mergeException; + final boolean mergeFromChild; + + MyPolicy(Route selectRoute, Reply selectReply, RuntimeException selectException, + Reply mergeReply, RuntimeException mergeException, boolean mergeFromChild) { + this.selectRoute = selectRoute; + this.selectReply = selectReply; + this.selectException = selectException; + this.mergeReply = mergeReply; + this.mergeException = mergeException; + this.mergeFromChild = mergeFromChild; + } + + @Override + public RoutingPolicy create(String param) { + return new RoutingPolicy() { + + @Override + public void select(RoutingContext context) { + if (selectRoute != null) { + context.addChild(selectRoute); + } + if (selectReply != null) { + context.setReply(selectReply); + } + if (selectException != null) { + throw selectException; + } + } + + @Override + public void merge(RoutingContext context) { + if (mergeReply != null) { + context.setReply(mergeReply); + } else if (mergeFromChild) { + context.setReply(context.getChildIterator().removeReply()); + } + if (mergeException != null) { + throw mergeException; + } + } + + @Override + public void destroy() { + + } + }; + } + + static Reply newErrorReply(int errCode, String errMessage) { + Reply reply = new EmptyReply(); + reply.addError(new Error(errCode, errMessage)); + return reply; + } + + static MyPolicy newSelectAndMerge(String select) { + return new MyPolicy(Route.parse(select), null, null, null, null, true); + } + + static MyPolicy newEmptySelection() { + return new MyPolicy(null, null, null, null, null, false); + } + + static MyPolicy newSelectError(int errCode, String errMessage) { + return new MyPolicy(null, newErrorReply(errCode, errMessage), null, null, null, false); + } + + static MyPolicy newSelectException(RuntimeException e) { + return new MyPolicy(null, null, e, null, null, false); + } + + static MyPolicy newSelectAndThrow(String select, RuntimeException e) { + return new MyPolicy(Route.parse(select), null, e, null, null, false); + } + + static MyPolicy newEmptyMerge(String select) { + return new MyPolicy(Route.parse(select), null, null, null, null, false); + } + + static MyPolicy newMergeError(String select, int errCode, String errMessage) { + return new MyPolicy(Route.parse(select), null, null, newErrorReply(errCode, errMessage), null, false); + } + + static MyPolicy newMergeException(String select, RuntimeException e) { + return new MyPolicy(Route.parse(select), null, null, null, e, false); + } + + static MyPolicy newMergeAndThrow(String select, RuntimeException e) { + return new MyPolicy(Route.parse(select), null, null, null, e, true); + } + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/test/QueueAdapterTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/test/QueueAdapterTestCase.java new file mode 100644 index 00000000000..000505f6349 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/test/QueueAdapterTestCase.java @@ -0,0 +1,108 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.test; + +import com.yahoo.messagebus.Message; +import com.yahoo.messagebus.Reply; +import com.yahoo.text.Utf8String; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.*; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class QueueAdapterTestCase { + + private static final int NO_WAIT = 0; + private static final int WAIT_FOREVER = 60; + + @Test + public void requireThatAccessorsWork() { + QueueAdapter queue = new QueueAdapter(); + assertTrue(queue.isEmpty()); + assertEquals(0, queue.size()); + + Message msg = new MyMessage(); + queue.handleMessage(msg); + assertFalse(queue.isEmpty()); + assertEquals(1, queue.size()); + + MyReply reply = new MyReply(); + queue.handleReply(reply); + assertFalse(queue.isEmpty()); + assertEquals(2, queue.size()); + + assertSame(msg, queue.dequeue()); + assertSame(reply, queue.dequeue()); + } + + @Test + public void requireThatSizeCanBeWaitedFor() { + final QueueAdapter queue = new QueueAdapter(); + assertTrue(queue.waitSize(0, NO_WAIT)); + assertFalse(queue.waitSize(1, NO_WAIT)); + queue.handleMessage(new MyMessage()); + assertFalse(queue.waitSize(0, NO_WAIT)); + assertTrue(queue.waitSize(1, NO_WAIT)); + + Thread thread = new Thread() { + + @Override + public void run() { + try { + Thread.sleep(100); + queue.handleMessage(new MyMessage()); + } catch (InterruptedException e) { + + } + } + }; + thread.start(); + assertTrue(queue.waitSize(2, WAIT_FOREVER)); + } + + @Test + public void requireThatWaitCanBeInterrupted() throws InterruptedException { + final QueueAdapter queue = new QueueAdapter(); + final AtomicReference<Boolean> result = new AtomicReference<>(); + Thread thread = new Thread() { + + @Override + public void run() { + result.set(queue.waitSize(1, WAIT_FOREVER)); + } + }; + thread.start(); + thread.interrupt(); + thread.join(); + assertEquals(Boolean.FALSE, result.get()); + } + + private static class MyMessage extends Message { + + @Override + public Utf8String getProtocol() { + return null; + } + + @Override + public int getType() { + return 0; + } + } + + private static class MyReply extends Reply { + + @Override + public Utf8String getProtocol() { + return null; + } + + @Override + public int getType() { + return 0; + } + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/test/ReceptorTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/test/ReceptorTestCase.java new file mode 100644 index 00000000000..a34f37b0196 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/test/ReceptorTestCase.java @@ -0,0 +1,143 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.test; + +import com.yahoo.messagebus.Message; +import com.yahoo.messagebus.Reply; +import com.yahoo.text.Utf8String; +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class ReceptorTestCase { + + @Test + public void requireThatAccessorsWork() { + Receptor receptor = new Receptor(); + assertNull(receptor.getMessage(0)); + Message msg = new MyMessage(); + receptor.handleMessage(msg); + assertSame(msg, receptor.getMessage(0)); + + Reply reply = new MyReply(); + receptor.handleReply(reply); + assertSame(reply, receptor.getReply(0)); + } + + @Test + public void requireThatMessagesAndRepliesAreTrackedIndividually() { + Receptor receptor = new Receptor(); + receptor.handleMessage(new MyMessage()); + receptor.handleReply(new MyReply()); + assertNotNull(receptor.getMessage(0)); + assertNotNull(receptor.getReply(0)); + + receptor.handleMessage(new MyMessage()); + receptor.handleReply(new MyReply()); + assertNotNull(receptor.getReply(0)); + assertNotNull(receptor.getMessage(0)); + } + + @Test + public void requireThatMessagesCanBeWaitedFor() { + final Receptor receptor = new Receptor(); + Thread thread = new Thread() { + + @Override + public void run() { + try { + Thread.sleep(100); + receptor.handleMessage(new MyMessage()); + } catch (InterruptedException e) { + + } + } + }; + thread.start(); + assertNotNull(receptor.getMessage(60)); + } + + @Test + public void requireThatMessageWaitCanBeInterrupted() throws InterruptedException { + final Receptor receptor = new Receptor(); + final CountDownLatch latch = new CountDownLatch(1); + Thread thread = new Thread() { + + @Override + public void run() { + receptor.getMessage(60); + latch.countDown(); + } + }; + thread.start(); + thread.interrupt(); + assertTrue(latch.await(30, TimeUnit.SECONDS)); + } + + @Test + public void requireThatRepliesCanBeWaitedFor() { + final Receptor receptor = new Receptor(); + Thread thread = new Thread() { + + @Override + public void run() { + try { + Thread.sleep(100); + receptor.handleReply(new MyReply()); + } catch (InterruptedException e) { + + } + } + }; + thread.start(); + assertNotNull(receptor.getReply(60)); + } + + @Test + public void requireThatReplyWaitCanBeInterrupted() throws InterruptedException { + final Receptor receptor = new Receptor(); + final CountDownLatch latch = new CountDownLatch(1); + Thread thread = new Thread() { + + @Override + public void run() { + receptor.getReply(60); + latch.countDown(); + } + }; + thread.start(); + thread.interrupt(); + assertTrue(latch.await(30, TimeUnit.SECONDS)); + } + + private static class MyMessage extends Message { + + @Override + public Utf8String getProtocol() { + return null; + } + + @Override + public int getType() { + return 0; + } + } + + private static class MyReply extends Reply { + + @Override + public Utf8String getProtocol() { + return null; + } + + @Override + public int getType() { + return 0; + } + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/test/SimpleMessageTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/test/SimpleMessageTestCase.java new file mode 100644 index 00000000000..ead0fdb88b4 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/test/SimpleMessageTestCase.java @@ -0,0 +1,25 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.test; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class SimpleMessageTestCase { + + @Test + public void requireThatAccessorsWork() { + SimpleMessage msg = new SimpleMessage("foo"); + assertEquals(SimpleProtocol.MESSAGE, msg.getType()); + assertEquals(SimpleProtocol.NAME, msg.getProtocol()); + assertEquals(3, msg.getApproxSize()); + assertEquals("foo", msg.getValue()); + msg.setValue("bar"); + assertEquals("bar", msg.getValue()); + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/test/SimpleProtocolTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/test/SimpleProtocolTestCase.java new file mode 100644 index 00000000000..ce42762f235 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/test/SimpleProtocolTestCase.java @@ -0,0 +1,65 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.test; + +import com.yahoo.component.Version; +import com.yahoo.messagebus.EmptyReply; +import com.yahoo.messagebus.Routable; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class SimpleProtocolTestCase { + + private static final Version VERSION = new Version(1); + private static final SimpleProtocol PROTOCOL = new SimpleProtocol(); + + @Test + public void requireThatNameIsSet() { + assertEquals(SimpleProtocol.NAME, PROTOCOL.getName()); + } + + @Test + public void requireThatMetricSetIsNull() { + assertNull(PROTOCOL.getMetrics()); + } + + @Test + public void requireThatMessageCanBeEncodedAndDecoded() { + SimpleMessage msg = new SimpleMessage("foo"); + byte[] buf = PROTOCOL.encode(Version.emptyVersion, msg); + Routable routable = PROTOCOL.decode(Version.emptyVersion, buf); + assertNotNull(routable); + assertEquals(SimpleMessage.class, routable.getClass()); + msg = (SimpleMessage)routable; + assertEquals("foo", msg.getValue()); + } + + @Test + public void requireThatReplyCanBeDecoded() { + SimpleReply reply = new SimpleReply("foo"); + byte[] buf = PROTOCOL.encode(Version.emptyVersion, reply); + Routable routable = PROTOCOL.decode(Version.emptyVersion, buf); + assertNotNull(routable); + assertEquals(SimpleReply.class, routable.getClass()); + reply = (SimpleReply)routable; + assertEquals("foo", reply.getValue()); + } + + @Test + public void requireThatUnknownRoutablesAreNotEncoded() { + assertNull(PROTOCOL.encode(VERSION, new EmptyReply())); + } + + @Test + public void requireThatEmptyBufferIsNotDecoded() { + assertNull(PROTOCOL.decode(VERSION, new byte[0])); + } + + @Test + public void requireThatUnknownBufferIsNotDecoded() { + assertNull(PROTOCOL.decode(VERSION, new byte[] { 'U' })); + } +} diff --git a/messagebus/src/test/java/com/yahoo/messagebus/test/SimpleReplyTestCase.java b/messagebus/src/test/java/com/yahoo/messagebus/test/SimpleReplyTestCase.java new file mode 100644 index 00000000000..474fea14ac7 --- /dev/null +++ b/messagebus/src/test/java/com/yahoo/messagebus/test/SimpleReplyTestCase.java @@ -0,0 +1,22 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.messagebus.test; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public class SimpleReplyTestCase { + + @Test + public void requireThatAccessorsWork() { + SimpleReply reply = new SimpleReply("foo"); + assertEquals(SimpleProtocol.REPLY, reply.getType()); + assertEquals(SimpleProtocol.NAME, reply.getProtocol()); + assertEquals("foo", reply.getValue()); + reply.setValue("bar"); + assertEquals("bar", reply.getValue()); + } +} diff --git a/messagebus/src/testlist.txt b/messagebus/src/testlist.txt new file mode 100644 index 00000000000..e096aa5a9a1 --- /dev/null +++ b/messagebus/src/testlist.txt @@ -0,0 +1,41 @@ +tests/advancedrouting +tests/auto-reply +tests/bucketsequence +tests/blob +tests/choke +tests/configagent +tests/context +tests/emptyreply +tests/error +tests/identity +tests/loadbalance +tests/messagebus +tests/messenger +tests/oos +tests/protocolrepository +tests/queue +tests/replygate +tests/resender +tests/result +tests/retrypolicy +tests/routable +tests/routablequeue +tests/routeparser +tests/routing +tests/routingcontext +tests/routingspec +tests/rpcserviceaddress +tests/sendadapter +tests/sequencer +tests/serviceaddress +tests/servicepool +tests/shutdown +tests/simple-roundtrip +tests/simpleprotocol +tests/slobrok +tests/sourcesession +tests/targetpool +tests/throttling +tests/timeout +tests/trace-roundtrip +tests/messageordering diff --git a/messagebus/src/testrun/.gitignore b/messagebus/src/testrun/.gitignore new file mode 100644 index 00000000000..b29b0c6486c --- /dev/null +++ b/messagebus/src/testrun/.gitignore @@ -0,0 +1,9 @@ +test-report.html +test-report.html.* +test.*.*.desc +test.*.*.file.* +test.*.*.files.html +test.*.*.log +tmp.* +xsync.log +/test.*.*.result diff --git a/messagebus/src/tests/.gitignore b/messagebus/src/tests/.gitignore new file mode 100644 index 00000000000..c473b24b98a --- /dev/null +++ b/messagebus/src/tests/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +testrunner +*_test diff --git a/messagebus/src/tests/CMakeLists.txt b/messagebus/src/tests/CMakeLists.txt new file mode 100644 index 00000000000..9991e8a8e8c --- /dev/null +++ b/messagebus/src/tests/CMakeLists.txt @@ -0,0 +1,41 @@ +add_subdirectory(advancedrouting) +add_subdirectory(auto-reply) +add_subdirectory(blob) +add_subdirectory(bucketsequence) +add_subdirectory(choke) +add_subdirectory(configagent) +add_subdirectory(context) +add_subdirectory(emptyreply) +add_subdirectory(error) +add_subdirectory(identity) +add_subdirectory(loadbalance) +add_subdirectory(messagebus) +add_subdirectory(messageordering) +add_subdirectory(messenger) +add_subdirectory(oos) +add_subdirectory(protocolrepository) +add_subdirectory(queue) +add_subdirectory(replygate) +add_subdirectory(resender) +add_subdirectory(result) +add_subdirectory(retrypolicy) +add_subdirectory(routable) +add_subdirectory(routablequeue) +add_subdirectory(routeparser) +add_subdirectory(routing) +add_subdirectory(routingcontext) +add_subdirectory(routingspec) +add_subdirectory(rpcserviceaddress) +add_subdirectory(sendadapter) +add_subdirectory(sequencer) +add_subdirectory(serviceaddress) +add_subdirectory(servicepool) +add_subdirectory(shutdown) +add_subdirectory(simple-roundtrip) +add_subdirectory(simpleprotocol) +add_subdirectory(slobrok) +add_subdirectory(sourcesession) +add_subdirectory(targetpool) +add_subdirectory(throttling) +add_subdirectory(timeout) +add_subdirectory(trace-roundtrip) diff --git a/messagebus/src/tests/advancedrouting/.gitignore b/messagebus/src/tests/advancedrouting/.gitignore new file mode 100644 index 00000000000..570db59096d --- /dev/null +++ b/messagebus/src/tests/advancedrouting/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +advancedrouting_test +messagebus_advancedrouting_test_app diff --git a/messagebus/src/tests/advancedrouting/CMakeLists.txt b/messagebus/src/tests/advancedrouting/CMakeLists.txt new file mode 100644 index 00000000000..99f5b037b69 --- /dev/null +++ b/messagebus/src/tests/advancedrouting/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_advancedrouting_test_app + SOURCES + advancedrouting.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_advancedrouting_test_app COMMAND messagebus_advancedrouting_test_app) diff --git a/messagebus/src/tests/advancedrouting/DESC b/messagebus/src/tests/advancedrouting/DESC new file mode 100644 index 00000000000..735d63dcdc3 --- /dev/null +++ b/messagebus/src/tests/advancedrouting/DESC @@ -0,0 +1 @@ +advancedrouting test. Take a look at advancedrouting.cpp for details. diff --git a/messagebus/src/tests/advancedrouting/FILES b/messagebus/src/tests/advancedrouting/FILES new file mode 100644 index 00000000000..61eb026ac3a --- /dev/null +++ b/messagebus/src/tests/advancedrouting/FILES @@ -0,0 +1 @@ +advancedrouting.cpp diff --git a/messagebus/src/tests/advancedrouting/advancedrouting.cpp b/messagebus/src/tests/advancedrouting/advancedrouting.cpp new file mode 100644 index 00000000000..976746b0738 --- /dev/null +++ b/messagebus/src/tests/advancedrouting/advancedrouting.cpp @@ -0,0 +1,188 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("routing_test"); + +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/routing/errordirective.h> +#include <vespa/messagebus/routing/retrytransienterrorspolicy.h> +#include <vespa/messagebus/testlib/custompolicy.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/vespalib/util/vstringfmt.h> + +using namespace mbus; + +class TestData { +public: + Slobrok _slobrok; + RetryTransientErrorsPolicy::SP _retryPolicy; + TestServer _srcServer; + SourceSession::UP _srcSession; + Receptor _srcHandler; + TestServer _dstServer; + DestinationSession::UP _fooSession; + Receptor _fooHandler; + DestinationSession::UP _barSession; + Receptor _barHandler; + DestinationSession::UP _bazSession; + Receptor _bazHandler; + +public: + TestData(); + bool start(); +}; + +class Test : public vespalib::TestApp { +private: + Message::UP createMessage(const string &msg); + bool testTrace(const std::vector<string> &expected, const Trace &trace); + +public: + int Main(); + void testAdvanced(TestData &data); +}; + +TEST_APPHOOK(Test); + +TestData::TestData() : + _slobrok(), + _retryPolicy(new RetryTransientErrorsPolicy()), + _srcServer(MessageBusParams().setRetryPolicy(_retryPolicy).addProtocol(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams().setSlobrokConfig(_slobrok.config())), + _srcSession(), + _srcHandler(), + _dstServer(MessageBusParams().addProtocol(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams().setIdentity(Identity("dst")).setSlobrokConfig(_slobrok.config())), + _fooSession(), + _fooHandler(), + _barSession(), + _barHandler(), + _bazSession(), + _bazHandler() +{ + _retryPolicy->setBaseDelay(0); +} + +bool +TestData::start() +{ + _srcSession = _srcServer.mb.createSourceSession(SourceSessionParams().setReplyHandler(_srcHandler)); + if (_srcSession.get() == NULL) { + LOG(error, "Could not create source session."); + return false; + } + _fooSession = _dstServer.mb.createDestinationSession(DestinationSessionParams().setName("foo").setMessageHandler(_fooHandler)); + if (_fooSession.get() == NULL) { + LOG(error, "Could not create foo session."); + return false; + } + _barSession = _dstServer.mb.createDestinationSession(DestinationSessionParams().setName("bar").setMessageHandler(_barHandler)); + if (_barSession.get() == NULL) { + LOG(error, "Could not create bar session."); + return false; + } + _bazSession = _dstServer.mb.createDestinationSession(DestinationSessionParams().setName("baz").setMessageHandler(_bazHandler)); + if (_bazSession.get() == NULL) { + LOG(error, "Could not create baz session."); + return false; + } + if (!_srcServer.waitSlobrok("dst/*", 3u)) { + return false; + } + return true; +} + +Message::UP +Test::createMessage(const string &msg) +{ + Message::UP ret(new SimpleMessage(msg)); + ret->getTrace().setLevel(9); + return ret; +} + +int +Test::Main() +{ + TEST_INIT("routing_test"); + + TestData data; + ASSERT_TRUE(data.start()); + + testAdvanced(data); TEST_FLUSH(); + + TEST_DONE(); +} + +void +Test::testAdvanced(TestData &data) +{ + const double TIMEOUT=60; + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("Custom", SimpleProtocol::IPolicyFactory::SP(new CustomPolicyFactory(false, ErrorCode::NO_ADDRESS_FOR_SERVICE))); + data._srcServer.mb.putProtocol(protocol); + data._srcServer.mb.setupRouting(RoutingSpec().addTable(RoutingTableSpec(SimpleProtocol::NAME) + .addHop(HopSpec("bar", "dst/bar")) + .addHop(HopSpec("baz", "dst/baz")) + .addRoute(RouteSpec("baz").addHop("baz")))); + string route = vespalib::make_vespa_string("[Custom:%s,bar,route:baz,dst/cox,?dst/unknown]", + data._fooSession->getConnectionSpec().c_str()); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse(route)).isAccepted()); + + // Initial send. + Message::UP msg = data._fooHandler.getMessage(TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + data._fooSession->acknowledge(std::move(msg)); + msg = data._barHandler.getMessage(TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + Reply::UP reply(new EmptyReply()); + reply->swapState(*msg); + reply->addError(Error(ErrorCode::TRANSIENT_ERROR, "bar")); + data._barSession->reply(std::move(reply)); + msg = data._bazHandler.getMessage(TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + reply.reset(new EmptyReply()); + reply->swapState(*msg); + reply->addError(Error(ErrorCode::TRANSIENT_ERROR, "baz1")); + data._bazSession->reply(std::move(reply)); + + // First retry. + msg = data._fooHandler.getMessage(0); + ASSERT_TRUE(msg.get() == NULL); + msg = data._barHandler.getMessage(TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + data._barSession->acknowledge(std::move(msg)); + msg = data._bazHandler.getMessage(TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + reply.reset(new EmptyReply()); + reply->swapState(*msg); + reply->addError(Error(ErrorCode::TRANSIENT_ERROR, "baz2")); + data._bazSession->reply(std::move(reply)); + + // Second retry. + msg = data._fooHandler.getMessage(0); + ASSERT_TRUE(msg.get() == NULL); + msg = data._barHandler.getMessage(0); + ASSERT_TRUE(msg.get() == NULL); + msg = data._bazHandler.getMessage(TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + reply.reset(new EmptyReply()); + reply->swapState(*msg); + reply->addError(Error(ErrorCode::FATAL_ERROR, "baz3")); + data._bazSession->reply(std::move(reply)); + + // Done. + reply = data._srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_EQUAL(2u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::FATAL_ERROR, reply->getError(0).getCode()); + EXPECT_EQUAL((uint32_t)ErrorCode::NO_ADDRESS_FOR_SERVICE, reply->getError(1).getCode()); +} diff --git a/messagebus/src/tests/auto-reply/.gitignore b/messagebus/src/tests/auto-reply/.gitignore new file mode 100644 index 00000000000..061d2f262bd --- /dev/null +++ b/messagebus/src/tests/auto-reply/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +auto-reply_test +messagebus_auto-reply_test_app diff --git a/messagebus/src/tests/auto-reply/CMakeLists.txt b/messagebus/src/tests/auto-reply/CMakeLists.txt new file mode 100644 index 00000000000..950ade550b7 --- /dev/null +++ b/messagebus/src/tests/auto-reply/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_auto-reply_test_app + SOURCES + auto-reply.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_auto-reply_test_app COMMAND messagebus_auto-reply_test_app) diff --git a/messagebus/src/tests/auto-reply/DESC b/messagebus/src/tests/auto-reply/DESC new file mode 100644 index 00000000000..2aec186bfac --- /dev/null +++ b/messagebus/src/tests/auto-reply/DESC @@ -0,0 +1,2 @@ +Test that a deleted Message or Reply with a non-empty call-stack will +generate an automatic Reply. diff --git a/messagebus/src/tests/auto-reply/FILES b/messagebus/src/tests/auto-reply/FILES new file mode 100644 index 00000000000..29f5dbbc1bb --- /dev/null +++ b/messagebus/src/tests/auto-reply/FILES @@ -0,0 +1 @@ +auto-reply.cpp diff --git a/messagebus/src/tests/auto-reply/auto-reply.cpp b/messagebus/src/tests/auto-reply/auto-reply.cpp new file mode 100644 index 00000000000..876755dfd0e --- /dev/null +++ b/messagebus/src/tests/auto-reply/auto-reply.cpp @@ -0,0 +1,40 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("auto-reply_test"); + +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/routablequeue.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +TEST_SETUP(Test); + +int +Test::Main() +{ + TEST_INIT("auto-reply_test"); + RoutableQueue q; + { + Message::UP msg(new SimpleMessage("test")); + } + EXPECT_TRUE(q.size() == 0); + { + Message::UP msg(new SimpleMessage("test")); + msg->pushHandler(q); + } + EXPECT_TRUE(q.size() == 1); + { + Reply::UP reply(new SimpleReply("test")); + } + EXPECT_TRUE(q.size() == 1); + { + Reply::UP reply(new SimpleReply("test")); + reply->pushHandler(q); + } + EXPECT_TRUE(q.size() == 2); + TEST_DONE(); +} diff --git a/messagebus/src/tests/blob/.gitignore b/messagebus/src/tests/blob/.gitignore new file mode 100644 index 00000000000..8602aa42ade --- /dev/null +++ b/messagebus/src/tests/blob/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +blob_test +messagebus_blob_test_app diff --git a/messagebus/src/tests/blob/CMakeLists.txt b/messagebus/src/tests/blob/CMakeLists.txt new file mode 100644 index 00000000000..d9a865519cb --- /dev/null +++ b/messagebus/src/tests/blob/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_blob_test_app + SOURCES + blob.cpp + DEPENDS + messagebus + messagebus_messagebus-test +) +vespa_add_test(NAME messagebus_blob_test_app COMMAND messagebus_blob_test_app) diff --git a/messagebus/src/tests/blob/DESC b/messagebus/src/tests/blob/DESC new file mode 100644 index 00000000000..b2ba59c187f --- /dev/null +++ b/messagebus/src/tests/blob/DESC @@ -0,0 +1 @@ +Test the Blob and BlobRef classes. diff --git a/messagebus/src/tests/blob/FILES b/messagebus/src/tests/blob/FILES new file mode 100644 index 00000000000..fd1396e55e3 --- /dev/null +++ b/messagebus/src/tests/blob/FILES @@ -0,0 +1 @@ +blob.cpp diff --git a/messagebus/src/tests/blob/blob.cpp b/messagebus/src/tests/blob/blob.cpp new file mode 100644 index 00000000000..9b0df1ae476 --- /dev/null +++ b/messagebus/src/tests/blob/blob.cpp @@ -0,0 +1,79 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("blob_test"); +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/blob.h> +#include <vespa/messagebus/blobref.h> + +using mbus::Blob; +using mbus::BlobRef; + +TEST_SETUP(Test); + +Blob makeBlob(const char *txt) { + Blob b(strlen(txt) + 1); + strcpy(b.data(), txt); + return b; +} + +BlobRef makeBlobRef(const Blob &b) { + return BlobRef(b.data(), b.size()); +} + +Blob returnBlob(Blob b) { + return b; +} + +BlobRef returnBlobRef(BlobRef br) { + return br; +} + +int +Test::Main() +{ + TEST_INIT("blob_test"); + + // create a blob + Blob b = makeBlob("test"); + EXPECT_TRUE(b.size() == strlen("test") + 1); + EXPECT_TRUE(strcmp("test", b.data()) == 0); + + // create a ref to a blob + BlobRef br = makeBlobRef(b); + EXPECT_TRUE(br.size() == strlen("test") + 1); + EXPECT_TRUE(strcmp("test", br.data()) == 0); + EXPECT_TRUE(b.data() == br.data()); + + // non-destructive copy of ref + BlobRef br2 = returnBlobRef(br); + EXPECT_TRUE(br.size() == strlen("test") + 1); + EXPECT_TRUE(strcmp("test", br.data()) == 0); + EXPECT_TRUE(b.data() == br.data()); + EXPECT_TRUE(br2.size() == strlen("test") + 1); + EXPECT_TRUE(strcmp("test", br2.data()) == 0); + EXPECT_TRUE(b.data() == br2.data()); + + br = br2; + EXPECT_TRUE(br.size() == strlen("test") + 1); + EXPECT_TRUE(strcmp("test", br.data()) == 0); + EXPECT_TRUE(b.data() == br.data()); + EXPECT_TRUE(br2.size() == strlen("test") + 1); + EXPECT_TRUE(strcmp("test", br2.data()) == 0); + EXPECT_TRUE(b.data() == br2.data()); + + // destructive copy of blob + Blob b2 = returnBlob(std::move(b)); + EXPECT_EQUAL(0u, b.size()); + EXPECT_TRUE(b.data() == 0); + EXPECT_TRUE(b2.size() == strlen("test") + 1); + EXPECT_TRUE(strcmp("test", b2.data()) == 0); + + b.swap(b2); + EXPECT_EQUAL(0u, b2.size()); + EXPECT_TRUE(b2.data() == 0); + EXPECT_TRUE(b.size() == strlen("test") + 1); + EXPECT_TRUE(strcmp("test", b.data()) == 0); + + TEST_DONE(); +} diff --git a/messagebus/src/tests/bucketsequence/.gitignore b/messagebus/src/tests/bucketsequence/.gitignore new file mode 100644 index 00000000000..cca77fed742 --- /dev/null +++ b/messagebus/src/tests/bucketsequence/.gitignore @@ -0,0 +1,3 @@ +/.depend +/Makefile +messagebus_bucketsequence_test_app diff --git a/messagebus/src/tests/bucketsequence/CMakeLists.txt b/messagebus/src/tests/bucketsequence/CMakeLists.txt new file mode 100644 index 00000000000..5ab01524d51 --- /dev/null +++ b/messagebus/src/tests/bucketsequence/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_bucketsequence_test_app + SOURCES + bucketsequence.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_bucketsequence_test_app COMMAND messagebus_bucketsequence_test_app) diff --git a/messagebus/src/tests/bucketsequence/DESC b/messagebus/src/tests/bucketsequence/DESC new file mode 100644 index 00000000000..b2e8d79519b --- /dev/null +++ b/messagebus/src/tests/bucketsequence/DESC @@ -0,0 +1 @@ +bucketsequence test. Take a look at bucketsequence.cpp for details. diff --git a/messagebus/src/tests/bucketsequence/FILES b/messagebus/src/tests/bucketsequence/FILES new file mode 100644 index 00000000000..6db6cc0a2cd --- /dev/null +++ b/messagebus/src/tests/bucketsequence/FILES @@ -0,0 +1 @@ +bucketsequence.cpp diff --git a/messagebus/src/tests/bucketsequence/bucketsequence.cpp b/messagebus/src/tests/bucketsequence/bucketsequence.cpp new file mode 100644 index 00000000000..6036c43f659 --- /dev/null +++ b/messagebus/src/tests/bucketsequence/bucketsequence.cpp @@ -0,0 +1,50 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("bucketsequence_test"); + +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/routing/retrytransienterrorspolicy.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +TEST_SETUP(Test); + +class MyMessage : public SimpleMessage { +public: + MyMessage() : SimpleMessage("foo") { } + bool hasBucketSequence() { return true; } +}; + +int +Test::Main() +{ + TEST_INIT("bucketsequence_test"); + + Slobrok slobrok; + TestServer server(MessageBusParams() + .addProtocol(IProtocol::SP(new SimpleProtocol())) + .setRetryPolicy(IRetryPolicy::SP(new RetryTransientErrorsPolicy())), + RPCNetworkParams() + .setSlobrokConfig(slobrok.config())); + Receptor receptor; + SourceSession::UP session = server.mb.createSourceSession( + SourceSessionParams() + .setReplyHandler(receptor)); + Message::UP msg(new MyMessage()); + msg->setRoute(Route::parse("foo")); + ASSERT_TRUE(session->send(std::move(msg)).isAccepted()); + Reply::UP reply = receptor.getReply(); + ASSERT_TRUE(reply.get() != NULL); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::SEQUENCE_ERROR, reply->getError(0).getCode()); + + TEST_DONE(); +} diff --git a/messagebus/src/tests/choke/.gitignore b/messagebus/src/tests/choke/.gitignore new file mode 100644 index 00000000000..320ee997500 --- /dev/null +++ b/messagebus/src/tests/choke/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +choke_test +messagebus_choke_test_app diff --git a/messagebus/src/tests/choke/CMakeLists.txt b/messagebus/src/tests/choke/CMakeLists.txt new file mode 100644 index 00000000000..02e2c14b943 --- /dev/null +++ b/messagebus/src/tests/choke/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_choke_test_app + SOURCES + choke.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_choke_test_app NO_VALGRIND COMMAND messagebus_choke_test_app) diff --git a/messagebus/src/tests/choke/DESC b/messagebus/src/tests/choke/DESC new file mode 100644 index 00000000000..fd1d4965b7d --- /dev/null +++ b/messagebus/src/tests/choke/DESC @@ -0,0 +1 @@ +choke test. Take a look at choke.cpp for details. diff --git a/messagebus/src/tests/choke/FILES b/messagebus/src/tests/choke/FILES new file mode 100644 index 00000000000..7a0d95feb52 --- /dev/null +++ b/messagebus/src/tests/choke/FILES @@ -0,0 +1 @@ +choke.cpp diff --git a/messagebus/src/tests/choke/choke.cpp b/messagebus/src/tests/choke/choke.cpp new file mode 100644 index 00000000000..567ec52db79 --- /dev/null +++ b/messagebus/src/tests/choke/choke.cpp @@ -0,0 +1,227 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("choke_test"); + +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/reply.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +//////////////////////////////////////////////////////////////////////////////// +// +// Setup +// +//////////////////////////////////////////////////////////////////////////////// + +class TestData { +public: + Slobrok _slobrok; + TestServer _srcServer; + SourceSession::UP _srcSession; + Receptor _srcHandler; + TestServer _dstServer; + DestinationSession::UP _dstSession; + Receptor _dstHandler; + +public: + TestData(); + bool start(); +}; + +class Test : public vespalib::TestApp { +private: + Message::UP createMessage(const string &msg); + +public: + int Main(); + void testMaxCount(TestData &data); + void testMaxSize(TestData &data); +}; + +TEST_APPHOOK(Test); + +TestData::TestData() : + _slobrok(), + _srcServer(MessageBusParams() + .setRetryPolicy(IRetryPolicy::SP()) + .addProtocol(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams() + .setSlobrokConfig(_slobrok.config())), + _srcSession(), + _srcHandler(), + _dstServer(MessageBusParams() + .addProtocol(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams() + .setIdentity(Identity("dst")) + .setSlobrokConfig(_slobrok.config())), + _dstSession(), + _dstHandler() +{ + // empty +} + +bool +TestData::start() +{ + _srcSession = _srcServer.mb.createSourceSession(SourceSessionParams() + .setThrottlePolicy(IThrottlePolicy::SP()) + .setReplyHandler(_srcHandler)); + if (_srcSession.get() == NULL) { + return false; + } + _dstSession = _dstServer.mb.createDestinationSession(DestinationSessionParams() + .setName("session") + .setMessageHandler(_dstHandler)); + if (_dstSession.get() == NULL) { + return false; + } + if (!_srcServer.waitSlobrok("dst/session", 1u)) { + return false; + } + return true; +} + +Message::UP +Test::createMessage(const string &msg) +{ + Message::UP ret(new SimpleMessage(msg)); + ret->getTrace().setLevel(9); + return ret; +} + +int +Test::Main() +{ + TEST_INIT("choke_test"); + + TestData data; + ASSERT_TRUE(data.start()); + + testMaxCount(data); TEST_FLUSH(); + testMaxSize(data); TEST_FLUSH(); + + TEST_DONE(); +} + +static const double TIMEOUT = 120; + +//////////////////////////////////////////////////////////////////////////////// +// +// Tests +// +//////////////////////////////////////////////////////////////////////////////// + +void +Test::testMaxCount(TestData &data) +{ + uint32_t max = 10; + data._dstServer.mb.setMaxPendingCount(max); + std::vector<Message*> lst; + for (uint32_t i = 0; i < max * 2; ++i) { + if (i < max) { + EXPECT_EQUAL(i, data._dstServer.mb.getPendingCount()); + } else { + EXPECT_EQUAL(max, data._dstServer.mb.getPendingCount()); + } + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("dst/session")).isAccepted()); + if (i < max) { + Message::UP msg = data._dstHandler.getMessage(TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + lst.push_back(msg.release()); + } else { + Reply::UP reply = data._srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::SESSION_BUSY, reply->getError(0).getCode()); + } + } + for (uint32_t i = 0; i < 5; ++i) { + Message::UP msg(lst[0]); + lst.erase(lst.begin()); + data._dstSession->acknowledge(std::move(msg)); + + Reply::UP reply = data._srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + EXPECT_TRUE(!reply->hasErrors()); + msg = reply->getMessage(); + ASSERT_TRUE(msg.get() != NULL); + EXPECT_TRUE(data._srcSession->send(std::move(msg), Route::parse("dst/session")).isAccepted()); + + msg = data._dstHandler.getMessage(TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + lst.push_back(msg.release()); + } + while (!lst.empty()) { + EXPECT_EQUAL(lst.size(), data._dstServer.mb.getPendingCount()); + Message::UP msg(lst[0]); + lst.erase(lst.begin()); + data._dstSession->acknowledge(std::move(msg)); + + Reply::UP reply = data._srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + EXPECT_TRUE(!reply->hasErrors()); + } + EXPECT_EQUAL(0u, data._dstServer.mb.getPendingCount()); +} + +void +Test::testMaxSize(TestData &data) +{ + uint32_t size = createMessage("msg")->getApproxSize(); + uint32_t max = size * 10; + data._dstServer.mb.setMaxPendingSize(max); + std::vector<Message*> lst; + for (uint32_t i = 0; i < max * 2; i += size) { + if (i < max) { + EXPECT_EQUAL(i, data._dstServer.mb.getPendingSize()); + } else { + EXPECT_EQUAL(max, data._dstServer.mb.getPendingSize()); + } + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("dst/session")).isAccepted()); + if (i < max) { + Message::UP msg = data._dstHandler.getMessage(TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + lst.push_back(msg.release()); + } else { + Reply::UP reply = data._srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::SESSION_BUSY, reply->getError(0).getCode()); + } + } + for (uint32_t i = 0; i < 5; ++i) { + Message::UP msg(lst[0]); + lst.erase(lst.begin()); + data._dstSession->acknowledge(std::move(msg)); + + Reply::UP reply = data._srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + EXPECT_TRUE(!reply->hasErrors()); + msg = reply->getMessage(); + ASSERT_TRUE(msg.get() != NULL); + EXPECT_TRUE(data._srcSession->send(std::move(msg), Route::parse("dst/session")).isAccepted()); + + msg = data._dstHandler.getMessage(TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + lst.push_back(msg.release()); + } + while (!lst.empty()) { + EXPECT_EQUAL(size * lst.size(), data._dstServer.mb.getPendingSize()); + Message::UP msg(lst[0]); + lst.erase(lst.begin()); + data._dstSession->acknowledge(std::move(msg)); + + Reply::UP reply = data._srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + EXPECT_TRUE(!reply->hasErrors()); + } + EXPECT_EQUAL(0u, data._dstServer.mb.getPendingSize()); +} diff --git a/messagebus/src/tests/configagent/.gitignore b/messagebus/src/tests/configagent/.gitignore new file mode 100644 index 00000000000..240f433156c --- /dev/null +++ b/messagebus/src/tests/configagent/.gitignore @@ -0,0 +1,5 @@ +.depend +Makefile +configagent_test +test.cfg +messagebus_configagent_test_app diff --git a/messagebus/src/tests/configagent/CMakeLists.txt b/messagebus/src/tests/configagent/CMakeLists.txt new file mode 100644 index 00000000000..04170cc9d05 --- /dev/null +++ b/messagebus/src/tests/configagent/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_configagent_test_app + SOURCES + configagent.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_configagent_test_app COMMAND messagebus_configagent_test_app) diff --git a/messagebus/src/tests/configagent/DESC b/messagebus/src/tests/configagent/DESC new file mode 100644 index 00000000000..b4db2789b01 --- /dev/null +++ b/messagebus/src/tests/configagent/DESC @@ -0,0 +1,2 @@ +Test that the config agent is able to configure a config handler using +config files. diff --git a/messagebus/src/tests/configagent/FILES b/messagebus/src/tests/configagent/FILES new file mode 100644 index 00000000000..49fd8684ac1 --- /dev/null +++ b/messagebus/src/tests/configagent/FILES @@ -0,0 +1,3 @@ +configagent.cpp +full.cfg +half.cfg diff --git a/messagebus/src/tests/configagent/configagent.cpp b/messagebus/src/tests/configagent/configagent.cpp new file mode 100644 index 00000000000..c08fdd25be0 --- /dev/null +++ b/messagebus/src/tests/configagent/configagent.cpp @@ -0,0 +1,122 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("configagent_test"); +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/config/print/fileconfigreader.h> +#include <vespa/messagebus/configagent.h> +#include <vespa/messagebus/iconfighandler.h> +#include <vespa/messagebus/routing/routingspec.h> +#include <vespa/messagebus/config-messagebus.h> + +using namespace mbus; +using namespace messagebus; +using namespace config; + +class Test : public vespalib::TestApp, public IConfigHandler { +private: + RoutingSpec _spec; + bool checkHalf(); + bool checkFull(); + bool checkTables(uint32_t numTables); + +public: + int Main(); + bool setupRouting(const RoutingSpec &spec); +}; + +TEST_APPHOOK(Test); + +bool +Test::setupRouting(const RoutingSpec &spec) +{ + _spec = spec; + return true; +} + +bool +Test::checkTables(uint32_t numTables) +{ + if (!EXPECT_EQUAL(numTables, _spec.getNumTables())) return false; + if (numTables > 0) { + if (!EXPECT_EQUAL("foo", _spec.getTable(0).getProtocol())) return false; + if (!EXPECT_EQUAL(2u, _spec.getTable(0).getNumHops())) return false; + if (!EXPECT_EQUAL("foo-h1", _spec.getTable(0).getHop(0).getName())) return false; + if (!EXPECT_EQUAL("foo-h1-sel", _spec.getTable(0).getHop(0).getSelector())) return false; + if (!EXPECT_EQUAL(2u, _spec.getTable(0).getHop(0).getNumRecipients())) return false; + if (!EXPECT_EQUAL("foo-h1-r1", _spec.getTable(0).getHop(0).getRecipient(0))) return false; + if (!EXPECT_EQUAL("foo-h1-r2", _spec.getTable(0).getHop(0).getRecipient(1))) return false; + if (!EXPECT_EQUAL(true, _spec.getTable(0).getHop(0).getIgnoreResult())) return false; + if (!EXPECT_EQUAL("foo-h2", _spec.getTable(0).getHop(1).getName())) return false; + if (!EXPECT_EQUAL("foo-h2-sel", _spec.getTable(0).getHop(1).getSelector())) return false; + if (!EXPECT_EQUAL(2u, _spec.getTable(0).getHop(1).getNumRecipients())) return false; + if (!EXPECT_EQUAL("foo-h2-r1", _spec.getTable(0).getHop(1).getRecipient(0))) return false; + if (!EXPECT_EQUAL("foo-h2-r2", _spec.getTable(0).getHop(1).getRecipient(1))) return false; + if (!EXPECT_EQUAL(2u, _spec.getTable(0).getNumRoutes())) return false; + if (!EXPECT_EQUAL("foo-r1", _spec.getTable(0).getRoute(0).getName())) return false; + if (!EXPECT_EQUAL(2u, _spec.getTable(0).getRoute(0).getNumHops())) return false; + if (!EXPECT_EQUAL("foo-h1", _spec.getTable(0).getRoute(0).getHop(0))) return false; + if (!EXPECT_EQUAL("foo-h2", _spec.getTable(0).getRoute(0).getHop(1))) return false; + if (!EXPECT_EQUAL("foo-r2", _spec.getTable(0).getRoute(1).getName())) return false; + if (!EXPECT_EQUAL(2u, _spec.getTable(0).getRoute(1).getNumHops())) return false; + if (!EXPECT_EQUAL("foo-h2", _spec.getTable(0).getRoute(1).getHop(0))) return false; + if (!EXPECT_EQUAL("foo-h1", _spec.getTable(0).getRoute(1).getHop(1))) return false; + } + if (numTables > 1) { + if (!EXPECT_EQUAL("bar", _spec.getTable(1).getProtocol())) return false; + if (!EXPECT_EQUAL(2u, _spec.getTable(1).getNumHops())) return false; + if (!EXPECT_EQUAL("bar-h1", _spec.getTable(1).getHop(0).getName())) return false; + if (!EXPECT_EQUAL("bar-h1-sel", _spec.getTable(1).getHop(0).getSelector())) return false; + if (!EXPECT_EQUAL(2u, _spec.getTable(1).getHop(0).getNumRecipients())) return false; + if (!EXPECT_EQUAL("bar-h1-r1", _spec.getTable(1).getHop(0).getRecipient(0))) return false; + if (!EXPECT_EQUAL("bar-h1-r2", _spec.getTable(1).getHop(0).getRecipient(1))) return false; + if (!EXPECT_EQUAL("bar-h2", _spec.getTable(1).getHop(1).getName())) return false; + if (!EXPECT_EQUAL("bar-h2-sel", _spec.getTable(1).getHop(1).getSelector())) return false; + if (!EXPECT_EQUAL(2u, _spec.getTable(1).getHop(1).getNumRecipients())) return false; + if (!EXPECT_EQUAL("bar-h2-r1", _spec.getTable(1).getHop(1).getRecipient(0))) return false; + if (!EXPECT_EQUAL("bar-h2-r2", _spec.getTable(1).getHop(1).getRecipient(1))) return false; + if (!EXPECT_EQUAL(2u, _spec.getTable(1).getNumRoutes())) return false; + if (!EXPECT_EQUAL("bar-r1", _spec.getTable(1).getRoute(0).getName())) return false; + if (!EXPECT_EQUAL(2u, _spec.getTable(1).getRoute(0).getNumHops())) return false; + if (!EXPECT_EQUAL("bar-h1", _spec.getTable(1).getRoute(0).getHop(0))) return false; + if (!EXPECT_EQUAL("bar-h2", _spec.getTable(1).getRoute(0).getHop(1))) return false; + if (!EXPECT_EQUAL("bar-r2", _spec.getTable(1).getRoute(1).getName())) return false; + if (!EXPECT_EQUAL(2u, _spec.getTable(1).getRoute(1).getNumHops())) return false; + if (!EXPECT_EQUAL("bar-h2", _spec.getTable(1).getRoute(1).getHop(0))) return false; + if (!EXPECT_EQUAL("bar-h1", _spec.getTable(1).getRoute(1).getHop(1))) return false; + } + return true; +} + +bool +Test::checkHalf() +{ + return _spec.getNumTables() == 1 && EXPECT_TRUE(checkTables(1)); +} + +bool +Test::checkFull() +{ + return _spec.getNumTables() == 2 && EXPECT_TRUE(checkTables(2)); +} + +int +Test::Main() +{ + TEST_INIT("configagent_test"); + EXPECT_TRUE(!checkHalf()); + EXPECT_TRUE(!checkFull()); + ConfigAgent agent(*this); + EXPECT_TRUE(!checkHalf()); + EXPECT_TRUE(!checkFull()); + agent.configure(FileConfigReader<MessagebusConfig>("full.cfg").read()); + EXPECT_TRUE(!checkHalf()); + EXPECT_TRUE(checkFull()); + agent.configure(FileConfigReader<MessagebusConfig>("half.cfg").read()); + EXPECT_TRUE(checkHalf()); + EXPECT_TRUE(!checkFull()); + agent.configure(FileConfigReader<MessagebusConfig>("full.cfg").read()); + EXPECT_TRUE(checkFull()); + EXPECT_TRUE(!checkHalf()); + TEST_DONE(); +} diff --git a/messagebus/src/tests/configagent/full.cfg b/messagebus/src/tests/configagent/full.cfg new file mode 100644 index 00000000000..addfcd3c080 --- /dev/null +++ b/messagebus/src/tests/configagent/full.cfg @@ -0,0 +1,44 @@ +routingtable[2] +routingtable[0].protocol "foo" +routingtable[0].hop[2] +routingtable[0].hop[0].name "foo-h1" +routingtable[0].hop[0].selector "foo-h1-sel" +routingtable[0].hop[0].recipient[2] +routingtable[0].hop[0].recipient[0] "foo-h1-r1" +routingtable[0].hop[0].recipient[1] "foo-h1-r2" +routingtable[0].hop[0].ignoreresult true +routingtable[0].hop[1].name "foo-h2" +routingtable[0].hop[1].selector "foo-h2-sel" +routingtable[0].hop[1].recipient[2] +routingtable[0].hop[1].recipient[0] "foo-h2-r1" +routingtable[0].hop[1].recipient[1] "foo-h2-r2" +routingtable[0].route[2] +routingtable[0].route[0].name "foo-r1" +routingtable[0].route[0].hop[2] +routingtable[0].route[0].hop[0] "foo-h1" +routingtable[0].route[0].hop[1] "foo-h2" +routingtable[0].route[1].name "foo-r2" +routingtable[0].route[1].hop[2] +routingtable[0].route[1].hop[0] "foo-h2" +routingtable[0].route[1].hop[1] "foo-h1" +routingtable[1].protocol "bar" +routingtable[1].hop[2] +routingtable[1].hop[0].name "bar-h1" +routingtable[1].hop[0].selector "bar-h1-sel" +routingtable[1].hop[0].recipient[2] +routingtable[1].hop[0].recipient[0] "bar-h1-r1" +routingtable[1].hop[0].recipient[1] "bar-h1-r2" +routingtable[1].hop[1].name "bar-h2" +routingtable[1].hop[1].selector "bar-h2-sel" +routingtable[1].hop[1].recipient[2] +routingtable[1].hop[1].recipient[0] "bar-h2-r1" +routingtable[1].hop[1].recipient[1] "bar-h2-r2" +routingtable[1].route[2] +routingtable[1].route[0].name "bar-r1" +routingtable[1].route[0].hop[2] +routingtable[1].route[0].hop[0] "bar-h1" +routingtable[1].route[0].hop[1] "bar-h2" +routingtable[1].route[1].name "bar-r2" +routingtable[1].route[1].hop[2] +routingtable[1].route[1].hop[0] "bar-h2" +routingtable[1].route[1].hop[1] "bar-h1" diff --git a/messagebus/src/tests/configagent/half.cfg b/messagebus/src/tests/configagent/half.cfg new file mode 100644 index 00000000000..12570a9a557 --- /dev/null +++ b/messagebus/src/tests/configagent/half.cfg @@ -0,0 +1,23 @@ +routingtable[1] +routingtable[0].protocol "foo" +routingtable[0].hop[2] +routingtable[0].hop[0].name "foo-h1" +routingtable[0].hop[0].selector "foo-h1-sel" +routingtable[0].hop[0].recipient[2] +routingtable[0].hop[0].recipient[0] "foo-h1-r1" +routingtable[0].hop[0].recipient[1] "foo-h1-r2" +routingtable[0].hop[0].ignoreresult true +routingtable[0].hop[1].name "foo-h2" +routingtable[0].hop[1].selector "foo-h2-sel" +routingtable[0].hop[1].recipient[2] +routingtable[0].hop[1].recipient[0] "foo-h2-r1" +routingtable[0].hop[1].recipient[1] "foo-h2-r2" +routingtable[0].route[2] +routingtable[0].route[0].name "foo-r1" +routingtable[0].route[0].hop[2] +routingtable[0].route[0].hop[0] "foo-h1" +routingtable[0].route[0].hop[1] "foo-h2" +routingtable[0].route[1].name "foo-r2" +routingtable[0].route[1].hop[2] +routingtable[0].route[1].hop[0] "foo-h2" +routingtable[0].route[1].hop[1] "foo-h1" diff --git a/messagebus/src/tests/context/.gitignore b/messagebus/src/tests/context/.gitignore new file mode 100644 index 00000000000..9b9771b64f0 --- /dev/null +++ b/messagebus/src/tests/context/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +context_test +messagebus_context_test_app diff --git a/messagebus/src/tests/context/CMakeLists.txt b/messagebus/src/tests/context/CMakeLists.txt new file mode 100644 index 00000000000..d9a85a1b8c9 --- /dev/null +++ b/messagebus/src/tests/context/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_context_test_app + SOURCES + context.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_context_test_app COMMAND messagebus_context_test_app) diff --git a/messagebus/src/tests/context/DESC b/messagebus/src/tests/context/DESC new file mode 100644 index 00000000000..5a40cc4f9a1 --- /dev/null +++ b/messagebus/src/tests/context/DESC @@ -0,0 +1 @@ +context test. Take a look at context.cpp for details. diff --git a/messagebus/src/tests/context/FILES b/messagebus/src/tests/context/FILES new file mode 100644 index 00000000000..a4c148657b9 --- /dev/null +++ b/messagebus/src/tests/context/FILES @@ -0,0 +1 @@ +context.cpp diff --git a/messagebus/src/tests/context/context.cpp b/messagebus/src/tests/context/context.cpp new file mode 100644 index 00000000000..ccb136e7b81 --- /dev/null +++ b/messagebus/src/tests/context/context.cpp @@ -0,0 +1,102 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("context_test"); + +#include <vespa/messagebus/destinationsession.h> +#include <vespa/messagebus/intermediatesession.h> +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/routablequeue.h> +#include <vespa/messagebus/routing/routingspec.h> +#include <vespa/messagebus/sourcesession.h> +#include <vespa/messagebus/sourcesessionparams.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +struct Handler : public IMessageHandler +{ + DestinationSession::UP session; + + Handler(MessageBus &mb) : session() { + session = mb.createDestinationSession("session", true, *this); + } + ~Handler() { + session.reset(); + } + virtual void handleMessage(Message::UP msg) { + session->acknowledge(std::move(msg)); + } +}; + +RoutingSpec getRouting() { + return RoutingSpec() + .addTable(RoutingTableSpec("Simple") + .addHop(HopSpec("test", "test/session")) + .addRoute(RouteSpec("test").addHop("test"))); +} + +TEST_SETUP(Test); + +int +Test::Main() +{ + TEST_INIT("context_test"); + + Slobrok slobrok; + TestServer src(Identity(""), getRouting(), slobrok); + TestServer dst(Identity("test"), getRouting(), slobrok); + Handler handler(dst.mb); + + ASSERT_TRUE(src.waitSlobrok("test/session")); + + RoutableQueue queue; + SourceSessionParams params; + params.setThrottlePolicy(IThrottlePolicy::SP()); + SourceSession::UP ss = src.mb.createSourceSession(queue, params); + + { + Message::UP msg(new SimpleMessage("test", true, 1)); + msg->setContext(Context((uint64_t)10)); + ss->send(std::move(msg), "test"); + } + { + Message::UP msg(new SimpleMessage("test", true, 1)); + msg->setContext(Context((uint64_t)20)); + ss->send(std::move(msg), "test"); + } + { + Message::UP msg(new SimpleMessage("test", true, 1)); + msg->setContext(Context((uint64_t)30)); + ss->send(std::move(msg), "test"); + } + for (uint32_t i = 0; i < 1000; ++i) { + if (queue.size() == 3) { + break; + } + FastOS_Thread::Sleep(10); + } + EXPECT_EQUAL(queue.size(), 3u); + { + Reply::UP reply = Reply::UP((Reply*)queue.dequeue(0).release()); + ASSERT_TRUE(reply.get() != 0); + EXPECT_EQUAL(reply->getContext().value.UINT64, 10u); + } + { + Reply::UP reply = Reply::UP((Reply*)queue.dequeue(0).release()); + ASSERT_TRUE(reply.get() != 0); + EXPECT_EQUAL(reply->getContext().value.UINT64, 20u); + } + { + Reply::UP reply = Reply::UP((Reply*)queue.dequeue(0).release()); + ASSERT_TRUE(reply.get() != 0); + EXPECT_EQUAL(reply->getContext().value.UINT64, 30u); + } + TEST_DONE(); +} diff --git a/messagebus/src/tests/create-test.sh b/messagebus/src/tests/create-test.sh new file mode 100755 index 00000000000..b5406dd24bd --- /dev/null +++ b/messagebus/src/tests/create-test.sh @@ -0,0 +1,74 @@ +#!/bin/sh +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +gen_ignore_file() { + echo "generating '$1' ..." + echo ".depend" > $1 + echo "Makefile" >> $1 + echo "${test}_test" >> $1 +} + +gen_project_file() { + echo "generating '$1' ..." + echo "APPLICATION ${test}_test" > $1 + echo "OBJS $test" >> $1 + echo "LIBS messagebus/testlib/messagebus-test" >> $1 + echo "LIBS messagebus/messagebus" >> $1 + echo "EXTERNALLIBS slobrokserver slobrok fnet vespalib config vespalog" >> $1 + echo "" >> $1 + echo "CUSTOMMAKE" >> $1 + echo "test: depend ${test}_test" >> $1 + echo -e "\t@./${test}_test" >> $1 +} + +gen_source() { + echo "generating '$1' ..." + echo "#include <vespa/log/log.h>" > $1 + echo "LOG_SETUP(\"${test}_test\");" >> $1 + echo "#include <vespa/fastos/fastos.h>" >> $1 + echo "#include <vespa/vespalib/testkit/testapp.h>" >> $1 + echo "" >> $1 + echo "// using namespace mbus;" >> $1 + echo "" >> $1 + echo "TEST_SETUP(Test);" >> $1 + echo "" >> $1 + echo "int" >> $1 + echo "Test::Main()" >> $1 + echo "{" >> $1 + echo " TEST_INIT(\"${test}_test\");" >> $1 + echo " TEST_DONE();" >> $1 + echo "}" >> $1 +} + +gen_desc() { + echo "generating '$1' ..." + echo "$test test. Take a look at $test.cpp for details." > $1 +} + +gen_file_list() { + echo "generating '$1' ..." + echo "$test.cpp" > $1 +} + +if [ $# -ne 1 ]; then + echo "usage: $0 <name>" + echo " name: name of the test to create" + exit 1 +fi + +test=$1 +if [ -e $test ]; then + echo "$test already present, don't want to mess it up..." + exit 1 +fi + +echo "creating directory '$test' ..." +mkdir -p $test || exit 1 +cd $test || exit 1 +test=`basename $test` + +gen_ignore_file .cvsignore +gen_project_file fastos.project +gen_source $test.cpp +gen_desc DESC +gen_file_list FILES diff --git a/messagebus/src/tests/emptyreply/.gitignore b/messagebus/src/tests/emptyreply/.gitignore new file mode 100644 index 00000000000..bfaf7f812cf --- /dev/null +++ b/messagebus/src/tests/emptyreply/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +emptyreply_test +messagebus_emptyreply_test_app diff --git a/messagebus/src/tests/emptyreply/CMakeLists.txt b/messagebus/src/tests/emptyreply/CMakeLists.txt new file mode 100644 index 00000000000..17f26719396 --- /dev/null +++ b/messagebus/src/tests/emptyreply/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_emptyreply_test_app + SOURCES + emptyreply.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_emptyreply_test_app COMMAND messagebus_emptyreply_test_app) diff --git a/messagebus/src/tests/emptyreply/DESC b/messagebus/src/tests/emptyreply/DESC new file mode 100644 index 00000000000..4db41c3c671 --- /dev/null +++ b/messagebus/src/tests/emptyreply/DESC @@ -0,0 +1 @@ +Simple test of the EmptyReply class. diff --git a/messagebus/src/tests/emptyreply/FILES b/messagebus/src/tests/emptyreply/FILES new file mode 100644 index 00000000000..5fbc80bb05c --- /dev/null +++ b/messagebus/src/tests/emptyreply/FILES @@ -0,0 +1 @@ +emptyreply.cpp diff --git a/messagebus/src/tests/emptyreply/emptyreply.cpp b/messagebus/src/tests/emptyreply/emptyreply.cpp new file mode 100644 index 00000000000..711a21edd17 --- /dev/null +++ b/messagebus/src/tests/emptyreply/emptyreply.cpp @@ -0,0 +1,21 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("emptyreply_test"); +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/emptyreply.h> + +using namespace mbus; + +TEST_SETUP(Test); + +int +Test::Main() +{ + TEST_INIT("emptyreply_test"); + Reply::UP empty(new EmptyReply()); + EXPECT_TRUE(empty->isReply()); + EXPECT_TRUE(empty->getProtocol() == ""); + EXPECT_TRUE(empty->getType() == 0); + TEST_DONE(); +} diff --git a/messagebus/src/tests/error/.gitignore b/messagebus/src/tests/error/.gitignore new file mode 100644 index 00000000000..3023dc1cb7a --- /dev/null +++ b/messagebus/src/tests/error/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +error_test +messagebus_error_test_app diff --git a/messagebus/src/tests/error/CMakeLists.txt b/messagebus/src/tests/error/CMakeLists.txt new file mode 100644 index 00000000000..7a5ea78fab3 --- /dev/null +++ b/messagebus/src/tests/error/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_error_test_app + SOURCES + error.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_error_test_app COMMAND messagebus_error_test_app) diff --git a/messagebus/src/tests/error/DESC b/messagebus/src/tests/error/DESC new file mode 100644 index 00000000000..87bd0cc23fa --- /dev/null +++ b/messagebus/src/tests/error/DESC @@ -0,0 +1 @@ +error test. Take a look at error.cpp for details. diff --git a/messagebus/src/tests/error/FILES b/messagebus/src/tests/error/FILES new file mode 100644 index 00000000000..779aee64a2c --- /dev/null +++ b/messagebus/src/tests/error/FILES @@ -0,0 +1 @@ +error.cpp diff --git a/messagebus/src/tests/error/error.cpp b/messagebus/src/tests/error/error.cpp new file mode 100644 index 00000000000..d5779aadeeb --- /dev/null +++ b/messagebus/src/tests/error/error.cpp @@ -0,0 +1,83 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("error_test"); + +#include <vespa/messagebus/destinationsession.h> +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/intermediatesession.h> +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/routing/routingspec.h> +#include <vespa/messagebus/sourcesession.h> +#include <vespa/messagebus/sourcesessionparams.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +TEST_SETUP(Test); + +RoutingSpec getRouting() { + return RoutingSpec() + .addTable(RoutingTableSpec("Simple") + .addHop(HopSpec("pxy", "test/pxy/session")) + .addHop(HopSpec("dst", "test/dst/session")) + .addRoute(RouteSpec("test").addHop("pxy").addHop("dst"))); +} + +int +Test::Main() +{ + TEST_INIT("error_test"); + + Slobrok slobrok; + TestServer srcNet(Identity("test/src"), getRouting(), slobrok); + TestServer pxyNet(Identity("test/pxy"), getRouting(), slobrok); + TestServer dstNet(Identity("test/dst"), getRouting(), slobrok); + + Receptor src; + Receptor pxy; + Receptor dst; + + SourceSession::UP ss = srcNet.mb.createSourceSession(src, SourceSessionParams()); + IntermediateSession::UP is = pxyNet.mb.createIntermediateSession("session", true, pxy, pxy); + DestinationSession::UP ds = dstNet.mb.createDestinationSession("session", true, dst); + + ASSERT_TRUE(srcNet.waitSlobrok("test/pxy/session")); + ASSERT_TRUE(srcNet.waitSlobrok("test/dst/session")); + ASSERT_TRUE(pxyNet.waitSlobrok("test/dst/session")); + + for (int i = 0; i < 5; i++) { + ASSERT_TRUE(ss->send(SimpleMessage::UP(new SimpleMessage("test message")), "test").isAccepted()); + Message::UP msg = pxy.getMessage(); + ASSERT_TRUE(msg.get() != 0); + is->forward(std::move(msg)); + + msg = dst.getMessage(); + ASSERT_TRUE(msg.get() != 0); + Reply::UP reply(new EmptyReply()); + msg->swapState(*reply); + reply->addError(Error(ErrorCode::APP_FATAL_ERROR, "fatality")); + ds->reply(std::move(reply)); + + reply = pxy.getReply(); + ASSERT_TRUE(reply.get() != 0); + EXPECT_EQUAL(reply->getNumErrors(), 1u); + EXPECT_EQUAL(reply->getError(0).getService(), "test/dst/session"); + reply->addError(Error(ErrorCode::APP_FATAL_ERROR, "fatality")); + is->forward(std::move(reply)); + + reply = src.getReply(); + ASSERT_TRUE(reply.get() != 0); + EXPECT_EQUAL(reply->getNumErrors(), 2u); + EXPECT_EQUAL(reply->getError(0).getService(), "test/dst/session"); + EXPECT_EQUAL(reply->getError(1).getService(), "test/pxy/session"); + } + TEST_DONE(); +} diff --git a/messagebus/src/tests/identity/.gitignore b/messagebus/src/tests/identity/.gitignore new file mode 100644 index 00000000000..9dd069cdb7a --- /dev/null +++ b/messagebus/src/tests/identity/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +identity_test +messagebus_identity_test_app diff --git a/messagebus/src/tests/identity/CMakeLists.txt b/messagebus/src/tests/identity/CMakeLists.txt new file mode 100644 index 00000000000..66aea485746 --- /dev/null +++ b/messagebus/src/tests/identity/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_identity_test_app + SOURCES + identity.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_identity_test_app COMMAND messagebus_identity_test_app) diff --git a/messagebus/src/tests/identity/DESC b/messagebus/src/tests/identity/DESC new file mode 100644 index 00000000000..a1fdfb95f8b --- /dev/null +++ b/messagebus/src/tests/identity/DESC @@ -0,0 +1 @@ +Test that the network identity may be obtained from config. diff --git a/messagebus/src/tests/identity/FILES b/messagebus/src/tests/identity/FILES new file mode 100644 index 00000000000..484caddcac7 --- /dev/null +++ b/messagebus/src/tests/identity/FILES @@ -0,0 +1,2 @@ +identity.cpp +test.cfg diff --git a/messagebus/src/tests/identity/identity.cpp b/messagebus/src/tests/identity/identity.cpp new file mode 100644 index 00000000000..95c499e3ff2 --- /dev/null +++ b/messagebus/src/tests/identity/identity.cpp @@ -0,0 +1,43 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("identity_test"); +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/network/identity.h> + +using namespace mbus; + +TEST_SETUP(Test); + +int +Test::Main() +{ + TEST_INIT("identity_test"); + Identity ident("foo/bar/baz"); + EXPECT_TRUE(ident.getServicePrefix() == "foo/bar/baz"); + { + std::vector<string> tmp = Identity::split("foo/bar/baz"); + ASSERT_TRUE(tmp.size() == 3); + EXPECT_TRUE(tmp[0] == "foo"); + EXPECT_TRUE(tmp[1] == "bar"); + EXPECT_TRUE(tmp[2] == "baz"); + } + { + std::vector<string> tmp = Identity::split("//"); + ASSERT_TRUE(tmp.size() == 3); + EXPECT_TRUE(tmp[0] == ""); + EXPECT_TRUE(tmp[1] == ""); + EXPECT_TRUE(tmp[2] == ""); + } + { + std::vector<string> tmp = Identity::split("foo"); + ASSERT_TRUE(tmp.size() == 1); + EXPECT_TRUE(tmp[0] == "foo"); + } + { + std::vector<string> tmp = Identity::split(""); + ASSERT_TRUE(tmp.size() == 1); + EXPECT_TRUE(tmp[0] == ""); + } + TEST_DONE(); +} diff --git a/messagebus/src/tests/loadbalance/.gitignore b/messagebus/src/tests/loadbalance/.gitignore new file mode 100644 index 00000000000..d1cbb5977f1 --- /dev/null +++ b/messagebus/src/tests/loadbalance/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +loadbalance_test +messagebus_loadbalance_test_app diff --git a/messagebus/src/tests/loadbalance/CMakeLists.txt b/messagebus/src/tests/loadbalance/CMakeLists.txt new file mode 100644 index 00000000000..68d0483ce5d --- /dev/null +++ b/messagebus/src/tests/loadbalance/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_loadbalance_test_app + SOURCES + loadbalance.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_loadbalance_test_app COMMAND messagebus_loadbalance_test_app) diff --git a/messagebus/src/tests/loadbalance/DESC b/messagebus/src/tests/loadbalance/DESC new file mode 100644 index 00000000000..67009371472 --- /dev/null +++ b/messagebus/src/tests/loadbalance/DESC @@ -0,0 +1,2 @@ +Test that service patterns with '*' performs load balancing between +the services the pattern resolves to. diff --git a/messagebus/src/tests/loadbalance/FILES b/messagebus/src/tests/loadbalance/FILES new file mode 100644 index 00000000000..6b28cce1716 --- /dev/null +++ b/messagebus/src/tests/loadbalance/FILES @@ -0,0 +1 @@ +loadbalance.cpp diff --git a/messagebus/src/tests/loadbalance/loadbalance.cpp b/messagebus/src/tests/loadbalance/loadbalance.cpp new file mode 100644 index 00000000000..f49ca13708c --- /dev/null +++ b/messagebus/src/tests/loadbalance/loadbalance.cpp @@ -0,0 +1,90 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("loadbalance_test"); + +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/destinationsession.h> +#include <vespa/messagebus/intermediatesession.h> +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/routablequeue.h> +#include <vespa/messagebus/sourcesession.h> +#include <vespa/messagebus/sourcesessionparams.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/routing/routingspec.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> + +using namespace mbus; + +struct Handler : public IMessageHandler +{ + DestinationSession::UP session; + uint32_t cnt; + + Handler(MessageBus &mb) : session(), cnt(0) { + session = mb.createDestinationSession("session", true, *this); + } + ~Handler() { + session.reset(); + } + virtual void handleMessage(Message::UP msg) { + ++cnt; + session->acknowledge(std::move(msg)); + } +}; + +RoutingSpec getRouting() { + return RoutingSpec() + .addTable(RoutingTableSpec("Simple") + .addHop(HopSpec("dst", "test/*/session")) + .addRoute(RouteSpec("test").addHop("dst"))); +} + +TEST_SETUP(Test); + +int +Test::Main() +{ + TEST_INIT("loadbalance_test"); + + Slobrok slobrok; + TestServer src(Identity(""), getRouting(), slobrok); + TestServer dst1(Identity("test/dst1"), getRouting(), slobrok); + TestServer dst2(Identity("test/dst2"), getRouting(), slobrok); + TestServer dst3(Identity("test/dst3"), getRouting(), slobrok); + + Handler h1(dst1.mb); + Handler h2(dst2.mb); + Handler h3(dst3.mb); + + ASSERT_TRUE(src.waitSlobrok("test/dst1/session")); + ASSERT_TRUE(src.waitSlobrok("test/dst2/session")); + ASSERT_TRUE(src.waitSlobrok("test/dst3/session")); + + RoutableQueue queue; + SourceSessionParams params; + params.setTimeout(30.0); + params.setThrottlePolicy(IThrottlePolicy::SP()); + SourceSession::UP ss = src.mb.createSourceSession(queue, params); + + uint32_t msgCnt = 90; + ASSERT_TRUE(msgCnt % 3 == 0); + for (uint32_t i = 0; i < msgCnt; ++i) { + ss->send(Message::UP(new SimpleMessage("test")), "test"); + } + for (uint32_t i = 0; i < 1000; ++i) { + if (queue.size() == msgCnt) { + break; + } + FastOS_Thread::Sleep(10); + } + EXPECT_TRUE(queue.size() == msgCnt); + EXPECT_TRUE(h1.cnt == msgCnt / 3); + EXPECT_TRUE(h2.cnt == msgCnt / 3); + EXPECT_TRUE(h3.cnt == msgCnt / 3); + TEST_DONE(); +} diff --git a/messagebus/src/tests/messagebus/.gitignore b/messagebus/src/tests/messagebus/.gitignore new file mode 100644 index 00000000000..b8b2aa5313c --- /dev/null +++ b/messagebus/src/tests/messagebus/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +messagebus_test +messagebus_messagebus_test_app diff --git a/messagebus/src/tests/messagebus/CMakeLists.txt b/messagebus/src/tests/messagebus/CMakeLists.txt new file mode 100644 index 00000000000..fc44bb60069 --- /dev/null +++ b/messagebus/src/tests/messagebus/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_messagebus_test_app + SOURCES + messagebus.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_messagebus_test_app COMMAND messagebus_messagebus_test_app) diff --git a/messagebus/src/tests/messagebus/DESC b/messagebus/src/tests/messagebus/DESC new file mode 100644 index 00000000000..19eb03c7048 --- /dev/null +++ b/messagebus/src/tests/messagebus/DESC @@ -0,0 +1 @@ +Generic messagebus test ported from Java. diff --git a/messagebus/src/tests/messagebus/FILES b/messagebus/src/tests/messagebus/FILES new file mode 100644 index 00000000000..0430f52149a --- /dev/null +++ b/messagebus/src/tests/messagebus/FILES @@ -0,0 +1 @@ +messagebus.cpp diff --git a/messagebus/src/tests/messagebus/messagebus.cpp b/messagebus/src/tests/messagebus/messagebus.cpp new file mode 100644 index 00000000000..a887759ac02 --- /dev/null +++ b/messagebus/src/tests/messagebus/messagebus.cpp @@ -0,0 +1,538 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("messagebus_test"); + +#include <vespa/messagebus/destinationsession.h> +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/error.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/intermediatesession.h> +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/routablequeue.h> +#include <vespa/messagebus/routing/route.h> +#include <vespa/messagebus/routing/routingcontext.h> +#include <vespa/messagebus/routing/routingnodeiterator.h> +#include <vespa/messagebus/routing/routingspec.h> +#include <vespa/messagebus/sourcesession.h> +#include <vespa/messagebus/sourcesessionparams.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/vespalib/util/vstringfmt.h> + +using namespace mbus; + +struct Base { + RoutableQueue queue; + Base() : queue() {} + virtual ~Base() { + while (queue.size() > 0) { + Routable::UP r = queue.dequeue(0); + r->getCallStack().discard(); + } + } + RoutingSpec getRouting() { + return RoutingSpec() + .addTable(RoutingTableSpec("Simple") + .addHop(HopSpec("DocProc", "docproc/*/session")) + .addHop(HopSpec("Search", "search/[All]/[Hash]/session") + .addRecipient("search/r.0/c.0/session") + .addRecipient("search/r.0/c.1/session") + .addRecipient("search/r.1/c.0/session") + .addRecipient("search/r.1/c.1/session")) + .addRoute(RouteSpec("Index").addHop("DocProc").addHop("Search")) + .addRoute(RouteSpec("DocProc").addHop("DocProc")) + .addRoute(RouteSpec("Search").addHop("Search"))); + } + bool waitQueueSize(uint32_t size) { + for (uint32_t i = 0; i < 1000; ++i) { + if (queue.size() == size) { + return true; + } + FastOS_Thread::Sleep(10); + } + return false; + } +}; + +struct Client : public Base { + typedef std::unique_ptr<Client> UP; + TestServer server; + SourceSession::UP session; + Client(Slobrok &slobrok) + : Base(), server(Identity(""), getRouting(), slobrok), session() + { + SourceSessionParams params; + params.setThrottlePolicy(IThrottlePolicy::SP()); + session = server.mb.createSourceSession(queue, params); + + } +}; + +struct Server : public Base { + TestServer server; + Server(const string &name, Slobrok &slobrok) + : Base(), server(Identity(name), getRouting(), slobrok) + { + // empty + } +}; + +struct DocProc : public Server { + typedef std::unique_ptr<DocProc> UP; + IntermediateSession::UP session; + DocProc(const string &name, Slobrok &slobrok) + : Server(name, slobrok), session() + { + session = server.mb.createIntermediateSession("session", true, queue, queue); + } +}; + +struct Search : public Server { + typedef std::unique_ptr<Search> UP; + DestinationSession::UP session; + Search(const string &name, Slobrok &slobrok) + : Server(name, slobrok), session() + { + session = server.mb.createDestinationSession("session", true, queue); + } +}; + +//----------------------------------------------------------------------------- + +class Test : public vespalib::TestApp { +private: + Slobrok::UP slobrok; + Client::UP client; + DocProc::UP dp0; + DocProc::UP dp1; + DocProc::UP dp2; + Search::UP search00; + Search::UP search01; + Search::UP search10; + Search::UP search11; + std::vector<DocProc*> dpVec; + std::vector<Search*> searchVec; + +public: + int Main(); + void testSendToAny(); + void testSendToCol(); + void testSendToAnyThenCol(); + void testDirectHop(); + void testDirectRoute(); + void testRoutingPolicyCache(); + void debugTrace(); + +private: + void setup(); + void teardown(); + + void assertSrc(Client& src); + void assertItr(DocProc& itr); + void assertDst(Search& dst); +}; + +TEST_APPHOOK(Test); + +int +Test::Main() +{ + TEST_INIT("messagebus_test"); + + testSendToAny(); TEST_FLUSH(); + testSendToCol(); TEST_FLUSH(); + testSendToAnyThenCol(); TEST_FLUSH(); + testDirectHop(); TEST_FLUSH(); + testDirectRoute(); TEST_FLUSH(); + testRoutingPolicyCache(); TEST_FLUSH(); + debugTrace(); TEST_FLUSH(); + + TEST_DONE(); +} + +void +Test::setup() +{ + slobrok.reset(new Slobrok()); + client.reset(new Client(*slobrok)); + dp0.reset(new DocProc("docproc/0", *slobrok)); + dp1.reset(new DocProc("docproc/1", *slobrok)); + dp2.reset(new DocProc("docproc/2", *slobrok)); + search00.reset(new Search("search/r.0/c.0", *slobrok)); + search01.reset(new Search("search/r.0/c.1", *slobrok)); + search10.reset(new Search("search/r.1/c.0", *slobrok)); + search11.reset(new Search("search/r.1/c.1", *slobrok)); + dpVec.push_back(dp0.get()); + dpVec.push_back(dp1.get()); + dpVec.push_back(dp2.get()); + searchVec.push_back(search00.get()); + searchVec.push_back(search01.get()); + searchVec.push_back(search10.get()); + searchVec.push_back(search11.get()); + ASSERT_TRUE(client->server.waitSlobrok("docproc/0/session")); + ASSERT_TRUE(client->server.waitSlobrok("docproc/1/session")); + ASSERT_TRUE(client->server.waitSlobrok("docproc/2/session")); + ASSERT_TRUE(client->server.waitSlobrok("search/r.0/c.0/session")); + ASSERT_TRUE(client->server.waitSlobrok("search/r.0/c.1/session")); + ASSERT_TRUE(client->server.waitSlobrok("search/r.1/c.0/session")); + ASSERT_TRUE(client->server.waitSlobrok("search/r.1/c.1/session")); + ASSERT_TRUE(dp0->server.waitSlobrok("search/r.0/c.0/session")); + ASSERT_TRUE(dp0->server.waitSlobrok("search/r.0/c.1/session")); + ASSERT_TRUE(dp0->server.waitSlobrok("search/r.1/c.0/session")); + ASSERT_TRUE(dp0->server.waitSlobrok("search/r.1/c.1/session")); + ASSERT_TRUE(dp1->server.waitSlobrok("search/r.0/c.0/session")); + ASSERT_TRUE(dp1->server.waitSlobrok("search/r.0/c.1/session")); + ASSERT_TRUE(dp1->server.waitSlobrok("search/r.1/c.0/session")); + ASSERT_TRUE(dp1->server.waitSlobrok("search/r.1/c.1/session")); + ASSERT_TRUE(dp2->server.waitSlobrok("search/r.0/c.0/session")); + ASSERT_TRUE(dp2->server.waitSlobrok("search/r.0/c.1/session")); + ASSERT_TRUE(dp2->server.waitSlobrok("search/r.1/c.0/session")); + ASSERT_TRUE(dp2->server.waitSlobrok("search/r.1/c.1/session")); +} + +void Test::teardown() +{ + dpVec.clear(); + searchVec.clear(); + search11.reset(); + search10.reset(); + search01.reset(); + search00.reset(); + dp2.reset(); + dp1.reset(); + dp0.reset(); + client.reset(); + slobrok.reset(); +} + +void +Test::testSendToAny() +{ + setup(); + for (uint32_t i = 0; i < 300; ++i) { + Message::UP msg(new SimpleMessage("test")); + EXPECT_TRUE(client->session->send(std::move(msg), "DocProc").isAccepted()); + } + EXPECT_TRUE(dp0->waitQueueSize(100)); + EXPECT_TRUE(dp1->waitQueueSize(100)); + EXPECT_TRUE(dp2->waitQueueSize(100)); + for (uint32_t i = 0; i < dpVec.size(); ++i) { + DocProc *p = dpVec[i]; + while (p->queue.size() > 0) { + Routable::UP msg = p->queue.dequeue(0); + ASSERT_TRUE(msg.get() != 0); + Reply::UP reply(new EmptyReply()); + msg->swapState(*reply); + reply->addError(Error(ErrorCode::FATAL_ERROR, "")); + p->session->forward(std::move(reply)); + } + } + EXPECT_TRUE(client->waitQueueSize(300)); + while (client->queue.size() > 0) { + Routable::UP reply = client->queue.dequeue(0); + ASSERT_TRUE(reply.get() != 0); + ASSERT_TRUE(reply->isReply()); + EXPECT_TRUE(static_cast<Reply&>(*reply).getNumErrors() == 1); + } + teardown(); +} + +void +Test::testSendToCol() +{ + setup(); + ASSERT_TRUE(SimpleMessage("msg").getHash() % 2 == 0); + for (uint32_t i = 0; i < 150; ++i) { + Message::UP msg(new SimpleMessage("msg")); + EXPECT_TRUE(client->session->send(std::move(msg), "Search").isAccepted()); + } + EXPECT_TRUE(search00->waitQueueSize(150)); + EXPECT_TRUE(search01->waitQueueSize(0)); + EXPECT_TRUE(search10->waitQueueSize(150)); + EXPECT_TRUE(search11->waitQueueSize(0)); + ASSERT_TRUE(SimpleMessage("msh").getHash() % 2 == 1); + for (uint32_t i = 0; i < 150; ++i) { + Message::UP msg(new SimpleMessage("msh")); + ASSERT_TRUE(client->session->send(std::move(msg), "Search").isAccepted()); + } + EXPECT_TRUE(search00->waitQueueSize(150)); + EXPECT_TRUE(search01->waitQueueSize(150)); + EXPECT_TRUE(search10->waitQueueSize(150)); + EXPECT_TRUE(search11->waitQueueSize(150)); + for (uint32_t i = 0; i < searchVec.size(); ++i) { + Search *s = searchVec[i]; + while (s->queue.size() > 0) { + Routable::UP msg = s->queue.dequeue(0); + ASSERT_TRUE(msg.get() != 0); + Reply::UP reply(new EmptyReply()); + msg->swapState(*reply); + s->session->reply(std::move(reply)); + } + } + client->waitQueueSize(300); + FastOS_Thread::Sleep(100); + client->waitQueueSize(300); + while (client->queue.size() > 0) { + Routable::UP reply = client->queue.dequeue(0); + ASSERT_TRUE(reply.get() != 0); + ASSERT_TRUE(reply->isReply()); + EXPECT_TRUE(static_cast<Reply&>(*reply).getNumErrors() == 0); + } + teardown(); +} + +void +Test::testSendToAnyThenCol() +{ + setup(); + ASSERT_TRUE(SimpleMessage("msg").getHash() % 2 == 0); + for (uint32_t i = 0; i < 150; ++i) { + Message::UP msg(new SimpleMessage("msg")); + EXPECT_TRUE(client->session->send(std::move(msg), "Index").isAccepted()); + } + EXPECT_TRUE(dp0->waitQueueSize(50)); + EXPECT_TRUE(dp1->waitQueueSize(50)); + EXPECT_TRUE(dp2->waitQueueSize(50)); + for (uint32_t i = 0; i < dpVec.size(); ++i) { + DocProc *p = dpVec[i]; + while (p->queue.size() > 0) { + Routable::UP r = p->queue.dequeue(0); + ASSERT_TRUE(r.get() != 0); + p->session->forward(std::move(r)); + } + } + EXPECT_TRUE(search00->waitQueueSize(150)); + EXPECT_TRUE(search01->waitQueueSize(0)); + EXPECT_TRUE(search10->waitQueueSize(150)); + EXPECT_TRUE(search11->waitQueueSize(0)); + ASSERT_TRUE(SimpleMessage("msh").getHash() % 2 == 1); + for (uint32_t i = 0; i < 150; ++i) { + Message::UP msg(new SimpleMessage("msh")); + ASSERT_TRUE(client->session->send(std::move(msg), "Index").isAccepted()); + } + EXPECT_TRUE(dp0->waitQueueSize(50)); + EXPECT_TRUE(dp1->waitQueueSize(50)); + EXPECT_TRUE(dp2->waitQueueSize(50)); + for (uint32_t i = 0; i < dpVec.size(); ++i) { + DocProc *p = dpVec[i]; + while (p->queue.size() > 0) { + Routable::UP r = p->queue.dequeue(0); + ASSERT_TRUE(r.get() != 0); + p->session->forward(std::move(r)); + } + } + EXPECT_TRUE(search00->waitQueueSize(150)); + EXPECT_TRUE(search01->waitQueueSize(150)); + EXPECT_TRUE(search10->waitQueueSize(150)); + EXPECT_TRUE(search11->waitQueueSize(150)); + for (uint32_t i = 0; i < searchVec.size(); ++i) { + Search *s = searchVec[i]; + while (s->queue.size() > 0) { + Routable::UP msg = s->queue.dequeue(0); + ASSERT_TRUE(msg.get() != 0); + Reply::UP reply(new EmptyReply()); + msg->swapState(*reply); + s->session->reply(std::move(reply)); + } + } + EXPECT_TRUE(dp0->waitQueueSize(100)); + EXPECT_TRUE(dp1->waitQueueSize(100)); + EXPECT_TRUE(dp2->waitQueueSize(100)); + for (uint32_t i = 0; i < dpVec.size(); ++i) { + DocProc *p = dpVec[i]; + while (p->queue.size() > 0) { + Routable::UP r = p->queue.dequeue(0); + ASSERT_TRUE(r.get() != 0); + p->session->forward(std::move(r)); + } + } + client->waitQueueSize(300); + FastOS_Thread::Sleep(100); + client->waitQueueSize(300); + while (client->queue.size() > 0) { + Routable::UP reply = client->queue.dequeue(0); + ASSERT_TRUE(reply.get() != 0); + ASSERT_TRUE(reply->isReply()); + EXPECT_TRUE(static_cast<Reply&>(*reply).getNumErrors() == 0); + } + teardown(); +} + +void +Test::testDirectHop() +{ + setup(); + for (int row = 0; row < 2; row++) { + for (int col = 0; col < 2; col++) { + Search* dst = searchVec[row * 2 + col]; + + // Send using name. + ASSERT_TRUE(client->session->send( + Message::UP(new SimpleMessage("empty")), + Route().addHop(vespalib::make_vespa_string("search/r.%d/c.%d/session", row, col))) + .isAccepted()); + assertDst(*dst); + assertSrc(*client); + + // Send using address. + ASSERT_TRUE(client->session->send( + Message::UP(new SimpleMessage("empty")), + Route().addHop(Hop(dst->session->getConnectionSpec().c_str()))) + .isAccepted()); + assertDst(*dst); + assertSrc(*client); + } + } + teardown(); +} + +void +Test::testDirectRoute() +{ + setup(); + ASSERT_TRUE(client->session->send( + Message::UP(new SimpleMessage("empty")), + Route() + .addHop(Hop("docproc/0/session")) + .addHop(Hop(dp0->session->getConnectionSpec())) + .addHop(Hop("docproc/1/session")) + .addHop(Hop(dp1->session->getConnectionSpec())) + .addHop(Hop("docproc/2/session")) + .addHop(Hop(dp2->session->getConnectionSpec())) + .addHop(Hop("search/r.0/c.0/session"))) + .isAccepted()); + assertItr(*dp0); + assertItr(*dp0); + assertItr(*dp1); + assertItr(*dp1); + assertItr(*dp2); + assertItr(*dp2); + assertDst(*search00); + assertItr(*dp2); + assertItr(*dp2); + assertItr(*dp1); + assertItr(*dp1); + assertItr(*dp0); + assertItr(*dp0); + assertSrc(*client); + + teardown(); +} + +void +Test::assertDst(Search& dst) +{ + ASSERT_TRUE(dst.waitQueueSize(1)); + Routable::UP msg = dst.queue.dequeue(0); + ASSERT_TRUE(msg.get() != 0); + dst.session->acknowledge(Message::UP(static_cast<Message*>(msg.release()))); +} + +void +Test::assertItr(DocProc& itr) +{ + ASSERT_TRUE(itr.waitQueueSize(1)); + Routable::UP msg = itr.queue.dequeue(0); + ASSERT_TRUE(msg.get() != 0); + itr.session->forward(std::move(msg)); +} + +void +Test::assertSrc(Client& src) +{ + ASSERT_TRUE(src.waitQueueSize(1)); + Routable::UP msg = src.queue.dequeue(0); + ASSERT_TRUE(msg.get() != 0); +} + +void +Test::testRoutingPolicyCache() +{ + setup(); + MessageBus &bus = client->server.mb; + + IRoutingPolicy::SP all = bus.getRoutingPolicy(SimpleProtocol::NAME, "All", ""); + ASSERT_TRUE(all.get() != NULL); + + IRoutingPolicy::SP ref = bus.getRoutingPolicy(SimpleProtocol::NAME, "All", ""); + ASSERT_TRUE(ref.get() != NULL); + ASSERT_TRUE(all.get() == ref.get()); + + IRoutingPolicy::SP allArg = bus.getRoutingPolicy(SimpleProtocol::NAME, "All", "Arg"); + ASSERT_TRUE(allArg.get() != NULL); + ASSERT_TRUE(all.get() != allArg.get()); + + IRoutingPolicy::SP refArg = bus.getRoutingPolicy(SimpleProtocol::NAME, "All", "Arg"); + ASSERT_TRUE(refArg.get() != NULL); + ASSERT_TRUE(allArg.get() == refArg.get()); + + teardown(); +} + +void +Test::debugTrace() +{ + setup(); + ASSERT_TRUE(SimpleMessage("msg").getHash() % 2 == 0); + for (uint32_t i = 0; i < 3; ++i) { + Message::UP msg(new SimpleMessage("msg")); + msg->getTrace().setLevel(4 + i); + EXPECT_TRUE(client->session->send(std::move(msg), "Index").isAccepted()); + } + EXPECT_TRUE(dp0->waitQueueSize(1)); + EXPECT_TRUE(dp1->waitQueueSize(1)); + EXPECT_TRUE(dp2->waitQueueSize(1)); + for (uint32_t i = 0; i < dpVec.size(); ++i) { + DocProc *p = dpVec[i]; + while (p->queue.size() > 0) { + Routable::UP r = p->queue.dequeue(0); + ASSERT_TRUE(r.get() != 0); + p->session->forward(std::move(r)); + } + } + EXPECT_TRUE(search00->waitQueueSize(3)); + EXPECT_TRUE(search01->waitQueueSize(0)); + EXPECT_TRUE(search10->waitQueueSize(3)); + EXPECT_TRUE(search11->waitQueueSize(0)); + for (uint32_t i = 0; i < searchVec.size(); ++i) { + Search *s = searchVec[i]; + while (s->queue.size() > 0) { + Routable::UP msg = s->queue.dequeue(0); + ASSERT_TRUE(msg.get() != 0); + Reply::UP reply(new EmptyReply()); + msg->swapState(*reply); + s->session->reply(std::move(reply)); + } + } + EXPECT_TRUE(dp0->waitQueueSize(1)); + EXPECT_TRUE(dp1->waitQueueSize(1)); + EXPECT_TRUE(dp2->waitQueueSize(1)); + for (uint32_t i = 0; i < dpVec.size(); ++i) { + DocProc *p = dpVec[i]; + while (p->queue.size() > 0) { + Routable::UP r = p->queue.dequeue(0); + ASSERT_TRUE(r.get() != 0); + p->session->forward(std::move(r)); + } + } + client->waitQueueSize(3); + Routable::UP reply = client->queue.dequeue(0); + fprintf(stderr, "\nTRACE DUMP(level=%d):\n%s\n\n", + reply->getTrace().getLevel(), + reply->getTrace().toString().c_str()); + reply = client->queue.dequeue(0); + fprintf(stderr, "\nTRACE DUMP(level=%d):\n%s\n\n", + reply->getTrace().getLevel(), + reply->getTrace().toString().c_str()); + reply = client->queue.dequeue(0); + fprintf(stderr, "\nTRACE DUMP(level=%d):\n%s\n\n", + reply->getTrace().getLevel(), + reply->getTrace().toString().c_str()); + teardown(); +} diff --git a/messagebus/src/tests/messageordering/.gitignore b/messagebus/src/tests/messageordering/.gitignore new file mode 100644 index 00000000000..1e4e97670de --- /dev/null +++ b/messagebus/src/tests/messageordering/.gitignore @@ -0,0 +1,3 @@ +/.depend +/Makefile +messagebus_messageordering_test_app diff --git a/messagebus/src/tests/messageordering/CMakeLists.txt b/messagebus/src/tests/messageordering/CMakeLists.txt new file mode 100644 index 00000000000..b3af6386684 --- /dev/null +++ b/messagebus/src/tests/messageordering/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_messageordering_test_app + SOURCES + messageordering.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test( + NAME messagebus_messageordering_test_app + COMMAND messagebus_messageordering_test_app +) diff --git a/messagebus/src/tests/messageordering/DESC b/messagebus/src/tests/messageordering/DESC new file mode 100644 index 00000000000..a4e636441ac --- /dev/null +++ b/messagebus/src/tests/messageordering/DESC @@ -0,0 +1 @@ +messageordering test. Take a look at messageordering.cpp for details. diff --git a/messagebus/src/tests/messageordering/FILES b/messagebus/src/tests/messageordering/FILES new file mode 100644 index 00000000000..51c47a40211 --- /dev/null +++ b/messagebus/src/tests/messageordering/FILES @@ -0,0 +1 @@ +messageordering.cpp diff --git a/messagebus/src/tests/messageordering/messageordering.cpp b/messagebus/src/tests/messageordering/messageordering.cpp new file mode 100644 index 00000000000..97daee80d99 --- /dev/null +++ b/messagebus/src/tests/messageordering/messageordering.cpp @@ -0,0 +1,178 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("messageordering_test"); +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/sourcesession.h> +#include <vespa/messagebus/destinationsession.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/messagebus/routing/routingspec.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/sourcesessionparams.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/vespalib/util/vstringfmt.h> +#include <stdexcept> + +using namespace mbus; + +TEST_SETUP(Test); + +RoutingSpec +getRouting() +{ + return RoutingSpec() + .addTable(RoutingTableSpec("Simple") + .addHop(HopSpec("dst", "test/dst/session")) + .addRoute(RouteSpec("test").addHop("dst"))); +} + +class MultiReceptor : public IMessageHandler +{ +private: + vespalib::Monitor _mon; + DestinationSession* _destinationSession; + int _messageCounter; + + MultiReceptor(const Receptor &); + MultiReceptor &operator=(const Receptor &); +public: + MultiReceptor() + : _mon(), + _destinationSession(0), + _messageCounter(0) + {} + virtual void handleMessage(Message::UP msg) + { + SimpleMessage& simpleMsg(dynamic_cast<SimpleMessage&>(*msg)); + LOG(spam, "Attempting to acquire lock for %s", + simpleMsg.getValue().c_str()); + + vespalib::MonitorGuard lock(_mon); + + vespalib::string expected(vespalib::make_vespa_string("%d", _messageCounter)); + LOG(debug, "Got message %p with %s, expecting %s", + msg.get(), + simpleMsg.getValue().c_str(), + expected.c_str()); + + SimpleReply::UP sr(new SimpleReply("test reply")); + msg->swapState(*sr); + + if (simpleMsg.getValue() != expected) { + std::stringstream ss; + ss << "Received out-of-sequence message! Expected " + << expected + << ", but got " + << simpleMsg.getValue(); + //LOG(warning, "%s", ss.str().c_str()); + sr->addError(Error(ErrorCode::FATAL_ERROR, ss.str())); + } + sr->setValue(simpleMsg.getValue()); + + ++_messageCounter; + _destinationSession->reply(Reply::UP(sr.release())); + } + void setDestinationSession(DestinationSession& sess) { + _destinationSession = &sess; + } +}; + +class VerifyReplyReceptor : public IReplyHandler +{ + vespalib::Monitor _mon; + std::string _failure; + int _replyCount; +public: + VerifyReplyReceptor() + : _mon(), + _failure(), + _replyCount(0) + {} + void handleReply(Reply::UP reply) + { + vespalib::MonitorGuard lock(_mon); + if (reply->hasErrors()) { + std::ostringstream ss; + ss << "Reply failed with " + << reply->getError(0).getMessage() + << "\n" + << reply->getTrace().toString(); + if (_failure.empty()) { + _failure = ss.str(); + } + LOG(warning, "%s", ss.str().c_str()); + } else { + vespalib::string expected(vespalib::make_vespa_string("%d", _replyCount)); + SimpleReply& simpleReply(static_cast<SimpleReply&>(*reply)); + if (simpleReply.getValue() != expected) { + std::stringstream ss; + ss << "Received out-of-sequence reply! Expected " + << expected + << ", but got " + << simpleReply.getValue(); + LOG(warning, "%s", ss.str().c_str()); + if (_failure.empty()) { + _failure = ss.str(); + } + } + } + ++_replyCount; + lock.broadcast(); + } + void waitUntilDone(int waitForCount) const + { + vespalib::MonitorGuard lock(_mon); + while (_replyCount < waitForCount) { + lock.wait(1000); + } + } + const std::string& getFailure() const { return _failure; } +}; + +int +Test::Main() +{ + TEST_INIT("messageordering_test"); + + Slobrok slobrok; + TestServer srcNet(Identity("test/src"), getRouting(), slobrok); + TestServer dstNet(Identity("test/dst"), getRouting(), slobrok); + + VerifyReplyReceptor src; + MultiReceptor dst; + + SourceSessionParams ssp; + ssp.setThrottlePolicy(IThrottlePolicy::SP()); + ssp.setTimeout(400); + SourceSession::UP ss = srcNet.mb.createSourceSession(src, ssp); + DestinationSession::UP ds = dstNet.mb.createDestinationSession("session", true, dst); + ASSERT_EQUAL(400u, ssp.getTimeout()); + + // wait for slobrok registration + ASSERT_TRUE(srcNet.waitSlobrok("test/dst/session")); + + // same message id for all messages in order to guarantee ordering + int commonMessageId = 42; + + // send messages on client + const int messageCount = 10000; + for (int i = 0; i < messageCount; ++i) { + vespalib::string str(vespalib::make_vespa_string("%d", i)); + //FastOS_Thread::Sleep(1); + SimpleMessage::UP msg(new SimpleMessage(str, true, commonMessageId)); + msg->getTrace().setLevel(9); + //LOG(debug, "Sending message %p for %d", msg.get(), i); + ASSERT_EQUAL(uint32_t(ErrorCode::NONE), + ss->send(std::move(msg), "test").getError().getCode()); + } + src.waitUntilDone(messageCount); + + ASSERT_EQUAL(std::string(), src.getFailure()); + + TEST_DONE(); +} diff --git a/messagebus/src/tests/messenger/.gitignore b/messagebus/src/tests/messenger/.gitignore new file mode 100644 index 00000000000..a7a74282fc7 --- /dev/null +++ b/messagebus/src/tests/messenger/.gitignore @@ -0,0 +1,3 @@ +/.depend +/Makefile +messagebus_messenger_test_app diff --git a/messagebus/src/tests/messenger/CMakeLists.txt b/messagebus/src/tests/messenger/CMakeLists.txt new file mode 100644 index 00000000000..6dfab750bf4 --- /dev/null +++ b/messagebus/src/tests/messenger/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_messenger_test_app + SOURCES + messenger.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_messenger_test_app COMMAND messagebus_messenger_test_app) diff --git a/messagebus/src/tests/messenger/DESC b/messagebus/src/tests/messenger/DESC new file mode 100644 index 00000000000..f4b52840a14 --- /dev/null +++ b/messagebus/src/tests/messenger/DESC @@ -0,0 +1 @@ +messenger test. Take a look at messenger.cpp for details. diff --git a/messagebus/src/tests/messenger/FILES b/messagebus/src/tests/messenger/FILES new file mode 100644 index 00000000000..620b105632e --- /dev/null +++ b/messagebus/src/tests/messenger/FILES @@ -0,0 +1 @@ +messenger.cpp diff --git a/messagebus/src/tests/messenger/messenger.cpp b/messagebus/src/tests/messenger/messenger.cpp new file mode 100644 index 00000000000..a736814aa3d --- /dev/null +++ b/messagebus/src/tests/messenger/messenger.cpp @@ -0,0 +1,61 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("messagebus_test"); + +#include <vespa/messagebus/messenger.h> +#include <vespa/vespalib/util/barrier.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +TEST_SETUP(Test); + +class ThrowException : public Messenger::ITask { +public: + void run() { + throw std::exception(); + } + + uint8_t priority() const { + return 0; + } +}; + +class BarrierTask : public Messenger::ITask { +private: + vespalib::Barrier &_barrier; + +public: + BarrierTask(vespalib::Barrier &barrier) + : _barrier(barrier) + { + // empty + } + + void run() { + _barrier.await(); + } + + uint8_t priority() const { + return 0; + } +}; + +int +Test::Main() +{ + TEST_INIT("messenger_test"); + + Messenger msn; + msn.start(); + + vespalib::Barrier barrier(2); + msn.enqueue(Messenger::ITask::UP(new ThrowException())); + msn.enqueue(Messenger::ITask::UP(new BarrierTask(barrier))); + + barrier.await(); + ASSERT_TRUE(msn.isEmpty()); + + TEST_DONE(); +} diff --git a/messagebus/src/tests/oos/.gitignore b/messagebus/src/tests/oos/.gitignore new file mode 100644 index 00000000000..a4771a9176b --- /dev/null +++ b/messagebus/src/tests/oos/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +oos_test +messagebus_oos_test_app diff --git a/messagebus/src/tests/oos/CMakeLists.txt b/messagebus/src/tests/oos/CMakeLists.txt new file mode 100644 index 00000000000..1d037ef6f6e --- /dev/null +++ b/messagebus/src/tests/oos/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_oos_test_app + SOURCES + oos.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_oos_test_app NO_VALGRIND COMMAND messagebus_oos_test_app) diff --git a/messagebus/src/tests/oos/DESC b/messagebus/src/tests/oos/DESC new file mode 100644 index 00000000000..16cd7a2f30d --- /dev/null +++ b/messagebus/src/tests/oos/DESC @@ -0,0 +1 @@ +oos test. Take a look at oos.cpp for details. diff --git a/messagebus/src/tests/oos/FILES b/messagebus/src/tests/oos/FILES new file mode 100644 index 00000000000..08cf509e1fd --- /dev/null +++ b/messagebus/src/tests/oos/FILES @@ -0,0 +1 @@ +oos.cpp diff --git a/messagebus/src/tests/oos/oos.cpp b/messagebus/src/tests/oos/oos.cpp new file mode 100644 index 00000000000..0ff93cecd7d --- /dev/null +++ b/messagebus/src/tests/oos/oos.cpp @@ -0,0 +1,231 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("oos_test"); + +#include <vespa/messagebus/destinationsession.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/intermediatesession.h> +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/routablequeue.h> +#include <vespa/messagebus/routing/routingspec.h> +#include <vespa/messagebus/sourcesession.h> +#include <vespa/messagebus/sourcesessionparams.h> +#include <vespa/messagebus/testlib/oosserver.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +struct Handler : public IMessageHandler +{ + DestinationSession::UP session; + Handler(MessageBus &mb) : session() { + session = mb.createDestinationSession("session", true, *this); + } + ~Handler() { + session.reset(); + } + virtual void handleMessage(Message::UP msg) { + session->acknowledge(std::move(msg)); + } +}; + + +class Test : public vespalib::TestApp { +private: + SourceSession::UP _session; + RoutableQueue _handler; + + bool checkError(const string &dst, uint32_t error); + +public: + Test(); + int Main(); +}; + +TEST_APPHOOK(Test); + +Test::Test() : + _session(), + _handler() +{ + // empty +} + +bool +Test::checkError(const string &dst, uint32_t error) +{ + if (!EXPECT_TRUE(_session.get() != NULL)) { + return false; + } + Message::UP msg(new SimpleMessage("msg")); + msg->getTrace().setLevel(9); + if (!EXPECT_TRUE(_session->send(std::move(msg), Route::parse(dst)).isAccepted())) { + return false; + } + Routable::UP reply = _handler.dequeue(10000); + if (!EXPECT_TRUE(reply.get() != NULL)) { + return false; + } + if (!EXPECT_TRUE(reply->isReply())) { + return false; + } + Reply &ref = static_cast<Reply&>(*reply); + printf("%s", ref.getTrace().toString().c_str()); + if (error == ErrorCode::NONE) { + if (!EXPECT_TRUE(!ref.hasErrors())) { + return false; + } + } else { + if (!EXPECT_TRUE(ref.hasErrors())) { + return false; + } + if (!EXPECT_EQUAL(error, ref.getError(0).getCode())) { + return false; + } + } + return true; +} + +int +Test::Main() +{ + TEST_INIT("oos_test"); + + Slobrok slobrok; + TestServer src(Identity(""), RoutingSpec(), slobrok, "oos/*"); + TestServer dst1(Identity("dst1"), RoutingSpec(), slobrok); + TestServer dst2(Identity("dst2"), RoutingSpec(), slobrok); + TestServer dst3(Identity("dst3"), RoutingSpec(), slobrok); + TestServer dst4(Identity("dst4"), RoutingSpec(), slobrok); + TestServer dst5(Identity("dst5"), RoutingSpec(), slobrok); + Handler h1(dst1.mb); + Handler h2(dst2.mb); + Handler h3(dst3.mb); + Handler h4(dst4.mb); + Handler h5(dst5.mb); + EXPECT_TRUE(src.waitSlobrok("*/session", 5)); + + _session = src.mb.createSourceSession(_handler); + EXPECT_TRUE(checkError("dst1/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst2/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst3/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst4/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst5/session", ErrorCode::NONE)); + TEST_FLUSH(); + OOSServer oosServer(slobrok, "oos/1", OOSState() + .add("dst2/session") + .add("dst3/session")); + EXPECT_TRUE(src.waitSlobrok("oos/*", 1)); + EXPECT_TRUE(src.waitState(OOSState() + .add("dst2/session") + .add("dst3/session"))); + EXPECT_TRUE(checkError("dst1/session", ErrorCode::NONE)); // test 9 + EXPECT_TRUE(checkError("dst2/session", ErrorCode::SERVICE_OOS)); // return without reply?!? + EXPECT_TRUE(checkError("dst3/session", ErrorCode::SERVICE_OOS)); + EXPECT_TRUE(checkError("dst4/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst5/session", ErrorCode::NONE)); + TEST_FLUSH(); + oosServer.setState(OOSState() + .add("dst2/session")); + EXPECT_TRUE(src.waitState(OOSState() + .add("dst2/session", true) + .add("dst3/session", false))); + EXPECT_TRUE(checkError("dst1/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst2/session", ErrorCode::SERVICE_OOS)); + EXPECT_TRUE(checkError("dst3/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst4/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst5/session", ErrorCode::NONE)); + TEST_FLUSH(); + { + OOSServer oosServer2(slobrok, "oos/2", OOSState() + .add("dst4/session") + .add("dst5/session")); + EXPECT_TRUE(src.waitSlobrok("oos/*", 2)); + EXPECT_TRUE(src.waitState(OOSState() + .add("dst2/session") + .add("dst4/session") + .add("dst5/session"))); + EXPECT_TRUE(checkError("dst1/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst2/session", ErrorCode::SERVICE_OOS)); + EXPECT_TRUE(checkError("dst3/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst4/session", ErrorCode::SERVICE_OOS)); + EXPECT_TRUE(checkError("dst5/session", ErrorCode::SERVICE_OOS)); + TEST_FLUSH(); + } + EXPECT_TRUE(src.waitSlobrok("oos/*", 1)); + EXPECT_TRUE(src.waitState(OOSState() + .add("dst1/session", false) + .add("dst2/session", true) + .add("dst3/session", false) + .add("dst4/session", false) + .add("dst5/session", false))); + EXPECT_TRUE(checkError("dst1/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst2/session", ErrorCode::SERVICE_OOS)); + EXPECT_TRUE(checkError("dst3/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst4/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst5/session", ErrorCode::NONE)); + TEST_FLUSH(); + { + OOSServer oosServer3(slobrok, "oos/3", OOSState() + .add("dst2/session") + .add("dst4/session")); + OOSServer oosServer4(slobrok, "oos/4", OOSState() + .add("dst2/session") + .add("dst3/session") + .add("dst5/session")); + EXPECT_TRUE(src.waitSlobrok("oos/*", 3)); + EXPECT_TRUE(src.waitState(OOSState() + .add("dst2/session") + .add("dst3/session") + .add("dst4/session") + .add("dst5/session"))); + EXPECT_TRUE(checkError("dst1/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst2/session", ErrorCode::SERVICE_OOS)); + EXPECT_TRUE(checkError("dst3/session", ErrorCode::SERVICE_OOS)); + EXPECT_TRUE(checkError("dst4/session", ErrorCode::SERVICE_OOS)); + EXPECT_TRUE(checkError("dst5/session", ErrorCode::SERVICE_OOS)); + TEST_FLUSH(); + oosServer3.setState(OOSState() + .add("dst2/session")); + oosServer4.setState(OOSState() + .add("dst1/session")); + EXPECT_TRUE(src.waitState(OOSState() + .add("dst1/session", true) + .add("dst2/session", true) + .add("dst3/session", false) + .add("dst4/session", false) + .add("dst5/session", false))); + EXPECT_TRUE(checkError("dst1/session", ErrorCode::SERVICE_OOS)); + EXPECT_TRUE(checkError("dst2/session", ErrorCode::SERVICE_OOS)); + EXPECT_TRUE(checkError("dst3/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst4/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst5/session", ErrorCode::NONE)); + TEST_FLUSH(); + } + EXPECT_TRUE(src.waitSlobrok("oos/*", 1)); + EXPECT_TRUE(src.waitState(OOSState() + .add("dst1/session", false) + .add("dst2/session", true) + .add("dst3/session", false) + .add("dst4/session", false) + .add("dst5/session", false))); + EXPECT_TRUE(checkError("dst1/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst2/session", ErrorCode::SERVICE_OOS)); + EXPECT_TRUE(checkError("dst3/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst4/session", ErrorCode::NONE)); + EXPECT_TRUE(checkError("dst5/session", ErrorCode::NONE)); + + h2.session.reset(); + EXPECT_TRUE(src.waitSlobrok("*/session", 4)); + EXPECT_TRUE(checkError("dst2/session", ErrorCode::SERVICE_OOS)); + + _session.reset(); + TEST_DONE(); +} diff --git a/messagebus/src/tests/oospolicy/.gitignore b/messagebus/src/tests/oospolicy/.gitignore new file mode 100644 index 00000000000..3bd6e47e0bc --- /dev/null +++ b/messagebus/src/tests/oospolicy/.gitignore @@ -0,0 +1,3 @@ +.depend +Makefile +oospolicy_test diff --git a/messagebus/src/tests/protocolrepository/.gitignore b/messagebus/src/tests/protocolrepository/.gitignore new file mode 100644 index 00000000000..824d12fc43f --- /dev/null +++ b/messagebus/src/tests/protocolrepository/.gitignore @@ -0,0 +1,3 @@ +/.depend +/Makefile +messagebus_protocolrepository_test_app diff --git a/messagebus/src/tests/protocolrepository/CMakeLists.txt b/messagebus/src/tests/protocolrepository/CMakeLists.txt new file mode 100644 index 00000000000..68156e5eee9 --- /dev/null +++ b/messagebus/src/tests/protocolrepository/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_protocolrepository_test_app + SOURCES + protocolrepository.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_protocolrepository_test_app NO_VALGRIND COMMAND messagebus_protocolrepository_test_app) diff --git a/messagebus/src/tests/protocolrepository/DESC b/messagebus/src/tests/protocolrepository/DESC new file mode 100644 index 00000000000..98e3dc9ef6e --- /dev/null +++ b/messagebus/src/tests/protocolrepository/DESC @@ -0,0 +1 @@ +protocolrepository test. Take a look at protocolrepository.cpp for details. diff --git a/messagebus/src/tests/protocolrepository/FILES b/messagebus/src/tests/protocolrepository/FILES new file mode 100644 index 00000000000..2fc199b4aef --- /dev/null +++ b/messagebus/src/tests/protocolrepository/FILES @@ -0,0 +1 @@ +protocolrepository.cpp diff --git a/messagebus/src/tests/protocolrepository/protocolrepository.cpp b/messagebus/src/tests/protocolrepository/protocolrepository.cpp new file mode 100644 index 00000000000..e1b1d1402dd --- /dev/null +++ b/messagebus/src/tests/protocolrepository/protocolrepository.cpp @@ -0,0 +1,75 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("protocolrepository_test"); + +#include <vespa/messagebus/protocolrepository.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +TEST_SETUP(Test); + +class TestProtocol : public IProtocol { +private: + const string _name; + +public: + + TestProtocol(const string &name) + : _name(name) + { + // empty + } + + const string & + getName() const + { + return _name; + } + + IRoutingPolicy::UP + createPolicy(const string &name, const string ¶m) const + { + (void)name; + (void)param; + throw std::exception(); + } + + Blob + encode(const vespalib::Version &version, const Routable &routable) const + { + (void)version; + (void)routable; + throw std::exception(); + } + + Routable::UP + decode(const vespalib::Version &version, BlobRef data) const + { + (void)version; + (void)data; + throw std::exception(); + } +}; + +int +Test::Main() +{ + TEST_INIT("protocolrepository_test"); + + ProtocolRepository repo; + IProtocol::SP prev; + prev = repo.putProtocol(IProtocol::SP(new TestProtocol("foo"))); + ASSERT_TRUE(prev.get() == NULL); + + IRoutingPolicy::SP policy = repo.getRoutingPolicy("foo", "bar", "baz"); + prev = repo.putProtocol(IProtocol::SP(new TestProtocol("foo"))); + ASSERT_TRUE(prev.get() != NULL); + ASSERT_NOT_EQUAL(prev.get(), repo.getProtocol("foo").get()); + + policy = repo.getRoutingPolicy("foo", "bar", "baz"); + ASSERT_TRUE(policy.get() == NULL); + + TEST_DONE(); +} diff --git a/messagebus/src/tests/queue/.gitignore b/messagebus/src/tests/queue/.gitignore new file mode 100644 index 00000000000..7b5371ea913 --- /dev/null +++ b/messagebus/src/tests/queue/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +queue_test +messagebus_queue_test_app diff --git a/messagebus/src/tests/queue/CMakeLists.txt b/messagebus/src/tests/queue/CMakeLists.txt new file mode 100644 index 00000000000..c330421ac8a --- /dev/null +++ b/messagebus/src/tests/queue/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_queue_test_app + SOURCES + queue.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_queue_test_app COMMAND messagebus_queue_test_app) diff --git a/messagebus/src/tests/queue/DESC b/messagebus/src/tests/queue/DESC new file mode 100644 index 00000000000..1d795755700 --- /dev/null +++ b/messagebus/src/tests/queue/DESC @@ -0,0 +1 @@ +queue test. Take a look at queue.cpp for details. diff --git a/messagebus/src/tests/queue/FILES b/messagebus/src/tests/queue/FILES new file mode 100644 index 00000000000..6fb01ca3173 --- /dev/null +++ b/messagebus/src/tests/queue/FILES @@ -0,0 +1 @@ +queue.cpp diff --git a/messagebus/src/tests/queue/queue.cpp b/messagebus/src/tests/queue/queue.cpp new file mode 100644 index 00000000000..78bf09e2c48 --- /dev/null +++ b/messagebus/src/tests/queue/queue.cpp @@ -0,0 +1,90 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("queue_test"); +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/queue.h> + +using namespace mbus; + +TEST_SETUP(Test); + +int +Test::Main() +{ + TEST_INIT("queue_test"); + Queue<int> q; + EXPECT_TRUE(q.size() == 0); + q.push(1); + EXPECT_TRUE(q.size() == 1); + EXPECT_TRUE(q.front() == 1); + q.push(2); + EXPECT_TRUE(q.size() == 2); + EXPECT_TRUE(q.front() == 1); + q.push(3); + EXPECT_TRUE(q.size() == 3); + EXPECT_TRUE(q.front() == 1); + q.push(4); + EXPECT_TRUE(q.size() == 4); + EXPECT_TRUE(q.front() == 1); + q.push(5); + EXPECT_TRUE(q.size() == 5); + EXPECT_TRUE(q.front() == 1); + q.push(6); + EXPECT_TRUE(q.size() == 6); + EXPECT_TRUE(q.front() == 1); + q.push(7); + EXPECT_TRUE(q.size() == 7); + EXPECT_TRUE(q.front() == 1); + q.push(8); + EXPECT_TRUE(q.size() == 8); + EXPECT_TRUE(q.front() == 1); + q.push(9); + EXPECT_TRUE(q.size() == 9); + EXPECT_TRUE(q.front() == 1); + q.pop(); + EXPECT_TRUE(q.size() == 8); + EXPECT_TRUE(q.front() == 2); + q.pop(); + EXPECT_TRUE(q.size() == 7); + EXPECT_TRUE(q.front() == 3); + q.pop(); + EXPECT_TRUE(q.size() == 6); + EXPECT_TRUE(q.front() == 4); + q.push(1); + EXPECT_TRUE(q.size() == 7); + EXPECT_TRUE(q.front() == 4); + q.push(2); + EXPECT_TRUE(q.size() == 8); + EXPECT_TRUE(q.front() == 4); + q.push(3); + EXPECT_TRUE(q.size() == 9); + EXPECT_TRUE(q.front() == 4); + q.pop(); + EXPECT_TRUE(q.size() == 8); + EXPECT_TRUE(q.front() == 5); + q.pop(); + EXPECT_TRUE(q.size() == 7); + EXPECT_TRUE(q.front() == 6); + q.pop(); + EXPECT_TRUE(q.size() == 6); + EXPECT_TRUE(q.front() == 7); + q.pop(); + EXPECT_TRUE(q.size() == 5); + EXPECT_TRUE(q.front() == 8); + q.pop(); + EXPECT_TRUE(q.size() == 4); + EXPECT_TRUE(q.front() == 9); + q.pop(); + EXPECT_TRUE(q.size() == 3); + EXPECT_TRUE(q.front() == 1); + q.pop(); + EXPECT_TRUE(q.size() == 2); + EXPECT_TRUE(q.front() == 2); + q.pop(); + EXPECT_TRUE(q.size() == 1); + EXPECT_TRUE(q.front() == 3); + q.pop(); + EXPECT_TRUE(q.size() == 0); + TEST_DONE(); +} diff --git a/messagebus/src/tests/replygate/.gitignore b/messagebus/src/tests/replygate/.gitignore new file mode 100644 index 00000000000..65ec6c9a372 --- /dev/null +++ b/messagebus/src/tests/replygate/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +replygate_test +messagebus_replygate_test_app diff --git a/messagebus/src/tests/replygate/CMakeLists.txt b/messagebus/src/tests/replygate/CMakeLists.txt new file mode 100644 index 00000000000..8844ef862dd --- /dev/null +++ b/messagebus/src/tests/replygate/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_replygate_test_app + SOURCES + replygate.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_replygate_test_app COMMAND messagebus_replygate_test_app) diff --git a/messagebus/src/tests/replygate/DESC b/messagebus/src/tests/replygate/DESC new file mode 100644 index 00000000000..2bb86cc490b --- /dev/null +++ b/messagebus/src/tests/replygate/DESC @@ -0,0 +1 @@ +replygate test. Take a look at replygate.cpp for details. diff --git a/messagebus/src/tests/replygate/FILES b/messagebus/src/tests/replygate/FILES new file mode 100644 index 00000000000..3169994c2ca --- /dev/null +++ b/messagebus/src/tests/replygate/FILES @@ -0,0 +1 @@ +replygate.cpp diff --git a/messagebus/src/tests/replygate/replygate.cpp b/messagebus/src/tests/replygate/replygate.cpp new file mode 100644 index 00000000000..09368da3974 --- /dev/null +++ b/messagebus/src/tests/replygate/replygate.cpp @@ -0,0 +1,91 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("replygate_test"); + +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/imessagehandler.h> +#include <vespa/messagebus/replygate.h> +#include <vespa/messagebus/routablequeue.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +struct MyGate : public ReplyGate +{ + static int ctorCnt; + static int dtorCnt; + MyGate(IMessageHandler &sender) : ReplyGate(sender) { + ++ctorCnt; + } + virtual ~MyGate() { + ++dtorCnt; + } +}; +int MyGate::ctorCnt = 0; +int MyGate::dtorCnt = 0; + +struct MyReply : public EmptyReply +{ + static int ctorCnt; + static int dtorCnt; + MyReply() : EmptyReply() { + ++ctorCnt; + } + virtual ~MyReply() { + ++dtorCnt; + } +}; +int MyReply::ctorCnt = 0; +int MyReply::dtorCnt = 0; + +struct MySender : public IMessageHandler +{ + // giving a sync reply here is against the API contract, but it is + // ok for testing. + virtual void handleMessage(Message::UP msg) { + Reply::UP reply(new MyReply()); + msg->swapState(*reply); + IReplyHandler &handler = reply->getCallStack().pop(*reply); + handler.handleReply(std::move(reply)); + } +}; + +TEST_SETUP(Test); + +int +Test::Main() +{ + TEST_INIT("replygate_test"); + { + RoutableQueue q; + MySender sender; + MyGate *gate = new MyGate(sender); + { + Message::UP msg(new SimpleMessage("test")); + msg->pushHandler(q); + gate->handleMessage(std::move(msg)); + } + EXPECT_TRUE(q.size() == 1); + EXPECT_TRUE(MyReply::ctorCnt == 1); + EXPECT_TRUE(MyReply::dtorCnt == 0); + gate->close(); + { + Message::UP msg(new SimpleMessage("test")); + msg->pushHandler(q); + gate->handleMessage(std::move(msg)); + } + EXPECT_TRUE(q.size() == 1); + EXPECT_TRUE(MyReply::ctorCnt == 2); + EXPECT_TRUE(MyReply::dtorCnt == 1); + EXPECT_TRUE(MyGate::ctorCnt == 1); + EXPECT_TRUE(MyGate::dtorCnt == 0); + gate->subRef(); + EXPECT_TRUE(MyGate::ctorCnt == 1); + EXPECT_TRUE(MyGate::dtorCnt == 1); + } + EXPECT_TRUE(MyReply::ctorCnt == 2); + EXPECT_TRUE(MyReply::dtorCnt == 2); + TEST_DONE(); +} diff --git a/messagebus/src/tests/replyset/.gitignore b/messagebus/src/tests/replyset/.gitignore new file mode 100644 index 00000000000..0efd61559c9 --- /dev/null +++ b/messagebus/src/tests/replyset/.gitignore @@ -0,0 +1,3 @@ +.depend +Makefile +replyset_test diff --git a/messagebus/src/tests/resender/.gitignore b/messagebus/src/tests/resender/.gitignore new file mode 100644 index 00000000000..ec4ca62b805 --- /dev/null +++ b/messagebus/src/tests/resender/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +resender_test +messagebus_resender_test_app diff --git a/messagebus/src/tests/resender/CMakeLists.txt b/messagebus/src/tests/resender/CMakeLists.txt new file mode 100644 index 00000000000..b253e582af8 --- /dev/null +++ b/messagebus/src/tests/resender/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_resender_test_app + SOURCES + resender.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_resender_test_app COMMAND messagebus_resender_test_app) diff --git a/messagebus/src/tests/resender/DESC b/messagebus/src/tests/resender/DESC new file mode 100644 index 00000000000..0b234bf57b9 --- /dev/null +++ b/messagebus/src/tests/resender/DESC @@ -0,0 +1 @@ +resender test. Take a look at resender.cpp for details. diff --git a/messagebus/src/tests/resender/FILES b/messagebus/src/tests/resender/FILES new file mode 100644 index 00000000000..16b7c7fe76b --- /dev/null +++ b/messagebus/src/tests/resender/FILES @@ -0,0 +1 @@ +resender.cpp diff --git a/messagebus/src/tests/resender/resender.cpp b/messagebus/src/tests/resender/resender.cpp new file mode 100644 index 00000000000..a067616d1ba --- /dev/null +++ b/messagebus/src/tests/resender/resender.cpp @@ -0,0 +1,310 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("routing_test"); + +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/routing/errordirective.h> +#include <vespa/messagebus/routing/retrytransienterrorspolicy.h> +#include <vespa/messagebus/testlib/custompolicy.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +//////////////////////////////////////////////////////////////////////////////// +// +// Utilities +// +//////////////////////////////////////////////////////////////////////////////// + +class StringList : public std::vector<string> { +public: + StringList &add(const string &str); +}; + +StringList & +StringList::add(const string &str) +{ + std::vector<string>::push_back(str); return *this; +} + +static const double GET_MESSAGE_TIMEOUT = 60.0; + +//////////////////////////////////////////////////////////////////////////////// +// +// Setup +// +//////////////////////////////////////////////////////////////////////////////// + +class TestData { +public: + Slobrok _slobrok; + RetryTransientErrorsPolicy::SP _retryPolicy; + TestServer _srcServer; + SourceSession::UP _srcSession; + Receptor _srcHandler; + TestServer _dstServer; + DestinationSession::UP _dstSession; + Receptor _dstHandler; + +public: + TestData(); + bool start(); +}; + +class Test : public vespalib::TestApp { +private: + Message::UP createMessage(const string &msg); + void replyFromDestination(TestData &data, Message::UP msg, uint32_t errorCode, double retryDelay); + +public: + int Main(); + void testRetryTag(TestData &data); + void testRetryEnabledTag(TestData &data); + void testTransientError(TestData &data); + void testFatalError(TestData &data); + void testDisableRetry(TestData &data); + void testRetryDelay(TestData &data); + void testRequestRetryDelay(TestData &data); +}; + +TEST_APPHOOK(Test); + +TestData::TestData() : + _slobrok(), + _retryPolicy(new RetryTransientErrorsPolicy()), + _srcServer(MessageBusParams().setRetryPolicy(_retryPolicy).addProtocol(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams().setSlobrokConfig(_slobrok.config())), + _srcSession(), + _srcHandler(), + _dstServer(MessageBusParams().addProtocol(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams().setIdentity(Identity("dst")).setSlobrokConfig(_slobrok.config())), + _dstSession(), + _dstHandler() +{ + // empty +} + +bool +TestData::start() +{ + _srcSession = _srcServer.mb.createSourceSession(SourceSessionParams().setReplyHandler(_srcHandler)); + if (_srcSession.get() == NULL) { + return false; + } + _dstSession = _dstServer.mb.createDestinationSession(DestinationSessionParams().setName("session").setMessageHandler(_dstHandler)); + if (_dstSession.get() == NULL) { + return false; + } + if (!_srcServer.waitSlobrok("dst/session", 1u)) { + return false; + } + return true; +} + +Message::UP +Test::createMessage(const string &msg) +{ + Message::UP ret(new SimpleMessage(msg)); + ret->getTrace().setLevel(9); + return ret; +} + +int +Test::Main() +{ + TEST_INIT("resender_test"); + + TestData data; + ASSERT_TRUE(data.start()); + + testRetryTag(data); TEST_FLUSH(); + testRetryEnabledTag(data); TEST_FLUSH(); + testTransientError(data); TEST_FLUSH(); + testFatalError(data); TEST_FLUSH(); + testDisableRetry(data); TEST_FLUSH(); + testRetryDelay(data); TEST_FLUSH(); + testRequestRetryDelay(data); TEST_FLUSH(); + + TEST_DONE(); +} + +void +Test::replyFromDestination(TestData &data, Message::UP msg, uint32_t errorCode, double retryDelay) +{ + Reply::UP reply(new EmptyReply()); + reply->swapState(*msg); + if (errorCode != ErrorCode::NONE) { + reply->addError(Error(errorCode, "err")); + } + reply->setRetryDelay(retryDelay); + data._dstSession->reply(std::move(reply)); +} + +//////////////////////////////////////////////////////////////////////////////// +// +// Tests +// +//////////////////////////////////////////////////////////////////////////////// + +void +Test::testRetryTag(TestData &data) +{ + data._retryPolicy->setEnabled(true); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("dst/session")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(GET_MESSAGE_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + for (uint32_t i = 0; i < 5; ++i) { + EXPECT_EQUAL(i, msg->getRetry()); + EXPECT_EQUAL(true, msg->getRetryEnabled()); + replyFromDestination(data, std::move(msg), ErrorCode::APP_TRANSIENT_ERROR, 0); + msg = data._dstHandler.getMessage(GET_MESSAGE_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + } + data._dstSession->acknowledge(std::move(msg)); + Reply::UP reply = data._srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + EXPECT_TRUE(!reply->hasErrors()); + msg = data._dstHandler.getMessage(0); + EXPECT_TRUE(msg.get() == NULL); + printf("%s", reply->getTrace().toString().c_str()); +} + +void +Test::testRetryEnabledTag(TestData &data) +{ + data._retryPolicy->setEnabled(true); + Message::UP msg = createMessage("msg"); + msg->setRetryEnabled(false); + EXPECT_TRUE(data._srcSession->send(std::move(msg), Route::parse("dst/session")).isAccepted()); + msg = data._dstHandler.getMessage(GET_MESSAGE_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + EXPECT_EQUAL(false, msg->getRetryEnabled()); + replyFromDestination(data, std::move(msg), ErrorCode::APP_TRANSIENT_ERROR, 0); + Reply::UP reply = data._srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + EXPECT_TRUE(reply->hasErrors()); + msg = data._dstHandler.getMessage(0); + EXPECT_TRUE(msg.get() == NULL); + printf("%s", reply->getTrace().toString().c_str()); +} + +void +Test::testTransientError(TestData &data) +{ + data._retryPolicy->setEnabled(true); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("dst/session")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(GET_MESSAGE_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + replyFromDestination(data, std::move(msg), ErrorCode::APP_TRANSIENT_ERROR, 0); + msg = data._dstHandler.getMessage(GET_MESSAGE_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + replyFromDestination(data, std::move(msg), ErrorCode::APP_FATAL_ERROR, 0); + Reply::UP reply = data._srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + EXPECT_TRUE(reply->hasFatalErrors()); + msg = data._dstHandler.getMessage(0); + EXPECT_TRUE(msg.get() == NULL); + printf("%s", reply->getTrace().toString().c_str()); +} + +void +Test::testFatalError(TestData &data) +{ + data._retryPolicy->setEnabled(true); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("dst/session")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(GET_MESSAGE_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + replyFromDestination(data, std::move(msg), ErrorCode::APP_FATAL_ERROR, 0); + Reply::UP reply = data._srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + EXPECT_TRUE(reply->hasFatalErrors()); + msg = data._dstHandler.getMessage(0); + EXPECT_TRUE(msg.get() == NULL); + printf("%s", reply->getTrace().toString().c_str()); +} + +void +Test::testDisableRetry(TestData &data) +{ + data._retryPolicy->setEnabled(false); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("dst/session")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(GET_MESSAGE_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + replyFromDestination(data, std::move(msg), ErrorCode::APP_TRANSIENT_ERROR, 0); + Reply::UP reply = data._srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + EXPECT_TRUE(reply->hasErrors()); + EXPECT_TRUE(!reply->hasFatalErrors()); + msg = data._dstHandler.getMessage(0); + EXPECT_TRUE(msg.get() == NULL); + printf("%s", reply->getTrace().toString().c_str()); +} + +void +Test::testRetryDelay(TestData &data) +{ + data._retryPolicy->setEnabled(true); + data._retryPolicy->setBaseDelay(0.01); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("dst/session")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(GET_MESSAGE_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + for (uint32_t i = 0; i < 5; ++i) { + EXPECT_EQUAL(i, msg->getRetry()); + replyFromDestination(data, std::move(msg), ErrorCode::APP_TRANSIENT_ERROR, -1); + msg = data._dstHandler.getMessage(GET_MESSAGE_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + } + replyFromDestination(data, std::move(msg), ErrorCode::APP_FATAL_ERROR, 0); + Reply::UP reply = data._srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + EXPECT_TRUE(reply->hasFatalErrors()); + msg = data._dstHandler.getMessage(0); + EXPECT_TRUE(msg.get() == NULL); + + string trace = reply->getTrace().toString(); + printf("%s", trace.c_str()); + EXPECT_TRUE(trace.find("retry 1 in 0.01") != string::npos); + EXPECT_TRUE(trace.find("retry 2 in 0.02") != string::npos); + EXPECT_TRUE(trace.find("retry 3 in 0.03") != string::npos); + EXPECT_TRUE(trace.find("retry 4 in 0.04") != string::npos); + EXPECT_TRUE(trace.find("retry 5 in 0.05") != string::npos); +} + +void +Test::testRequestRetryDelay(TestData &data) +{ + data._retryPolicy->setEnabled(true); + data._retryPolicy->setBaseDelay(1); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("dst/session")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(GET_MESSAGE_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + for (uint32_t i = 0; i < 5; ++i) { + EXPECT_EQUAL(i, msg->getRetry()); + replyFromDestination(data, std::move(msg), ErrorCode::APP_TRANSIENT_ERROR, i / 50.0); + msg = data._dstHandler.getMessage(GET_MESSAGE_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + } + replyFromDestination(data, std::move(msg), ErrorCode::APP_FATAL_ERROR, 0); + Reply::UP reply = data._srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + EXPECT_TRUE(reply->hasFatalErrors()); + msg = data._dstHandler.getMessage(0); + EXPECT_TRUE(msg.get() == NULL); + + string trace = reply->getTrace().toString(); + printf("%s", trace.c_str()); + EXPECT_TRUE(trace.find("retry 1 in 0") != string::npos); + EXPECT_TRUE(trace.find("retry 2 in 0.02") != string::npos); + EXPECT_TRUE(trace.find("retry 3 in 0.04") != string::npos); + EXPECT_TRUE(trace.find("retry 4 in 0.06") != string::npos); + EXPECT_TRUE(trace.find("retry 5 in 0.08") != string::npos); +} + diff --git a/messagebus/src/tests/result/.gitignore b/messagebus/src/tests/result/.gitignore new file mode 100644 index 00000000000..2f5999ef7d0 --- /dev/null +++ b/messagebus/src/tests/result/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +result_test +messagebus_result_test_app diff --git a/messagebus/src/tests/result/CMakeLists.txt b/messagebus/src/tests/result/CMakeLists.txt new file mode 100644 index 00000000000..bd1ed96ab5f --- /dev/null +++ b/messagebus/src/tests/result/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_result_test_app + SOURCES + result.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_result_test_app COMMAND messagebus_result_test_app) diff --git a/messagebus/src/tests/result/DESC b/messagebus/src/tests/result/DESC new file mode 100644 index 00000000000..8192ff0830c --- /dev/null +++ b/messagebus/src/tests/result/DESC @@ -0,0 +1 @@ +Simple test of the Result class. diff --git a/messagebus/src/tests/result/FILES b/messagebus/src/tests/result/FILES new file mode 100644 index 00000000000..55a888fd93f --- /dev/null +++ b/messagebus/src/tests/result/FILES @@ -0,0 +1 @@ +result.cpp diff --git a/messagebus/src/tests/result/result.cpp b/messagebus/src/tests/result/result.cpp new file mode 100644 index 00000000000..1b081f5fc9e --- /dev/null +++ b/messagebus/src/tests/result/result.cpp @@ -0,0 +1,76 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("result_test"); +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/result.h> +#include <vespa/messagebus/error.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/testlib/simplemessage.h> + +using namespace mbus; + +struct MyMessage : public SimpleMessage +{ + static int ctorCnt; + static int dtorCnt; + MyMessage(const string &str) : SimpleMessage(str) { + ++ctorCnt; + } + virtual ~MyMessage() { + ++dtorCnt; + } +}; +int MyMessage::ctorCnt = 0; +int MyMessage::dtorCnt = 0; + +struct Test : public vespalib::TestApp +{ + Result sendOk(Message::UP msg); + Result sendFail(Message::UP msg); + int Main(); +}; + +Result +Test::sendOk(Message::UP msg) { + (void) msg; + return Result(); +} + +Result +Test::sendFail(Message::UP msg) { + return Result(Error(ErrorCode::FATAL_ERROR, "error"), std::move(msg)); +} + +int +Test::Main() +{ + TEST_INIT("result_test"); + { // test accepted + Message::UP msg(new MyMessage("test")); + Result res = sendOk(std::move(msg)); + EXPECT_TRUE(msg.get() == 0); + EXPECT_TRUE(res.isAccepted()); + EXPECT_TRUE(res.getError().getCode() == ErrorCode::NONE); + EXPECT_TRUE(res.getError().getMessage() == ""); + Message::UP back = res.getMessage(); + EXPECT_TRUE(back.get() == 0); + } + { // test failed + Message::UP msg(new MyMessage("test")); + Message *raw = msg.get(); + EXPECT_TRUE(raw != 0); + Result res = sendFail(std::move(msg)); + EXPECT_TRUE(msg.get() == 0); + EXPECT_TRUE(!res.isAccepted()); + EXPECT_TRUE(res.getError().getCode() == ErrorCode::FATAL_ERROR); + EXPECT_TRUE(res.getError().getMessage() == "error"); + Message::UP back = res.getMessage(); + EXPECT_TRUE(back.get() == raw); + } + EXPECT_TRUE(MyMessage::ctorCnt == 2); + EXPECT_TRUE(MyMessage::dtorCnt == 2); + TEST_DONE(); +} + +TEST_APPHOOK(Test); diff --git a/messagebus/src/tests/retrypolicy/.gitignore b/messagebus/src/tests/retrypolicy/.gitignore new file mode 100644 index 00000000000..672e4f58b21 --- /dev/null +++ b/messagebus/src/tests/retrypolicy/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +retrypolicy_test +messagebus_retrypolicy_test_app diff --git a/messagebus/src/tests/retrypolicy/CMakeLists.txt b/messagebus/src/tests/retrypolicy/CMakeLists.txt new file mode 100644 index 00000000000..2a2a2a20e6e --- /dev/null +++ b/messagebus/src/tests/retrypolicy/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_retrypolicy_test_app + SOURCES + retrypolicy.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_retrypolicy_test_app COMMAND messagebus_retrypolicy_test_app) diff --git a/messagebus/src/tests/retrypolicy/DESC b/messagebus/src/tests/retrypolicy/DESC new file mode 100644 index 00000000000..0037ca01c5c --- /dev/null +++ b/messagebus/src/tests/retrypolicy/DESC @@ -0,0 +1 @@ +retrypolicy test. Take a look at retrypolicy.cpp for details. diff --git a/messagebus/src/tests/retrypolicy/FILES b/messagebus/src/tests/retrypolicy/FILES new file mode 100644 index 00000000000..11a520524fb --- /dev/null +++ b/messagebus/src/tests/retrypolicy/FILES @@ -0,0 +1 @@ +retrypolicy.cpp diff --git a/messagebus/src/tests/retrypolicy/retrypolicy.cpp b/messagebus/src/tests/retrypolicy/retrypolicy.cpp new file mode 100644 index 00000000000..5426a1123cd --- /dev/null +++ b/messagebus/src/tests/retrypolicy/retrypolicy.cpp @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("retrypolicy_test"); + +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/routing/retrytransienterrorspolicy.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +TEST_SETUP(Test); + +int +Test::Main() +{ + TEST_INIT("retrypolicy_test"); + + RetryTransientErrorsPolicy policy; + for (uint32_t i = 0; i < 5; ++i) { + double delay = i / 3.0; + policy.setBaseDelay(delay); + for (uint32_t j = 0; j < 5; ++j) { + EXPECT_EQUAL((int)(j * delay), (int)policy.getRetryDelay(j)); + } + for (uint32_t j = ErrorCode::NONE; j < ErrorCode::ERROR_LIMIT; ++j) { + policy.setEnabled(true); + if (j < ErrorCode::FATAL_ERROR) { + EXPECT_TRUE(policy.canRetry(j)); + } else { + EXPECT_TRUE(!policy.canRetry(j)); + } + policy.setEnabled(false); + EXPECT_TRUE(!policy.canRetry(j)); + } + } + + TEST_DONE(); +} diff --git a/messagebus/src/tests/routable/.gitignore b/messagebus/src/tests/routable/.gitignore new file mode 100644 index 00000000000..beb78afca54 --- /dev/null +++ b/messagebus/src/tests/routable/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +routable_test +messagebus_routable_test_app diff --git a/messagebus/src/tests/routable/CMakeLists.txt b/messagebus/src/tests/routable/CMakeLists.txt new file mode 100644 index 00000000000..d44eca1dc71 --- /dev/null +++ b/messagebus/src/tests/routable/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_routable_test_app + SOURCES + routable.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_routable_test_app COMMAND messagebus_routable_test_app) diff --git a/messagebus/src/tests/routable/DESC b/messagebus/src/tests/routable/DESC new file mode 100644 index 00000000000..2aa64df8271 --- /dev/null +++ b/messagebus/src/tests/routable/DESC @@ -0,0 +1 @@ +routable test. Take a look at routable.cpp for details. diff --git a/messagebus/src/tests/routable/FILES b/messagebus/src/tests/routable/FILES new file mode 100644 index 00000000000..bacc8b159f8 --- /dev/null +++ b/messagebus/src/tests/routable/FILES @@ -0,0 +1 @@ +routable.cpp diff --git a/messagebus/src/tests/routable/routable.cpp b/messagebus/src/tests/routable/routable.cpp new file mode 100644 index 00000000000..adde39dc51c --- /dev/null +++ b/messagebus/src/tests/routable/routable.cpp @@ -0,0 +1,94 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("routable_test"); +#include <vespa/messagebus/error.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/message.h> +#include <vespa/messagebus/reply.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +TEST_SETUP(Test); + +int +Test::Main() +{ + TEST_INIT("routable_test"); + + { + // Test message swap state. + SimpleMessage foo("foo"); + Route fooRoute = Route::parse("foo"); + foo.setRoute(fooRoute); + foo.setRetry(1); + foo.setTimeReceivedNow(); + foo.setTimeRemaining(2); + + SimpleMessage bar("bar"); + Route barRoute = Route::parse("bar"); + bar.setRoute(barRoute); + bar.setRetry(3); + bar.setTimeReceivedNow(); + bar.setTimeRemaining(4); + + foo.swapState(bar); + EXPECT_EQUAL(barRoute.toString(), foo.getRoute().toString()); + EXPECT_EQUAL(fooRoute.toString(), bar.getRoute().toString()); + EXPECT_EQUAL(3u, foo.getRetry()); + EXPECT_EQUAL(1u, bar.getRetry()); + EXPECT_TRUE(foo.getTimeReceived() >= bar.getTimeReceived()); + EXPECT_EQUAL(4u, foo.getTimeRemaining()); + EXPECT_EQUAL(2u, bar.getTimeRemaining()); + } + { + // Test reply swap state. + SimpleReply foo("foo"); + foo.setMessage(Message::UP(new SimpleMessage("foo"))); + foo.setRetryDelay(1); + foo.addError(Error(ErrorCode::APP_FATAL_ERROR, "fatal")); + foo.addError(Error(ErrorCode::APP_TRANSIENT_ERROR, "transient")); + + SimpleReply bar("bar"); + bar.setMessage(Message::UP(new SimpleMessage("bar"))); + bar.setRetryDelay(2); + bar.addError(Error(ErrorCode::ERROR_LIMIT, "err")); + + foo.swapState(bar); + EXPECT_EQUAL("bar", static_cast<SimpleMessage&>(*foo.getMessage()).getValue()); + EXPECT_EQUAL("foo", static_cast<SimpleMessage&>(*bar.getMessage()).getValue()); + EXPECT_EQUAL(2.0, foo.getRetryDelay()); + EXPECT_EQUAL(1.0, bar.getRetryDelay()); + EXPECT_EQUAL(1u, foo.getNumErrors()); + EXPECT_EQUAL(2u, bar.getNumErrors()); + } + { + // Test message discard logic. + Receptor handler; + SimpleMessage msg("foo"); + msg.pushHandler(handler); + msg.discard(); + + Reply::UP reply = handler.getReply(0); + ASSERT_TRUE(reply.get() == NULL); + } + { + // Test reply discard logic. + Receptor handler; + SimpleMessage msg("foo"); + msg.pushHandler(handler); + + SimpleReply reply("bar"); + reply.swapState(msg); + reply.discard(); + + Reply::UP ap = handler.getReply(0); + ASSERT_TRUE(ap.get() == NULL); + } + + TEST_DONE(); +} diff --git a/messagebus/src/tests/routablequeue/.gitignore b/messagebus/src/tests/routablequeue/.gitignore new file mode 100644 index 00000000000..19c961345c9 --- /dev/null +++ b/messagebus/src/tests/routablequeue/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +routablequeue_test +messagebus_routablequeue_test_app diff --git a/messagebus/src/tests/routablequeue/CMakeLists.txt b/messagebus/src/tests/routablequeue/CMakeLists.txt new file mode 100644 index 00000000000..18e2be0a748 --- /dev/null +++ b/messagebus/src/tests/routablequeue/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_routablequeue_test_app + SOURCES + routablequeue.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_routablequeue_test_app COMMAND messagebus_routablequeue_test_app) diff --git a/messagebus/src/tests/routablequeue/DESC b/messagebus/src/tests/routablequeue/DESC new file mode 100644 index 00000000000..b1613b4b2f2 --- /dev/null +++ b/messagebus/src/tests/routablequeue/DESC @@ -0,0 +1 @@ +routablequeue test. Take a look at routablequeue.cpp for details. diff --git a/messagebus/src/tests/routablequeue/FILES b/messagebus/src/tests/routablequeue/FILES new file mode 100644 index 00000000000..44a342e00a3 --- /dev/null +++ b/messagebus/src/tests/routablequeue/FILES @@ -0,0 +1 @@ +routablequeue.cpp diff --git a/messagebus/src/tests/routablequeue/routablequeue.cpp b/messagebus/src/tests/routablequeue/routablequeue.cpp new file mode 100644 index 00000000000..09d14b03983 --- /dev/null +++ b/messagebus/src/tests/routablequeue/routablequeue.cpp @@ -0,0 +1,110 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("routablequeue_test"); + +#include <vespa/messagebus/routablequeue.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +class TestMessage : public SimpleMessage { +private: + uint32_t _id; + static uint32_t _cnt; +public: + TestMessage(uint32_t id) : SimpleMessage(""), _id(id) { ++_cnt; } + virtual ~TestMessage() { --_cnt; } + virtual uint32_t getType() const { return _id; } + static uint32_t getCnt() { return _cnt; } +}; +uint32_t TestMessage::_cnt = 0; + +class TestReply : public SimpleReply { +private: + uint32_t _id; + static uint32_t _cnt; +public: + TestReply(uint32_t id) : SimpleReply(""), _id(id) { ++_cnt; } + virtual ~TestReply() { --_cnt; } + virtual uint32_t getType() const { return _id; } + static uint32_t getCnt() { return _cnt; } +}; +uint32_t TestReply::_cnt = 0; + +TEST_SETUP(Test); + +int +Test::Main() +{ + TEST_INIT("routablequeue_test"); + { + RoutableQueue rq; + EXPECT_TRUE(rq.size() == 0); + EXPECT_TRUE(rq.dequeue(0).get() == 0); + EXPECT_TRUE(rq.dequeue(100).get() == 0); + EXPECT_TRUE(TestMessage::getCnt() == 0); + EXPECT_TRUE(TestReply::getCnt() == 0); + rq.enqueue(Routable::UP(new TestMessage(101))); + EXPECT_TRUE(rq.size() == 1); + EXPECT_TRUE(TestMessage::getCnt() == 1); + EXPECT_TRUE(TestReply::getCnt() == 0); + rq.enqueue(Routable::UP(new TestReply(201))); + EXPECT_TRUE(rq.size() == 2); + EXPECT_TRUE(TestMessage::getCnt() == 1); + EXPECT_TRUE(TestReply::getCnt() == 1); + rq.handleMessage(Message::UP(new TestMessage(102))); + EXPECT_TRUE(rq.size() == 3); + EXPECT_TRUE(TestMessage::getCnt() == 2); + EXPECT_TRUE(TestReply::getCnt() == 1); + rq.handleReply(Reply::UP(new TestReply(202))); + EXPECT_TRUE(rq.size() == 4); + EXPECT_TRUE(TestMessage::getCnt() == 2); + EXPECT_TRUE(TestReply::getCnt() == 2); + { + Routable::UP r = rq.dequeue(0); + ASSERT_TRUE(r.get() != 0); + EXPECT_TRUE(rq.size() == 3); + EXPECT_TRUE(r->getType() == 101); + } + EXPECT_TRUE(TestMessage::getCnt() == 1); + EXPECT_TRUE(TestReply::getCnt() == 2); + { + Routable::UP r = rq.dequeue(0); + ASSERT_TRUE(r.get() != 0); + EXPECT_TRUE(rq.size() == 2); + EXPECT_TRUE(r->getType() == 201); + } + EXPECT_TRUE(TestMessage::getCnt() == 1); + EXPECT_TRUE(TestReply::getCnt() == 1); + rq.handleMessage(Message::UP(new TestMessage(103))); + EXPECT_TRUE(rq.size() == 3); + EXPECT_TRUE(TestMessage::getCnt() == 2); + EXPECT_TRUE(TestReply::getCnt() == 1); + rq.handleReply(Reply::UP(new TestReply(203))); + EXPECT_TRUE(rq.size() == 4); + EXPECT_TRUE(TestMessage::getCnt() == 2); + EXPECT_TRUE(TestReply::getCnt() == 2); + { + Routable::UP r = rq.dequeue(0); + ASSERT_TRUE(r.get() != 0); + EXPECT_TRUE(rq.size() == 3); + EXPECT_TRUE(r->getType() == 102); + } + EXPECT_TRUE(TestMessage::getCnt() == 1); + EXPECT_TRUE(TestReply::getCnt() == 2); + { + Routable::UP r = rq.dequeue(0); + ASSERT_TRUE(r.get() != 0); + EXPECT_TRUE(rq.size() == 2); + EXPECT_TRUE(r->getType() == 202); + } + EXPECT_TRUE(TestMessage::getCnt() == 1); + EXPECT_TRUE(TestReply::getCnt() == 1); + } + EXPECT_TRUE(TestMessage::getCnt() == 0); + EXPECT_TRUE(TestReply::getCnt() == 0); + TEST_DONE(); +} diff --git a/messagebus/src/tests/routeparser/.gitignore b/messagebus/src/tests/routeparser/.gitignore new file mode 100644 index 00000000000..8df44f3b268 --- /dev/null +++ b/messagebus/src/tests/routeparser/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +routeparser_test +messagebus_routeparser_test_app diff --git a/messagebus/src/tests/routeparser/CMakeLists.txt b/messagebus/src/tests/routeparser/CMakeLists.txt new file mode 100644 index 00000000000..712672c6b20 --- /dev/null +++ b/messagebus/src/tests/routeparser/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_routeparser_test_app + SOURCES + routeparser.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_routeparser_test_app COMMAND messagebus_routeparser_test_app) diff --git a/messagebus/src/tests/routeparser/DESC b/messagebus/src/tests/routeparser/DESC new file mode 100644 index 00000000000..4f38ec8b2bd --- /dev/null +++ b/messagebus/src/tests/routeparser/DESC @@ -0,0 +1 @@ +routeparser test. Take a look at routeparser.cpp for details. diff --git a/messagebus/src/tests/routeparser/FILES b/messagebus/src/tests/routeparser/FILES new file mode 100644 index 00000000000..3a562440161 --- /dev/null +++ b/messagebus/src/tests/routeparser/FILES @@ -0,0 +1 @@ +routeparser.cpp diff --git a/messagebus/src/tests/routeparser/routeparser.cpp b/messagebus/src/tests/routeparser/routeparser.cpp new file mode 100644 index 00000000000..d5522a7167a --- /dev/null +++ b/messagebus/src/tests/routeparser/routeparser.cpp @@ -0,0 +1,280 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("routeparser_test"); + +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/routing/errordirective.h> +#include <vespa/messagebus/routing/policydirective.h> +#include <vespa/messagebus/routing/route.h> +#include <vespa/messagebus/routing/routedirective.h> +#include <vespa/messagebus/routing/tcpdirective.h> +#include <vespa/messagebus/routing/verbatimdirective.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +class Test : public vespalib::TestApp { +public: + int Main(); + void testHopParser(); + void testHopParserErrors(); + void testRouteParser(); + void testRouteParserErrors(); + +private: + bool testError(const Route &route, const string &msg); + bool testError(const Hop &hop, const string &msg); + bool testErrorDirective(IHopDirective::SP dir, const string &msg); + bool testPolicyDirective(IHopDirective::SP dir, const string &name, const string ¶m); + bool testRouteDirective(IHopDirective::SP dir, const string &name); + bool testTcpDirective(IHopDirective::SP dir, const string &host, uint32_t port, const string &session); + bool testVerbatimDirective(IHopDirective::SP dir, const string &image); +}; + +TEST_APPHOOK(Test); + +int +Test::Main() +{ + TEST_INIT("routeparser_test"); + + testHopParser(); TEST_FLUSH(); + testHopParserErrors(); TEST_FLUSH(); + testRouteParser(); TEST_FLUSH(); + testRouteParserErrors(); TEST_FLUSH(); + + TEST_DONE(); +} + +bool +Test::testError(const Route &route, const string &msg) +{ + if (!EXPECT_EQUAL(1u, route.getNumHops())) { + return false; + } + if (!testError(route.getHop(0), msg)) { + return false; + } + return true; +} + +bool +Test::testError(const Hop &hop, const string &msg) +{ + LOG(info, "%s", hop.toDebugString().c_str()); + if (!EXPECT_EQUAL(1u, hop.getNumDirectives())) { + return false; + } + if (!testErrorDirective(hop.getDirective(0), msg)) { + return false; + } + return true; +} + +bool +Test::testErrorDirective(IHopDirective::SP dir, const string &msg) +{ + if (!EXPECT_TRUE(dir.get() != NULL)) { + return false; + } + if (!EXPECT_EQUAL(IHopDirective::TYPE_ERROR, dir->getType())) { + return false; + } + if (!EXPECT_EQUAL(msg, static_cast<const ErrorDirective&>(*dir).getMessage())) { + return false; + } + return true; +} + +bool +Test::testPolicyDirective(IHopDirective::SP dir, const string &name, const string ¶m) +{ + if (!EXPECT_TRUE(dir.get() != NULL)) { + return false; + } + if (!EXPECT_EQUAL(IHopDirective::TYPE_POLICY, dir->getType())) { + return false; + } + if (!EXPECT_EQUAL(name, static_cast<const PolicyDirective&>(*dir).getName())) { + return false; + } + if (!EXPECT_EQUAL(param, static_cast<const PolicyDirective&>(*dir).getParam())) { + return false; + } + return true; +} + +bool +Test::testRouteDirective(IHopDirective::SP dir, const string &name) +{ + if (!EXPECT_TRUE(dir.get() != NULL)) { + return false; + } + if (!EXPECT_EQUAL(IHopDirective::TYPE_ROUTE, dir->getType())) { + return false; + } + if (!EXPECT_EQUAL(name, static_cast<const RouteDirective&>(*dir).getName())) { + return false; + } + return true; +} + +bool +Test::testTcpDirective(IHopDirective::SP dir, const string &host, uint32_t port, const string &session) +{ + if (!EXPECT_TRUE(dir.get() != NULL)) { + return false; + } + if (!EXPECT_EQUAL(IHopDirective::TYPE_TCP, dir->getType())) { + return false; + } + if (!EXPECT_EQUAL(host, static_cast<const TcpDirective&>(*dir).getHost())) { + return false; + } + if (!EXPECT_EQUAL(port, static_cast<const TcpDirective&>(*dir).getPort())) { + return false; + } + if (!EXPECT_EQUAL(session, static_cast<const TcpDirective&>(*dir).getSession())) { + return false; + } + return true; +} + +bool +Test::testVerbatimDirective(IHopDirective::SP dir, const string &image) +{ + if (!EXPECT_TRUE(dir.get() != NULL)) { + return false; + } + if (!EXPECT_EQUAL(IHopDirective::TYPE_VERBATIM, dir->getType())) { + return false; + } + if (!EXPECT_EQUAL(image, static_cast<const VerbatimDirective&>(*dir).getImage())) { + return false; + } + return true; +} + +void +Test::testHopParser() +{ + { + Hop hop = Hop::parse("foo"); + EXPECT_EQUAL(1u, hop.getNumDirectives()); + EXPECT_TRUE(testVerbatimDirective(hop.getDirective(0), "foo")); + } + { + Hop hop = Hop::parse("foo/bar"); + EXPECT_EQUAL(2u, hop.getNumDirectives()); + EXPECT_TRUE(testVerbatimDirective(hop.getDirective(0), "foo")); + EXPECT_TRUE(testVerbatimDirective(hop.getDirective(1), "bar")); + } + { + Hop hop = Hop::parse("tcp/foo:666/bar"); + EXPECT_EQUAL(1u, hop.getNumDirectives()); + EXPECT_TRUE(testTcpDirective(hop.getDirective(0), "foo", 666, "bar")); + } + { + Hop hop = Hop::parse("route:foo"); + EXPECT_EQUAL(1u, hop.getNumDirectives()); + EXPECT_TRUE(testRouteDirective(hop.getDirective(0), "foo")); + } + { + Hop hop = Hop::parse("[Extern:tcp/localhost:3619;foo/bar]"); + EXPECT_EQUAL(1u, hop.getNumDirectives()); + EXPECT_TRUE(testPolicyDirective(hop.getDirective(0), "Extern", "tcp/localhost:3619;foo/bar")); + } + { + Hop hop = Hop::parse("[AND:foo bar]"); + EXPECT_EQUAL(1u, hop.getNumDirectives()); + EXPECT_TRUE(testPolicyDirective(hop.getDirective(0), "AND", "foo bar")); + } + { + Hop hop = Hop::parse("[DocumentRouteSelector:raw:route[2]\n" + "route[0].name \"foo\"\n" + "route[0].selector \"testdoc\"\n" + "route[0].feed \"myfeed\"\n" + "route[1].name \"bar\"\n" + "route[1].selector \"other\"\n" + "route[1].feed \"myfeed\"\n" + "]"); + EXPECT_EQUAL(1u, hop.getNumDirectives()); + EXPECT_TRUE(testPolicyDirective(hop.getDirective(0), "DocumentRouteSelector", + "raw:route[2]\n" + "route[0].name \"foo\"\n" + "route[0].selector \"testdoc\"\n" + "route[0].feed \"myfeed\"\n" + "route[1].name \"bar\"\n" + "route[1].selector \"other\"\n" + "route[1].feed \"myfeed\"\n")); + } + { + Hop hop = Hop::parse("[DocumentRouteSelector:raw:route[1]\n" + "route[0].name \"docproc/cluster.foo\"\n" + "route[0].selector \"testdoc\"\n" + "route[0].feed \"myfeed\"\n" + "]"); + EXPECT_EQUAL(1u, hop.getNumDirectives()); + EXPECT_TRUE(testPolicyDirective(hop.getDirective(0), "DocumentRouteSelector", + "raw:route[1]\n" + "route[0].name \"docproc/cluster.foo\"\n" + "route[0].selector \"testdoc\"\n" + "route[0].feed \"myfeed\"\n")); + } +} + +void +Test::testHopParserErrors() +{ + EXPECT_TRUE(testError(Hop::parse(""), "Failed to parse empty string.")); + EXPECT_TRUE(testError(Hop::parse("[foo"), "Unexpected token '': syntax error")); + EXPECT_TRUE(testError(Hop::parse("foo/[bar]]"), "Unexpected token ']': syntax error")); + EXPECT_TRUE(testError(Hop::parse("foo bar"), "Failed to completely parse 'foo bar'.")); +} + +void +Test::testRouteParser() +{ + { + Route route = Route::parse("foo bar/baz"); + EXPECT_EQUAL(2u, route.getNumHops()); + { + const Hop &hop = route.getHop(0); + EXPECT_EQUAL(1u, hop.getNumDirectives()); + EXPECT_TRUE(testVerbatimDirective(hop.getDirective(0), "foo")); + } + { + const Hop &hop = route.getHop(1); + EXPECT_EQUAL(2u, hop.getNumDirectives()); + EXPECT_TRUE(testVerbatimDirective(hop.getDirective(0), "bar")); + EXPECT_TRUE(testVerbatimDirective(hop.getDirective(1), "baz")); + } + } + { + Route route = Route::parse("[Extern:tcp/localhost:3633;itr/session] default"); + EXPECT_EQUAL(2u, route.getNumHops()); + { + const Hop &hop = route.getHop(0); + EXPECT_EQUAL(1u, hop.getNumDirectives()); + EXPECT_TRUE(testPolicyDirective(hop.getDirective(0), "Extern", "tcp/localhost:3633;itr/session")); + } + { + const Hop &hop = route.getHop(1); + EXPECT_EQUAL(1u, hop.getNumDirectives()); + EXPECT_TRUE(testVerbatimDirective(hop.getDirective(0), "default")); + } + } +} + +void +Test::testRouteParserErrors() +{ + EXPECT_TRUE(testError(Route::parse(""), "Failed to parse empty string.")); + EXPECT_TRUE(testError(Route::parse("foo [bar"), "Unexpected token '': syntax error")); + EXPECT_TRUE(testError(Route::parse("foo bar/[baz]]"), "Unexpected token ']': syntax error")); +} diff --git a/messagebus/src/tests/routing/.gitignore b/messagebus/src/tests/routing/.gitignore new file mode 100644 index 00000000000..9a8bd74d953 --- /dev/null +++ b/messagebus/src/tests/routing/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +routing_test +messagebus_routing_test_app diff --git a/messagebus/src/tests/routing/CMakeLists.txt b/messagebus/src/tests/routing/CMakeLists.txt new file mode 100644 index 00000000000..c2b38e4c4b4 --- /dev/null +++ b/messagebus/src/tests/routing/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_routing_test_app + SOURCES + routing.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_routing_test_app COMMAND messagebus_routing_test_app) diff --git a/messagebus/src/tests/routing/DESC b/messagebus/src/tests/routing/DESC new file mode 100644 index 00000000000..bb46d61d6cc --- /dev/null +++ b/messagebus/src/tests/routing/DESC @@ -0,0 +1 @@ +routing test. Take a look at routing.cpp for details. diff --git a/messagebus/src/tests/routing/FILES b/messagebus/src/tests/routing/FILES new file mode 100644 index 00000000000..fec1f48186a --- /dev/null +++ b/messagebus/src/tests/routing/FILES @@ -0,0 +1 @@ +routing.cpp diff --git a/messagebus/src/tests/routing/routing.cpp b/messagebus/src/tests/routing/routing.cpp new file mode 100644 index 00000000000..b9c673263b5 --- /dev/null +++ b/messagebus/src/tests/routing/routing.cpp @@ -0,0 +1,1580 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("routing_test"); + +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/routing/errordirective.h> +#include <vespa/messagebus/routing/retrytransienterrorspolicy.h> +#include <vespa/messagebus/routing/routingcontext.h> +#include <vespa/messagebus/testlib/custompolicy.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/messagebus/vtag.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +//////////////////////////////////////////////////////////////////////////////// +// +// Utilities +// +//////////////////////////////////////////////////////////////////////////////// + +class StringList : public std::vector<string> { +public: + StringList &add(const string &str); +}; + +StringList & +StringList::add(const string &str) +{ + std::vector<string>::push_back(str); + return *this; +} + +class UIntList : public std::vector<uint32_t> { +public: + UIntList &add(uint32_t i); +}; + +UIntList & +UIntList::add(uint32_t i) +{ + std::vector<uint32_t>::push_back(i); + return *this; +} + +class RemoveReplyPolicy : public CustomPolicy { +private: + uint32_t _idxRemove; +public: + RemoveReplyPolicy(bool selectOnRetry, + const std::vector<uint32_t> consumableErrors, + const std::vector<Route> routes, + uint32_t idxRemove); + void merge(RoutingContext &ctx); +}; + +RemoveReplyPolicy::RemoveReplyPolicy(bool selectOnRetry, + const std::vector<uint32_t> consumableErrors, + const std::vector<Route> routes, + uint32_t idxRemove) : + CustomPolicy::CustomPolicy(selectOnRetry, consumableErrors, routes), + _idxRemove(idxRemove) +{ + // empty +} + +void +RemoveReplyPolicy::merge(RoutingContext &ctx) +{ + ctx.setReply(ctx.getChildIterator().skip(_idxRemove).removeReply()); +} + +class RemoveReplyPolicyFactory : public SimpleProtocol::IPolicyFactory { +private: + bool _selectOnRetry; + std::vector<uint32_t> _consumableErrors; + uint32_t _idxRemove; +public: + RemoveReplyPolicyFactory(bool selectOnRetry, + const std::vector<uint32_t> &consumableErrors, + uint32_t idxRemove); + IRoutingPolicy::UP create(const string ¶m); +}; + +RemoveReplyPolicyFactory::RemoveReplyPolicyFactory(bool selectOnRetry, + const std::vector<uint32_t> &consumableErrors, + uint32_t idxRemove) : + _selectOnRetry(selectOnRetry), + _consumableErrors(consumableErrors), + _idxRemove(idxRemove) +{ + // empty +} + +IRoutingPolicy::UP +RemoveReplyPolicyFactory::create(const string ¶m) +{ + std::vector<Route> routes; + CustomPolicyFactory::parseRoutes(param, routes); + return IRoutingPolicy::UP(new RemoveReplyPolicy(_selectOnRetry, _consumableErrors, routes, _idxRemove)); +} + +class ReuseReplyPolicy : public CustomPolicy { +private: + std::vector<uint32_t> _errorMask; +public: + ReuseReplyPolicy(bool selectOnRetry, + const std::vector<uint32_t> &errorMask, + const std::vector<Route> &routes); + void merge(RoutingContext &ctx); +}; + +ReuseReplyPolicy::ReuseReplyPolicy(bool selectOnRetry, + const std::vector<uint32_t> &errorMask, + const std::vector<Route> &routes) : + CustomPolicy::CustomPolicy(selectOnRetry, errorMask, routes), + _errorMask(errorMask) +{ + // empty +} + +void +ReuseReplyPolicy::merge(RoutingContext &ctx) +{ + Reply::UP ret(new EmptyReply()); + uint32_t idx = 0; + int idxFirstOk = -1; + for (RoutingNodeIterator it = ctx.getChildIterator(); + it.isValid(); it.next(), ++idx) + { + const Reply &ref = it.getReplyRef(); + if (!ref.hasErrors()) { + if (idxFirstOk < 0) { + idxFirstOk = idx; + } + } else { + for (uint32_t i = 0; i < ref.getNumErrors(); ++i) { + Error err = ref.getError(i); + if (find(_errorMask.begin(), _errorMask.end(), err.getCode()) == _errorMask.end()) { + ret->addError(err); + } + } + } + } + if (ret->hasErrors()) { + ctx.setReply(std::move(ret)); + } else { + ctx.setReply(ctx.getChildIterator().skip(idxFirstOk).removeReply()); + } +} + +class ReuseReplyPolicyFactory : public SimpleProtocol::IPolicyFactory { +private: + bool _selectOnRetry; + std::vector<uint32_t> _errorMask; +public: + ReuseReplyPolicyFactory(bool selectOnRetry, + const std::vector<uint32_t> &errorMask); + IRoutingPolicy::UP create(const string ¶m); +}; + +ReuseReplyPolicyFactory::ReuseReplyPolicyFactory(bool selectOnRetry, + const std::vector<uint32_t> &errorMask) : + _selectOnRetry(selectOnRetry), + _errorMask(errorMask) +{ + // empty +} + +IRoutingPolicy::UP +ReuseReplyPolicyFactory::create(const string ¶m) +{ + std::vector<Route> routes; + CustomPolicyFactory::parseRoutes(param, routes); + return IRoutingPolicy::UP(new ReuseReplyPolicy(_selectOnRetry, _errorMask, routes)); +} + +class SetReplyPolicy : public IRoutingPolicy { +private: + bool _selectOnRetry; + std::vector<uint32_t> _errors; + string _param; + uint32_t _idx; +public: + SetReplyPolicy(bool selectOnRetry, + const std::vector<uint32_t> &errors, + const string ¶m); + void select(RoutingContext &ctx); + void merge(RoutingContext &ctx); +}; + +SetReplyPolicy::SetReplyPolicy(bool selectOnRetry, + const std::vector<uint32_t> &errors, + const string ¶m) : + _selectOnRetry(selectOnRetry), + _errors(errors), + _param(param), + _idx(0) +{ + // empty +} + +void +SetReplyPolicy::select(RoutingContext &ctx) +{ + uint32_t idx = _idx++; + uint32_t err = _errors[idx < _errors.size() ? idx : _errors.size() - 1]; + if (err != ErrorCode::NONE) { + ctx.setError(err, _param); + } else { + ctx.setReply(Reply::UP(new EmptyReply())); + } + ctx.setSelectOnRetry(_selectOnRetry); +} + +void +SetReplyPolicy::merge(RoutingContext &ctx) +{ + Reply::UP reply(new EmptyReply()); + reply->addError(Error(ErrorCode::FATAL_ERROR, "Merge should not be called when select() sets a reply.")); + ctx.setReply(std::move(reply)); +} + +class SetReplyPolicyFactory : public SimpleProtocol::IPolicyFactory { +private: + bool _selectOnRetry; + std::vector<uint32_t> _errors; +public: + SetReplyPolicyFactory(bool selectOnRetry, + const std::vector<uint32_t> &errors); + IRoutingPolicy::UP create(const string ¶m); +}; + +SetReplyPolicyFactory::SetReplyPolicyFactory(bool selectOnRetry, + const std::vector<uint32_t> &errors) : + _selectOnRetry(selectOnRetry), + _errors(errors) +{ + // empty +} + +IRoutingPolicy::UP +SetReplyPolicyFactory::create(const string ¶m) +{ + return IRoutingPolicy::UP(new SetReplyPolicy(_selectOnRetry, _errors, param)); +} + +class TestException : public std::exception { + virtual const char* what() const throw() { + return "{test exception}"; + } +}; + +class SelectExceptionPolicy : public IRoutingPolicy { +public: + void select(RoutingContext &ctx) { + (void)ctx; + throw TestException(); + } + + void merge(RoutingContext &ctx) { + (void)ctx; + } +}; + +class SelectExceptionPolicyFactory : public SimpleProtocol::IPolicyFactory { +public: + IRoutingPolicy::UP create(const string ¶m) { + (void)param; + return IRoutingPolicy::UP(new SelectExceptionPolicy()); + } +}; + +class MergeExceptionPolicy : public IRoutingPolicy { +private: + const string _select; + +public: + MergeExceptionPolicy(const string ¶m) + : _select(param) + { + // empty + } + + void select(RoutingContext &ctx) { + ctx.addChild(Route::parse(_select)); + } + + void merge(RoutingContext &ctx) { + (void)ctx; + throw TestException(); + } +}; + +class MergeExceptionPolicyFactory : public SimpleProtocol::IPolicyFactory { +public: + IRoutingPolicy::UP create(const string ¶m) { + return IRoutingPolicy::UP(new MergeExceptionPolicy(param)); + } +}; + +class MyPolicyFactory : public SimpleProtocol::IPolicyFactory { +private: + string _selectRoute; + uint32_t _selectError; + bool _selectException; + bool _mergeFromChild; + uint32_t _mergeError; + bool _mergeException; + +public: + friend class MyPolicy; + + MyPolicyFactory(const string &selectRoute, + uint32_t &selectError, + bool selectException, + bool mergeFromChild, + uint32_t mergeError, + bool mergeException) : + _selectRoute(selectRoute), + _selectError(selectError), + _selectException(selectException), + _mergeFromChild(mergeFromChild), + _mergeError(mergeError), + _mergeException(mergeException) + { + // empty + } + + IRoutingPolicy::UP + create(const string ¶m); + + static MyPolicyFactory::SP + newInstance(const string &selectRoute, + uint32_t selectError, + bool selectException, + bool mergeFromChild, + uint32_t mergeError, + bool mergeException) + { + MyPolicyFactory::SP ptr; + ptr.reset(new MyPolicyFactory(selectRoute, selectError, selectException, + mergeFromChild, mergeError, mergeException)); + return ptr; + } + + static MyPolicyFactory::SP + newSelectAndMerge(const string &select) + { + return newInstance(select, ErrorCode::NONE, false, true, ErrorCode::NONE, false); + } + + static MyPolicyFactory::SP + newEmptySelection() + { + return newInstance("", ErrorCode::NONE, false, false, ErrorCode::NONE, false); + } + + static MyPolicyFactory::SP + newSelectError(uint32_t errCode) + { + return newInstance("", errCode, false, false, ErrorCode::NONE, false); + } + + static MyPolicyFactory::SP + newSelectException() + { + return newInstance("", ErrorCode::NONE, true, false, ErrorCode::NONE, false); + } + + static MyPolicyFactory::SP + newSelectAndThrow(const string &select) + { + return newInstance(select, ErrorCode::NONE, true, false, ErrorCode::NONE, false); + } + + static MyPolicyFactory::SP + newEmptyMerge(const string &select) + { + return newInstance(select, ErrorCode::NONE, false, false, ErrorCode::NONE, false); + } + + static MyPolicyFactory::SP + newMergeError(const string &select, int errCode) + { + return newInstance(select, ErrorCode::NONE, false, false, errCode, false); + } + + static MyPolicyFactory::SP + newMergeException(const string &select) + { + return newInstance(select, ErrorCode::NONE, false, false, ErrorCode::NONE, true); + } + + static MyPolicyFactory::SP + newMergeAndThrow(const string &select) + { + return newInstance(select, ErrorCode::NONE, false, true, ErrorCode::NONE, true); + } +}; + +class MyPolicy : public IRoutingPolicy { +private: + const MyPolicyFactory &_parent; + +public: + MyPolicy(const MyPolicyFactory &parent) : + _parent(parent) + { + // empty + } + + virtual void + select(RoutingContext &ctx) + { + if (!_parent._selectRoute.empty()) { + ctx.addChild(Route::parse(_parent._selectRoute)); + } + if (_parent._selectError != ErrorCode::NONE) { + Reply::UP reply(new EmptyReply()); + reply->addError(Error(_parent._selectError, "err")); + ctx.setReply(std::move(reply)); + } + if (_parent._selectException) { + throw TestException(); + } + } + + virtual void + merge(RoutingContext &ctx) + { + if (_parent._mergeError != ErrorCode::NONE) { + Reply::UP reply(new EmptyReply()); + reply->addError(Error(_parent._mergeError, "err")); + ctx.setReply(std::move(reply)); + } else if (_parent._mergeFromChild) { + ctx.setReply(ctx.getChildIterator().removeReply()); + } + if (_parent._mergeException) { + throw TestException(); + } + } +}; + +IRoutingPolicy::UP +MyPolicyFactory::create(const string ¶m) +{ + (void)param; + return IRoutingPolicy::UP(new MyPolicy(*this)); +} + +//////////////////////////////////////////////////////////////////////////////// +// +// Setup +// +//////////////////////////////////////////////////////////////////////////////// + +class TestData { +public: + Slobrok _slobrok; + RetryTransientErrorsPolicy::SP _retryPolicy; + TestServer _srcServer; + SourceSession::UP _srcSession; + Receptor _srcHandler; + TestServer _dstServer; + DestinationSession::UP _dstSession; + Receptor _dstHandler; + +public: + TestData(); + bool start(); +}; + +class Test : public vespalib::TestApp { +private: + Message::UP createMessage(const string &msg, uint32_t level = 9); + void setupRouting(TestData &data, const RoutingTableSpec &spec); + void setupPolicy(TestData &data, const string &policyName, + SimpleProtocol::IPolicyFactory::SP policy); + bool testAcknowledge(TestData &data); + bool testSend(TestData &data, const string &route, uint32_t level = 9); + bool testTrace(TestData &data, const std::vector<string> &expected); + bool testTrace(const std::vector<string> &expected, const Trace &trace); + + static const double RECEPTOR_TIMEOUT; + +public: + int Main(); + void testNoRoutingTable(TestData &data); + void testUnknownRoute(TestData &data); + void testNoRoute(TestData &data); + void testRecognizeHopName(TestData &data); + void testRecognizeRouteDirective(TestData &data); + void testRecognizeRouteName(TestData &data); + void testHopResolutionOverflow(TestData &data); + void testRouteResolutionOverflow(TestData &data); + void testInsertRoute(TestData &data); + void testErrorDirective(TestData &data); + void testSelectError(TestData &data); + void testSelectNone(TestData &data); + void testSelectOne(TestData &data); + void testResend1(TestData &data); + void testResend2(TestData &data); + void testNoResend(TestData &data); + void testSelectOnResend(TestData &data); + void testNoSelectOnResend(TestData &data); + void testCanConsumeError(TestData &data); + void testCantConsumeError(TestData &data); + void testNestedPolicies(TestData &data); + void testRemoveReply(TestData &data); + void testSetReply(TestData &data); + void testResendSetAndReuseReply(TestData &data); + void testResendSetAndRemoveReply(TestData &data); + void testHopIgnoresReply(TestData &data); + void testHopBlueprintIgnoresReply(TestData &data); + void testAcceptEmptyRoute(TestData &data); + void testAbortOnlyActiveNodes(TestData &data); + void testTimeout(TestData &data); + void testUnknownPolicy(TestData &data); + void testSelectException(TestData &data); + void testMergeException(TestData &data); + + void requireThatIgnoreFlagPersistsThroughHopLookup(TestData &data); + void requireThatIgnoreFlagPersistsThroughRouteLookup(TestData &data); + void requireThatIgnoreFlagPersistsThroughPolicySelect(TestData &data); + void requireThatIgnoreFlagIsSerializedWithMessage(TestData &data); + void requireThatIgnoreFlagDoesNotInterfere(TestData &data); + void requireThatEmptySelectionCanBeIgnored(TestData &data); + void requireThatSelectErrorCanBeIgnored(TestData &data); + void requireThatSelectExceptionCanBeIgnored(TestData &data); + void requireThatSelectAndThrowCanBeIgnored(TestData &data); + void requireThatEmptyMergeCanBeIgnored(TestData &data); + void requireThatMergeErrorCanBeIgnored(TestData &data); + void requireThatMergeExceptionCanBeIgnored(TestData &data); + void requireThatMergeAndThrowCanBeIgnored(TestData &data); + void requireThatAllocServiceCanBeIgnored(TestData &data); + void requireThatDepthLimitCanBeIgnored(TestData &data); +}; + +const double Test::RECEPTOR_TIMEOUT = 120.0; + +TEST_APPHOOK(Test); + +TestData::TestData() : + _slobrok(), + _retryPolicy(new RetryTransientErrorsPolicy()), + _srcServer(MessageBusParams() + .setRetryPolicy(_retryPolicy) + .addProtocol(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams() + .setSlobrokConfig(_slobrok.config())), + _srcSession(), + _srcHandler(), + _dstServer(MessageBusParams() + .addProtocol(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams() + .setIdentity(Identity("dst")) + .setSlobrokConfig(_slobrok.config())), + _dstSession(), + _dstHandler() +{ + _retryPolicy->setBaseDelay(0); +} + +bool +TestData::start() +{ + _srcSession = _srcServer.mb.createSourceSession(SourceSessionParams() + .setThrottlePolicy(IThrottlePolicy::SP()) + .setReplyHandler(_srcHandler)); + if (_srcSession.get() == NULL) { + return false; + } + _dstSession = _dstServer.mb.createDestinationSession(DestinationSessionParams() + .setName("session") + .setMessageHandler(_dstHandler)); + if (_dstSession.get() == NULL) { + return false; + } + if (!_srcServer.waitSlobrok("dst/session", 1u)) { + return false; + } + return true; +} + +Message::UP +Test::createMessage(const string &msg, uint32_t level) +{ + Message::UP ret(new SimpleMessage(msg)); + ret->getTrace().setLevel(level); + return ret; +} + +void +Test::setupRouting(TestData &data, const RoutingTableSpec &spec) +{ + data._srcServer.mb.setupRouting(RoutingSpec().addTable(spec)); +} + +void +Test::setupPolicy(TestData &data, const string &policyName, + SimpleProtocol::IPolicyFactory::SP policy) +{ + IProtocol::SP ptr(new SimpleProtocol()); + static_cast<SimpleProtocol&>(*ptr).addPolicyFactory(policyName, policy); + data._srcServer.mb.putProtocol(ptr); +} + +bool +Test::testAcknowledge(TestData &data) +{ + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + if (!EXPECT_TRUE(msg.get() != NULL)) { + return false; + } + data._dstSession->acknowledge(std::move(msg)); + return true; +} + +bool +Test::testSend(TestData &data, const string &route, uint32_t level) +{ + Message::UP msg = createMessage("msg", level); + msg->setRoute(Route::parse(route)); + return data._srcSession->send(std::move(msg)).isAccepted(); +} + +bool +Test::testTrace(TestData &data, const std::vector<string> &expected) +{ + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + if (!EXPECT_TRUE(reply.get() != NULL)) { + return false; + } + printf("%s", reply->getTrace().toString().c_str()); + if (!EXPECT_TRUE(!reply->hasErrors())) { + return false; + } + return testTrace(expected, reply->getTrace()); +} + +bool +Test::testTrace(const std::vector<string> &expected, const Trace &trace) +{ + string version = Vtag::currentVersion.toString(); + string actual = trace.toString(); + size_t pos = 0; + for (uint32_t i = 0; i < expected.size(); ++i) { + string line = expected[i]; + size_t versionIdx = line.find("${VERSION}"); + if (versionIdx != string::npos) { + line = line.replace(versionIdx, 10, version); + } + if (line[0] == '-') { + string str = line.substr(1); + if (!EXPECT_TRUE(actual.find(str, pos) == string::npos)) { + LOG(error, "Line %d '%s' not expected.", i, str.c_str()); + return false; + } + } else { + pos = actual.find(line, pos); + if (!EXPECT_TRUE(pos != string::npos)) { + LOG(error, "Line %d '%s' missing.", i, line.c_str()); + return false; + } + ++pos; + } + } + return true; +} + +int +Test::Main() +{ + TEST_INIT("routing_test"); + + TestData data; + ASSERT_TRUE(data.start()); + + testNoRoutingTable(data); TEST_FLUSH(); + testUnknownRoute(data); TEST_FLUSH(); + testNoRoute(data); TEST_FLUSH(); + testRecognizeHopName(data); TEST_FLUSH(); + testRecognizeRouteDirective(data); TEST_FLUSH(); + testRecognizeRouteName(data); TEST_FLUSH(); + testHopResolutionOverflow(data); TEST_FLUSH(); + testRouteResolutionOverflow(data); TEST_FLUSH(); + testInsertRoute(data); TEST_FLUSH(); + testErrorDirective(data); TEST_FLUSH(); + testSelectError(data); TEST_FLUSH(); + testSelectNone(data); TEST_FLUSH(); + testSelectOne(data); TEST_FLUSH(); + testResend1(data); TEST_FLUSH(); + testResend2(data); TEST_FLUSH(); + testNoResend(data); TEST_FLUSH(); + testSelectOnResend(data); TEST_FLUSH(); + testNoSelectOnResend(data); TEST_FLUSH(); + testCanConsumeError(data); TEST_FLUSH(); + testCantConsumeError(data); TEST_FLUSH(); + testNestedPolicies(data); TEST_FLUSH(); + testRemoveReply(data); TEST_FLUSH(); + testSetReply(data); TEST_FLUSH(); + testResendSetAndReuseReply(data); TEST_FLUSH(); + testResendSetAndRemoveReply(data); TEST_FLUSH(); + testHopIgnoresReply(data); TEST_FLUSH(); + testHopBlueprintIgnoresReply(data); TEST_FLUSH(); + testAcceptEmptyRoute(data); TEST_FLUSH(); + testAbortOnlyActiveNodes(data); TEST_FLUSH(); + testUnknownPolicy(data); TEST_FLUSH(); + testSelectException(data); TEST_FLUSH(); + testMergeException(data); TEST_FLUSH(); + + requireThatIgnoreFlagPersistsThroughHopLookup(data); TEST_FLUSH(); + requireThatIgnoreFlagPersistsThroughRouteLookup(data); TEST_FLUSH(); + requireThatIgnoreFlagPersistsThroughPolicySelect(data); TEST_FLUSH(); + requireThatIgnoreFlagIsSerializedWithMessage(data); TEST_FLUSH(); + requireThatIgnoreFlagDoesNotInterfere(data); TEST_FLUSH(); + requireThatEmptySelectionCanBeIgnored(data); TEST_FLUSH(); + requireThatSelectErrorCanBeIgnored(data); TEST_FLUSH(); + requireThatSelectExceptionCanBeIgnored(data); TEST_FLUSH(); + requireThatSelectAndThrowCanBeIgnored(data); TEST_FLUSH(); + requireThatEmptyMergeCanBeIgnored(data); TEST_FLUSH(); + requireThatMergeErrorCanBeIgnored(data); TEST_FLUSH(); + requireThatMergeExceptionCanBeIgnored(data); TEST_FLUSH(); + requireThatMergeAndThrowCanBeIgnored(data); TEST_FLUSH(); + requireThatAllocServiceCanBeIgnored(data); TEST_FLUSH(); + requireThatDepthLimitCanBeIgnored(data); TEST_FLUSH(); + + // This needs to be last because it changes timeouts: + testTimeout(data); TEST_FLUSH(); + + TEST_DONE(); +} + +//////////////////////////////////////////////////////////////////////////////// +// +// Tests +// +//////////////////////////////////////////////////////////////////////////////// + +void +Test::testNoRoutingTable(TestData &data) +{ + Result res = data._srcSession->send(createMessage("msg"), "foo"); + EXPECT_TRUE(!res.isAccepted()); + EXPECT_EQUAL((uint32_t)ErrorCode::ILLEGAL_ROUTE, res.getError().getCode()); + printf("%s\n", res.getError().getMessage().c_str()); + Message::UP msg = res.getMessage(); + EXPECT_TRUE(msg.get() != NULL); +} + +void +Test::testUnknownRoute(TestData &data) +{ + data._srcServer.mb.setupRouting(RoutingSpec().addTable(RoutingTableSpec(SimpleProtocol::NAME) + .addHop(HopSpec("foo", "bar")))); + Result res = data._srcSession->send(createMessage("msg"), "baz"); + EXPECT_TRUE(!res.isAccepted()); + EXPECT_EQUAL((uint32_t)ErrorCode::ILLEGAL_ROUTE, res.getError().getCode()); + printf("%s\n", res.getError().getMessage().c_str()); + Message::UP msg = res.getMessage(); + EXPECT_TRUE(msg.get() != NULL); +} + +void +Test::testNoRoute(TestData &data) +{ + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route()).isAccepted()); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::ILLEGAL_ROUTE, reply->getError(0).getCode()); +} + +void +Test::testRecognizeHopName(TestData &data) +{ + data._srcServer.mb.setupRouting(RoutingSpec().addTable(RoutingTableSpec(SimpleProtocol::NAME) + .addHop(HopSpec("dst", "dst/session")))); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("dst")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + data._dstSession->acknowledge(std::move(msg)); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_TRUE(!reply->hasErrors()); +} + +void +Test::testRecognizeRouteDirective(TestData &data) +{ + data._srcServer.mb.setupRouting(RoutingSpec().addTable(RoutingTableSpec(SimpleProtocol::NAME) + .addRoute(RouteSpec("dst").addHop("dst/session")) + .addHop(HopSpec("dir", "route:dst")))); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("dir")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + data._dstSession->acknowledge(std::move(msg)); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_TRUE(!reply->hasErrors()); +} + +void +Test::testRecognizeRouteName(TestData &data) +{ + data._srcServer.mb.setupRouting(RoutingSpec().addTable(RoutingTableSpec(SimpleProtocol::NAME) + .addRoute(RouteSpec("dst").addHop("dst/session")))); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("dst")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + data._dstSession->acknowledge(std::move(msg)); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_TRUE(!reply->hasErrors()); +} + +void +Test::testHopResolutionOverflow(TestData &data) +{ + data._srcServer.mb.setupRouting(RoutingSpec().addTable(RoutingTableSpec(SimpleProtocol::NAME) + .addHop(HopSpec("foo", "bar")) + .addHop(HopSpec("bar", "foo")))); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("foo")).isAccepted()); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::ILLEGAL_ROUTE, reply->getError(0).getCode()); +} + +void +Test::testRouteResolutionOverflow(TestData &data) +{ + data._srcServer.mb.setupRouting(RoutingSpec().addTable(RoutingTableSpec(SimpleProtocol::NAME) + .addRoute(RouteSpec("foo").addHop("route:foo")))); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), "foo").isAccepted()); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::ILLEGAL_ROUTE, reply->getError(0).getCode()); +} + +void +Test::testInsertRoute(TestData &data) +{ + data._srcServer.mb.setupRouting(RoutingSpec().addTable(RoutingTableSpec(SimpleProtocol::NAME) + .addRoute(RouteSpec("foo").addHop("dst/session").addHop("bar")))); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("route:foo baz")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + EXPECT_EQUAL(2u, msg->getRoute().getNumHops()); + EXPECT_EQUAL("bar", msg->getRoute().getHop(0).toString()); + EXPECT_EQUAL("baz", msg->getRoute().getHop(1).toString()); + data._dstSession->acknowledge(std::move(msg)); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_TRUE(!reply->hasErrors()); +} + +void +Test::testErrorDirective(TestData &data) +{ + Route route = Route::parse("foo/bar/baz"); + route.getHop(0).setDirective(1, IHopDirective::SP(new ErrorDirective("err"))); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), route).isAccepted()); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::ILLEGAL_ROUTE, reply->getError(0).getCode()); + EXPECT_EQUAL("err", reply->getError(0).getMessage()); +} + +void +Test::testSelectError(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("Custom", SimpleProtocol::IPolicyFactory::SP(new CustomPolicyFactory())); + data._srcServer.mb.putProtocol(protocol); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("[Custom: ]")).isAccepted()); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + LOG(info, "testSelectError trace=%s", reply->getTrace().toString().c_str()); + LOG(info, "testSelectError error=%s", reply->getError(0).toString().c_str()); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::ILLEGAL_ROUTE, reply->getError(0).getCode()); +} + +void +Test::testSelectNone(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("Custom", SimpleProtocol::IPolicyFactory::SP(new CustomPolicyFactory())); + data._srcServer.mb.putProtocol(protocol); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("[Custom]")).isAccepted()); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::NO_SERVICES_FOR_ROUTE, reply->getError(0).getCode()); +} + +void +Test::testSelectOne(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("Custom", SimpleProtocol::IPolicyFactory::SP(new CustomPolicyFactory())); + data._srcServer.mb.putProtocol(protocol); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("[Custom:dst/session]")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + data._dstSession->acknowledge(std::move(msg)); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_TRUE(!reply->hasErrors()); +} + +void +Test::testResend1(TestData &data) +{ + data._retryPolicy->setEnabled(true); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("dst/session")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + Reply::UP reply(new EmptyReply()); + reply->swapState(*msg); + reply->addError(Error(ErrorCode::APP_TRANSIENT_ERROR, "err1")); + data._dstSession->reply(std::move(reply)); + msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + reply.reset(new EmptyReply()); + reply->swapState(*msg); + reply->addError(Error(ErrorCode::APP_TRANSIENT_ERROR, "err2")); + data._dstSession->reply(std::move(reply)); + msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + data._dstSession->acknowledge(std::move(msg)); + reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_TRUE(!reply->hasErrors()); + EXPECT_TRUE(testTrace(StringList() + .add("[APP_TRANSIENT_ERROR @ localhost]: err1") + .add("-[APP_TRANSIENT_ERROR @ localhost]: err1") + .add("[APP_TRANSIENT_ERROR @ localhost]: err2") + .add("-[APP_TRANSIENT_ERROR @ localhost]: err2"), + reply->getTrace())); +} + +void +Test::testResend2(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("Custom", SimpleProtocol::IPolicyFactory::SP(new CustomPolicyFactory())); + data._srcServer.mb.putProtocol(protocol); + data._retryPolicy->setEnabled(true); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("[Custom:dst/session]")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + Reply::UP reply(new EmptyReply()); + reply->swapState(*msg); + reply->addError(Error(ErrorCode::APP_TRANSIENT_ERROR, "err1")); + data._dstSession->reply(std::move(reply)); + msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + reply.reset(new EmptyReply()); + reply->swapState(*msg); + reply->addError(Error(ErrorCode::APP_TRANSIENT_ERROR, "err2")); + data._dstSession->reply(std::move(reply)); + msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + data._dstSession->acknowledge(std::move(msg)); + reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_TRUE(!reply->hasErrors()); + EXPECT_TRUE(testTrace(StringList() + .add("Source session accepted a 3 byte message. 1 message(s) now pending.") + .add("Running routing policy 'Custom'.") + .add("Selecting { 'dst/session' }.") + .add("Component 'dst/session' selected by policy 'Custom'.") + .add("Resolving 'dst/session'.") + .add("Sending message (version ${VERSION}) from client to 'dst/session'") + .add("Message (type 1) received at 'dst' for session 'session'.") + .add("[APP_TRANSIENT_ERROR @ localhost]: err1") + .add("Sending reply (version ${VERSION}) from 'dst'.") + .add("Reply (type 0) received at client.") + .add("Routing policy 'Custom' merging replies.") + .add("Merged { 'dst/session' }.") + .add("Message scheduled for retry 1 in 0.00 seconds.") + .add("Resender resending message.") + .add("Running routing policy 'Custom'.") + .add("Selecting { 'dst/session' }.") + .add("Component 'dst/session' selected by policy 'Custom'.") + .add("Resolving 'dst/session'.") + .add("Sending message (version ${VERSION}) from client to 'dst/session'") + .add("Message (type 1) received at 'dst' for session 'session'.") + .add("[APP_TRANSIENT_ERROR @ localhost]: err2") + .add("Sending reply (version ${VERSION}) from 'dst'.") + .add("Reply (type 0) received at client.") + .add("Routing policy 'Custom' merging replies.") + .add("Merged { 'dst/session' }.") + .add("Message scheduled for retry 2 in 0.00 seconds.") + .add("Resender resending message.") + .add("Running routing policy 'Custom'.") + .add("Selecting { 'dst/session' }.") + .add("Component 'dst/session' selected by policy 'Custom'.") + .add("Resolving 'dst/session'.") + .add("Sending message (version ${VERSION}) from client to 'dst/session'") + .add("Message (type 1) received at 'dst' for session 'session'.") + .add("Sending reply (version ${VERSION}) from 'dst'.") + .add("Reply (type 0) received at client.") + .add("Routing policy 'Custom' merging replies.") + .add("Merged { 'dst/session' }.") + .add("Source session received reply. 0 message(s) now pending."), + reply->getTrace())); +} + +void +Test::testNoResend(TestData &data) +{ + data._retryPolicy->setEnabled(false); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("dst/session")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + Reply::UP reply(new EmptyReply()); + reply->swapState(*msg); + reply->addError(Error(ErrorCode::APP_TRANSIENT_ERROR, "err1")); + data._dstSession->reply(std::move(reply)); + reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::APP_TRANSIENT_ERROR, reply->getError(0).getCode()); +} + +void +Test::testSelectOnResend(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("Custom", SimpleProtocol::IPolicyFactory::SP(new CustomPolicyFactory())); + data._srcServer.mb.putProtocol(protocol); + data._retryPolicy->setEnabled(true); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("[Custom:dst/session]")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + Reply::UP reply(new EmptyReply()); + reply->swapState(*msg); + reply->addError(Error(ErrorCode::APP_TRANSIENT_ERROR, "err")); + data._dstSession->reply(std::move(reply)); + msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + data._dstSession->acknowledge(std::move(msg)); + reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_TRUE(!reply->hasErrors()); + EXPECT_TRUE(testTrace(StringList() + .add("Selecting { 'dst/session' }.") + .add("[APP_TRANSIENT_ERROR @ localhost]") + .add("-[APP_TRANSIENT_ERROR @ localhost]") + .add("Merged { 'dst/session' }.") + .add("Selecting { 'dst/session' }.") + .add("Sending reply") + .add("Merged { 'dst/session' }."), + reply->getTrace())); +} + +void +Test::testNoSelectOnResend(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("Custom", SimpleProtocol::IPolicyFactory::SP(new CustomPolicyFactory(false))); + data._srcServer.mb.putProtocol(protocol); + data._retryPolicy->setEnabled(true); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("[Custom:dst/session]")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + Reply::UP reply(new EmptyReply()); + reply->swapState(*msg); + reply->addError(Error(ErrorCode::APP_TRANSIENT_ERROR, "err")); + data._dstSession->reply(std::move(reply)); + msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + data._dstSession->acknowledge(std::move(msg)); + reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_TRUE(!reply->hasErrors()); + EXPECT_TRUE(testTrace(StringList() + .add("Selecting { 'dst/session' }.") + .add("[APP_TRANSIENT_ERROR @ localhost]") + .add("-[APP_TRANSIENT_ERROR @ localhost]") + .add("Merged { 'dst/session' }.") + .add("-Selecting { 'dst/session' }.") + .add("Sending reply") + .add("Merged { 'dst/session' }."), + reply->getTrace())); +} + +void +Test::testCanConsumeError(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("Custom", SimpleProtocol::IPolicyFactory::SP(new CustomPolicyFactory(true, ErrorCode::NO_ADDRESS_FOR_SERVICE))); + data._srcServer.mb.putProtocol(protocol); + data._retryPolicy->setEnabled(false); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("[Custom:dst/session,dst/unknown]")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + data._dstSession->acknowledge(std::move(msg)); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::NO_ADDRESS_FOR_SERVICE, reply->getError(0).getCode()); + EXPECT_TRUE(testTrace(StringList() + .add("Selecting { 'dst/session', 'dst/unknown' }.") + .add("[NO_ADDRESS_FOR_SERVICE @ localhost]") + .add("Sending reply") + .add("Merged { 'dst/session', 'dst/unknown' }."), + reply->getTrace())); +} + +void +Test::testCantConsumeError(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("Custom", SimpleProtocol::IPolicyFactory::SP(new CustomPolicyFactory())); + data._srcServer.mb.putProtocol(protocol); + data._retryPolicy->setEnabled(false); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("[Custom:dst/unknown]")).isAccepted()); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::NO_ADDRESS_FOR_SERVICE, reply->getError(0).getCode()); + EXPECT_TRUE(testTrace(StringList() + .add("Selecting { 'dst/unknown' }.") + .add("[NO_ADDRESS_FOR_SERVICE @ localhost]") + .add("Merged { 'dst/unknown' }."), + reply->getTrace())); +} + +void +Test::testNestedPolicies(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("Custom", SimpleProtocol::IPolicyFactory::SP(new CustomPolicyFactory(true, ErrorCode::NO_ADDRESS_FOR_SERVICE))); + data._srcServer.mb.putProtocol(protocol); + data._retryPolicy->setEnabled(false); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("[Custom:[Custom:dst/session],[Custom:dst/unknown]]")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + data._dstSession->acknowledge(std::move(msg)); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::NO_ADDRESS_FOR_SERVICE, reply->getError(0).getCode()); +} + +void +Test::testRemoveReply(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("Custom", SimpleProtocol::IPolicyFactory::SP(new RemoveReplyPolicyFactory( + true, + UIntList().add(ErrorCode::NO_ADDRESS_FOR_SERVICE), + 0))); + data._srcServer.mb.putProtocol(protocol); + data._retryPolicy->setEnabled(false); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("[Custom:[Custom:dst/session],[Custom:dst/unknown]]")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + data._dstSession->acknowledge(std::move(msg)); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_TRUE(!reply->hasErrors()); + EXPECT_TRUE(testTrace(StringList() + .add("[NO_ADDRESS_FOR_SERVICE @ localhost]") + .add("-[NO_ADDRESS_FOR_SERVICE @ localhost]") + .add("Sending message") + .add("-Sending message"), + reply->getTrace())); +} + +void +Test::testSetReply(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("Select", SimpleProtocol::IPolicyFactory::SP(new CustomPolicyFactory(true, ErrorCode::APP_FATAL_ERROR))); + simple.addPolicyFactory("SetReply", SimpleProtocol::IPolicyFactory::SP(new SetReplyPolicyFactory(true, UIntList().add(ErrorCode::APP_FATAL_ERROR)))); + data._srcServer.mb.putProtocol(protocol); + data._retryPolicy->setEnabled(false); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("[Select:[SetReply:foo],dst/session]")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + data._dstSession->acknowledge(std::move(msg)); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::APP_FATAL_ERROR, reply->getError(0).getCode()); + EXPECT_EQUAL("foo", reply->getError(0).getMessage()); +} + +void +Test::testResendSetAndReuseReply(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("ReuseReply", SimpleProtocol::IPolicyFactory::SP(new ReuseReplyPolicyFactory( + false, + UIntList().add(ErrorCode::APP_FATAL_ERROR)))); + simple.addPolicyFactory("SetReply", SimpleProtocol::IPolicyFactory::SP(new SetReplyPolicyFactory( + false, + UIntList().add(ErrorCode::APP_FATAL_ERROR)))); + data._srcServer.mb.putProtocol(protocol); + data._retryPolicy->setEnabled(true); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("[ReuseReply:[SetReply:foo],dst/session]")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + Reply::UP reply(new EmptyReply()); + reply->swapState(*msg); + reply->addError(Error(ErrorCode::APP_TRANSIENT_ERROR, "dst")); + data._dstSession->reply(std::move(reply)); + msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + data._dstSession->acknowledge(std::move(msg)); + reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_TRUE(!reply->hasErrors()); +} + +void +Test::testResendSetAndRemoveReply(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("RemoveReply", SimpleProtocol::IPolicyFactory::SP(new RemoveReplyPolicyFactory( + false, + UIntList().add(ErrorCode::APP_TRANSIENT_ERROR), + 0))); + simple.addPolicyFactory("SetReply", SimpleProtocol::IPolicyFactory::SP(new SetReplyPolicyFactory( + false, + UIntList().add(ErrorCode::APP_TRANSIENT_ERROR).add(ErrorCode::APP_FATAL_ERROR)))); + data._srcServer.mb.putProtocol(protocol); + data._retryPolicy->setEnabled(true); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("[RemoveReply:[SetReply:foo],dst/session]")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + data._dstSession->acknowledge(std::move(msg)); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::APP_FATAL_ERROR, reply->getError(0).getCode()); + EXPECT_EQUAL("foo", reply->getError(0).getMessage()); + EXPECT_TRUE(testTrace(StringList() + .add("Resolving '[SetReply:foo]'.") + .add("Resolving 'dst/session'.") + .add("Resender resending message.") + .add("Resolving 'dst/session'.") + .add("Resolving '[SetReply:foo]'."), + reply->getTrace())); +} + +void +Test::testHopIgnoresReply(TestData &data) +{ + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("?dst/session")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + Reply::UP reply(new EmptyReply()); + reply->swapState(*msg); + reply->addError(Error(ErrorCode::APP_FATAL_ERROR, "dst")); + data._dstSession->reply(std::move(reply)); + reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_TRUE(!reply->hasErrors()); + EXPECT_TRUE(testTrace(StringList() + .add("Not waiting for a reply from 'dst/session'."), + reply->getTrace())); +} + +void +Test::testHopBlueprintIgnoresReply(TestData &data) +{ + data._srcServer.mb.setupRouting(RoutingSpec().addTable(RoutingTableSpec(SimpleProtocol::NAME) + .addHop(HopSpec("foo", "dst/session").setIgnoreResult(true)))); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("foo")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + Reply::UP reply(new EmptyReply()); + reply->swapState(*msg); + reply->addError(Error(ErrorCode::APP_FATAL_ERROR, "dst")); + data._dstSession->reply(std::move(reply)); + reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_TRUE(!reply->hasErrors()); + EXPECT_TRUE(testTrace(StringList() + .add("Not waiting for a reply from 'dst/session'."), + reply->getTrace())); +} + +void +Test::testAcceptEmptyRoute(TestData &data) +{ + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("dst/session")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + const Route &route = msg->getRoute(); + EXPECT_EQUAL(0u, route.getNumHops()); + data._dstSession->acknowledge(std::move(msg)); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); +} + +void +Test::testAbortOnlyActiveNodes(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("Custom", SimpleProtocol::IPolicyFactory::SP(new CustomPolicyFactory(false))); + simple.addPolicyFactory("SetReply", SimpleProtocol::IPolicyFactory::SP(new SetReplyPolicyFactory( + false, + UIntList() + .add(ErrorCode::APP_TRANSIENT_ERROR) + .add(ErrorCode::APP_TRANSIENT_ERROR) + .add(ErrorCode::APP_FATAL_ERROR)))); + data._srcServer.mb.putProtocol(protocol); + data._retryPolicy->setEnabled(true); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("[Custom:[SetReply:foo],?bar,dst/session]")).isAccepted()); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_EQUAL(2u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::APP_FATAL_ERROR, reply->getError(0).getCode()); + EXPECT_EQUAL((uint32_t)ErrorCode::SEND_ABORTED, reply->getError(1).getCode()); +} + +void +Test::testUnknownPolicy(TestData &data) +{ + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("[Unknown]")).isAccepted()); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::UNKNOWN_POLICY, reply->getError(0).getCode()); +} + +void +Test::testSelectException(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("SelectException", + SimpleProtocol::IPolicyFactory::SP( + new SelectExceptionPolicyFactory())); + data._srcServer.mb.putProtocol(protocol); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), + Route::parse("[SelectException]")) + .isAccepted()); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::POLICY_ERROR, + reply->getError(0).getCode()); + EXPECT_EQUAL("Policy 'SelectException' threw an exception; {test exception}", + reply->getError(0).getMessage()); +} + +void +Test::testMergeException(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("MergeException", + SimpleProtocol::IPolicyFactory::SP( + new MergeExceptionPolicyFactory())); + data._srcServer.mb.putProtocol(protocol); + Route route = Route::parse("[MergeException:dst/session]"); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), route) + .isAccepted()); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + data._dstSession->acknowledge(std::move(msg)); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::POLICY_ERROR, + reply->getError(0).getCode()); + EXPECT_EQUAL("Policy 'MergeException' threw an exception; {test exception}", + reply->getError(0).getMessage()); +} + +void +Test::requireThatIgnoreFlagPersistsThroughHopLookup(TestData &data) +{ + setupRouting(data, RoutingTableSpec(SimpleProtocol::NAME).addHop(HopSpec("foo", "dst/unknown"))); + ASSERT_TRUE(testSend(data, "?foo")); + ASSERT_TRUE(testTrace(data, StringList().add("Ignoring errors in reply."))); +} + +void +Test::requireThatIgnoreFlagPersistsThroughRouteLookup(TestData &data) +{ + setupRouting(data, RoutingTableSpec(SimpleProtocol::NAME).addRoute(RouteSpec("foo").addHop("dst/unknown"))); + ASSERT_TRUE(testSend(data, "?foo")); + ASSERT_TRUE(testTrace(data, StringList().add("Ignoring errors in reply."))); +} + +void +Test::requireThatIgnoreFlagPersistsThroughPolicySelect(TestData &data) +{ + setupPolicy(data, "Custom", MyPolicyFactory::newSelectAndMerge("dst/unknown")); + ASSERT_TRUE(testSend(data, "?[Custom]")); + ASSERT_TRUE(testTrace(data, StringList().add("Ignoring errors in reply."))); +} + +void +Test::requireThatIgnoreFlagIsSerializedWithMessage(TestData &data) +{ + ASSERT_TRUE(testSend(data, "dst/session foo ?bar")); + Message::UP msg = data._dstHandler.getMessage(RECEPTOR_TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + Route route = msg->getRoute(); + EXPECT_EQUAL(2u, route.getNumHops()); + Hop hop = route.getHop(0); + EXPECT_EQUAL("foo", hop.toString()); + EXPECT_TRUE(!hop.getIgnoreResult()); + hop = route.getHop(1); + EXPECT_EQUAL("?bar", hop.toString()); + EXPECT_TRUE(hop.getIgnoreResult()); + data._dstSession->acknowledge(std::move(msg)); + ASSERT_TRUE(testTrace(data, StringList().add("-Ignoring errors in reply."))); +} + +void +Test::requireThatIgnoreFlagDoesNotInterfere(TestData &data) +{ + setupPolicy(data, "Custom", MyPolicyFactory::newSelectAndMerge("dst/session")); + ASSERT_TRUE(testSend(data, "?[Custom]")); + ASSERT_TRUE(testTrace(data, StringList().add("-Ignoring errors in reply."))); + ASSERT_TRUE(testAcknowledge(data)); +} + +void +Test::requireThatEmptySelectionCanBeIgnored(TestData &data) +{ + setupPolicy(data, "Custom", MyPolicyFactory::newEmptySelection()); + ASSERT_TRUE(testSend(data, "?[Custom]")); + ASSERT_TRUE(testTrace(data, StringList().add("Ignoring errors in reply."))); +} + +void +Test::requireThatSelectErrorCanBeIgnored(TestData &data) +{ + setupPolicy(data, "Custom", MyPolicyFactory::newSelectError(ErrorCode::APP_FATAL_ERROR)); + ASSERT_TRUE(testSend(data, "?[Custom]")); + ASSERT_TRUE(testTrace(data, StringList().add("Ignoring errors in reply."))); +} + +void +Test::requireThatSelectExceptionCanBeIgnored(TestData &data) +{ + setupPolicy(data, "Custom", MyPolicyFactory::newSelectException()); + ASSERT_TRUE(testSend(data, "?[Custom]")); + ASSERT_TRUE(testTrace(data, StringList().add("Ignoring errors in reply."))); +} + +void +Test::requireThatSelectAndThrowCanBeIgnored(TestData &data) +{ + setupPolicy(data, "Custom", MyPolicyFactory::newSelectAndThrow("dst/session")); + ASSERT_TRUE(testSend(data, "?[Custom]")); + ASSERT_TRUE(testTrace(data, StringList().add("Ignoring errors in reply."))); +} + +void +Test::requireThatEmptyMergeCanBeIgnored(TestData &data) +{ + setupPolicy(data, "Custom", MyPolicyFactory::newEmptyMerge("dst/session")); + ASSERT_TRUE(testSend(data, "?[Custom]")); + ASSERT_TRUE(testAcknowledge(data)); + ASSERT_TRUE(testTrace(data, StringList().add("Ignoring errors in reply."))); +} + +void +Test::requireThatMergeErrorCanBeIgnored(TestData &data) +{ + setupPolicy(data, "Custom", MyPolicyFactory::newMergeError("dst/session", ErrorCode::APP_FATAL_ERROR)); + ASSERT_TRUE(testSend(data, "?[Custom]")); + ASSERT_TRUE(testAcknowledge(data)); + ASSERT_TRUE(testTrace(data, StringList().add("Ignoring errors in reply."))); +} + +void +Test::requireThatMergeExceptionCanBeIgnored(TestData &data) +{ + setupPolicy(data, "Custom", MyPolicyFactory::newMergeException("dst/session")); + ASSERT_TRUE(testSend(data, "?[Custom]")); + ASSERT_TRUE(testAcknowledge(data)); + ASSERT_TRUE(testTrace(data, StringList().add("Ignoring errors in reply."))); +} + +void +Test::requireThatMergeAndThrowCanBeIgnored(TestData &data) +{ + setupPolicy(data, "Custom", MyPolicyFactory::newMergeAndThrow("dst/session")); + ASSERT_TRUE(testSend(data, "?[Custom]")); + ASSERT_TRUE(testAcknowledge(data)); + ASSERT_TRUE(testTrace(data, StringList().add("Ignoring errors in reply."))); +} + +void +Test::requireThatAllocServiceCanBeIgnored(TestData &data) +{ + ASSERT_TRUE(testSend(data, "?dst/unknown")); + ASSERT_TRUE(testTrace(data, StringList().add("Ignoring errors in reply."))); +} + +void +Test::requireThatDepthLimitCanBeIgnored(TestData &data) +{ + setupPolicy(data, "Custom", MyPolicyFactory::newSelectAndMerge("[Custom]")); + ASSERT_TRUE(testSend(data, "?[Custom]", 0)); + ASSERT_TRUE(testTrace(data, StringList())); +} + +void +Test::testTimeout(TestData &data) +{ + data._retryPolicy->setEnabled(true); + data._retryPolicy->setBaseDelay(0.01); + data._srcSession->setTimeout(0.5); + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("dst/unknown")).isAccepted()); + Reply::UP reply = data._srcHandler.getReply(RECEPTOR_TIMEOUT); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_EQUAL(2u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::NO_ADDRESS_FOR_SERVICE, reply->getError(0).getCode()); + EXPECT_EQUAL((uint32_t)ErrorCode::TIMEOUT, reply->getError(1).getCode()); +} diff --git a/messagebus/src/tests/routingcontext/.gitignore b/messagebus/src/tests/routingcontext/.gitignore new file mode 100644 index 00000000000..7c3133e4bca --- /dev/null +++ b/messagebus/src/tests/routingcontext/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +routingcontext_test +messagebus_routingcontext_test_app diff --git a/messagebus/src/tests/routingcontext/CMakeLists.txt b/messagebus/src/tests/routingcontext/CMakeLists.txt new file mode 100644 index 00000000000..064487d2d71 --- /dev/null +++ b/messagebus/src/tests/routingcontext/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_routingcontext_test_app + SOURCES + routingcontext.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_routingcontext_test_app COMMAND messagebus_routingcontext_test_app) diff --git a/messagebus/src/tests/routingcontext/DESC b/messagebus/src/tests/routingcontext/DESC new file mode 100644 index 00000000000..9e52d1d8055 --- /dev/null +++ b/messagebus/src/tests/routingcontext/DESC @@ -0,0 +1 @@ +routingcontext test. Take a look at routingcontext.cpp for details. diff --git a/messagebus/src/tests/routingcontext/FILES b/messagebus/src/tests/routingcontext/FILES new file mode 100644 index 00000000000..8eb1e780a73 --- /dev/null +++ b/messagebus/src/tests/routingcontext/FILES @@ -0,0 +1 @@ +routingcontext.cpp diff --git a/messagebus/src/tests/routingcontext/routingcontext.cpp b/messagebus/src/tests/routingcontext/routingcontext.cpp new file mode 100644 index 00000000000..02c7ef6dd72 --- /dev/null +++ b/messagebus/src/tests/routingcontext/routingcontext.cpp @@ -0,0 +1,389 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("routingcontext_test"); + +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/routing/retrytransienterrorspolicy.h> +#include <vespa/messagebus/routing/routingcontext.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/vespalib/util/vstringfmt.h> + +using namespace mbus; + +//////////////////////////////////////////////////////////////////////////////// +// +// Utilities +// +//////////////////////////////////////////////////////////////////////////////// + +using vespalib::make_vespa_string; + +static const double TIMEOUT = 120; + +class StringList : public std::vector<string> { +public: + StringList &add(const string &str); +}; + +StringList & +StringList::add(const string &str) +{ + std::vector<string>::push_back(str); return *this; +} + +class CustomPolicyFactory : public SimpleProtocol::IPolicyFactory { +private: + friend class CustomPolicy; + + bool _forward; + std::vector<string> _expectedAll; + std::vector<string> _expectedMatched; + +public: + CustomPolicyFactory(bool forward, + const std::vector<string> &all, + const std::vector<string> &matched); + IRoutingPolicy::UP create(const string ¶m); +}; + +class CustomPolicy : public IRoutingPolicy { +private: + CustomPolicyFactory &_factory; + +public: + CustomPolicy(CustomPolicyFactory &factory); + void select(RoutingContext &ctx); + void merge(RoutingContext &ctx); +}; + +CustomPolicy::CustomPolicy(CustomPolicyFactory &factory) : + _factory(factory) +{ + // empty +} + +void +CustomPolicy::select(RoutingContext &ctx) +{ + Reply::UP reply(new EmptyReply()); + reply->getTrace().setLevel(9); + + const std::vector<Route> &all = ctx.getAllRecipients(); + if (_factory._expectedAll.size() == all.size()) { + ctx.trace(1, make_vespa_string("Got %d expected recipients.", (uint32_t)all.size())); + for (std::vector<Route>::const_iterator it = all.begin(); + it != all.end(); ++it) + { + if (find(_factory._expectedAll.begin(), _factory._expectedAll.end(), it->toString()) != _factory._expectedAll.end()) { + ctx.trace(1, make_vespa_string("Got expected recipient '%s'.", it->toString().c_str())); + } else { + reply->addError(Error(ErrorCode::APP_FATAL_ERROR, + make_vespa_string("Matched recipient '%s' not expected.", + it->toString().c_str()))); + } + } + } else { + reply->addError(Error(ErrorCode::APP_FATAL_ERROR, + make_vespa_string("Expected %d recipients, got %d.", + (uint32_t)_factory._expectedAll.size(), + (uint32_t)all.size()))); + } + + if (ctx.getNumRecipients() == all.size()) { + for (uint32_t i = 0; i < all.size(); ++i) { + if (all[i].toString() == ctx.getRecipient(i).toString()) { + ctx.trace(1, make_vespa_string("getRecipient(%d) matches getAllRecipients()[%d]", i, i)); + } else { + reply->addError(Error(ErrorCode::APP_FATAL_ERROR, + make_vespa_string("getRecipient(%d) differs from getAllRecipients()[%d]", i, i))); + } + } + } else { + reply->addError(Error(ErrorCode::APP_FATAL_ERROR, + "getNumRecipients() differs from getAllRecipients().size()")); + } + + std::vector<Route> matched; + ctx.getMatchedRecipients(matched); + if (_factory._expectedMatched.size() == matched.size()) { + ctx.trace(1, make_vespa_string("Got %d expected recipients.", (uint32_t)matched.size())); + for (std::vector<Route>::iterator it = matched.begin(); + it != matched.end(); ++it) + { + if (find(_factory._expectedMatched.begin(), _factory._expectedMatched.end(), it->toString()) != _factory._expectedMatched.end()) { + ctx.trace(1, make_vespa_string("Got matched recipient '%s'.", it->toString().c_str())); + } else { + reply->addError(Error(ErrorCode::APP_FATAL_ERROR, + make_vespa_string("Matched recipient '%s' not expected.", + it->toString().c_str()))); + } + } + } else { + reply->addError(Error(ErrorCode::APP_FATAL_ERROR, + make_vespa_string("Expected %d matched recipients, got %d.", + (uint32_t)_factory._expectedMatched.size(), + (uint32_t)matched.size()))); + } + + if (!reply->hasErrors() && _factory._forward) { + for (std::vector<Route>::iterator it = matched.begin(); + it != matched.end(); ++it) + { + ctx.addChild(*it); + } + } else { + ctx.setReply(std::move(reply)); + } +} + +void +CustomPolicy::merge(RoutingContext &ctx) +{ + Reply::UP ret(new EmptyReply()); + for (RoutingNodeIterator it = ctx.getChildIterator(); + it.isValid(); it.next()) + { + const Reply &reply = it.getReplyRef(); + for (uint32_t i = 0; i < reply.getNumErrors(); ++i) { + ret->addError(reply.getError(i)); + } + } + ctx.setReply(std::move(ret)); +} + +CustomPolicyFactory::CustomPolicyFactory(bool forward, + const std::vector<string> &all, + const std::vector<string> &matched) : + _forward(forward), + _expectedAll(all), + _expectedMatched(matched) +{ + // empty +} + +IRoutingPolicy::UP +CustomPolicyFactory::create(const string &) +{ + return IRoutingPolicy::UP(new CustomPolicy(*this)); +} + +Message::UP +createMessage(const string &msg) +{ + Message::UP ret(new SimpleMessage(msg)); + ret->getTrace().setLevel(9); + return ret; +} + + +//////////////////////////////////////////////////////////////////////////////// +// +// Setup +// +//////////////////////////////////////////////////////////////////////////////// + +class TestData { +public: + Slobrok _slobrok; + RetryTransientErrorsPolicy::SP _retryPolicy; + TestServer _srcServer; + SourceSession::UP _srcSession; + Receptor _srcHandler; + TestServer _dstServer; + DestinationSession::UP _dstSession; + Receptor _dstHandler; + +public: + TestData(); + bool start(); +}; + +class Test : public vespalib::TestApp { +private: + Message::UP createMessage(const string &msg); + +public: + int Main(); + void testSingleDirective(TestData &data); + void testMoreDirectives(TestData &data); + void testRecipientsRemain(TestData &data); + void testConstRoute(TestData &data); +}; + +TEST_APPHOOK(Test); + +TestData::TestData() : + _slobrok(), + _retryPolicy(new RetryTransientErrorsPolicy()), + _srcServer(MessageBusParams().setRetryPolicy(_retryPolicy).addProtocol(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams().setSlobrokConfig(_slobrok.config())), + _srcSession(), + _srcHandler(), + _dstServer(MessageBusParams().addProtocol(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams().setIdentity(Identity("dst")).setSlobrokConfig(_slobrok.config())), + _dstSession(), + _dstHandler() +{ + _retryPolicy->setBaseDelay(0); +} + +bool +TestData::start() +{ + _srcSession = _srcServer.mb.createSourceSession(SourceSessionParams().setReplyHandler(_srcHandler)); + if (_srcSession.get() == NULL) { + return false; + } + _dstSession = _dstServer.mb.createDestinationSession(DestinationSessionParams().setName("session").setMessageHandler(_dstHandler)); + if (_dstSession.get() == NULL) { + return false; + } + if (!_srcServer.waitSlobrok("dst/session", 1u)) { + return false; + } + return true; +} + +Message::UP +Test::createMessage(const string &msg) +{ + Message::UP ret(new SimpleMessage(msg)); + ret->getTrace().setLevel(9); + return ret; +} + +int +Test::Main() +{ + TEST_INIT("routingcontext_test"); + + TestData data; + ASSERT_TRUE(data.start()); + + testSingleDirective(data); TEST_FLUSH(); + testMoreDirectives(data); TEST_FLUSH(); + testRecipientsRemain(data); TEST_FLUSH(); + testConstRoute(data); TEST_FLUSH(); + + TEST_DONE(); +} + +//////////////////////////////////////////////////////////////////////////////// +// +// Tests +// +//////////////////////////////////////////////////////////////////////////////// + +void +Test::testSingleDirective(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("Custom", SimpleProtocol::IPolicyFactory::SP(new CustomPolicyFactory( + false, + StringList().add("foo").add("bar").add("baz/cox"), + StringList().add("foo").add("bar")))); + data._srcServer.mb.putProtocol(protocol); + data._srcServer.mb.setupRouting(RoutingSpec().addTable(RoutingTableSpec(SimpleProtocol::NAME) + .addRoute(RouteSpec("myroute").addHop("myhop")) + .addHop(HopSpec("myhop", "[Custom]") + .addRecipient("foo") + .addRecipient("bar") + .addRecipient("baz/cox")))); + for (uint32_t i = 0; i < 2; ++i) { + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), "myroute").isAccepted()); + Reply::UP reply = data._srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_TRUE(!reply->hasErrors()); + } +} + +void +Test::testMoreDirectives(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("Custom", SimpleProtocol::IPolicyFactory::SP(new CustomPolicyFactory( + false, + StringList().add("foo").add("foo/bar").add("foo/bar0/baz").add("foo/bar1/baz").add("foo/bar/baz/cox"), + StringList().add("foo/bar0/baz").add("foo/bar1/baz")))); + data._srcServer.mb.putProtocol(protocol); + data._srcServer.mb.setupRouting(RoutingSpec().addTable(RoutingTableSpec(SimpleProtocol::NAME) + .addRoute(RouteSpec("myroute").addHop("myhop")) + .addHop(HopSpec("myhop", "foo/[Custom]/baz") + .addRecipient("foo") + .addRecipient("foo/bar") + .addRecipient("foo/bar0/baz") + .addRecipient("foo/bar1/baz") + .addRecipient("foo/bar/baz/cox")))); + for (uint32_t i = 0; i < 2; ++i) { + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), "myroute").isAccepted()); + Reply::UP reply = data._srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_TRUE(!reply->hasErrors()); + } +} + +void +Test::testRecipientsRemain(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("First", SimpleProtocol::IPolicyFactory::SP(new CustomPolicyFactory( + true, + StringList().add("foo/bar"), + StringList().add("foo/[Second]")))); + simple.addPolicyFactory("Second", SimpleProtocol::IPolicyFactory::SP(new CustomPolicyFactory( + false, + StringList().add("foo/bar"), + StringList().add("foo/bar")))); + data._srcServer.mb.putProtocol(protocol); + data._srcServer.mb.setupRouting(RoutingSpec().addTable(RoutingTableSpec(SimpleProtocol::NAME) + .addRoute(RouteSpec("myroute").addHop("myhop")) + .addHop(HopSpec("myhop", "[First]/[Second]") + .addRecipient("foo/bar")))); + for (uint32_t i = 0; i < 2; ++i) { + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), "myroute").isAccepted()); + Reply::UP reply = data._srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_TRUE(!reply->hasErrors()); + } +} + +void +Test::testConstRoute(TestData &data) +{ + IProtocol::SP protocol(new SimpleProtocol()); + SimpleProtocol &simple = static_cast<SimpleProtocol&>(*protocol); + simple.addPolicyFactory("DocumentRouteSelector", + SimpleProtocol::IPolicyFactory::SP(new CustomPolicyFactory( + true, + StringList().add("dst"), + StringList().add("dst")))); + data._srcServer.mb.putProtocol(protocol); + data._srcServer.mb.setupRouting(RoutingSpec().addTable(RoutingTableSpec(SimpleProtocol::NAME) + .addRoute(RouteSpec("default").addHop("indexing")) + .addHop(HopSpec("indexing", "[DocumentRouteSelector]").addRecipient("dst")) + .addHop(HopSpec("dst", "dst/session")))); + for (uint32_t i = 0; i < 2; ++i) { + EXPECT_TRUE(data._srcSession->send(createMessage("msg"), Route::parse("route:default")).isAccepted()); + Message::UP msg = data._dstHandler.getMessage(TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + data._dstSession->acknowledge(std::move(msg)); + Reply::UP reply = data._srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + printf("%s", reply->getTrace().toString().c_str()); + EXPECT_TRUE(!reply->hasErrors()); + } +} + diff --git a/messagebus/src/tests/routingspec/.gitignore b/messagebus/src/tests/routingspec/.gitignore new file mode 100644 index 00000000000..cd168e7016f --- /dev/null +++ b/messagebus/src/tests/routingspec/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +routingspec_test +messagebus_routingspec_test_app diff --git a/messagebus/src/tests/routingspec/CMakeLists.txt b/messagebus/src/tests/routingspec/CMakeLists.txt new file mode 100644 index 00000000000..43539e07af5 --- /dev/null +++ b/messagebus/src/tests/routingspec/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_routingspec_test_app + SOURCES + routingspec.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_routingspec_test_app COMMAND messagebus_routingspec_test_app) diff --git a/messagebus/src/tests/routingspec/DESC b/messagebus/src/tests/routingspec/DESC new file mode 100644 index 00000000000..28d7f54decc --- /dev/null +++ b/messagebus/src/tests/routingspec/DESC @@ -0,0 +1 @@ +routingspec test. Take a look at routingspec.cpp for details. diff --git a/messagebus/src/tests/routingspec/FILES b/messagebus/src/tests/routingspec/FILES new file mode 100644 index 00000000000..4ae228ad5b9 --- /dev/null +++ b/messagebus/src/tests/routingspec/FILES @@ -0,0 +1 @@ +routingspec.cpp diff --git a/messagebus/src/tests/routingspec/routingspec.cpp b/messagebus/src/tests/routingspec/routingspec.cpp new file mode 100644 index 00000000000..d5317dc3bb0 --- /dev/null +++ b/messagebus/src/tests/routingspec/routingspec.cpp @@ -0,0 +1,251 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("routingspec_test"); + +#include <vespa/config/config.h> +#include <vespa/messagebus/configagent.h> +#include <vespa/messagebus/iconfighandler.h> +#include <vespa/messagebus/routing/routingspec.h> +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/config-messagebus.h> + +using namespace mbus; +using namespace messagebus; +using namespace config; + +class ConfigStore : public IConfigHandler { +private: + RoutingSpec _routing; + +public: + ConfigStore() : _routing() { + // empty + } + + bool setupRouting(const RoutingSpec &spec) { + _routing = spec; + return true; + } + + const RoutingSpec &getRoutingSpec() { + return _routing; + } +}; + +class Test : public vespalib::TestApp { +private: + bool testRouting(const RoutingSpec &spec); + bool testConfig(const RoutingSpec &spec); + +public: + void testConstructors(); + void testConfigGeneration(); + int Main(); +}; + +TEST_APPHOOK(Test); + +int +Test::Main() +{ + TEST_INIT("routingspec_test"); + + testConstructors(); TEST_FLUSH(); + testConfigGeneration(); TEST_FLUSH(); + + TEST_DONE(); +} + +void +Test::testConstructors() +{ + { + RoutingSpec spec; + spec.addTable(RoutingTableSpec("foo")); + spec.getTable(0).addHop(HopSpec("foo-h1", "foo-h1-sel")); + spec.getTable(0).getHop(0).addRecipient("foo-h1-r1"); + spec.getTable(0).getHop(0).addRecipient("foo-h1-r2"); + spec.getTable(0).addHop(HopSpec("foo-h2", "foo-h2-sel")); + spec.getTable(0).getHop(1).addRecipient("foo-h2-r1"); + spec.getTable(0).getHop(1).addRecipient("foo-h2-r2"); + spec.getTable(0).addRoute(RouteSpec("foo-r1")); + spec.getTable(0).getRoute(0).addHop("foo-h1"); + spec.getTable(0).getRoute(0).addHop("foo-h2"); + spec.getTable(0).addRoute(RouteSpec("foo-r2")); + spec.getTable(0).getRoute(1).addHop("foo-h2"); + spec.getTable(0).getRoute(1).addHop("foo-h1"); + spec.addTable(RoutingTableSpec("bar")); + spec.getTable(1).addHop(HopSpec("bar-h1", "bar-h1-sel")); + spec.getTable(1).getHop(0).addRecipient("bar-h1-r1"); + spec.getTable(1).getHop(0).addRecipient("bar-h1-r2"); + spec.getTable(1).addHop(HopSpec("bar-h2", "bar-h2-sel")); + spec.getTable(1).getHop(1).addRecipient("bar-h2-r1"); + spec.getTable(1).getHop(1).addRecipient("bar-h2-r2"); + spec.getTable(1).addRoute(RouteSpec("bar-r1")); + spec.getTable(1).getRoute(0).addHop("bar-h1"); + spec.getTable(1).getRoute(0).addHop("bar-h2"); + spec.getTable(1).addRoute(RouteSpec("bar-r2")); + spec.getTable(1).getRoute(1).addHop("bar-h2"); + spec.getTable(1).getRoute(1).addHop("bar-h1"); + EXPECT_TRUE(testRouting(spec)); + + RoutingSpec specCopy = spec; + EXPECT_TRUE(testRouting(specCopy)); + } + { + RoutingSpec spec = RoutingSpec() + .addTable(RoutingTableSpec("foo") + .addHop(HopSpec("foo-h1", "foo-h1-sel") + .addRecipient("foo-h1-r1") + .addRecipient("foo-h1-r2")) + .addHop(HopSpec("foo-h2", "foo-h2-sel") + .addRecipient("foo-h2-r1") + .addRecipient("foo-h2-r2")) + .addRoute(RouteSpec("foo-r1") + .addHop("foo-h1") + .addHop("foo-h2")) + .addRoute(RouteSpec("foo-r2") + .addHop("foo-h2") + .addHop("foo-h1"))) + .addTable(RoutingTableSpec("bar") + .addHop(HopSpec("bar-h1", "bar-h1-sel") + .addRecipient("bar-h1-r1") + .addRecipient("bar-h1-r2")) + .addHop(HopSpec("bar-h2", "bar-h2-sel") + .addRecipient("bar-h2-r1") + .addRecipient("bar-h2-r2")) + .addRoute(RouteSpec("bar-r1") + .addHop("bar-h1") + .addHop("bar-h2")) + .addRoute(RouteSpec("bar-r2") + .addHop("bar-h2") + .addHop("bar-h1"))); + EXPECT_TRUE(testRouting(spec)); + + RoutingSpec specCopy = spec; + EXPECT_TRUE(testRouting(specCopy)); + } +} + +bool +Test::testRouting(const RoutingSpec &spec) +{ + if (!ASSERT_TRUE(spec.getNumTables() == 2)) { return false; } + if (!EXPECT_TRUE(spec.getTable(0).getProtocol() == "foo")) { return false; } + if (!ASSERT_TRUE(spec.getTable(0).getNumHops() == 2)) { return false; } + if (!EXPECT_TRUE(spec.getTable(0).getHop(0).getName() == "foo-h1")) { return false; } + if (!EXPECT_TRUE(spec.getTable(0).getHop(0).getSelector() == "foo-h1-sel")) { return false; } + if (!ASSERT_TRUE(spec.getTable(0).getHop(0).getNumRecipients() == 2)) { return false; } + if (!EXPECT_TRUE(spec.getTable(0).getHop(0).getRecipient(0) == "foo-h1-r1")) { return false; } + if (!EXPECT_TRUE(spec.getTable(0).getHop(0).getRecipient(1) == "foo-h1-r2")) { return false; } + if (!EXPECT_TRUE(spec.getTable(0).getHop(1).getName() == "foo-h2")) { return false; } + if (!EXPECT_TRUE(spec.getTable(0).getHop(1).getSelector() == "foo-h2-sel")) { return false; } + if (!ASSERT_TRUE(spec.getTable(0).getHop(1).getNumRecipients() == 2)) { return false; } + if (!EXPECT_TRUE(spec.getTable(0).getHop(1).getRecipient(0) == "foo-h2-r1")) { return false; } + if (!EXPECT_TRUE(spec.getTable(0).getHop(1).getRecipient(1) == "foo-h2-r2")) { return false; } + if (!ASSERT_TRUE(spec.getTable(0).getNumRoutes() == 2)) { return false; } + if (!EXPECT_TRUE(spec.getTable(0).getRoute(0).getName() == "foo-r1")) { return false; } + if (!ASSERT_TRUE(spec.getTable(0).getRoute(0).getNumHops() == 2)) { return false; } + if (!EXPECT_TRUE(spec.getTable(0).getRoute(0).getHop(0) == "foo-h1")) { return false; } + if (!EXPECT_TRUE(spec.getTable(0).getRoute(0).getHop(1) == "foo-h2")) { return false; } + if (!EXPECT_TRUE(spec.getTable(0).getRoute(1).getName() == "foo-r2")) { return false; } + if (!ASSERT_TRUE(spec.getTable(0).getRoute(1).getNumHops() == 2)) { return false; } + if (!EXPECT_TRUE(spec.getTable(0).getRoute(1).getHop(0) == "foo-h2")) { return false; } + if (!EXPECT_TRUE(spec.getTable(0).getRoute(1).getHop(1) == "foo-h1")) { return false; } + if (!EXPECT_TRUE(spec.getTable(1).getProtocol() == "bar")) { return false; } + if (!ASSERT_TRUE(spec.getTable(1).getNumHops() == 2)) { return false; } + if (!EXPECT_TRUE(spec.getTable(1).getHop(0).getName() == "bar-h1")) { return false; } + if (!EXPECT_TRUE(spec.getTable(1).getHop(0).getSelector() == "bar-h1-sel")) { return false; } + if (!ASSERT_TRUE(spec.getTable(1).getHop(0).getNumRecipients() == 2)) { return false; } + if (!EXPECT_TRUE(spec.getTable(1).getHop(0).getRecipient(0) == "bar-h1-r1")) { return false; } + if (!EXPECT_TRUE(spec.getTable(1).getHop(0).getRecipient(1) == "bar-h1-r2")) { return false; } + if (!EXPECT_TRUE(spec.getTable(1).getHop(1).getName() == "bar-h2")) { return false; } + if (!EXPECT_TRUE(spec.getTable(1).getHop(1).getSelector() == "bar-h2-sel")) { return false; } + if (!ASSERT_TRUE(spec.getTable(1).getHop(1).getNumRecipients() == 2)) { return false; } + if (!EXPECT_TRUE(spec.getTable(1).getHop(1).getRecipient(0) == "bar-h2-r1")) { return false; } + if (!EXPECT_TRUE(spec.getTable(1).getHop(1).getRecipient(1) == "bar-h2-r2")) { return false; } + if (!ASSERT_TRUE(spec.getTable(1).getNumRoutes() == 2)) { return false; } + if (!EXPECT_TRUE(spec.getTable(1).getRoute(0).getName() == "bar-r1")) { return false; } + if (!ASSERT_TRUE(spec.getTable(1).getRoute(0).getNumHops() == 2)) { return false; } + if (!EXPECT_TRUE(spec.getTable(1).getRoute(0).getHop(0) == "bar-h1")) { return false; } + if (!EXPECT_TRUE(spec.getTable(1).getRoute(0).getHop(1) == "bar-h2")) { return false; } + if (!EXPECT_TRUE(spec.getTable(1).getRoute(1).getName() == "bar-r2")) { return false; } + if (!ASSERT_TRUE(spec.getTable(1).getRoute(1).getNumHops() == 2)) { return false; } + if (!EXPECT_TRUE(spec.getTable(1).getRoute(1).getHop(0) == "bar-h2")) { return false; } + if (!EXPECT_TRUE(spec.getTable(1).getRoute(1).getHop(1) == "bar-h1")) { return false; } + return true; +} + +void +Test::testConfigGeneration() +{ + EXPECT_TRUE(testConfig(RoutingSpec())); + EXPECT_TRUE(testConfig(RoutingSpec().addTable(RoutingTableSpec("mytable1")))); + EXPECT_TRUE(testConfig(RoutingSpec().addTable(RoutingTableSpec("mytable1") + .addHop(HopSpec("myhop1", "myselector1"))))); + EXPECT_TRUE(testConfig(RoutingSpec().addTable(RoutingTableSpec("mytable1") + .addHop(HopSpec("myhop1", "myselector1")) + .addRoute(RouteSpec("myroute1").addHop("myhop1"))))); + EXPECT_TRUE(testConfig(RoutingSpec().addTable(RoutingTableSpec("mytable1") + .addHop(HopSpec("myhop1", "myselector1")) + .addHop(HopSpec("myhop2", "myselector2")) + .addRoute(RouteSpec("myroute1").addHop("myhop1")) + .addRoute(RouteSpec("myroute2").addHop("myhop2")) + .addRoute(RouteSpec("myroute12").addHop("myhop1").addHop("myhop2"))))); + EXPECT_TRUE(testConfig(RoutingSpec() + .addTable(RoutingTableSpec("mytable1") + .addHop(HopSpec("myhop1", "myselector1")) + .addHop(HopSpec("myhop2", "myselector2")) + .addRoute(RouteSpec("myroute1").addHop("myhop1")) + .addRoute(RouteSpec("myroute2").addHop("myhop2")) + .addRoute(RouteSpec("myroute12").addHop("myhop1").addHop("myhop2"))) + .addTable(RoutingTableSpec("mytable2")))); + + EXPECT_EQUAL("routingtable[2]\n" + "routingtable[0].protocol \"mytable1\"\n" + "routingtable[1].protocol \"mytable2\"\n" + "routingtable[1].hop[3]\n" + "routingtable[1].hop[0].name \"myhop1\"\n" + "routingtable[1].hop[0].selector \"myselector1\"\n" + "routingtable[1].hop[1].name \"myhop2\"\n" + "routingtable[1].hop[1].selector \"myselector2\"\n" + "routingtable[1].hop[1].ignoreresult true\n" + "routingtable[1].hop[2].name \"myhop1\"\n" + "routingtable[1].hop[2].selector \"myselector3\"\n" + "routingtable[1].hop[2].recipient[2]\n" + "routingtable[1].hop[2].recipient[0] \"myrecipient1\"\n" + "routingtable[1].hop[2].recipient[1] \"myrecipient2\"\n" + "routingtable[1].route[1]\n" + "routingtable[1].route[0].name \"myroute1\"\n" + "routingtable[1].route[0].hop[1]\n" + "routingtable[1].route[0].hop[0] \"myhop1\"\n", + RoutingSpec() + .addTable(RoutingTableSpec("mytable1")) + .addTable(RoutingTableSpec("mytable2") + .addHop(HopSpec("myhop1", "myselector1")) + .addHop(HopSpec("myhop2", "myselector2").setIgnoreResult(true)) + .addHop(HopSpec("myhop1", "myselector3") + .addRecipient("myrecipient1") + .addRecipient("myrecipient2")) + .addRoute(RouteSpec("myroute1").addHop("myhop1"))).toString()); +} + +bool +Test::testConfig(const RoutingSpec &spec) +{ + if (!EXPECT_TRUE(spec == spec)) { + return false; + } + if (!EXPECT_TRUE(spec == RoutingSpec(spec))) { + return false; + } + ConfigStore store; + ConfigAgent agent(store); + agent.configure(ConfigGetter<MessagebusConfig>().getConfig("", RawSpec(spec.toString()))); + if (!EXPECT_TRUE(store.getRoutingSpec() == spec)) { + return false; + } + return true; +} + diff --git a/messagebus/src/tests/rpcserviceaddress/.gitignore b/messagebus/src/tests/rpcserviceaddress/.gitignore new file mode 100644 index 00000000000..fcb95aca804 --- /dev/null +++ b/messagebus/src/tests/rpcserviceaddress/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +rpcserviceaddress_test +messagebus_rpcserviceaddress_test_app diff --git a/messagebus/src/tests/rpcserviceaddress/CMakeLists.txt b/messagebus/src/tests/rpcserviceaddress/CMakeLists.txt new file mode 100644 index 00000000000..e4ada1c8c1b --- /dev/null +++ b/messagebus/src/tests/rpcserviceaddress/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_rpcserviceaddress_test_app + SOURCES + rpcserviceaddress.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_rpcserviceaddress_test_app COMMAND messagebus_rpcserviceaddress_test_app) diff --git a/messagebus/src/tests/rpcserviceaddress/DESC b/messagebus/src/tests/rpcserviceaddress/DESC new file mode 100644 index 00000000000..2c0f5565509 --- /dev/null +++ b/messagebus/src/tests/rpcserviceaddress/DESC @@ -0,0 +1 @@ +rpcserviceaddress test. Take a look at rpcserviceaddress.cpp for details. diff --git a/messagebus/src/tests/rpcserviceaddress/FILES b/messagebus/src/tests/rpcserviceaddress/FILES new file mode 100644 index 00000000000..ea9edf09a87 --- /dev/null +++ b/messagebus/src/tests/rpcserviceaddress/FILES @@ -0,0 +1 @@ +rpcserviceaddress.cpp diff --git a/messagebus/src/tests/rpcserviceaddress/rpcserviceaddress.cpp b/messagebus/src/tests/rpcserviceaddress/rpcserviceaddress.cpp new file mode 100644 index 00000000000..d5a002adf89 --- /dev/null +++ b/messagebus/src/tests/rpcserviceaddress/rpcserviceaddress.cpp @@ -0,0 +1,44 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("rpcserviceaddress_test"); +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/network/rpcserviceaddress.h> + +using namespace mbus; + +TEST_SETUP(Test); + +int +Test::Main() +{ + TEST_INIT("rpcserviceaddress_test"); + { + EXPECT_TRUE(RPCServiceAddress("", "bar").isMalformed()); + EXPECT_TRUE(RPCServiceAddress("foo", "bar").isMalformed()); + EXPECT_TRUE(RPCServiceAddress("foo/", "bar").isMalformed()); + EXPECT_TRUE(RPCServiceAddress("/foo", "bar").isMalformed()); + } + { + RPCServiceAddress addr("foo/bar/baz", "tcp/foo.com:42"); + EXPECT_TRUE(!addr.isMalformed()); + EXPECT_TRUE(addr.getServiceName() == "foo/bar/baz"); + EXPECT_TRUE(addr.getConnectionSpec() == "tcp/foo.com:42"); + EXPECT_TRUE(addr.getSessionName() == "baz"); + } + { + RPCServiceAddress addr("foo/bar", "tcp/foo.com:42"); + EXPECT_TRUE(!addr.isMalformed()); + EXPECT_TRUE(addr.getServiceName() == "foo/bar"); + EXPECT_TRUE(addr.getConnectionSpec() == "tcp/foo.com:42"); + EXPECT_TRUE(addr.getSessionName() == "bar"); + } + { + RPCServiceAddress addr("", "tcp/foo.com:42"); + EXPECT_TRUE(addr.isMalformed()); + EXPECT_TRUE(addr.getServiceName() == ""); + EXPECT_TRUE(addr.getConnectionSpec() == "tcp/foo.com:42"); + EXPECT_TRUE(addr.getSessionName() == ""); + } + TEST_DONE(); +} diff --git a/messagebus/src/tests/selector/.gitignore b/messagebus/src/tests/selector/.gitignore new file mode 100644 index 00000000000..8ffe3b2ac91 --- /dev/null +++ b/messagebus/src/tests/selector/.gitignore @@ -0,0 +1,3 @@ +.depend +Makefile +selector_test diff --git a/messagebus/src/tests/sendadapter/.gitignore b/messagebus/src/tests/sendadapter/.gitignore new file mode 100644 index 00000000000..7a13e0d4ee1 --- /dev/null +++ b/messagebus/src/tests/sendadapter/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +sendadapter_test +messagebus_sendadapter_test_app diff --git a/messagebus/src/tests/sendadapter/CMakeLists.txt b/messagebus/src/tests/sendadapter/CMakeLists.txt new file mode 100644 index 00000000000..32c41b40c3c --- /dev/null +++ b/messagebus/src/tests/sendadapter/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_sendadapter_test_app + SOURCES + sendadapter.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_sendadapter_test_app COMMAND messagebus_sendadapter_test_app) diff --git a/messagebus/src/tests/sendadapter/DESC b/messagebus/src/tests/sendadapter/DESC new file mode 100644 index 00000000000..35a50283921 --- /dev/null +++ b/messagebus/src/tests/sendadapter/DESC @@ -0,0 +1 @@ +sendadapter test. Take a look at sendadapter.cpp for details. diff --git a/messagebus/src/tests/sendadapter/FILES b/messagebus/src/tests/sendadapter/FILES new file mode 100644 index 00000000000..c43cbb6a53c --- /dev/null +++ b/messagebus/src/tests/sendadapter/FILES @@ -0,0 +1 @@ +sendadapter.cpp diff --git a/messagebus/src/tests/sendadapter/sendadapter.cpp b/messagebus/src/tests/sendadapter/sendadapter.cpp new file mode 100644 index 00000000000..b25240acdac --- /dev/null +++ b/messagebus/src/tests/sendadapter/sendadapter.cpp @@ -0,0 +1,252 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("sendadapter_test"); + +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +//////////////////////////////////////////////////////////////////////////////// +// +// Setup +// +//////////////////////////////////////////////////////////////////////////////// + +class TestProtocol : public mbus::SimpleProtocol { +private: + mutable vespalib::Version _lastVersion; + +public: + typedef std::shared_ptr<TestProtocol> SP; + mbus::Blob encode(const vespalib::Version &version, const mbus::Routable &routable) const { + _lastVersion = version; + return mbus::SimpleProtocol::encode(version, routable); + } + mbus::Routable::UP decode(const vespalib::Version &version, mbus::BlobRef blob) const { + _lastVersion = version; + return mbus::SimpleProtocol::decode(version, blob); + } + const vespalib::Version &getLastVersion() { return _lastVersion; } +}; + +class TestData { +public: + Slobrok _slobrok; + TestProtocol::SP _srcProtocol; + TestServer _srcServer; + SourceSession::UP _srcSession; + Receptor _srcHandler; + TestProtocol::SP _itrProtocol; + TestServer _itrServer; + IntermediateSession::UP _itrSession; + Receptor _itrHandler; + TestProtocol::SP _dstProtocol; + TestServer _dstServer; + DestinationSession::UP _dstSession; + Receptor _dstHandler; + +public: + TestData(); + bool start(); +}; + +class Test : public vespalib::TestApp { +private: + static const int TIMEOUT_SECS = 60; + + bool testVersionedSend(TestData &data, + const vespalib::Version &srcVersion, + const vespalib::Version &itrVersion, + const vespalib::Version &dstVersion); + void testSendAdapters(TestData &data); + +public: + int Main(); +}; + +TEST_APPHOOK(Test); + +TestData::TestData() : + _slobrok(), + _srcProtocol(new TestProtocol()), + _srcServer(MessageBusParams().setRetryPolicy(IRetryPolicy::SP()).addProtocol(_srcProtocol), + RPCNetworkParams().setSlobrokConfig(_slobrok.config())), + _srcSession(), + _srcHandler(), + _itrProtocol(new TestProtocol()), + _itrServer(MessageBusParams().addProtocol(_itrProtocol), + RPCNetworkParams().setIdentity(Identity("itr")).setSlobrokConfig(_slobrok.config())), + _itrSession(), + _itrHandler(), + _dstProtocol(new TestProtocol()), + _dstServer(MessageBusParams().addProtocol(_dstProtocol), + RPCNetworkParams().setIdentity(Identity("dst")).setSlobrokConfig(_slobrok.config())), + _dstSession(), + _dstHandler() +{ + // empty +} + +bool +TestData::start() +{ + _srcSession = _srcServer.mb.createSourceSession(SourceSessionParams().setReplyHandler(_srcHandler)); + if (_srcSession.get() == NULL) { + return false; + } + _itrSession = _itrServer.mb.createIntermediateSession(IntermediateSessionParams().setName("session").setMessageHandler(_itrHandler).setReplyHandler(_itrHandler)); + if (_itrSession.get() == NULL) { + return false; + } + _dstSession = _dstServer.mb.createDestinationSession(DestinationSessionParams().setName("session").setMessageHandler(_dstHandler)); + if (_dstSession.get() == NULL) { + return false; + } + if (!_srcServer.waitSlobrok("*/session", 2u)) { + return false; + } + return true; +} + +int +Test::Main() +{ + TEST_INIT("sendadapter_test"); + + TestData data; + ASSERT_TRUE(data.start()); + + testSendAdapters(data); TEST_FLUSH(); + + TEST_DONE(); +} + +//////////////////////////////////////////////////////////////////////////////// +// +// Tests +// +//////////////////////////////////////////////////////////////////////////////// + +void +Test::testSendAdapters(TestData &data) +{ + std::vector<vespalib::Version> versions; + versions.push_back(vespalib::Version(5, 0)); + versions.push_back(vespalib::Version(5, 1)); + + for (std::vector<vespalib::Version>::const_iterator srcVersion = versions.begin(); + srcVersion != versions.end(); ++srcVersion) + { + for (std::vector<vespalib::Version>::const_iterator itrVersion = versions.begin(); + itrVersion != versions.end(); ++itrVersion) + { + for (std::vector<vespalib::Version>::const_iterator dstVersion = versions.begin(); + dstVersion != versions.end(); ++dstVersion) + { + EXPECT_TRUE(testVersionedSend(data, *srcVersion, *itrVersion, *dstVersion)); + } + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// +// Utilities +// +//////////////////////////////////////////////////////////////////////////////// + +bool +Test::testVersionedSend(TestData &data, + const vespalib::Version &srcVersion, + const vespalib::Version &itrVersion, + const vespalib::Version &dstVersion) +{ + LOG(info, "Sending from %s through %s to %s.", + srcVersion.toString().c_str(), itrVersion.toString().c_str(), dstVersion.toString().c_str()); + data._srcServer.net.setVersion(srcVersion); + data._itrServer.net.setVersion(itrVersion); + data._dstServer.net.setVersion(dstVersion); + + Message::UP msg(new SimpleMessage("foo")); + msg->getTrace().setLevel(9); + if (!EXPECT_TRUE(data._srcSession->send(std::move(msg), Route::parse("itr/session dst/session")).isAccepted())) { + return false; + } + msg = data._itrHandler.getMessage(TIMEOUT_SECS); + if (!EXPECT_TRUE(msg.get() != NULL)) { + return false; + } + LOG(info, "Message version %s serialized at source.", + data._srcProtocol->getLastVersion().toString().c_str()); + vespalib::Version minVersion = std::min(srcVersion, itrVersion); + if (!EXPECT_TRUE(minVersion == data._srcProtocol->getLastVersion())) { + return false; + } + + LOG(info, "Message version %s reached intermediate.", + data._itrProtocol->getLastVersion().toString().c_str()); + if (!EXPECT_TRUE(minVersion == data._itrProtocol->getLastVersion())) { + return false; + } + data._itrSession->forward(std::move(msg)); + msg = data._dstHandler.getMessage(TIMEOUT_SECS); + if (!EXPECT_TRUE(msg.get() != NULL)) { + return false; + } + LOG(info, "Message version %s serialized at intermediate.", + data._itrProtocol->getLastVersion().toString().c_str()); + minVersion = std::min(itrVersion, dstVersion); + if (!EXPECT_TRUE(minVersion == data._itrProtocol->getLastVersion())) { + return false; + } + + LOG(info, "Message version %s reached destination.", + data._dstProtocol->getLastVersion().toString().c_str()); + if (!EXPECT_TRUE(minVersion == data._dstProtocol->getLastVersion())) { + return false; + } + Reply::UP reply(new SimpleReply("bar")); + reply->swapState(*msg); + data._dstSession->reply(std::move(reply)); + reply = data._itrHandler.getReply(); + if (!EXPECT_TRUE(reply.get() != NULL)) { + return false; + } + LOG(info, "Reply version %s serialized at destination.", + data._dstProtocol->getLastVersion().toString().c_str()); + if (!EXPECT_TRUE(minVersion == data._dstProtocol->getLastVersion())) { + return false; + } + + LOG(info, "Reply version %s reached intermediate.", + data._itrProtocol->getLastVersion().toString().c_str()); + if (!EXPECT_TRUE(minVersion == data._itrProtocol->getLastVersion())) { + return false; + } + data._itrSession->forward(std::move(reply)); + reply = data._srcHandler.getReply(); + if (!EXPECT_TRUE(reply.get() != NULL)) { + return false; + } + LOG(info, "Reply version %s serialized at intermediate.", + data._dstProtocol->getLastVersion().toString().c_str()); + minVersion = std::min(srcVersion, itrVersion); + if (!EXPECT_TRUE(minVersion == data._itrProtocol->getLastVersion())) { + return false; + } + + LOG(info, "Reply version %s reached source.", + data._srcProtocol->getLastVersion().toString().c_str()); + if (!EXPECT_TRUE(minVersion == data._srcProtocol->getLastVersion())) { + return false; + } + return true; +} diff --git a/messagebus/src/tests/sequencer/.gitignore b/messagebus/src/tests/sequencer/.gitignore new file mode 100644 index 00000000000..cc673f20668 --- /dev/null +++ b/messagebus/src/tests/sequencer/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +sequencer_test +messagebus_sequencer_test_app diff --git a/messagebus/src/tests/sequencer/CMakeLists.txt b/messagebus/src/tests/sequencer/CMakeLists.txt new file mode 100644 index 00000000000..dab54431722 --- /dev/null +++ b/messagebus/src/tests/sequencer/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_sequencer_test_app + SOURCES + sequencer.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_sequencer_test_app COMMAND messagebus_sequencer_test_app) diff --git a/messagebus/src/tests/sequencer/DESC b/messagebus/src/tests/sequencer/DESC new file mode 100644 index 00000000000..761c069aa92 --- /dev/null +++ b/messagebus/src/tests/sequencer/DESC @@ -0,0 +1 @@ +sequencer test. Take a look at sequencer.cpp for details. diff --git a/messagebus/src/tests/sequencer/FILES b/messagebus/src/tests/sequencer/FILES new file mode 100644 index 00000000000..a8d6aeae540 --- /dev/null +++ b/messagebus/src/tests/sequencer/FILES @@ -0,0 +1 @@ +sequencer.cpp diff --git a/messagebus/src/tests/sequencer/sequencer.cpp b/messagebus/src/tests/sequencer/sequencer.cpp new file mode 100644 index 00000000000..b2818cfa57d --- /dev/null +++ b/messagebus/src/tests/sequencer/sequencer.cpp @@ -0,0 +1,194 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("sequencer_test"); + +#include <vespa/messagebus/sequencer.h> +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/routablequeue.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +// -------------------------------------------------------------------------------- +// +// Setup. +// +// -------------------------------------------------------------------------------- + +struct MyQueue : public RoutableQueue { + + virtual ~MyQueue() { + while (size() > 0) { + Routable::UP obj = dequeue(0); + obj->getCallStack().discard(); + } + } + + bool checkReply(bool hasSeqId, uint64_t seqId) { + if (size() == 0) { + LOG(error, "checkReply(): No reply in queue."); + return false; + } + Routable::UP obj = dequeue(0); + if (!obj->isReply()) { + LOG(error, "checkReply(): Got message when expecting reply."); + return false; + } + Reply::UP reply(static_cast<Reply*>(obj.release())); + Message::UP msg = reply->getMessage(); + if (msg.get() == NULL) { + LOG(error, "checkReply(): Reply has no message attached."); + return false; + } + if (hasSeqId) { + if (!msg->hasSequenceId()) { + LOG(error, "checkReply(): Expected sequence id %" PRIu64 ", got none.", + seqId); + return false; + } + if (msg->getSequenceId() != seqId) { + LOG(error, "checkReply(): Expected sequence id %" PRIu64 ", got %" PRIu64 ".", + seqId, msg->getSequenceId()); + return false; + } + } else { + if (msg->hasSequenceId()) { + LOG(error, "checkReply(): Message has unexpected sequence id %" PRIu64 ".", + msg->getSequenceId()); + return false; + } + } + return true; + } + + void replyNext() { + Routable::UP obj = dequeue(0); + Message::UP msg(static_cast<Message*>(obj.release())); + + Reply::UP reply(new EmptyReply()); + reply->swapState(*msg); + reply->setMessage(std::move(msg)); + IReplyHandler &handler = reply->getCallStack().pop(*reply); + handler.handleReply(std::move(reply)); + } + + Message::UP createMessage(bool hasSeqId, uint64_t seqId) { + Message::UP ret(new SimpleMessage("foo", hasSeqId, seqId)); + ret->pushHandler(*this); + return ret; + } +}; + +class Test : public vespalib::TestApp { +private: + void testSyncNone(); + void testSyncId(); + +public: + int Main() { + TEST_INIT("sequencer_test"); + + testSyncNone(); TEST_FLUSH(); + testSyncId(); TEST_FLUSH(); + + TEST_DONE(); + } +}; + +TEST_APPHOOK(Test); + +// -------------------------------------------------------------------------------- +// +// Tests. +// +// -------------------------------------------------------------------------------- + +void +Test::testSyncNone() +{ + MyQueue src; + MyQueue dst; + Sequencer seq(dst); + + seq.handleMessage(src.createMessage(false, 0)); + seq.handleMessage(src.createMessage(false, 0)); + seq.handleMessage(src.createMessage(false, 0)); + seq.handleMessage(src.createMessage(false, 0)); + seq.handleMessage(src.createMessage(false, 0)); + EXPECT_EQUAL(0u, src.size()); + EXPECT_EQUAL(5u, dst.size()); + + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + EXPECT_EQUAL(5u, src.size()); + EXPECT_EQUAL(0u, dst.size()); + + EXPECT_TRUE(src.checkReply(false, 0)); + EXPECT_TRUE(src.checkReply(false, 0)); + EXPECT_TRUE(src.checkReply(false, 0)); + EXPECT_TRUE(src.checkReply(false, 0)); + EXPECT_TRUE(src.checkReply(false, 0)); + EXPECT_EQUAL(0u, src.size()); + EXPECT_EQUAL(0u, dst.size()); +} + +void +Test::testSyncId() +{ + MyQueue src; + MyQueue dst; + Sequencer seq(dst); + + seq.handleMessage(src.createMessage(true, 1)); + seq.handleMessage(src.createMessage(true, 2)); + seq.handleMessage(src.createMessage(true, 3)); + seq.handleMessage(src.createMessage(true, 4)); + seq.handleMessage(src.createMessage(true, 5)); + EXPECT_EQUAL(0u, src.size()); + EXPECT_EQUAL(5u, dst.size()); + + seq.handleMessage(src.createMessage(true, 1)); + seq.handleMessage(src.createMessage(true, 5)); + seq.handleMessage(src.createMessage(true, 2)); + seq.handleMessage(src.createMessage(true, 10)); + seq.handleMessage(src.createMessage(true, 4)); + seq.handleMessage(src.createMessage(true, 3)); + EXPECT_EQUAL(0u, src.size()); + EXPECT_EQUAL(6u, dst.size()); + + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + EXPECT_EQUAL(5u, src.size()); + EXPECT_EQUAL(6u, dst.size()); + + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + dst.replyNext(); + EXPECT_EQUAL(11u, src.size()); + EXPECT_EQUAL(0u, dst.size()); + + EXPECT_TRUE(src.checkReply(true, 1)); + EXPECT_TRUE(src.checkReply(true, 2)); + EXPECT_TRUE(src.checkReply(true, 3)); + EXPECT_TRUE(src.checkReply(true, 4)); + EXPECT_TRUE(src.checkReply(true, 5)); + EXPECT_TRUE(src.checkReply(true, 10)); + EXPECT_TRUE(src.checkReply(true, 1)); + EXPECT_TRUE(src.checkReply(true, 2)); + EXPECT_TRUE(src.checkReply(true, 3)); + EXPECT_TRUE(src.checkReply(true, 4)); + EXPECT_TRUE(src.checkReply(true, 5)); + EXPECT_EQUAL(0u, src.size()); + EXPECT_EQUAL(0u, dst.size()); +} diff --git a/messagebus/src/tests/serviceaddress/.gitignore b/messagebus/src/tests/serviceaddress/.gitignore new file mode 100644 index 00000000000..62275eb8c04 --- /dev/null +++ b/messagebus/src/tests/serviceaddress/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +serviceaddress_test +messagebus_serviceaddress_test_app diff --git a/messagebus/src/tests/serviceaddress/CMakeLists.txt b/messagebus/src/tests/serviceaddress/CMakeLists.txt new file mode 100644 index 00000000000..630e28dc94f --- /dev/null +++ b/messagebus/src/tests/serviceaddress/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_serviceaddress_test_app + SOURCES + serviceaddress.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_serviceaddress_test_app COMMAND messagebus_serviceaddress_test_app) diff --git a/messagebus/src/tests/serviceaddress/DESC b/messagebus/src/tests/serviceaddress/DESC new file mode 100644 index 00000000000..38fa7c16b1a --- /dev/null +++ b/messagebus/src/tests/serviceaddress/DESC @@ -0,0 +1 @@ +serviceaddress test. Take a look at serviceaddress.cpp for details. diff --git a/messagebus/src/tests/serviceaddress/FILES b/messagebus/src/tests/serviceaddress/FILES new file mode 100644 index 00000000000..37e17b66b5e --- /dev/null +++ b/messagebus/src/tests/serviceaddress/FILES @@ -0,0 +1 @@ +serviceaddress.cpp diff --git a/messagebus/src/tests/serviceaddress/serviceaddress.cpp b/messagebus/src/tests/serviceaddress/serviceaddress.cpp new file mode 100644 index 00000000000..36d7776f732 --- /dev/null +++ b/messagebus/src/tests/serviceaddress/serviceaddress.cpp @@ -0,0 +1,137 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("serviceaddress_test"); + +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/vespalib/util/vstringfmt.h> +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/sourcesession.h> +#include <vespa/messagebus/intermediatesession.h> +#include <vespa/messagebus/destinationsession.h> +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/error.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/routing/routingspec.h> +#include <vespa/messagebus/network/rpcservice.h> +#include <vespa/messagebus/sourcesessionparams.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> + +using namespace mbus; + +class Test : public vespalib::TestApp { +public: + int Main(); + void testAddrServiceAddress(); + void testNameServiceAddress(); + +private: + bool waitSlobrok(RPCNetwork &network, const string &pattern, size_t num); + bool testAddress(RPCNetwork& network, const string &pattern, + const string &expectedSpec, const string &expectedSession); + bool testNullAddress(RPCNetwork &network, const string &pattern); +}; + +int +Test::Main() +{ + TEST_INIT("serviceaddress_test"); + + testAddrServiceAddress(); TEST_FLUSH(); + testNameServiceAddress(); TEST_FLUSH(); + + TEST_DONE(); +} + +TEST_APPHOOK(Test); + +void +Test::testAddrServiceAddress() +{ + Slobrok slobrok; + RPCNetwork network(RPCNetworkParams() + .setIdentity(Identity("foo")) + .setSlobrokConfig(slobrok.config())); + ASSERT_TRUE(network.start()); + + EXPECT_TRUE(testNullAddress(network, "tcp")); + EXPECT_TRUE(testNullAddress(network, "tcp/")); + EXPECT_TRUE(testNullAddress(network, "tcp/localhost")); + EXPECT_TRUE(testNullAddress(network, "tcp/localhost:")); + EXPECT_TRUE(testNullAddress(network, "tcp/localhost:1977")); + EXPECT_TRUE(testNullAddress(network, "tcp/localhost:1977/")); + EXPECT_TRUE(testAddress(network, "tcp/localhost:1977/session", "tcp/localhost:1977", "session")); + EXPECT_TRUE(testNullAddress(network, "tcp/localhost:/session")); + EXPECT_TRUE(testNullAddress(network, "tcp/:1977/session")); + EXPECT_TRUE(testNullAddress(network, "tcp/:/session")); + + network.shutdown(); +} + +void +Test::testNameServiceAddress() +{ + Slobrok slobrok; + RPCNetwork network(RPCNetworkParams() + .setIdentity(Identity("foo")) + .setSlobrokConfig(slobrok.config())); + ASSERT_TRUE(network.start()); + + network.unregisterSession("session"); + ASSERT_TRUE(waitSlobrok(network, "foo/session", 0)); + EXPECT_TRUE(testNullAddress(network, "foo/session")); + + network.registerSession("session"); + ASSERT_TRUE(waitSlobrok(network, "foo/session", 1)); + EXPECT_TRUE(testAddress(network, "foo/session", network.getConnectionSpec().c_str(), "session")); + + network.shutdown(); +} + +bool +Test::waitSlobrok(RPCNetwork &network, const string &pattern, size_t num) +{ + for (int i = 0; i < 1000; i++) { + slobrok::api::MirrorAPI::SpecList res = network.getMirror().lookup(pattern); + if (res.size() == num) { + return true; + } + FastOS_Thread::Sleep(10); + } + return false; +} + +bool +Test::testNullAddress(RPCNetwork &network, const string &pattern) +{ + RPCService service(network.getMirror(), pattern); + RPCServiceAddress::UP obj = service.resolve(); + if (!EXPECT_TRUE(obj.get() == NULL)) { + return false; + } + return true; +} + +bool +Test::testAddress(RPCNetwork &network, const string &pattern, + const string &expectedSpec, const string &expectedSession) +{ + RPCService service(network.getMirror(), pattern); + RPCServiceAddress::UP obj = service.resolve(); + if (!EXPECT_TRUE(obj.get() != NULL)) { + return false; + } + if (!EXPECT_EQUAL(expectedSpec, obj->getConnectionSpec())) { + return false; + } + if (!EXPECT_EQUAL(expectedSession, obj->getSessionName())) { + return false; + } + return true; +} + diff --git a/messagebus/src/tests/servicepool/.gitignore b/messagebus/src/tests/servicepool/.gitignore new file mode 100644 index 00000000000..d8b2f3b9b09 --- /dev/null +++ b/messagebus/src/tests/servicepool/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +servicepool_test +messagebus_servicepool_test_app diff --git a/messagebus/src/tests/servicepool/CMakeLists.txt b/messagebus/src/tests/servicepool/CMakeLists.txt new file mode 100644 index 00000000000..0d6cbc54862 --- /dev/null +++ b/messagebus/src/tests/servicepool/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_servicepool_test_app + SOURCES + servicepool.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_servicepool_test_app COMMAND messagebus_servicepool_test_app) diff --git a/messagebus/src/tests/servicepool/DESC b/messagebus/src/tests/servicepool/DESC new file mode 100644 index 00000000000..21484039b7a --- /dev/null +++ b/messagebus/src/tests/servicepool/DESC @@ -0,0 +1 @@ +servicepool test. Take a look at servicepool.cpp for details. diff --git a/messagebus/src/tests/servicepool/FILES b/messagebus/src/tests/servicepool/FILES new file mode 100644 index 00000000000..22d1bbb2ba8 --- /dev/null +++ b/messagebus/src/tests/servicepool/FILES @@ -0,0 +1 @@ +servicepool.cpp diff --git a/messagebus/src/tests/servicepool/servicepool.cpp b/messagebus/src/tests/servicepool/servicepool.cpp new file mode 100644 index 00000000000..5cf4b8b6132 --- /dev/null +++ b/messagebus/src/tests/servicepool/servicepool.cpp @@ -0,0 +1,71 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("servicepool_test"); + +#include <vespa/messagebus/network/rpcnetwork.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +class Test : public vespalib::TestApp { +private: + void testMaxSize(); + +public: + int Main() { + TEST_INIT("servicepool_test"); + + testMaxSize(); TEST_FLUSH(); + + TEST_DONE(); + } +}; + +TEST_APPHOOK(Test); + +void +Test::testMaxSize() +{ + Slobrok slobrok; + RPCNetwork net(RPCNetworkParams().setSlobrokConfig(slobrok.config())); + RPCServicePool pool(net, 2); + net.start(); + + pool.resolve("foo"); + EXPECT_EQUAL(1u, pool.getSize()); + EXPECT_TRUE(pool.hasService("foo")); + EXPECT_TRUE(!pool.hasService("bar")); + EXPECT_TRUE(!pool.hasService("baz")); + + pool.resolve("foo"); + EXPECT_EQUAL(1u, pool.getSize()); + EXPECT_TRUE(pool.hasService("foo")); + EXPECT_TRUE(!pool.hasService("bar")); + EXPECT_TRUE(!pool.hasService("baz")); + + pool.resolve("bar"); + EXPECT_EQUAL(2u, pool.getSize()); + EXPECT_TRUE(pool.hasService("foo")); + EXPECT_TRUE(pool.hasService("bar")); + EXPECT_TRUE(!pool.hasService("baz")); + + pool.resolve("baz"); + EXPECT_EQUAL(2u, pool.getSize()); + EXPECT_TRUE(!pool.hasService("foo")); + EXPECT_TRUE(pool.hasService("bar")); + EXPECT_TRUE(pool.hasService("baz")); + + pool.resolve("bar"); + EXPECT_EQUAL(2u, pool.getSize()); + EXPECT_TRUE(!pool.hasService("foo")); + EXPECT_TRUE(pool.hasService("bar")); + EXPECT_TRUE(pool.hasService("baz")); + + pool.resolve("foo"); + EXPECT_EQUAL(2u, pool.getSize()); + EXPECT_TRUE(pool.hasService("foo")); + EXPECT_TRUE(pool.hasService("bar")); + EXPECT_TRUE(!pool.hasService("baz")); +} diff --git a/messagebus/src/tests/shutdown/.gitignore b/messagebus/src/tests/shutdown/.gitignore new file mode 100644 index 00000000000..f3a6f7e0061 --- /dev/null +++ b/messagebus/src/tests/shutdown/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +shutdown_test +messagebus_shutdown_test_app diff --git a/messagebus/src/tests/shutdown/CMakeLists.txt b/messagebus/src/tests/shutdown/CMakeLists.txt new file mode 100644 index 00000000000..69c849ae5fb --- /dev/null +++ b/messagebus/src/tests/shutdown/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_shutdown_test_app + SOURCES + shutdown.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_shutdown_test_app COMMAND messagebus_shutdown_test_app) diff --git a/messagebus/src/tests/shutdown/DESC b/messagebus/src/tests/shutdown/DESC new file mode 100644 index 00000000000..1f289ba9c23 --- /dev/null +++ b/messagebus/src/tests/shutdown/DESC @@ -0,0 +1 @@ +shutdown test. Take a look at shutdown.cpp for details. diff --git a/messagebus/src/tests/shutdown/FILES b/messagebus/src/tests/shutdown/FILES new file mode 100644 index 00000000000..ce150a62325 --- /dev/null +++ b/messagebus/src/tests/shutdown/FILES @@ -0,0 +1 @@ +shutdown.cpp diff --git a/messagebus/src/tests/shutdown/shutdown.cpp b/messagebus/src/tests/shutdown/shutdown.cpp new file mode 100644 index 00000000000..d4c5544d469 --- /dev/null +++ b/messagebus/src/tests/shutdown/shutdown.cpp @@ -0,0 +1,159 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("shutdown_test"); + +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/routing/errordirective.h> +#include <vespa/messagebus/routing/retrytransienterrorspolicy.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/vespalib/util/exceptions.h> +#include <vespa/vespalib/util/vstringfmt.h> + +using namespace mbus; + +class Test : public vespalib::TestApp { +private: + void requireThatListenFailedIsExceptionSafe(); + void requireThatShutdownOnSourceWithPendingIsSafe(); + void requireThatShutdownOnIntermediateWithPendingIsSafe(); + +public: + int Main() { + TEST_INIT("shutdown_test"); + + requireThatListenFailedIsExceptionSafe(); TEST_FLUSH(); + requireThatShutdownOnSourceWithPendingIsSafe(); TEST_FLUSH(); + requireThatShutdownOnIntermediateWithPendingIsSafe(); TEST_FLUSH(); + + TEST_DONE(); + } +}; + +static const double TIMEOUT = 120; + +TEST_APPHOOK(Test); + +void +Test::requireThatListenFailedIsExceptionSafe() +{ + FRT_Supervisor orb; + ASSERT_TRUE(orb.Listen(0)); + ASSERT_TRUE(orb.Start()); + + Slobrok slobrok; + try { + TestServer bar(MessageBusParams(), + RPCNetworkParams() + .setListenPort(orb.GetListenPort()) + .setSlobrokConfig(slobrok.config())); + EXPECT_TRUE(false); + } catch (vespalib::Exception &e) { + EXPECT_EQUAL("Failed to start network.", + e.getMessage()); + } + orb.ShutDown(true); +} + +void +Test::requireThatShutdownOnSourceWithPendingIsSafe() +{ + Slobrok slobrok; + TestServer dstServer(MessageBusParams() + .addProtocol(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams() + .setIdentity(Identity("dst")) + .setSlobrokConfig(slobrok.config())); + Receptor dstHandler; + DestinationSession::UP dstSession = dstServer.mb.createDestinationSession( + DestinationSessionParams() + .setName("session") + .setMessageHandler(dstHandler)); + ASSERT_TRUE(dstSession.get() != NULL); + + for (uint32_t i = 0; i < 10; ++i) { + Message::UP msg(new SimpleMessage("msg")); + { + TestServer srcServer(MessageBusParams() + .setRetryPolicy(IRetryPolicy::SP(new RetryTransientErrorsPolicy())) + .addProtocol(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams() + .setSlobrokConfig(slobrok.config())); + Receptor srcHandler; + SourceSession::UP srcSession = srcServer.mb.createSourceSession(SourceSessionParams() + .setThrottlePolicy(IThrottlePolicy::SP()) + .setReplyHandler(srcHandler)); + ASSERT_TRUE(srcSession.get() != NULL); + ASSERT_TRUE(srcServer.waitSlobrok("dst/session", 1)); + ASSERT_TRUE(srcSession->send(std::move(msg), "dst/session", true).isAccepted()); + msg = dstHandler.getMessage(TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + } + dstSession->acknowledge(std::move(msg)); + } +} + +void +Test::requireThatShutdownOnIntermediateWithPendingIsSafe() +{ + Slobrok slobrok; + TestServer dstServer(MessageBusParams() + .addProtocol(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams() + .setIdentity(Identity("dst")) + .setSlobrokConfig(slobrok.config())); + Receptor dstHandler; + DestinationSession::UP dstSession = dstServer.mb.createDestinationSession( + DestinationSessionParams() + .setName("session") + .setMessageHandler(dstHandler)); + ASSERT_TRUE(dstSession.get() != NULL); + + TestServer srcServer(MessageBusParams() + .setRetryPolicy(IRetryPolicy::SP()) + .addProtocol(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams() + .setSlobrokConfig(slobrok.config())); + Receptor srcHandler; + SourceSession::UP srcSession = srcServer.mb.createSourceSession(SourceSessionParams() + .setThrottlePolicy(IThrottlePolicy::SP()) + .setReplyHandler(srcHandler)); + ASSERT_TRUE(srcSession.get() != NULL); + ASSERT_TRUE(srcServer.waitSlobrok("dst/session", 1)); + + for (uint32_t i = 0; i < 10; ++i) { + Message::UP msg(new SimpleMessage("msg")); + { + TestServer itrServer(MessageBusParams() + .setRetryPolicy(IRetryPolicy::SP(new RetryTransientErrorsPolicy())) + .addProtocol(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams() + .setIdentity(Identity("itr")) + .setSlobrokConfig(slobrok.config())); + Receptor itrHandler; + IntermediateSession::UP itrSession = itrServer.mb.createIntermediateSession( + IntermediateSessionParams() + .setName("session") + .setMessageHandler(itrHandler) + .setReplyHandler(itrHandler)); + ASSERT_TRUE(itrSession.get() != NULL); + ASSERT_TRUE(srcServer.waitSlobrok("itr/session", 1)); + ASSERT_TRUE(srcSession->send(std::move(msg), "itr/session dst/session", true).isAccepted()); + msg = itrHandler.getMessage(TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + itrSession->forward(std::move(msg)); + msg = dstHandler.getMessage(TIMEOUT); + ASSERT_TRUE(msg.get() != NULL); + } + ASSERT_TRUE(srcServer.waitSlobrok("itr/session", 0)); + dstSession->acknowledge(std::move(msg)); + dstServer.mb.sync(); + } +} diff --git a/messagebus/src/tests/simple-roundtrip/.gitignore b/messagebus/src/tests/simple-roundtrip/.gitignore new file mode 100644 index 00000000000..c34bf4e6ca8 --- /dev/null +++ b/messagebus/src/tests/simple-roundtrip/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +simple-roundtrip_test +messagebus_simple-roundtrip_test_app diff --git a/messagebus/src/tests/simple-roundtrip/CMakeLists.txt b/messagebus/src/tests/simple-roundtrip/CMakeLists.txt new file mode 100644 index 00000000000..dff6ebf3e55 --- /dev/null +++ b/messagebus/src/tests/simple-roundtrip/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_simple-roundtrip_test_app + SOURCES + simple-roundtrip.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_simple-roundtrip_test_app COMMAND messagebus_simple-roundtrip_test_app) diff --git a/messagebus/src/tests/simple-roundtrip/DESC b/messagebus/src/tests/simple-roundtrip/DESC new file mode 100644 index 00000000000..ad88203d593 --- /dev/null +++ b/messagebus/src/tests/simple-roundtrip/DESC @@ -0,0 +1 @@ +simple-roundtrip test. Take a look at simple-roundtrip.cpp for details. diff --git a/messagebus/src/tests/simple-roundtrip/FILES b/messagebus/src/tests/simple-roundtrip/FILES new file mode 100644 index 00000000000..c6a24435fe2 --- /dev/null +++ b/messagebus/src/tests/simple-roundtrip/FILES @@ -0,0 +1 @@ +simple-roundtrip.cpp diff --git a/messagebus/src/tests/simple-roundtrip/simple-roundtrip.cpp b/messagebus/src/tests/simple-roundtrip/simple-roundtrip.cpp new file mode 100644 index 00000000000..c59f072bd09 --- /dev/null +++ b/messagebus/src/tests/simple-roundtrip/simple-roundtrip.cpp @@ -0,0 +1,101 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("simple-roundtrip_test"); + +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/sourcesession.h> +#include <vespa/messagebus/intermediatesession.h> +#include <vespa/messagebus/destinationsession.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/messagebus/routing/routingspec.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/sourcesessionparams.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> + +using namespace mbus; + +TEST_SETUP(Test); + +RoutingSpec getRouting() { + return RoutingSpec() + .addTable(RoutingTableSpec("Simple") + .addHop(HopSpec("pxy", "test/pxy/session")) + .addHop(HopSpec("dst", "test/dst/session")) + .addRoute(RouteSpec("test") + .addHop("pxy") + .addHop("dst"))); +} + +int +Test::Main() +{ + TEST_INIT("simple-roundtrip_test"); + + Slobrok slobrok; + TestServer srcNet(Identity("test/src"), getRouting(), slobrok); + TestServer pxyNet(Identity("test/pxy"), getRouting(), slobrok); + TestServer dstNet(Identity("test/dst"), getRouting(), slobrok); + + Receptor src; + Receptor pxy; + Receptor dst; + + SourceSession::UP ss = srcNet.mb.createSourceSession(src, SourceSessionParams()); + IntermediateSession::UP is = pxyNet.mb.createIntermediateSession("session", true, pxy, pxy); + DestinationSession::UP ds = dstNet.mb.createDestinationSession("session", true, dst); + + // wait for slobrok registration + ASSERT_TRUE(srcNet.waitSlobrok("test/pxy/session")); + ASSERT_TRUE(srcNet.waitSlobrok("test/dst/session")); + ASSERT_TRUE(pxyNet.waitSlobrok("test/dst/session")); + + // send message on client + ss->send(SimpleMessage::UP(new SimpleMessage("test message")), "test"); + + // check message on proxy + Message::UP msg = pxy.getMessage(); + ASSERT_TRUE(msg.get() != 0); + EXPECT_TRUE(msg->getProtocol() == SimpleProtocol::NAME); + EXPECT_TRUE(msg->getType() == SimpleProtocol::MESSAGE); + EXPECT_TRUE(static_cast<SimpleMessage&>(*msg).getValue() == "test message"); + + // forward message on proxy + static_cast<SimpleMessage&>(*msg).setValue("test message pxy"); + is->forward(std::move(msg)); + + // check message on server + msg = dst.getMessage(); + ASSERT_TRUE(msg.get() != 0); + EXPECT_TRUE(msg->getProtocol() == SimpleProtocol::NAME); + EXPECT_TRUE(msg->getType() == SimpleProtocol::MESSAGE); + EXPECT_TRUE(static_cast<SimpleMessage&>(*msg).getValue() == "test message pxy"); + + // send reply on server + SimpleReply::UP sr(new SimpleReply("test reply")); + msg->swapState(*sr); + ds->reply(Reply::UP(sr.release())); + + // check reply on proxy + Reply::UP reply = pxy.getReply(); + ASSERT_TRUE(reply.get() != 0); + EXPECT_TRUE(reply->getProtocol() == SimpleProtocol::NAME); + EXPECT_TRUE(reply->getType() == SimpleProtocol::REPLY); + EXPECT_TRUE(static_cast<SimpleReply&>(*reply).getValue() == "test reply"); + + // forward reply on proxy + static_cast<SimpleReply&>(*reply).setValue("test reply pxy"); + is->forward(std::move(reply)); + + // check reply on client + reply = src.getReply(); + ASSERT_TRUE(reply.get() != 0); + EXPECT_TRUE(reply->getProtocol() == SimpleProtocol::NAME); + EXPECT_TRUE(reply->getType() == SimpleProtocol::REPLY); + EXPECT_TRUE(static_cast<SimpleReply&>(*reply).getValue() == "test reply pxy"); + TEST_DONE(); +} diff --git a/messagebus/src/tests/simpleprotocol/.gitignore b/messagebus/src/tests/simpleprotocol/.gitignore new file mode 100644 index 00000000000..8a096b651d4 --- /dev/null +++ b/messagebus/src/tests/simpleprotocol/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +simpleprotocol_test +messagebus_simpleprotocol_test_app diff --git a/messagebus/src/tests/simpleprotocol/CMakeLists.txt b/messagebus/src/tests/simpleprotocol/CMakeLists.txt new file mode 100644 index 00000000000..4b4e777ea57 --- /dev/null +++ b/messagebus/src/tests/simpleprotocol/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_simpleprotocol_test_app + SOURCES + simpleprotocol.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_simpleprotocol_test_app COMMAND messagebus_simpleprotocol_test_app) diff --git a/messagebus/src/tests/simpleprotocol/DESC b/messagebus/src/tests/simpleprotocol/DESC new file mode 100644 index 00000000000..91d0fa36c57 --- /dev/null +++ b/messagebus/src/tests/simpleprotocol/DESC @@ -0,0 +1,3 @@ +Small test of the simple protocol defined in the test library. The +protocol will be used to test other messagebus features, including +cross-language compatibility. diff --git a/messagebus/src/tests/simpleprotocol/FILES b/messagebus/src/tests/simpleprotocol/FILES new file mode 100644 index 00000000000..f3c58f7d66e --- /dev/null +++ b/messagebus/src/tests/simpleprotocol/FILES @@ -0,0 +1 @@ +simpleprotocol.cpp diff --git a/messagebus/src/tests/simpleprotocol/simpleprotocol.cpp b/messagebus/src/tests/simpleprotocol/simpleprotocol.cpp new file mode 100644 index 00000000000..eaf609a2be1 --- /dev/null +++ b/messagebus/src/tests/simpleprotocol/simpleprotocol.cpp @@ -0,0 +1,72 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("simpleprotocol_test"); + +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/ireplyhandler.h> +#include <vespa/messagebus/network/identity.h> +#include <vespa/messagebus/routing/routingcontext.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/messagebus/vtag.h> + +using namespace mbus; + +TEST_SETUP(Test); + +int +Test::Main() +{ + TEST_INIT("simpleprotocol_test"); + + vespalib::Version version = Vtag::currentVersion; + SimpleProtocol protocol; + EXPECT_TRUE(protocol.getName() == "Simple"); + + { + // test protocol + IRoutingPolicy::UP bogus = protocol.createPolicy("bogus", ""); + EXPECT_TRUE(bogus.get() == 0); + } + TEST_FLUSH(); + { + // test SimpleMessage + Message::UP msg(new SimpleMessage("test")); + EXPECT_TRUE(!msg->isReply()); + EXPECT_TRUE(msg->getProtocol() == SimpleProtocol::NAME); + EXPECT_TRUE(msg->getType() == SimpleProtocol::MESSAGE); + EXPECT_TRUE(static_cast<SimpleMessage&>(*msg).getValue() == "test"); + Blob b = protocol.encode(version, *msg); + EXPECT_TRUE(b.size() > 0); + Routable::UP tmp = protocol.decode(version, BlobRef(b)); + ASSERT_TRUE(tmp.get() != 0); + EXPECT_TRUE(!tmp->isReply()); + EXPECT_TRUE(tmp->getProtocol() == SimpleProtocol::NAME); + EXPECT_TRUE(tmp->getType() == SimpleProtocol::MESSAGE); + EXPECT_TRUE(static_cast<SimpleMessage&>(*tmp).getValue() == "test"); + } + TEST_FLUSH(); + { + // test SimpleReply + Reply::UP reply(new SimpleReply("reply")); + EXPECT_TRUE(reply->isReply()); + EXPECT_TRUE(reply->getProtocol() == SimpleProtocol::NAME); + EXPECT_TRUE(reply->getType() == SimpleProtocol::REPLY); + EXPECT_TRUE(static_cast<SimpleReply&>(*reply).getValue() == "reply"); + Blob b = protocol.encode(version, *reply); + EXPECT_TRUE(b.size() > 0); + Routable::UP tmp = protocol.decode(version, BlobRef(b)); + ASSERT_TRUE(tmp.get() != 0); + EXPECT_TRUE(tmp->isReply()); + EXPECT_TRUE(tmp->getProtocol() == SimpleProtocol::NAME); + EXPECT_TRUE(tmp->getType() == SimpleProtocol::REPLY); + EXPECT_TRUE(static_cast<SimpleReply&>(*tmp).getValue() == "reply"); + } + TEST_DONE(); +} diff --git a/messagebus/src/tests/slobrok/.gitignore b/messagebus/src/tests/slobrok/.gitignore new file mode 100644 index 00000000000..6176a4876be --- /dev/null +++ b/messagebus/src/tests/slobrok/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +slobrok_test +messagebus_slobrok_test_app diff --git a/messagebus/src/tests/slobrok/CMakeLists.txt b/messagebus/src/tests/slobrok/CMakeLists.txt new file mode 100644 index 00000000000..d21768c1f6b --- /dev/null +++ b/messagebus/src/tests/slobrok/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_slobrok_test_app + SOURCES + slobrok.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_slobrok_test_app COMMAND messagebus_slobrok_test_app) diff --git a/messagebus/src/tests/slobrok/DESC b/messagebus/src/tests/slobrok/DESC new file mode 100644 index 00000000000..7d68f120d91 --- /dev/null +++ b/messagebus/src/tests/slobrok/DESC @@ -0,0 +1,2 @@ +A simple test to ensure we are able to perform +register/unregister/lookup of messagebus networks against the slobrok. diff --git a/messagebus/src/tests/slobrok/FILES b/messagebus/src/tests/slobrok/FILES new file mode 100644 index 00000000000..3fc79ffa0cb --- /dev/null +++ b/messagebus/src/tests/slobrok/FILES @@ -0,0 +1 @@ +slobrok.cpp diff --git a/messagebus/src/tests/slobrok/slobrok.cpp b/messagebus/src/tests/slobrok/slobrok.cpp new file mode 100644 index 00000000000..d51a5330cd5 --- /dev/null +++ b/messagebus/src/tests/slobrok/slobrok.cpp @@ -0,0 +1,134 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("slobrok_test"); +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <string> +#include <sstream> +#include <vespa/slobrok/sbmirror.h> +#include <vespa/messagebus/network/rpcnetwork.h> +#include <vespa/vespalib/util/host_name.h> + +using slobrok::api::IMirrorAPI; + +using namespace mbus; + +string +createSpec(int port) +{ + std::ostringstream str; + str << "tcp/"; + str << vespalib::HostName::get(); + str << ":"; + str << port; + return str.str(); +} + +struct SpecList +{ + IMirrorAPI::SpecList _specList; + SpecList() : _specList() {} + SpecList(IMirrorAPI::SpecList input) : _specList(input) {} + SpecList &add(const string &name, const string &spec) { + _specList.push_back(std::make_pair(string(name), + string(spec))); + return *this; + } + void sort() { + std::sort(_specList.begin(), _specList.end()); + } + bool operator==(SpecList &rhs) { // NB: MUTATE! + sort(); + rhs.sort(); + return _specList == rhs._specList; + } +}; + +bool +compare(const IMirrorAPI &api, const string &pattern, SpecList expect) +{ + for (int i = 0; i < 250; ++i) { + SpecList actual(api.lookup(pattern)); + if (actual == expect) { + return true; + } + FastOS_Thread::Sleep(100); + } + return false; +} + +TEST_SETUP(Test); + +int +Test::Main() +{ + TEST_INIT("slobrok_test"); + Slobrok slobrok; + RPCNetwork net1(RPCNetworkParams() + .setIdentity(Identity("net/a")) + .setSlobrokConfig(slobrok.config())); + RPCNetwork net2(RPCNetworkParams() + .setIdentity(Identity("net/b")) + .setSlobrokConfig(slobrok.config())); + RPCNetwork net3(RPCNetworkParams() + .setIdentity(Identity("net/c")) + .setSlobrokConfig(slobrok.config())); + ASSERT_TRUE(net1.start()); + ASSERT_TRUE(net2.start()); + ASSERT_TRUE(net3.start()); + string spec1 = createSpec(net1.getPort()); + string spec2 = createSpec(net2.getPort()); + string spec3 = createSpec(net3.getPort()); + + net1.registerSession("foo"); + net2.registerSession("foo"); + net2.registerSession("bar"); + net3.registerSession("foo"); + net3.registerSession("bar"); + net3.registerSession("baz"); + + EXPECT_TRUE(compare(net1.getMirror(), "*/*/*", SpecList() + .add("net/a/foo", spec1) + .add("net/b/foo", spec2) + .add("net/b/bar", spec2) + .add("net/c/foo", spec3) + .add("net/c/bar", spec3) + .add("net/c/baz", spec3))); + EXPECT_TRUE(compare(net2.getMirror(), "*/*/*", SpecList() + .add("net/a/foo", spec1) + .add("net/b/foo", spec2) + .add("net/b/bar", spec2) + .add("net/c/foo", spec3) + .add("net/c/bar", spec3) + .add("net/c/baz", spec3))); + EXPECT_TRUE(compare(net3.getMirror(), "*/*/*", SpecList() + .add("net/a/foo", spec1) + .add("net/b/foo", spec2) + .add("net/b/bar", spec2) + .add("net/c/foo", spec3) + .add("net/c/bar", spec3) + .add("net/c/baz", spec3))); + + net2.unregisterSession("bar"); + net3.unregisterSession("bar"); + net3.unregisterSession("baz"); + + EXPECT_TRUE(compare(net1.getMirror(), "*/*/*", SpecList() + .add("net/a/foo", spec1) + .add("net/b/foo", spec2) + .add("net/c/foo", spec3))); + EXPECT_TRUE(compare(net2.getMirror(), "*/*/*", SpecList() + .add("net/a/foo", spec1) + .add("net/b/foo", spec2) + .add("net/c/foo", spec3))); + EXPECT_TRUE(compare(net3.getMirror(), "*/*/*", SpecList() + .add("net/a/foo", spec1) + .add("net/b/foo", spec2) + .add("net/c/foo", spec3))); + + net3.shutdown(); + net2.shutdown(); + net1.shutdown(); + TEST_DONE(); +} diff --git a/messagebus/src/tests/sourcesession/.gitignore b/messagebus/src/tests/sourcesession/.gitignore new file mode 100644 index 00000000000..c6400268d94 --- /dev/null +++ b/messagebus/src/tests/sourcesession/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +sourcesession_test +messagebus_sourcesession_test_app diff --git a/messagebus/src/tests/sourcesession/CMakeLists.txt b/messagebus/src/tests/sourcesession/CMakeLists.txt new file mode 100644 index 00000000000..c2cf4f59682 --- /dev/null +++ b/messagebus/src/tests/sourcesession/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_sourcesession_test_app + SOURCES + sourcesession.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_sourcesession_test_app COMMAND messagebus_sourcesession_test_app) diff --git a/messagebus/src/tests/sourcesession/DESC b/messagebus/src/tests/sourcesession/DESC new file mode 100644 index 00000000000..3417f6b602b --- /dev/null +++ b/messagebus/src/tests/sourcesession/DESC @@ -0,0 +1,3 @@ +Simple test to verify the basic behavior of the resender and sequencer +components in a full setup. This test is complemented by the resender +and sequencer tests. diff --git a/messagebus/src/tests/sourcesession/FILES b/messagebus/src/tests/sourcesession/FILES new file mode 100644 index 00000000000..b7d8703e7f9 --- /dev/null +++ b/messagebus/src/tests/sourcesession/FILES @@ -0,0 +1 @@ +sourcesession.cpp diff --git a/messagebus/src/tests/sourcesession/sourcesession.cpp b/messagebus/src/tests/sourcesession/sourcesession.cpp new file mode 100644 index 00000000000..f70789d81af --- /dev/null +++ b/messagebus/src/tests/sourcesession/sourcesession.cpp @@ -0,0 +1,339 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("sourcesession_test"); + +#include <vespa/messagebus/destinationsession.h> +#include <vespa/messagebus/error.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/routablequeue.h> +#include <vespa/messagebus/sourcesession.h> +#include <vespa/messagebus/sourcesessionparams.h> +#include <vespa/messagebus/routing/retrytransienterrorspolicy.h> +#include <vespa/messagebus/routing/routingcontext.h> +#include <vespa/messagebus/routing/routingspec.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +struct DelayedHandler : public IMessageHandler +{ + DestinationSession::UP session; + uint32_t delay; + + DelayedHandler(MessageBus &mb, uint32_t d) : session(), delay(d) { + session = mb.createDestinationSession("session", true, *this); + } + ~DelayedHandler() { + session.reset(); + } + virtual void handleMessage(Message::UP msg) { + // this will block the transport thread in the server messagebus, + // but that should be ok, as we only want to test the timing in the + // client messagebus... + FastOS_Thread::Sleep(delay); + session->acknowledge(std::move(msg)); + } +}; + +RoutingSpec getRouting() { + return RoutingSpec() + .addTable(RoutingTableSpec("Simple") + .addHop(HopSpec("dst", "dst/session")) + .addRoute(RouteSpec("dst").addHop("dst"))); +} + +RoutingSpec getBadRouting() { + return RoutingSpec() + .addTable(RoutingTableSpec("Simple") + .addHop(HopSpec("dst", "dst/session")) + .addRoute(RouteSpec("dst").addHop("dst"))); +} + +bool waitQueueSize(RoutableQueue &queue, uint32_t size) { + for (uint32_t i = 0; i < 60000; ++i) { + if (queue.size() == size) { + return true; + } + FastOS_Thread::Sleep(1); + } + return false; +} + +class Test : public vespalib::TestApp +{ +public: + void testSequencing(); + void testResendError(); + void testResendConnDown(); + void testIllegalRoute(); + void testNoServices(); + void testBlockingClose(); + void testNonBlockingClose(); + int Main(); +}; + +void +Test::testSequencing() +{ + Slobrok slobrok; + TestServer src(Identity(""), getRouting(), slobrok); + TestServer dst(Identity("dst"), getRouting(), slobrok); + + RoutableQueue srcQ; + RoutableQueue dstQ; + + SourceSessionParams params; + params.setThrottlePolicy(IThrottlePolicy::SP()); + + SourceSession::UP ss = src.mb.createSourceSession(srcQ, params); + DestinationSession::UP ds = dst.mb.createDestinationSession("session", true, dstQ); + + ASSERT_TRUE(src.waitSlobrok("dst/session")); + + EXPECT_TRUE(ss->send(Message::UP(new SimpleMessage("foo", true, 1)), "dst").isAccepted()); + EXPECT_TRUE(ss->send(Message::UP(new SimpleMessage("foo", true, 2)), "dst").isAccepted()); + EXPECT_TRUE(ss->send(Message::UP(new SimpleMessage("foo", true, 1)), "dst").isAccepted()); + EXPECT_TRUE(waitQueueSize(dstQ, 2)); + FastOS_Thread::Sleep(250); + EXPECT_TRUE(waitQueueSize(dstQ, 2)); + EXPECT_TRUE(waitQueueSize(srcQ, 0)); + ds->acknowledge(Message::UP((Message*)dstQ.dequeue(0).release())); + ds->acknowledge(Message::UP((Message*)dstQ.dequeue(0).release())); + EXPECT_TRUE(waitQueueSize(srcQ, 2)); + EXPECT_TRUE(waitQueueSize(dstQ, 1)); + ds->acknowledge(Message::UP((Message*)dstQ.dequeue(0).release())); + ASSERT_TRUE(waitQueueSize(srcQ, 3)); + ASSERT_TRUE(waitQueueSize(dstQ, 0)); +} + +void +Test::testResendError() +{ + Slobrok slobrok; + RetryTransientErrorsPolicy::SP retryPolicy(new RetryTransientErrorsPolicy()); + retryPolicy->setBaseDelay(0); + TestServer src(MessageBusParams().addProtocol(IProtocol::SP(new SimpleProtocol())).setRetryPolicy(retryPolicy), + RPCNetworkParams().setSlobrokConfig(slobrok.config())); + src.mb.setupRouting(getRouting()); + TestServer dst(Identity("dst"), getRouting(), slobrok); + + RoutableQueue srcQ; + RoutableQueue dstQ; + + SourceSession::UP ss = src.mb.createSourceSession(srcQ); + DestinationSession::UP ds = dst.mb.createDestinationSession("session", true, dstQ); + + ASSERT_TRUE(src.waitSlobrok("dst/session")); + + { + Message::UP msg(new SimpleMessage("foo")); + msg->getTrace().setLevel(9); + EXPECT_TRUE(ss->send(std::move(msg), "dst").isAccepted()); + } + EXPECT_TRUE(waitQueueSize(dstQ, 1)); + { + Routable::UP r = dstQ.dequeue(0); + Reply::UP reply(new EmptyReply()); + r->swapState(*reply); + reply->addError(Error(ErrorCode::FATAL_ERROR, "error")); + ds->reply(std::move(reply)); + } + EXPECT_TRUE(waitQueueSize(srcQ, 1)); + EXPECT_TRUE(waitQueueSize(dstQ, 0)); + + { + Message::UP msg(new SimpleMessage("foo")); + msg->getTrace().setLevel(9); + EXPECT_TRUE(ss->send(std::move(msg), "dst").isAccepted()); + } + EXPECT_TRUE(waitQueueSize(dstQ, 1)); + { + Routable::UP r = dstQ.dequeue(0); + Reply::UP reply(new EmptyReply()); + r->swapState(*reply); + reply->addError(Error(ErrorCode::TRANSIENT_ERROR, "error")); + ds->reply(std::move(reply)); + } + EXPECT_TRUE(waitQueueSize(dstQ, 1)); + EXPECT_TRUE(waitQueueSize(srcQ, 1)); + ds->acknowledge(Message::UP((Message*)dstQ.dequeue(0).release())); + ASSERT_TRUE(waitQueueSize(srcQ, 2)); + ASSERT_TRUE(waitQueueSize(dstQ, 0)); + { + string trace1 = srcQ.dequeue(0)->getTrace().toString(); + string trace2 = srcQ.dequeue(0)->getTrace().toString(); + fprintf(stderr, "\nTRACE DUMP:\n%s\n\n", trace1.c_str()); + fprintf(stderr, "\nTRACE DUMP:\n%s\n\n", trace2.c_str()); + } +} + +void +Test::testResendConnDown() +{ + Slobrok slobrok; + RetryTransientErrorsPolicy::SP retryPolicy(new RetryTransientErrorsPolicy()); + retryPolicy->setBaseDelay(0); + TestServer src(MessageBusParams().addProtocol(IProtocol::SP(new SimpleProtocol())).setRetryPolicy(retryPolicy), + RPCNetworkParams().setSlobrokConfig(slobrok.config())); + src.mb.setupRouting(RoutingSpec().addTable(RoutingTableSpec(SimpleProtocol::NAME) + .addHop(HopSpec("dst", "dst2/session")) + .addHop(HopSpec("pxy", "[All]").addRecipient("dst")) + .addRoute(RouteSpec("dst").addHop("pxy")))); + RoutableQueue srcQ; + SourceSession::UP ss = src.mb.createSourceSession(srcQ); + + TestServer dst(Identity("dst"), RoutingSpec(), slobrok); + RoutableQueue dstQ; + DestinationSession::UP ds = dst.mb.createDestinationSession("session", true, dstQ); + ASSERT_TRUE(src.waitSlobrok("dst/session", 1)); + + { + TestServer dst2(Identity("dst2"), RoutingSpec(), slobrok); + RoutableQueue dst2Q; + DestinationSession::UP ds2 = dst2.mb.createDestinationSession("session", true, dst2Q); + ASSERT_TRUE(src.waitSlobrok("dst2/session", 1)); + + Message::UP msg(new SimpleMessage("foo")); + msg->getTrace().setLevel(9); + EXPECT_TRUE(ss->send(std::move(msg), "dst").isAccepted()); + EXPECT_TRUE(waitQueueSize(dst2Q, 1)); + Routable::UP obj = dst2Q.dequeue(0); + obj->discard(); + src.mb.setupRouting(RoutingSpec().addTable(RoutingTableSpec(SimpleProtocol::NAME) + .addHop(HopSpec("dst", "dst/session")))); + } // dst2 goes down, resend with new config + + ASSERT_TRUE(waitQueueSize(dstQ, 1)); // fails + ASSERT_TRUE(waitQueueSize(srcQ, 0)); + ds->acknowledge(Message::UP((Message*)dstQ.dequeue(0).release())); + ASSERT_TRUE(waitQueueSize(srcQ, 1)); + ASSERT_TRUE(waitQueueSize(dstQ, 0)); + + string trace = srcQ.dequeue(0)->getTrace().toString(); + fprintf(stderr, "\nTRACE DUMP:\n%s\n\n", trace.c_str()); +} + +void +Test::testIllegalRoute() +{ + Slobrok slobrok; + TestServer src(MessageBusParams() + .addProtocol(IProtocol::SP(new SimpleProtocol())) + .setRetryPolicy(IRetryPolicy::SP()), + RPCNetworkParams() + .setSlobrokConfig(slobrok.config())); + src.mb.setupRouting(getRouting()); + + RoutableQueue srcQ; + SourceSession::UP ss = src.mb.createSourceSession(srcQ, SourceSessionParams()); + { + // no such hop + Message::UP msg(new SimpleMessage("foo")); + msg->getTrace().setLevel(9); + msg->setRoute(Route::parse("bogus")); + EXPECT_TRUE(ss->send(std::move(msg)).isAccepted()); + } + ASSERT_TRUE(waitQueueSize(srcQ, 1)); + { + while (srcQ.size() > 0) { + Routable::UP routable = srcQ.dequeue(0); + ASSERT_TRUE(routable->isReply()); + Reply::UP r(static_cast<Reply*>(routable.release())); + EXPECT_EQUAL(1u, r->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::NO_ADDRESS_FOR_SERVICE, r->getError(0).getCode()); + string trace = r->getTrace().toString(); + fprintf(stderr, "\nTRACE DUMP:\n%s\n\n", trace.c_str()); + } + } +} + +void +Test::testNoServices() +{ + Slobrok slobrok; + TestServer src(MessageBusParams() + .addProtocol(IProtocol::SP(new SimpleProtocol())) + .setRetryPolicy(IRetryPolicy::SP()), + RPCNetworkParams() + .setSlobrokConfig(slobrok.config())); + src.mb.setupRouting(getBadRouting()); + + RoutableQueue srcQ; + SourceSession::UP ss = src.mb.createSourceSession(srcQ); + { + // no services for hop + Message::UP msg(new SimpleMessage("foo")); + msg->getTrace().setLevel(9); + EXPECT_TRUE(ss->send(std::move(msg), "dst").isAccepted()); + } + ASSERT_TRUE(waitQueueSize(srcQ, 1)); + { + while (srcQ.size() > 0) { + Routable::UP routable = srcQ.dequeue(0); + ASSERT_TRUE(routable->isReply()); + Reply::UP r(static_cast<Reply*>(routable.release())); + EXPECT_TRUE(r->getNumErrors() == 1); + EXPECT_TRUE(r->getError(0).getCode() == ErrorCode::NO_ADDRESS_FOR_SERVICE); + string trace = r->getTrace().toString(); + fprintf(stderr, "\nTRACE DUMP:\n%s\n\n", trace.c_str()); + } + } +} + +void +Test::testBlockingClose() +{ + Slobrok slobrok; + TestServer src(Identity(""), getRouting(), slobrok); + TestServer dst(Identity("dst"), getRouting(), slobrok); + + RoutableQueue srcQ; + DelayedHandler dstH(dst.mb, 1000); + ASSERT_TRUE(src.waitSlobrok("dst/session")); + + SourceSessionParams params; + SourceSession::UP ss = src.mb.createSourceSession(srcQ, params); + + EXPECT_TRUE(ss->send(Message::UP(new SimpleMessage("foo")), "dst").isAccepted()); + ss->close(); + srcQ.handleMessage(Message::UP(new SimpleMessage("bogus"))); + Routable::UP routable = srcQ.dequeue(0); + EXPECT_TRUE(routable->isReply()); +} + +void +Test::testNonBlockingClose() +{ + Slobrok slobrok; + TestServer src(Identity(""), getRouting(), slobrok); + + RoutableQueue srcQ; + + SourceSessionParams params; + SourceSession::UP ss = src.mb.createSourceSession(srcQ, params); + ss->close(); // this should not hang +} + +int +Test::Main() +{ + TEST_INIT("sourcesession_test"); + testSequencing(); TEST_FLUSH(); + testResendError(); TEST_FLUSH(); + testResendConnDown(); TEST_FLUSH(); + testIllegalRoute(); TEST_FLUSH(); + testNoServices(); TEST_FLUSH(); + testBlockingClose(); TEST_FLUSH(); + testNonBlockingClose(); TEST_FLUSH(); + TEST_DONE(); +} + +TEST_APPHOOK(Test); diff --git a/messagebus/src/tests/targetpool/.gitignore b/messagebus/src/tests/targetpool/.gitignore new file mode 100644 index 00000000000..d6736ff12f1 --- /dev/null +++ b/messagebus/src/tests/targetpool/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +targetpool_test +messagebus_targetpool_test_app diff --git a/messagebus/src/tests/targetpool/CMakeLists.txt b/messagebus/src/tests/targetpool/CMakeLists.txt new file mode 100644 index 00000000000..2fedf07d03d --- /dev/null +++ b/messagebus/src/tests/targetpool/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_targetpool_test_app + SOURCES + targetpool.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_targetpool_test_app COMMAND messagebus_targetpool_test_app) diff --git a/messagebus/src/tests/targetpool/DESC b/messagebus/src/tests/targetpool/DESC new file mode 100644 index 00000000000..8ba567d0efd --- /dev/null +++ b/messagebus/src/tests/targetpool/DESC @@ -0,0 +1 @@ +targetpool test. Take a look at targetpool.cpp for details. diff --git a/messagebus/src/tests/targetpool/FILES b/messagebus/src/tests/targetpool/FILES new file mode 100644 index 00000000000..5fb34e2994b --- /dev/null +++ b/messagebus/src/tests/targetpool/FILES @@ -0,0 +1 @@ +targetpool.cpp diff --git a/messagebus/src/tests/targetpool/targetpool.cpp b/messagebus/src/tests/targetpool/targetpool.cpp new file mode 100644 index 00000000000..0e63be19547 --- /dev/null +++ b/messagebus/src/tests/targetpool/targetpool.cpp @@ -0,0 +1,100 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("targetpool_test"); + +#include <vespa/messagebus/vtag.h> +#include <vespa/messagebus/network/rpctargetpool.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/vespalib/testkit/testapp.h> + +using namespace mbus; + +class PoolTimer : public ITimer { +public: + uint64_t millis; + + PoolTimer() : millis(0) { + // empty + } + + uint64_t getMilliTime() const { + return millis; + } +}; + +TEST_SETUP(Test); + +int +Test::Main() +{ + TEST_INIT("targetpool_test"); + + // Necessary setup to be able to resolve targets. + Slobrok slobrok; + TestServer srv1(Identity("srv1"), RoutingSpec(), slobrok); + RPCServiceAddress adr1("", srv1.mb.getConnectionSpec()); + TestServer srv2(Identity("srv2"), RoutingSpec(), slobrok); + RPCServiceAddress adr2("", srv2.mb.getConnectionSpec()); + TestServer srv3(Identity("srv3"), RoutingSpec(), slobrok); + RPCServiceAddress adr3("", srv3.mb.getConnectionSpec()); + + FRT_Supervisor orb(1024u, 1); + ASSERT_TRUE(orb.Start()); + std::unique_ptr<PoolTimer> ptr(new PoolTimer()); + PoolTimer &timer = *ptr; + RPCTargetPool pool(std::move(ptr), 0.666); + + // Assert that all connections expire. + RPCTarget::SP target; + ASSERT_TRUE((target = pool.getTarget(orb, adr1)).get() != NULL); target.reset(); + ASSERT_TRUE((target = pool.getTarget(orb, adr2)).get() != NULL); target.reset(); + ASSERT_TRUE((target = pool.getTarget(orb, adr3)).get() != NULL); target.reset(); + EXPECT_EQUAL(3u, pool.size()); + for (uint32_t i = 0; i < 10; ++i) { + pool.flushTargets(false); + EXPECT_EQUAL(3u, pool.size()); + } + timer.millis += 999; + pool.flushTargets(false); + EXPECT_EQUAL(0u, pool.size()); + + // Assert that only idle connections expire. + ASSERT_TRUE((target = pool.getTarget(orb, adr1)).get() != NULL); target.reset(); + ASSERT_TRUE((target = pool.getTarget(orb, adr2)).get() != NULL); target.reset(); + ASSERT_TRUE((target = pool.getTarget(orb, adr3)).get() != NULL); target.reset(); + EXPECT_EQUAL(3u, pool.size()); + timer.millis += 444; + pool.flushTargets(false); + EXPECT_EQUAL(3u, pool.size()); + ASSERT_TRUE((target = pool.getTarget(orb, adr2)).get() != NULL); target.reset(); + ASSERT_TRUE((target = pool.getTarget(orb, adr3)).get() != NULL); target.reset(); + timer.millis += 444; + pool.flushTargets(false); + EXPECT_EQUAL(2u, pool.size()); + ASSERT_TRUE((target = pool.getTarget(orb, adr3)).get() != NULL); target.reset(); + timer.millis += 444; + pool.flushTargets(false); + EXPECT_EQUAL(1u, pool.size()); + timer.millis += 444; + pool.flushTargets(false); + EXPECT_EQUAL(0u, pool.size()); + + // Assert that connections never expire while they are referenced. + ASSERT_TRUE((target = pool.getTarget(orb, adr1)).get() != NULL); + EXPECT_EQUAL(1u, pool.size()); + for (int i = 0; i < 10; ++i) { + timer.millis += 999; + pool.flushTargets(false); + EXPECT_EQUAL(1u, pool.size()); + } + target.reset(); + timer.millis += 999; + pool.flushTargets(false); + EXPECT_EQUAL(0u, pool.size()); + + orb.ShutDown(true); + + TEST_DONE(); +} diff --git a/messagebus/src/tests/throttling/.gitignore b/messagebus/src/tests/throttling/.gitignore new file mode 100644 index 00000000000..86ed5cc9f60 --- /dev/null +++ b/messagebus/src/tests/throttling/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +throttling_test +messagebus_throttling_test_app diff --git a/messagebus/src/tests/throttling/CMakeLists.txt b/messagebus/src/tests/throttling/CMakeLists.txt new file mode 100644 index 00000000000..28c27971e6f --- /dev/null +++ b/messagebus/src/tests/throttling/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_throttling_test_app + SOURCES + throttling.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_throttling_test_app COMMAND messagebus_throttling_test_app) diff --git a/messagebus/src/tests/throttling/DESC b/messagebus/src/tests/throttling/DESC new file mode 100644 index 00000000000..4e8dfc56357 --- /dev/null +++ b/messagebus/src/tests/throttling/DESC @@ -0,0 +1 @@ +throttling test. Take a look at throttling.cpp for details. diff --git a/messagebus/src/tests/throttling/FILES b/messagebus/src/tests/throttling/FILES new file mode 100644 index 00000000000..037f6cd99b9 --- /dev/null +++ b/messagebus/src/tests/throttling/FILES @@ -0,0 +1 @@ +throttling.cpp diff --git a/messagebus/src/tests/throttling/throttling.cpp b/messagebus/src/tests/throttling/throttling.cpp new file mode 100644 index 00000000000..6d543318559 --- /dev/null +++ b/messagebus/src/tests/throttling/throttling.cpp @@ -0,0 +1,362 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("throttling_test"); + +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/destinationsession.h> +#include <vespa/messagebus/dynamicthrottlepolicy.h> +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/routablequeue.h> +#include <vespa/messagebus/routing/retrytransienterrorspolicy.h> +#include <vespa/messagebus/routing/routingspec.h> +#include <vespa/messagebus/sourcesession.h> +#include <vespa/messagebus/sourcesessionparams.h> +#include <vespa/messagebus/staticthrottlepolicy.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/testserver.h> + +using namespace mbus; + +//////////////////////////////////////////////////////////////////////////////// +// +// Utilities +// +//////////////////////////////////////////////////////////////////////////////// + +class DynamicTimer : public ITimer { +public: + uint64_t _millis; + + DynamicTimer() : _millis(0) { + // empty + } + + uint64_t getMilliTime() const { + return _millis; + } +}; + +RoutingSpec getRouting() +{ + return RoutingSpec() + .addTable(RoutingTableSpec("Simple") + .addHop(HopSpec("dst", "dst/session")) + .addRoute(RouteSpec("dst").addHop("dst"))); +} + +bool waitQueueSize(RoutableQueue &queue, uint32_t size) +{ + for (uint32_t i = 0; i < 10000; ++i) { + if (queue.size() == size) { + return true; + } + FastOS_Thread::Sleep(10); + } + return false; +} + +bool waitPending(SourceSession& session, uint32_t size) +{ + for (uint32_t i = 0; i < 60000; ++i) { + if (session.getPendingCount() == size) { + return true; + } + FastOS_Thread::Sleep(1); + } + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// +// Setup +// +//////////////////////////////////////////////////////////////////////////////// + +class Test : public vespalib::TestApp { +private: + uint32_t getWindowSize(DynamicThrottlePolicy &policy, DynamicTimer &timer, uint32_t maxPending); + +protected: + void testMaxPendingCount(); + void testMaxPendingSize(); + void testMinOne(); + void testDynamicWindowSize(); + void testIdleTimePeriod(); + void testMinWindowSize(); + void testMaxWindowSize(); + +public: + int Main(); +}; + +int +Test::Main() +{ + TEST_INIT("throttling_test"); + + testMaxPendingCount(); TEST_FLUSH(); + testMaxPendingSize(); TEST_FLUSH(); + testMinOne(); TEST_FLUSH(); + testDynamicWindowSize(); TEST_FLUSH(); + testIdleTimePeriod(); TEST_FLUSH(); + testMinWindowSize(); TEST_FLUSH(); + testMaxWindowSize(); TEST_FLUSH(); + + TEST_DONE(); +} + +TEST_APPHOOK(Test); + +//////////////////////////////////////////////////////////////////////////////// +// +// Tests +// +//////////////////////////////////////////////////////////////////////////////// + +void +Test::testMaxPendingCount() +{ + Slobrok slobrok; + TestServer src(Identity(""), getRouting(), slobrok); + TestServer dst(Identity("dst"), getRouting(), slobrok); + + RoutableQueue srcQ; + RoutableQueue dstQ; + + SourceSessionParams params; + StaticThrottlePolicy::SP policy(new StaticThrottlePolicy()); + policy->setMaxPendingCount(5); + policy->setMaxPendingSize(0); // unlimited + params.setThrottlePolicy(policy); + + SourceSession::UP ss = src.mb.createSourceSession(srcQ, params); + DestinationSession::UP ds = dst.mb.createDestinationSession("session", true, dstQ); + + ASSERT_TRUE(src.waitSlobrok("dst/session")); + + for (uint32_t i = 0; i < 5; ++i) { + EXPECT_TRUE(ss->send(Message::UP(new SimpleMessage("1234567890")), "dst").isAccepted()); + } + EXPECT_TRUE(!ss->send(Message::UP(new SimpleMessage("1234567890")), "dst").isAccepted()); + + EXPECT_TRUE(waitQueueSize(dstQ, 5)); + ds->acknowledge(Message::UP((Message*)dstQ.dequeue(0).release())); + ASSERT_TRUE(waitQueueSize(srcQ, 1)); + + EXPECT_TRUE(ss->send(Message::UP(new SimpleMessage("1234567890")), "dst").isAccepted()); + EXPECT_TRUE(!ss->send(Message::UP(new SimpleMessage("1234567890")), "dst").isAccepted()); + + EXPECT_TRUE(waitQueueSize(dstQ, 5)); + ds->acknowledge(Message::UP((Message*)dstQ.dequeue(0).release())); + ds->acknowledge(Message::UP((Message*)dstQ.dequeue(0).release())); + ASSERT_TRUE(waitQueueSize(srcQ, 3)); + + EXPECT_TRUE(ss->send(Message::UP(new SimpleMessage("1234567890")), "dst").isAccepted()); + EXPECT_TRUE(ss->send(Message::UP(new SimpleMessage("1234567890")), "dst").isAccepted()); + EXPECT_TRUE(!ss->send(Message::UP(new SimpleMessage("1234567890")), "dst").isAccepted()); + EXPECT_TRUE(!ss->send(Message::UP(new SimpleMessage("1234567890")), "dst").isAccepted()); + + EXPECT_TRUE(waitQueueSize(dstQ, 5)); + ds->acknowledge(Message::UP((Message*)dstQ.dequeue(0).release())); + ds->acknowledge(Message::UP((Message*)dstQ.dequeue(0).release())); + ds->acknowledge(Message::UP((Message*)dstQ.dequeue(0).release())); + ds->acknowledge(Message::UP((Message*)dstQ.dequeue(0).release())); + ds->acknowledge(Message::UP((Message*)dstQ.dequeue(0).release())); + ASSERT_TRUE(waitQueueSize(srcQ, 8)); + ASSERT_TRUE(waitQueueSize(dstQ, 0)); +} + +void +Test::testMaxPendingSize() +{ + ASSERT_TRUE(SimpleMessage("1234567890").getApproxSize() == 10); + ASSERT_TRUE(SimpleMessage("123456").getApproxSize() == 6); + ASSERT_TRUE(SimpleMessage("12345").getApproxSize() == 5); + ASSERT_TRUE(SimpleMessage("1").getApproxSize() == 1); + ASSERT_TRUE(SimpleMessage("").getApproxSize() == 0); + + Slobrok slobrok; + TestServer src(Identity(""), getRouting(), slobrok); + TestServer dst(Identity("dst"), getRouting(), slobrok); + + RoutableQueue srcQ; + RoutableQueue dstQ; + + SourceSessionParams params; + StaticThrottlePolicy::SP policy(new StaticThrottlePolicy()); + policy->setMaxPendingCount(0); // unlimited + policy->setMaxPendingSize(2); + params.setThrottlePolicy(policy); + + SourceSession::UP ss = src.mb.createSourceSession(srcQ, params); + DestinationSession::UP ds = dst.mb.createDestinationSession("session", true, dstQ); + + ASSERT_TRUE(src.waitSlobrok("dst/session")); + EXPECT_EQUAL(1u, SimpleMessage("1").getApproxSize()); + EXPECT_EQUAL(2u, SimpleMessage("12").getApproxSize()); + + EXPECT_TRUE(ss->send(Message::UP(new SimpleMessage("1")), "dst").isAccepted()); + EXPECT_TRUE(ss->send(Message::UP(new SimpleMessage("12")), "dst").isAccepted()); + EXPECT_TRUE(!ss->send(Message::UP(new SimpleMessage("1")), "dst").isAccepted()); + + EXPECT_TRUE(waitQueueSize(dstQ, 2)); + ds->acknowledge(Message::UP((Message*)dstQ.dequeue(0).release())); + ASSERT_TRUE(waitQueueSize(srcQ, 1)); + + EXPECT_TRUE(!ss->send(Message::UP(new SimpleMessage("1")), "dst").isAccepted()); + ds->acknowledge(Message::UP((Message*)dstQ.dequeue(0).release())); + ASSERT_TRUE(waitQueueSize(srcQ, 2)); + + EXPECT_TRUE(ss->send(Message::UP(new SimpleMessage("12")), "dst").isAccepted()); + EXPECT_TRUE(!ss->send(Message::UP(new SimpleMessage("1")), "dst").isAccepted()); + EXPECT_TRUE(waitQueueSize(dstQ, 1)); + ds->acknowledge(Message::UP((Message*)dstQ.dequeue(0).release())); + ASSERT_TRUE(waitQueueSize(srcQ, 3)); +} + +void +Test::testMinOne() +{ + ASSERT_TRUE(SimpleMessage("1234567890").getApproxSize() == 10); + ASSERT_TRUE(SimpleMessage("").getApproxSize() == 0); + + Slobrok slobrok; + TestServer src(Identity(""), getRouting(), slobrok); + TestServer dst(Identity("dst"), getRouting(), slobrok); + + RoutableQueue srcQ; + RoutableQueue dstQ; + + SourceSessionParams params; + StaticThrottlePolicy::SP policy(new StaticThrottlePolicy()); + policy->setMaxPendingCount(0); // unlimited + policy->setMaxPendingSize(5); + params.setThrottlePolicy(policy); + + SourceSession::UP ss = src.mb.createSourceSession(srcQ, params); + DestinationSession::UP ds = dst.mb.createDestinationSession("session", true, dstQ); + + ASSERT_TRUE(src.waitSlobrok("dst/session")); + + EXPECT_TRUE(ss->send(Message::UP(new SimpleMessage("1234567890")), "dst").isAccepted()); + EXPECT_TRUE(!ss->send(Message::UP(new SimpleMessage("")), "dst").isAccepted()); + + EXPECT_TRUE(waitQueueSize(dstQ, 1)); + ds->acknowledge(Message::UP((Message*)dstQ.dequeue(0).release())); + ASSERT_TRUE(waitQueueSize(srcQ, 1)); + EXPECT_TRUE(waitQueueSize(dstQ, 0)); +} + + +void +Test::testDynamicWindowSize() +{ + std::unique_ptr<DynamicTimer> ptr(new DynamicTimer()); + DynamicTimer *timer = ptr.get(); + DynamicThrottlePolicy policy(std::move(ptr)); + + policy.setWindowSizeIncrement(5); + + double windowSize = getWindowSize(policy, *timer, 100); + ASSERT_TRUE(windowSize >= 90 && windowSize <= 110); + + windowSize = getWindowSize(policy, *timer, 200); + ASSERT_TRUE(windowSize >= 90 && windowSize <= 210); + + windowSize = getWindowSize(policy, *timer, 50); + ASSERT_TRUE(windowSize >= 9 && windowSize <= 55); + + windowSize = getWindowSize(policy, *timer, 500); + ASSERT_TRUE(windowSize >= 90 && windowSize <= 505); + + windowSize = getWindowSize(policy, *timer, 100); + ASSERT_TRUE(windowSize >= 90 && windowSize <= 110); +} + +void +Test::testIdleTimePeriod() +{ + ITimer::UP ptr(new DynamicTimer()); + DynamicTimer *timer = static_cast<DynamicTimer*>(ptr.get()); + DynamicThrottlePolicy policy(std::move(ptr)); + + policy.setWindowSizeIncrement(5); + + double windowSize = getWindowSize(policy, *timer, 100); + ASSERT_TRUE(windowSize >= 90 && windowSize <= 110); + + SimpleMessage msg("foo"); + timer->_millis += 30001; + ASSERT_TRUE(policy.canSend(msg, 0)); + ASSERT_TRUE(windowSize >= 90 && windowSize <= 110); + + timer->_millis += 60001; + ASSERT_TRUE(policy.canSend(msg, 50)); + EXPECT_EQUAL(55u, policy.getMaxPendingCount()); + + timer->_millis += 60001; + ASSERT_TRUE(policy.canSend(msg, 0)); + EXPECT_EQUAL(5u, policy.getMaxPendingCount()); +} + +void +Test::testMinWindowSize() +{ + ITimer::UP ptr(new DynamicTimer()); + DynamicTimer *timer = static_cast<DynamicTimer*>(ptr.get()); + DynamicThrottlePolicy policy(std::move(ptr)); + + policy.setWindowSizeIncrement(5); + policy.setMinWindowSize(150); + + double windowSize = getWindowSize(policy, *timer, 200); + ASSERT_TRUE(windowSize >= 150 && windowSize <= 210); +} + +void +Test::testMaxWindowSize() +{ + ITimer::UP ptr(new DynamicTimer()); + DynamicTimer *timer = static_cast<DynamicTimer*>(ptr.get()); + DynamicThrottlePolicy policy(std::move(ptr)); + + policy.setWindowSizeIncrement(5); + policy.setMaxWindowSize(50); + + double windowSize = getWindowSize(policy, *timer, 100); + ASSERT_TRUE(windowSize >= 40 && windowSize <= 50); + + policy.setMaxPendingCount(15); + windowSize = getWindowSize(policy, *timer, 100); + ASSERT_TRUE(windowSize >= 10 && windowSize <= 15); + +} + +uint32_t +Test::getWindowSize(DynamicThrottlePolicy &policy, DynamicTimer &timer, uint32_t maxPending) +{ + SimpleMessage msg("foo"); + SimpleReply reply("bar"); + + for (uint32_t i = 0; i < 999; ++i) { + uint32_t numPending = 0; + while (policy.canSend(msg, numPending)) { + policy.processMessage(msg); + ++numPending; + } + + uint64_t tripTime = (numPending < maxPending) ? 1000 : 1000 + (numPending - maxPending) * 1000; + timer._millis += tripTime; + + for( ; numPending > 0 ; --numPending) { + policy.processReply(reply); + } + } + uint32_t ret = policy.getMaxPendingCount(); + printf("getWindowSize() = %d\n", ret); + return ret; +} diff --git a/messagebus/src/tests/timeout/.gitignore b/messagebus/src/tests/timeout/.gitignore new file mode 100644 index 00000000000..c63e63d1685 --- /dev/null +++ b/messagebus/src/tests/timeout/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +timeout_test +messagebus_timeout_test_app diff --git a/messagebus/src/tests/timeout/CMakeLists.txt b/messagebus/src/tests/timeout/CMakeLists.txt new file mode 100644 index 00000000000..f50b81ff03f --- /dev/null +++ b/messagebus/src/tests/timeout/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_timeout_test_app + SOURCES + timeout.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_timeout_test_app COMMAND messagebus_timeout_test_app) diff --git a/messagebus/src/tests/timeout/DESC b/messagebus/src/tests/timeout/DESC new file mode 100644 index 00000000000..c90169db16e --- /dev/null +++ b/messagebus/src/tests/timeout/DESC @@ -0,0 +1 @@ +timeout test. Take a look at timeout.cpp for details. diff --git a/messagebus/src/tests/timeout/FILES b/messagebus/src/tests/timeout/FILES new file mode 100644 index 00000000000..b36cdeb4ddf --- /dev/null +++ b/messagebus/src/tests/timeout/FILES @@ -0,0 +1 @@ +timeout.cpp diff --git a/messagebus/src/tests/timeout/timeout.cpp b/messagebus/src/tests/timeout/timeout.cpp new file mode 100644 index 00000000000..8d6b1739776 --- /dev/null +++ b/messagebus/src/tests/timeout/timeout.cpp @@ -0,0 +1,83 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("timeout_test"); +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/sourcesession.h> +#include <vespa/messagebus/sourcesessionparams.h> +#include <vespa/messagebus/destinationsession.h> +#include <vespa/messagebus/network/identity.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> + +using namespace mbus; + +class Test : public vespalib::TestApp { +public: + int Main(); + void testZeroTimeout(); + void testMessageExpires(); +}; + +TEST_APPHOOK(Test); + +int +Test::Main() +{ + TEST_INIT("timeout_test"); + + testZeroTimeout(); TEST_FLUSH(); + testMessageExpires(); TEST_FLUSH(); + + TEST_DONE(); +} + +void +Test::testZeroTimeout() +{ + Slobrok slobrok; + TestServer srcServer(Identity("src"), RoutingSpec(), slobrok); + TestServer dstServer(Identity("dst"), RoutingSpec(), slobrok); + + Receptor srcHandler; + SourceSession::UP srcSession = srcServer.mb.createSourceSession(srcHandler, SourceSessionParams().setTimeout(0)); + Receptor dstHandler; + DestinationSession::UP dstSession = dstServer.mb.createDestinationSession("session", true, dstHandler); + + ASSERT_TRUE(srcServer.waitSlobrok("dst/session", 1)); + ASSERT_TRUE(srcSession->send(Message::UP(new SimpleMessage("msg")), "dst/session", true).isAccepted()); + + Reply::UP reply = srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::TIMEOUT, reply->getError(0).getCode()); +} + +void +Test::testMessageExpires() +{ + Slobrok slobrok; + TestServer srcServer(Identity("src"), RoutingSpec(), slobrok); + TestServer dstServer(Identity("dst"), RoutingSpec(), slobrok); + + Receptor srcHandler, dstHandler; + SourceSession::UP srcSession = srcServer.mb.createSourceSession(srcHandler, SourceSessionParams().setTimeout(1)); + DestinationSession::UP dstSession = dstServer.mb.createDestinationSession("session", true, dstHandler); + + ASSERT_TRUE(srcServer.waitSlobrok("dst/session", 1)); + ASSERT_TRUE(srcSession->send(Message::UP(new SimpleMessage("msg")), "dst/session", true).isAccepted()); + + Reply::UP reply = srcHandler.getReply(); + ASSERT_TRUE(reply.get() != NULL); + EXPECT_EQUAL(1u, reply->getNumErrors()); + EXPECT_EQUAL((uint32_t)ErrorCode::TIMEOUT, reply->getError(0).getCode()); + + Message::UP msg = dstHandler.getMessage(1); + if (msg.get() != NULL) { + msg->discard(); + } +} diff --git a/messagebus/src/tests/trace-roundtrip/.gitignore b/messagebus/src/tests/trace-roundtrip/.gitignore new file mode 100644 index 00000000000..cd1669fd7a8 --- /dev/null +++ b/messagebus/src/tests/trace-roundtrip/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +trace-roundtrip_test +messagebus_trace-roundtrip_test_app diff --git a/messagebus/src/tests/trace-roundtrip/CMakeLists.txt b/messagebus/src/tests/trace-roundtrip/CMakeLists.txt new file mode 100644 index 00000000000..94ed14c8d99 --- /dev/null +++ b/messagebus/src/tests/trace-roundtrip/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_trace-roundtrip_test_app + SOURCES + trace-roundtrip.cpp + DEPENDS + messagebus_messagebus-test + messagebus +) +vespa_add_test(NAME messagebus_trace-roundtrip_test_app COMMAND messagebus_trace-roundtrip_test_app) diff --git a/messagebus/src/tests/trace-roundtrip/DESC b/messagebus/src/tests/trace-roundtrip/DESC new file mode 100644 index 00000000000..eb0d3b38e63 --- /dev/null +++ b/messagebus/src/tests/trace-roundtrip/DESC @@ -0,0 +1 @@ +trace-roundtrip test. Take a look at trace-roundtrip.cpp for details. diff --git a/messagebus/src/tests/trace-roundtrip/FILES b/messagebus/src/tests/trace-roundtrip/FILES new file mode 100644 index 00000000000..13b3374345f --- /dev/null +++ b/messagebus/src/tests/trace-roundtrip/FILES @@ -0,0 +1 @@ +trace-roundtrip.cpp diff --git a/messagebus/src/tests/trace-roundtrip/trace-roundtrip.cpp b/messagebus/src/tests/trace-roundtrip/trace-roundtrip.cpp new file mode 100644 index 00000000000..72158383807 --- /dev/null +++ b/messagebus/src/tests/trace-roundtrip/trace-roundtrip.cpp @@ -0,0 +1,127 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("simple-roundtrip_test"); +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/sourcesession.h> +#include <vespa/messagebus/intermediatesession.h> +#include <vespa/messagebus/destinationsession.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/messagebus/routing/routingspec.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/sourcesessionparams.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> + +using namespace mbus; + +//----------------------------------------------------------------------------- + +class Proxy : public IMessageHandler, + public IReplyHandler +{ +private: + IntermediateSession::UP _session; +public: + Proxy(MessageBus &bus); + void handleMessage(Message::UP msg); + void handleReply(Reply::UP reply); +}; + +Proxy::Proxy(MessageBus &bus) + : _session(bus.createIntermediateSession("session", true, *this, *this)) +{ +} + +void +Proxy::handleMessage(Message::UP msg) { + msg->getTrace().trace(1, "Proxy message", false); + _session->forward(std::move(msg)); +} + +void +Proxy::handleReply(Reply::UP reply) { + reply->getTrace().trace(1, "Proxy reply", false); + _session->forward(std::move(reply)); +} + +//----------------------------------------------------------------------------- + +class Server : public IMessageHandler +{ +private: + DestinationSession::UP _session; +public: + Server(MessageBus &bus); + void handleMessage(Message::UP msg); +}; + +Server::Server(MessageBus &bus) + : _session(bus.createDestinationSession("session", true, *this)) +{ +} + +void +Server::handleMessage(Message::UP msg) { + msg->getTrace().trace(1, "Server message", false); + Reply::UP reply(new EmptyReply()); + msg->swapState(*reply); + reply->getTrace().trace(1, "Server reply", false); + _session->reply(std::move(reply)); +} + +//----------------------------------------------------------------------------- + +TEST_SETUP(Test); + +RoutingSpec getRouting() { + return RoutingSpec() + .addTable(RoutingTableSpec("Simple") + .addHop(HopSpec("pxy", "test/pxy/session")) + .addHop(HopSpec("dst", "test/dst/session")) + .addRoute(RouteSpec("test").addHop("pxy").addHop("dst"))); +} + +int +Test::Main() +{ + TEST_INIT("simple-roundtrip_test"); + + Slobrok slobrok; + TestServer srcNet(Identity("test/src"), getRouting(), slobrok); + TestServer pxyNet(Identity("test/pxy"), getRouting(), slobrok); + TestServer dstNet(Identity("test/dst"), getRouting(), slobrok); + + Receptor src; + Proxy pxy(pxyNet.mb); + Server dst(dstNet.mb); + + SourceSession::UP ss = srcNet.mb.createSourceSession(src, SourceSessionParams()); + + // wait for slobrok registration + ASSERT_TRUE(srcNet.waitSlobrok("test/pxy/session")); + ASSERT_TRUE(srcNet.waitSlobrok("test/dst/session")); + ASSERT_TRUE(pxyNet.waitSlobrok("test/dst/session")); + + Message::UP msg(new SimpleMessage("")); + msg->getTrace().setLevel(1); + msg->getTrace().trace(1, "Client message", false); + ss->send(std::move(msg), "test"); + Reply::UP reply = src.getReply(); + reply->getTrace().trace(1, "Client reply", false); + EXPECT_TRUE(reply->getNumErrors() == 0); + + TraceNode t = TraceNode() + .addChild("Client message") + .addChild("Proxy message") + .addChild("Server message") + .addChild("Server reply") + .addChild("Proxy reply") + .addChild("Client reply"); + EXPECT_TRUE(reply->getTrace().getRoot().encode() == t.encode()); + TEST_DONE(); +} diff --git a/messagebus/src/vespa/messagebus/.gitignore b/messagebus/src/vespa/messagebus/.gitignore new file mode 100644 index 00000000000..873d9af7094 --- /dev/null +++ b/messagebus/src/vespa/messagebus/.gitignore @@ -0,0 +1,7 @@ +.depend +Makefile +config-messagebus.cpp +config-messagebus.h +messagebus.def +printversion +/libmessagebus.so.5.1 diff --git a/messagebus/src/vespa/messagebus/CMakeLists.txt b/messagebus/src/vespa/messagebus/CMakeLists.txt new file mode 100644 index 00000000000..81b86d8e0dc --- /dev/null +++ b/messagebus/src/vespa/messagebus/CMakeLists.txt @@ -0,0 +1,41 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_library(messagebus + SOURCES + blob.cpp + blobref.cpp + callstack.cpp + configagent.cpp + destinationsession.cpp + destinationsessionparams.cpp + dynamicthrottlepolicy.cpp + emptyreply.cpp + error.cpp + errorcode.cpp + intermediatesession.cpp + intermediatesessionparams.cpp + message.cpp + messagebus.cpp + messagebusparams.cpp + messenger.cpp + protocolrepository.cpp + protocolset.cpp + reply.cpp + replygate.cpp + result.cpp + routable.cpp + routablequeue.cpp + rpcmessagebus.cpp + sendproxy.cpp + sequencer.cpp + sourcesession.cpp + sourcesessionparams.cpp + staticthrottlepolicy.cpp + systemtimer.cpp + vtag.cpp + $<TARGET_OBJECTS:messagebus_routing> + $<TARGET_OBJECTS:messagebus_network> + INSTALL lib64 + DEPENDS +) +vespa_generate_config(messagebus ../../main/config/messagebus.def) +install(FILES ../../main/config/messagebus.def DESTINATION var/db/vespa/config_server/serverdb/classes) diff --git a/messagebus/src/vespa/messagebus/blob.cpp b/messagebus/src/vespa/messagebus/blob.cpp new file mode 100644 index 00000000000..ec5a8978b2e --- /dev/null +++ b/messagebus/src/vespa/messagebus/blob.cpp @@ -0,0 +1,10 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".blob"); +#include "blob.h" + +namespace mbus { + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/blob.h b/messagebus/src/vespa/messagebus/blob.h new file mode 100644 index 00000000000..101ea989a92 --- /dev/null +++ b/messagebus/src/vespa/messagebus/blob.h @@ -0,0 +1,67 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <vespa/vespalib/util/alloc.h> + +namespace mbus { + +/** + * This class encapsulates a blob that is owned by the object. Objects + * of this class have destructive copy. Use Blob objects when you want + * to transfer the ownership of a blob, like when it is returned by a + * method. + **/ +class Blob +{ +public: + /** + * Create a blob that will contain uninitialized memory with the + * given size. + * + * @param s size of the data to be created + **/ + Blob(uint32_t s) : + _payload(s), + _sz(s) + { } + Blob(Blob && rhs) : + _payload(std::move(rhs._payload)), + _sz(rhs._sz) + { + rhs._sz = 0; + } + Blob & operator = (Blob && rhs) { + swap(rhs); + return *this; + } + + void swap(Blob & rhs) { + _payload.swap(rhs._payload); + std::swap(_sz, rhs._sz); + } + + /** + * Obtain the data owned by this Blob + * + * @return data + **/ + char *data() { return static_cast<char *>(_payload.get()); } + + /** + * Obtain the data owned by this Blob + * + * @return data + **/ + const char *data() const { return static_cast<const char *>(_payload.get()); } + + vespalib::DefaultAlloc & payload() { return _payload; } + const vespalib::DefaultAlloc & payload() const { return _payload; } + size_t size() const { return _sz; } +private: + vespalib::DefaultAlloc _payload; + size_t _sz; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/blobref.cpp b/messagebus/src/vespa/messagebus/blobref.cpp new file mode 100644 index 00000000000..24109b38109 --- /dev/null +++ b/messagebus/src/vespa/messagebus/blobref.cpp @@ -0,0 +1,10 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".blobref"); +#include "blobref.h" + +namespace mbus { + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/blobref.h b/messagebus/src/vespa/messagebus/blobref.h new file mode 100644 index 00000000000..1ade53749df --- /dev/null +++ b/messagebus/src/vespa/messagebus/blobref.h @@ -0,0 +1,55 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <stdint.h> +#include "blob.h" + +namespace mbus { + +/** + * This class encapsulates a reference to a blob owned by someone + * else. This object can be copied freely, but does not own + * anything. This means that parameters of this class will typically + * only be valid during the invocation of the current method. + **/ +class BlobRef +{ +private: + const char *_data; // default copy is ok + uint32_t _size; + +public: + /** + * Create a new BlobRef referring to the given memory. + * + * @param d the actual data + * @param s the size of the data in bytes + **/ + BlobRef(const char *d, uint32_t s) : _data(d), _size(s) { } + + /** + * Create a new BlobRef referring the memory owned by the given + * Blob. + * + * @param b blob owning the data we want a reference to + **/ + BlobRef(const Blob &b) : _data(b.data()), _size(b.size()) { } + + /** + * Obtain a pointer to the raw data referenced by this object. + * + * @return raw data pointer + **/ + const char *data() const { return _data; } + + /** + * Obtain the size of the data referenced by this object + * + * @return raw data size + **/ + uint32_t size() const { return _size; } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/callstack.cpp b/messagebus/src/vespa/messagebus/callstack.cpp new file mode 100644 index 00000000000..87cff3088bf --- /dev/null +++ b/messagebus/src/vespa/messagebus/callstack.cpp @@ -0,0 +1,53 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".callstack"); +#include "callstack.h" +#include "message.h" +#include "reply.h" +#include "idiscardhandler.h" + +namespace mbus { + +void +CallStack::discard() +{ + while (!_stack.empty()) { + const Frame &frame = _stack.back(); + if (frame.discardHandler != NULL) { + frame.discardHandler->handleDiscard(frame.ctx); + } + _stack.pop_back(); + } +} + +void +CallStack::swap(CallStack &dst) +{ + _stack.swap(dst._stack); +} + +void +CallStack::push(IReplyHandler &replyHandler, Context ctx, + IDiscardHandler *discardHandler) +{ + Frame frame; + frame.replyHandler = &replyHandler; + frame.discardHandler = discardHandler; + frame.ctx = ctx; + _stack.push_back(frame); +} + +IReplyHandler & +CallStack::pop(Reply &reply) +{ + LOG_ASSERT(!_stack.empty()); + const Frame &frame = _stack.back(); + IReplyHandler *handler = frame.replyHandler; + reply.setContext(frame.ctx); + _stack.pop_back(); + return *handler; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/callstack.h b/messagebus/src/vespa/messagebus/callstack.h new file mode 100644 index 00000000000..80e673ee550 --- /dev/null +++ b/messagebus/src/vespa/messagebus/callstack.h @@ -0,0 +1,88 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <boost/utility.hpp> +#include <vector> +#include "context.h" + +namespace mbus { + +class IDiscardHandler; +class IReplyHandler; +class Reply; + +/** + * A CallStack is used to ensure that a Reply travels the inverse path + * of its Message. Each Routable has a CallStack used to track its + * path. Each stack frame contains a pointer to an IReplyHandler and a + * message Context for that handler. Note that a CallStack does not + * own any objects. Also note that the CallStack object will not be + * copied when copying a Routable, as it is not part of the object + * value. This class is intended for internal messagebus use only. + **/ +class CallStack : public boost::noncopyable +{ +private: + struct Frame { + IReplyHandler *replyHandler; + IDiscardHandler *discardHandler; + Context ctx; + }; + + typedef std::vector<Frame> Stack; + + Stack _stack; + +public: + /** + * Create a new empty CallStack. + **/ + CallStack() : _stack() { } + + /** + * Swap the content of this and the argument stack. + * + * @param dst The stack to swap content with. + **/ + void swap(CallStack &dst); + + /** + * Discard this CallStack. This method should only be used when you are + * certain that it is safe to just throw away the stack. It has similar + * effects to stopping a thread, you need to know where it is safe to do so. + **/ + void discard(); + + /** + * Obtain the number of frames currently on this stack. + * + * @return stack size in frames + **/ + uint32_t size() const { return _stack.size(); } + + /** + * Push a frame on this stack. The discard handler is an optional handler, + * and may be null. + * + * @param replyHandler The handler for the correponding reply. + * @param ctx The context to store. + * @param discardHandler The handler for discarded messages. + **/ + void push(IReplyHandler &replyHandler, Context ctx, + IDiscardHandler *discardHandler = NULL); + + /** + * Pop a frame from this stack. The handler part of the frame will + * be returned and the context part will be set on the given + * Reply. Invoke this method on an empty stack and terrible things + * will happen. + * + * @return the next handler on the stack + * @param reply Reply that will receive the next context + **/ + IReplyHandler &pop(Reply &reply); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/common.h b/messagebus/src/vespa/messagebus/common.h new file mode 100644 index 00000000000..6b12c642a68 --- /dev/null +++ b/messagebus/src/vespa/messagebus/common.h @@ -0,0 +1,13 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vespa/vespalib/stllike/string.h> +#include <vespa/vespalib/util/vstringfmt.h> + +namespace mbus { + +// Decide the type of string used once +typedef vespalib::string string; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/configagent.cpp b/messagebus/src/vespa/messagebus/configagent.cpp new file mode 100644 index 00000000000..34fb0849f43 --- /dev/null +++ b/messagebus/src/vespa/messagebus/configagent.cpp @@ -0,0 +1,50 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".configagent"); +#include <vespa/messagebus/routing/routingspec.h> +#include "configagent.h" +#include "iconfighandler.h" + +using namespace config; +using namespace messagebus; + +namespace mbus { + +ConfigAgent::ConfigAgent(IConfigHandler & handler) + : _handler(handler) +{ } + +void +ConfigAgent::configure(std::unique_ptr<MessagebusConfig> config) +{ + const MessagebusConfig &cfg(*config); + RoutingSpec spec; + typedef MessagebusConfig CFG; + for (uint32_t t = 0; t < cfg.routingtable.size(); ++t) { + const CFG::Routingtable &table = cfg.routingtable[t]; + RoutingTableSpec tableSpec(table.protocol); + for (uint32_t h = 0; h < table.hop.size(); ++h) { + const CFG::Routingtable::Hop &hop = table.hop[h]; + HopSpec hopSpec(hop.name, hop.selector); + for (uint32_t i = 0; i < hop.recipient.size(); ++i) { + hopSpec.addRecipient(hop.recipient[i]); + } + hopSpec.setIgnoreResult(hop.ignoreresult); + tableSpec.addHop(hopSpec); + } + for (uint32_t r = 0; r < table.route.size(); ++r) { + const CFG::Routingtable::Route &route = table.route[r]; + RouteSpec routeSpec(route.name); + for (uint32_t i = 0; i < route.hop.size(); ++i) { + routeSpec.addHop(route.hop[i]); + } + tableSpec.addRoute(routeSpec); + } + spec.addTable(tableSpec); + } + _handler.setupRouting(spec); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/configagent.h b/messagebus/src/vespa/messagebus/configagent.h new file mode 100644 index 00000000000..82724667336 --- /dev/null +++ b/messagebus/src/vespa/messagebus/configagent.h @@ -0,0 +1,32 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <memory> +#include <vespa/messagebus/common.h> +#include <vespa/config/helper/configfetcher.h> +#include <vespa/messagebus/config-messagebus.h> + +namespace mbus { + +class IConfigHandler; + +/** + * A ConfigAgent will register with the config server and obtain + * config on behalf of a IConfigHandler. + **/ +class ConfigAgent : public config::IFetcherCallback<messagebus::MessagebusConfig>, + public vespalib::noncopyable +{ +private: + IConfigHandler &_handler; + +public: + ConfigAgent(IConfigHandler & handler); + + // Implements IFetcherCallback + void configure(std::unique_ptr<messagebus::MessagebusConfig> config); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/context.h b/messagebus/src/vespa/messagebus/context.h new file mode 100644 index 00000000000..087d41f8b80 --- /dev/null +++ b/messagebus/src/vespa/messagebus/context.h @@ -0,0 +1,56 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <string.h> +#include <stdint.h> + +namespace mbus { + +/** + * A context is an application specific unit of information that can + * be attached to a routable. Specifically, messagebus will ensure + * that when a reply is obtained, it will have the same context as the + * original message. + **/ +struct Context +{ + /** + * This is a region of memory that can be interpreted as either an + * integer, a floating-point number or a pointer. + **/ + union { + uint64_t UINT64; + double DOUBLE; + void *PTR; + } value; + + /** + * Create a context that is set to 0, however you interpret it. + **/ + Context() { memset(&value, 0, sizeof(value)); } + + /** + * Create a contex from an integer. + * + * @param v the value + **/ + Context(uint64_t v) { value.UINT64 = v; } + + /** + * Create a context from a floating-point number + * + * @param v the value + **/ + Context(double v) { value.DOUBLE = v; } + + /** + * Create a context from a pointer + * + * @param pt the pointer + **/ + Context(void *pt) { value.PTR = pt; } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/create-class-cpp.sh b/messagebus/src/vespa/messagebus/create-class-cpp.sh new file mode 100755 index 00000000000..f7c209427a8 --- /dev/null +++ b/messagebus/src/vespa/messagebus/create-class-cpp.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +class=$1 +guard=`echo $class | tr 'a-z' 'A-Z'` +name=`echo $class | tr 'A-Z' 'a-z'` + +cat <<EOF +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/log/log.h> +LOG_SETUP(".$name"); +#include <vespa/fastos/fastos.h> +#include "$name.h" + +namespace mbus { + +$class::$class() +{ +} + +$class::~$class() +{ +} + +} // namespace mbus +EOF diff --git a/messagebus/src/vespa/messagebus/create-class-h.sh b/messagebus/src/vespa/messagebus/create-class-h.sh new file mode 100755 index 00000000000..c4df87c4f8a --- /dev/null +++ b/messagebus/src/vespa/messagebus/create-class-h.sh @@ -0,0 +1,26 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once +#!/bin/sh + +class=$1 +guard=`echo $class | tr 'a-z' 'A-Z'` + +cat <<EOF +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + + +namespace mbus { + +class $class +{ +private: + $class(const $class &); + $class &operator=(const $class &); +public: + $class(); + virtual ~$class(); +}; + +} // namespace mbus + +EOF diff --git a/messagebus/src/vespa/messagebus/create-interface.sh b/messagebus/src/vespa/messagebus/create-interface.sh new file mode 100755 index 00000000000..e6f93b24355 --- /dev/null +++ b/messagebus/src/vespa/messagebus/create-interface.sh @@ -0,0 +1,22 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once +#!/bin/sh + +class=$1 +guard=`echo $class | tr 'a-z' 'A-Z'` + +cat <<EOF +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + + +namespace mbus { + +class $class +{ +public: + virtual ~$class() {} +}; + +} // namespace mbus + +EOF diff --git a/messagebus/src/vespa/messagebus/destinationsession.cpp b/messagebus/src/vespa/messagebus/destinationsession.cpp new file mode 100644 index 00000000000..2cf074ae3e2 --- /dev/null +++ b/messagebus/src/vespa/messagebus/destinationsession.cpp @@ -0,0 +1,60 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".destinationsession"); + +#include <vespa/vespalib/util/vstringfmt.h> +#include "destinationsession.h" +#include "messagebus.h" +#include "emptyreply.h" + +namespace mbus { + +DestinationSession::DestinationSession(MessageBus &mbus, const DestinationSessionParams ¶ms) : + _mbus(mbus), + _name(params.getName()), + _msgHandler(params.getMessageHandler()) +{ + // empty +} + +DestinationSession::~DestinationSession() +{ + close(); +} + +void +DestinationSession::close() +{ + _mbus.unregisterSession(_name); + _mbus.sync(); +} + +void +DestinationSession::acknowledge(Message::UP msg) +{ + Reply::UP ack(new EmptyReply()); + ack->swapState(*msg); + reply(std::move(ack)); +} + +void +DestinationSession::reply(Reply::UP ret) +{ + IReplyHandler &handler = ret->getCallStack().pop(*ret); + handler.handleReply(std::move(ret)); +} + +void +DestinationSession::handleMessage(Message::UP msg) +{ + _msgHandler.handleMessage(std::move(msg)); +} + +const string +DestinationSession::getConnectionSpec() const +{ + return _mbus.getConnectionSpec() + "/" + _name; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/destinationsession.h b/messagebus/src/vespa/messagebus/destinationsession.h new file mode 100644 index 00000000000..f381a5cd7b7 --- /dev/null +++ b/messagebus/src/vespa/messagebus/destinationsession.h @@ -0,0 +1,104 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <boost/utility.hpp> +#include <memory> +#include <string> +#include "destinationsessionparams.h" +#include "imessagehandler.h" +#include "reply.h" + +namespace mbus { + +class MessageBus; + +/** + * A DestinationSession is used to receive Message objects and reply + * with Reply objects. + */ +class DestinationSession : public boost::noncopyable, public IMessageHandler { +private: + friend class MessageBus; + + MessageBus &_mbus; + string _name; + IMessageHandler &_msgHandler; + + /** + * This constructor is package private since only MessageBus is supposed to + * instantiate it. + * + * @param mbus The message bus that created this instance. + * @param params The parameter object for this session. + */ + DestinationSession(MessageBus &mbus, const DestinationSessionParams ¶ms); + +public: + /** + * Convenience typedef for an auto pointer to a DestinationSession object. + */ + typedef std::unique_ptr<DestinationSession> UP; + + /** + * The destructor untangles from messagebus. After this method returns, + * messagebus will not invoke any handlers associated with this session. + */ + virtual ~DestinationSession(); + + /** + * This method unregisters this session from message bus, effectively + * disabling any more messages from being delivered to the message + * handler. After unregistering, this method calls {@link + * com.yahoo.messagebus.MessageBus#sync()} as to ensure that there are no + * threads currently entangled in the handler. + * + * This method will deadlock if you call it from the message handler. + */ + void close(); + + /** + * Convenience method used to acknowledge a Message. This method will create + * an EmptyReply object, transfer the state from the Message to it and + * invoke the reply method in this object. + * + * @param msg the Message you want to acknowledge + */ + void acknowledge(Message::UP msg); + + /** + * Send a Reply as a response to a Message. The Reply will be routed back to + * where the Message came from. For this to work, it is important that the + * messagebus state is transferred from the Message (you want to reply to) + * to the Reply (you want to reply with). This is done with the + * Routable::transferState method. + * + * @param reply the Reply + */ + void reply(Reply::UP reply); + + /** + * Handle a Message obtained from messagebus. + * + * @param message the Message + */ + void handleMessage(Message::UP message); + + /** + * Returns the message handler of this session. + * + * @return The message handler. + */ + IMessageHandler &getMessageHandler() { return _msgHandler; } + + /** + * Returns the connection spec string for this session. This returns a + * combination of the owning message bus' own spec string and the name of + * this session. + * + * @return The connection string. + */ + const string getConnectionSpec() const; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/destinationsessionparams.cpp b/messagebus/src/vespa/messagebus/destinationsessionparams.cpp new file mode 100644 index 00000000000..38e8e4c8a3e --- /dev/null +++ b/messagebus/src/vespa/messagebus/destinationsessionparams.cpp @@ -0,0 +1,15 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include "destinationsessionparams.h" + +namespace mbus { + +DestinationSessionParams::DestinationSessionParams() : + _name("destination"), + _broadcastName(true), + _handler(NULL) +{ + // empty +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/destinationsessionparams.h b/messagebus/src/vespa/messagebus/destinationsessionparams.h new file mode 100644 index 00000000000..4026cdbff91 --- /dev/null +++ b/messagebus/src/vespa/messagebus/destinationsessionparams.h @@ -0,0 +1,77 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <string> +#include "imessagehandler.h" + +namespace mbus { + +/** + * To facilitate several configuration parameters to the {@link MessageBus#createDestinationSession(MessageHandler, + * DestinationSessionParams)}, all parameters are held by this class. This class has reasonable default values for each + * parameter. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @version $Id$ + */ +class DestinationSessionParams { +private: + string _name; + bool _broadcastName; + IMessageHandler *_handler; + +public: + /** + * Constructs a new instance of this class with default values. + */ + DestinationSessionParams(); + + /** + * Returns the name to register with message bus. + * + * @return The name. + */ + const string &getName() const { return _name; } + + /** + * Sets the name to register with message bus. + * + * @param name The name to set. + * @return This, to allow chaining. + */ + DestinationSessionParams &setName(const string &name) { _name = name; return *this; } + + /** + * Returns whether or not to broadcast the name of this session on the network. + * + * @return True to broadcast, false otherwise. + */ + bool getBroadcastName() const { return _broadcastName; } + + /** + * Sets whether or not to broadcast the name of this session on the network. + * + * @param broadcastName True to broadcast, false otherwise. + * @return This, to allow chaining. + */ + DestinationSessionParams &setBroadcastName(bool broadcastName) { _broadcastName = broadcastName; return *this; } + + /** + * Returns the handler to receive incoming messages. If you call this method without first assigning a + * message handler to this object, you wil de-ref null. + * + * @return The handler. + */ + IMessageHandler &getMessageHandler() const { return *_handler; } + + /** + * Sets the handler to receive incoming messages. + * + * @param handler The handler to set. + * @return This, to allow chaining. + */ + DestinationSessionParams &setMessageHandler(IMessageHandler &handler) { _handler = &handler; return *this; } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/dynamicthrottlepolicy.cpp b/messagebus/src/vespa/messagebus/dynamicthrottlepolicy.cpp new file mode 100644 index 00000000000..523e9a31721 --- /dev/null +++ b/messagebus/src/vespa/messagebus/dynamicthrottlepolicy.cpp @@ -0,0 +1,205 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".dynamicthrottlepolicy"); + +#include <vespa/vespalib/util/atomic.h> +#include "dynamicthrottlepolicy.h" +#include "systemtimer.h" +#include <math.h> +#include <climits> + +namespace mbus { + +DynamicThrottlePolicy::DynamicThrottlePolicy() : + _timer(new SystemTimer()), + _numSent(0), + _numOk(0), + _resizeRate(3), + _resizeTime(_timer->getMilliTime()), + _timeOfLastMessage(_timer->getMilliTime()), + _idleTimePeriod(60000), + _efficiencyThreshold(1), + _windowSizeIncrement(20), + _windowSize(_windowSizeIncrement), + _maxWindowSize(INT_MAX), + _minWindowSize(_windowSizeIncrement), + _windowSizeBackOff(0.9), + _weight(1), + _localMaxThroughput(0) +{ + // empty +} + +DynamicThrottlePolicy::DynamicThrottlePolicy(double windowSizeIncrement) : + _timer(new SystemTimer()), + _numSent(0), + _numOk(0), + _resizeRate(3), + _resizeTime(_timer->getMilliTime()), + _timeOfLastMessage(_timer->getMilliTime()), + _idleTimePeriod(60000), + _efficiencyThreshold(1), + _windowSizeIncrement(windowSizeIncrement), + _windowSize(_windowSizeIncrement), + _maxWindowSize(INT_MAX), + _minWindowSize(_windowSizeIncrement), + _windowSizeBackOff(0.9), + _weight(1), + _localMaxThroughput(0) +{ + // empty +} + +DynamicThrottlePolicy::DynamicThrottlePolicy(ITimer::UP timer) : + _timer(std::move(timer)), + _numSent(0), + _numOk(0), + _resizeRate(3), + _resizeTime(_timer->getMilliTime()), + _timeOfLastMessage(_timer->getMilliTime()), + _idleTimePeriod(60000), + _efficiencyThreshold(1), + _windowSizeIncrement(20), + _windowSize(_windowSizeIncrement), + _maxWindowSize(INT_MAX), + _minWindowSize(_windowSizeIncrement), + _windowSizeBackOff(0.9), + _weight(1), + _localMaxThroughput(0) +{ + // empty +} + +DynamicThrottlePolicy & +DynamicThrottlePolicy::setEfficiencyThreshold(double efficiencyThreshold) +{ + _efficiencyThreshold = efficiencyThreshold; + return *this; +} + +DynamicThrottlePolicy & +DynamicThrottlePolicy::setWindowSizeIncrement(double windowSizeIncrement) +{ + _windowSizeIncrement = windowSizeIncrement; + return *this; +} + +DynamicThrottlePolicy & +DynamicThrottlePolicy::setWindowSizeBackOff(double windowSizeBackOff) +{ + _windowSizeBackOff = windowSizeBackOff; + return *this; +} + +DynamicThrottlePolicy & +DynamicThrottlePolicy::setResizeRate(uint32_t resizeRate) +{ + _resizeRate = resizeRate; + return *this; +} + +DynamicThrottlePolicy & +DynamicThrottlePolicy::setWeight(double weight) +{ + _weight = weight; + return *this; +} + +DynamicThrottlePolicy & +DynamicThrottlePolicy::setIdleTimePeriod(uint64_t period) +{ + _idleTimePeriod = period; + return *this; +} + +DynamicThrottlePolicy & +DynamicThrottlePolicy::setMaxWindowSize(double max) +{ + _maxWindowSize = max; + return *this; +} + +DynamicThrottlePolicy & +DynamicThrottlePolicy::setMinWindowSize(double min) +{ + _minWindowSize = min; + return *this; +} + +DynamicThrottlePolicy & +DynamicThrottlePolicy::setMaxPendingCount(uint32_t maxCount) +{ + StaticThrottlePolicy::setMaxPendingCount(maxCount); + _maxWindowSize = maxCount; + return *this; +} + +bool +DynamicThrottlePolicy::canSend(const Message &msg, uint32_t pendingCount) +{ + if (!StaticThrottlePolicy::canSend(msg, pendingCount)) { + return false; + } + uint64_t time = _timer->getMilliTime(); + if (time - _timeOfLastMessage > _idleTimePeriod) { + _windowSize = std::min(_windowSize, (double) pendingCount + _windowSizeIncrement); + } + _timeOfLastMessage = time; + return pendingCount < _windowSize; +} + +void +DynamicThrottlePolicy::processMessage(Message &msg) +{ + StaticThrottlePolicy::processMessage(msg); + if (++_numSent < _windowSize * _resizeRate) { + return; + } + + uint64_t time = _timer->getMilliTime(); + double elapsed = time - _resizeTime; + _resizeTime = time; + + double throughput = _numOk / elapsed; + _numSent = 0; + _numOk = 0; + + if (throughput > _localMaxThroughput * 1.01) { + LOG(debug, "WindowSize = %.2f, Throughput = %f", _windowSize, throughput); + _localMaxThroughput = throughput; + _windowSize += _weight*_windowSizeIncrement; + } else { + // scale up/down throughput for comparing to window size + double period = 1; + while(throughput*period/_windowSize < 2) { + period *= 10; + } + while(throughput*period/_windowSize > 2) { + period *= 0.1; + } + double efficiency = throughput*period/_windowSize; + LOG(debug, "WindowSize = %.2f, Throughput = %f, Efficiency = %.2f, Elapsed = %.2f, Period = %.2f", _windowSize, throughput, efficiency, elapsed, period); + + if (efficiency < _efficiencyThreshold) { + double newSize = std::min(throughput * period, _windowSize); + _windowSize = std::min(newSize * _windowSizeBackOff, _windowSize - 2 * _windowSizeIncrement); + _localMaxThroughput = 0; + } else { + _windowSize += _weight*_windowSizeIncrement; + } + } + _windowSize = std::max(_minWindowSize, _windowSize); + _windowSize = std::min(_maxWindowSize, _windowSize); +} + +void +DynamicThrottlePolicy::processReply(Reply &reply) +{ + StaticThrottlePolicy::processReply(reply); + if (!reply.hasErrors()) { + ++_numOk; + } +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/dynamicthrottlepolicy.h b/messagebus/src/vespa/messagebus/dynamicthrottlepolicy.h new file mode 100644 index 00000000000..1e29d82cfc7 --- /dev/null +++ b/messagebus/src/vespa/messagebus/dynamicthrottlepolicy.h @@ -0,0 +1,178 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "itimer.h" +#include "staticthrottlepolicy.h" + +namespace mbus { + +/** + * This is an implementatin of the {@link ThrottlePolicy} that offers dynamic limits to the number of pending + * messages a {@link SourceSession} is allowed to have. + * + * <b>NOTE:</b> By context, "pending" is refering to the number of sent messages that have not been replied to + * yet. + */ +class DynamicThrottlePolicy: public StaticThrottlePolicy { +public: +private: + ITimer::UP _timer; + uint32_t _numSent; + uint32_t _numOk; + uint32_t _resizeRate; + uint64_t _resizeTime; + uint64_t _timeOfLastMessage; + uint64_t _idleTimePeriod; + double _efficiencyThreshold; + double _windowSizeIncrement; + double _windowSize; + double _maxWindowSize; + double _minWindowSize; + double _windowSizeBackOff; + double _weight; + double _localMaxThroughput; + +public: + /** + * Convenience typedefs. + */ + typedef std::unique_ptr<DynamicThrottlePolicy> UP; + typedef std::shared_ptr<DynamicThrottlePolicy> SP; + + /** + * Constructs a new instance of this policy and sets the appropriate default values of member data. + */ + DynamicThrottlePolicy(); + + /** + * Constructs a new instance of this policy and sets the appropriate default values of member data. + * + * @param windowSizeIncrement Initial value for window size increment. Also used + * to set initial values for current window size and minimum window size. + */ + DynamicThrottlePolicy(double windowSizeIncrement); + + /** + * Constructs a new instance of this class using the given clock to calculate efficiency. + * + * @param timer The timer to use. + */ + DynamicThrottlePolicy(ITimer::UP timer); + + /** + * Sets the lower efficiency threshold at which the algorithm should perform window size back + * off. Efficiency is the correlation between throughput and window size. The algorithm will increase the + * window size until efficiency drops below the efficiency of the local maxima times this value. + * + * @param efficiencyThreshold The limit to set. + * @return This, to allow chaining. + * @see #setWindowSizeBackOff(double) + */ + DynamicThrottlePolicy &setEfficiencyThreshold(double efficiencyThreshold); + + /** + * Sets the step size used when increasing window size. + * + * @param windowSizeIncrement The step size to set. + * @return This, to allow chaining. + */ + DynamicThrottlePolicy &setWindowSizeIncrement(double windowSizeIncrement); + + /** + * Sets the factor of window size to back off to when the algorithm determines that efficiency is not + * increasing. A value of 1 means that there is no back off from the local maxima, and means that the + * algorithm will fail to reduce window size to something lower than a previous maxima. This value is + * capped to the [0, 1] range. + * + * @param windowSizeBackOff The back off to set. + * @return This, to allow chaining. + */ + DynamicThrottlePolicy &setWindowSizeBackOff(double windowSizeBackOff); + + /** + * Sets the rate at which the window size is updated. The larger the value, the less responsive the + * resizing becomes. However, the smaller the value, the less accurate the measurements become. + * + * @param resizeRate The rate to set. + * @return This, to allow chaining. + */ + DynamicThrottlePolicy &setResizeRate(uint32_t resizeRate); + + /** + * Sets the weight for this client. The larger the value, the more resources + * will be allocated to this clients. Resources are shared between clients + * proportiannally to their weights. + * + * @param weight The weight to set. + * @return This, to allow chaining. + */ + DynamicThrottlePolicy &setWeight(double weight); + + /** + * Sets the idle time period for this client. If nothing is sent trhoughout + * this time period, the dynamic window will retract. + * + * @param period The time period to set. + * @return This, to allow chaining. + */ + DynamicThrottlePolicy &setIdleTimePeriod(uint64_t period); + + /** + * Sets the maximium number of pending operations allowed at any time, in + * order to avoid using too much resources. + * + * @param max The max to set. + * @return This, to allow chaining. + */ + DynamicThrottlePolicy &setMaxWindowSize(double max); + + /** + * Sets the maximum number of pending messages allowed. + * + * @param maxCount The max count. + * @return This, to allow chaining. + */ + DynamicThrottlePolicy &setMaxPendingCount(uint32_t maxCount); + + /** + * Get the maximum number of pending operations allowed at any time. + * + * @return The maximum number of operations. + */ + double getMaxWindowSize() const { return _maxWindowSize; } + + /** + * Sets the minimium number of pending operations allowed at any time, in + * order to keep a level of performance. + * + * @param min The min to set. + * @return This, to allow chaining. + */ + DynamicThrottlePolicy &setMinWindowSize(double min); + + /** + * Get the minimum number of pending operations allowed at any time. + * + * @return The minimum number of operations. + */ + double getMinWindowSize() const { return _minWindowSize; } + + /** + * Returns the maximum number of pending messages allowed. + * + * @return The max limit. + */ + uint32_t getMaxPendingCount() const { return (uint32_t)_windowSize; } + + // Implements IThrottlePolicy. + bool canSend(const Message &msg, uint32_t pendingCount); + + // Implements IThrottlePolicy. + void processMessage(Message &msg); + + // Implements IThrottlePolicy. + void processReply(Reply &reply); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/emptyreply.cpp b/messagebus/src/vespa/messagebus/emptyreply.cpp new file mode 100644 index 00000000000..cf13c2bf857 --- /dev/null +++ b/messagebus/src/vespa/messagebus/emptyreply.cpp @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".emptyreply"); + +#include "emptyreply.h" + +namespace { + +static mbus::string EmptyReplyProtocolName = ""; + +} // namespace anon + +namespace mbus { + +EmptyReply::EmptyReply() +{ + // empty +} + +const string & +EmptyReply::getProtocol() const +{ + return EmptyReplyProtocolName; +} + +uint32_t +EmptyReply::getType() const +{ + return 0; +} + +Blob +EmptyReply::encode() const +{ + return Blob(0); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/emptyreply.h b/messagebus/src/vespa/messagebus/emptyreply.h new file mode 100644 index 00000000000..53639e0fef0 --- /dev/null +++ b/messagebus/src/vespa/messagebus/emptyreply.h @@ -0,0 +1,49 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "reply.h" + +namespace mbus { + +/** + * A concrete reply that contains no protocol-specific data. This is needed to + * enable messagebus to reply to messages that result in an error. It may also + * be used by the application for ack type replies. Objects of this class will + * identify as type 0, which is reserved for this use. Also note that whenever a + * protocol-specific reply encodes to an empty blob it will be decoded to an + * EmptyReply at its network peer. + */ +class EmptyReply : public Reply { +public: + /** + * Constructs a new instance of this class. + */ + EmptyReply(); + + /** + * This method returns the empty string to signal that it does not belong to + * a protocol. + * + * @return "" + */ + virtual const string & getProtocol() const; + + /** + * This method returns the message type id reserved for empty replies: 0 + * + * @return 0 + */ + virtual uint32_t getType() const; + + /** + * Encodes this reply into an empty blob. + * + * @return empty blob + */ + virtual Blob encode() const; + + uint8_t priority() const { return 8; } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/error.cpp b/messagebus/src/vespa/messagebus/error.cpp new file mode 100644 index 00000000000..736cda5e16b --- /dev/null +++ b/messagebus/src/vespa/messagebus/error.cpp @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/vespalib/util/vstringfmt.h> +#include <vespa/log/log.h> +LOG_SETUP(".error"); +#include "error.h" +#include "errorcode.h" + +namespace mbus { + +Error::Error() + : _code(ErrorCode::NONE), + _msg(), + _service() +{ + // empty +} + +Error::Error(uint32_t c, const string &m, const string &s) + : _code(c), + _msg(m), + _service(s) +{ + // empty +} + +string +Error::toString() const +{ + string name(ErrorCode::getName(_code)); + if (name.empty()) { + name = vespalib::make_vespa_string("%u", _code); + } + return vespalib::make_vespa_string("[%s @ %s]: %s", name.c_str(), + _service.empty() ? "localhost" : _service.c_str(), + _msg.c_str()); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/error.h b/messagebus/src/vespa/messagebus/error.h new file mode 100644 index 00000000000..f94e6532307 --- /dev/null +++ b/messagebus/src/vespa/messagebus/error.h @@ -0,0 +1,71 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <stdint.h> +#include <vespa/messagebus/common.h> + +#ifdef Error +#undef Error +#endif + +namespace mbus { + +/** + * An Error contains an error code (@ref ErrorCode) combined with an + * error message. + **/ +class Error +{ +private: + uint32_t _code; + string _msg; + string _service; + +public: + /** + * Create an error with error code NONE and an empty message. This + * constructor is not intended for application use, but is needed + * for standard library containers. + **/ + Error(); + + /** + * Create a new error with the given code and message + * + * @param c error code + * @param m error message + * @param s error service + **/ + Error(uint32_t c, const string &m, const string &s = ""); + + /** + * Obtain the error code of this error. + * + * @return error code + **/ + uint32_t getCode() const { return _code; } + + /** + * Obtain the error message of this error. + * + * @return error message + **/ + const string &getMessage() const { return _msg; } + + /** + * Obtain the service string of this error. + * + * @return service string + **/ + const string &getService() const { return _service; } + + /** + * Obtain a string representation of this error. + * + * @return string representation + **/ + string toString() const; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/errorcode.cpp b/messagebus/src/vespa/messagebus/errorcode.cpp new file mode 100644 index 00000000000..21d3c91544f --- /dev/null +++ b/messagebus/src/vespa/messagebus/errorcode.cpp @@ -0,0 +1,54 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".errorcode"); +#include "errorcode.h" +#include <vespa/vespalib/stllike/asciistream.h> + +namespace mbus { + +ErrorCode::ErrorCode() +{ + // empty +} + +string +ErrorCode::getName(uint32_t errorCode) +{ + switch (errorCode) { + case APP_FATAL_ERROR : return "APP_FATAL_ERROR"; + case APP_TRANSIENT_ERROR : return "APP_TRANSIENT_ERROR"; + case CONNECTION_ERROR : return "CONNECTION_ERROR"; + case DECODE_ERROR : return "DECODE_ERROR"; + case ENCODE_ERROR : return "ENCODE_ERROR"; + case FATAL_ERROR : return "FATAL_ERROR"; + case HANDSHAKE_FAILED : return "HANDSHAKE_FAILED"; + case ILLEGAL_ROUTE : return "ILLEGAL_ROUTE"; + case INCOMPATIBLE_VERSION : return "INCOMPATIBLE_VERSION"; + case NETWORK_ERROR : return "NETWORK_ERROR"; + case NETWORK_SHUTDOWN : return "NETWORK_SHUTDOWN"; + case NO_ADDRESS_FOR_SERVICE : return "NO_ADDRESS_FOR_SERVICE"; + case NO_SERVICES_FOR_ROUTE : return "NO_SERVICES_FOR_ROUTE"; + case NONE : return "NONE"; + case POLICY_ERROR : return "POLICY_ERROR"; + case SEND_ABORTED : return "SEND_ABORTED"; + case SEND_QUEUE_CLOSED : return "SEND_QUEUE_CLOSED"; + case SEND_QUEUE_FULL : return "SEND_QUEUE_FULL"; + case SEQUENCE_ERROR : return "SEQUENCE_ERROR"; + case SERVICE_OOS : return "SERVICE_OOS"; + case SESSION_BUSY : return "SESSION_BUSY"; + case TIMEOUT : return "TIMEOUT"; + case TRANSIENT_ERROR : return "TRANSIENT_ERROR"; + case UNKNOWN_POLICY : return "UNKNOWN_POLICY"; + case UNKNOWN_PROTOCOL : return "UNKNOWN_PROTOCOL"; + case UNKNOWN_SESSION : return "UNKNOWN_SESSION"; + default : { + vespalib::asciistream os; + os << "UNKNOWN(" << errorCode << ')'; + return os.str(); + } + } +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/errorcode.h b/messagebus/src/vespa/messagebus/errorcode.h new file mode 100644 index 00000000000..7667c75725e --- /dev/null +++ b/messagebus/src/vespa/messagebus/errorcode.h @@ -0,0 +1,128 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <stdint.h> +#include <vespa/messagebus/common.h> + +namespace mbus { + +/** + * This class contains the reserved error codes that are used by + * messagebus. It also defines the error code ranges that may be used + * by applications. Note that this class should never be instantiated. + * An error code is a number with some added semantics. Legal error + * codes are separated into 4 value ranges. An error can be either + * fatal or transient. Inside each category, the error can be either + * messagebus specific or application specific. A fatal error signals + * that something is wrong and that it will not help to resend the + * message. A transient error signals that it may help to resend the + * message at a later time. + * <pre> + * transient errors: + * messagebus: [100000, 150000> + * application: [150000, 200000> + * fatal errors: + * messagebus: [200000, 250000> + * application: [250000, 300000> + * </pre> + **/ +class ErrorCode { +public: + enum { + // The code is here for completeness. + NONE = 0, + + // A general transient error, resending is possible. + TRANSIENT_ERROR = 100000, + + // Sending was rejected because throttler capacity is full. + SEND_QUEUE_FULL = TRANSIENT_ERROR + 1, + + // No addresses found for the services of the message route. + NO_ADDRESS_FOR_SERVICE = TRANSIENT_ERROR + 2, + + // A connection problem occured while sending. + CONNECTION_ERROR = TRANSIENT_ERROR + 3, + + // The session specified for the message is unknown. + UNKNOWN_SESSION = TRANSIENT_ERROR + 4, + + // The recipient session is busy. + SESSION_BUSY = TRANSIENT_ERROR + 5, + + // Sending aborted by route verification. + SEND_ABORTED = TRANSIENT_ERROR + 6, + + // Version handshake failed for any reason. + HANDSHAKE_FAILED = TRANSIENT_ERROR + 7, + + // An application specific transient error. + APP_TRANSIENT_ERROR = TRANSIENT_ERROR + 50000, + + // A general non-recoverable error, resending is not possible. + FATAL_ERROR = 200000, + + // Sending was rejected because throttler is closed. + SEND_QUEUE_CLOSED = FATAL_ERROR + 1, + + // The route of the message is illegal. + ILLEGAL_ROUTE = FATAL_ERROR + 2, + + // No services found for the message route. + NO_SERVICES_FOR_ROUTE = FATAL_ERROR + 3, + + // The selected service was out of service. + SERVICE_OOS = FATAL_ERROR + 4, + + // An error occured while encoding the message. + ENCODE_ERROR = FATAL_ERROR + 5, + + // A fatal network error occured while sending. + NETWORK_ERROR = FATAL_ERROR + 6, + + // The protocol specified for the message is unknown. + UNKNOWN_PROTOCOL = FATAL_ERROR + 7, + + // An error occured while decoding the message. + DECODE_ERROR = FATAL_ERROR + 8, + + // A timeout occured while sending. + TIMEOUT = FATAL_ERROR + 9, + + // The target is running an incompatible version. + INCOMPATIBLE_VERSION = FATAL_ERROR + 10, + + // The policy specified in a route is unknown. + UNKNOWN_POLICY = FATAL_ERROR + 11, + + // The network was shut down when attempting to send. + NETWORK_SHUTDOWN = FATAL_ERROR + 12, + + // Exception thrown by routing policy. + POLICY_ERROR = FATAL_ERROR + 13, + + // Exception thrown by routing policy. + SEQUENCE_ERROR = FATAL_ERROR + 14, + + // An application specific non-recoverable error. + APP_FATAL_ERROR = FATAL_ERROR + 50000, + + // No error codes are allowed to be this big. + ERROR_LIMIT = APP_FATAL_ERROR + 50000 + }; + + /** + * Translates the given error code into its symbolic name. + * + * @param errorCode The error code to translate. + * @return The symbolic name. + **/ + static string getName(uint32_t errorCode); + +private: + ErrorCode(); // hide +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/iconfighandler.h b/messagebus/src/vespa/messagebus/iconfighandler.h new file mode 100644 index 00000000000..1a4dd4ab9ac --- /dev/null +++ b/messagebus/src/vespa/messagebus/iconfighandler.h @@ -0,0 +1,33 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +namespace mbus { + +class RoutingSpec; + +/** + * This interface contains the method(s) used by the ConfigAgent to + * programmatically configure messagebus. It acts as insulation between + * the ConfigAgent and MessageBus to simplify testing of the config + * agent. + **/ +class IConfigHandler +{ +public: + virtual ~IConfigHandler() {} + + /** + * This method will be invoked to initialize or change the routing + * setup. The return value indicates whether the new setup was + * accepted or not. If false is returned the new routing was + * rejected and no change in the current setup have been done. + * + * @return true if new setup was accepted + * @param spec spec of new routing setup + **/ + virtual bool setupRouting(const RoutingSpec &spec) = 0; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/idiscardhandler.h b/messagebus/src/vespa/messagebus/idiscardhandler.h new file mode 100644 index 00000000000..4c55080328b --- /dev/null +++ b/messagebus/src/vespa/messagebus/idiscardhandler.h @@ -0,0 +1,31 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "context.h" + +namespace mbus { + +/** + * This interface is implemented by application components that require special + * handling when discarding a message with a non-empty callstack. + */ +class IDiscardHandler +{ +public: + /** + * Virtual destructor required for inheritance. + */ + virtual ~IDiscardHandler() { } + + /** + * This method is invoked by message bus when a routable is being + * dicarded. This is invoked INSTEAD of the corresponding {@link + * ReplyHandler}. + * + * @param ctx The context of the discarded reply. + */ + virtual void handleDiscard(Context ctx) = 0; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/imessagehandler.h b/messagebus/src/vespa/messagebus/imessagehandler.h new file mode 100644 index 00000000000..d21936bb0bc --- /dev/null +++ b/messagebus/src/vespa/messagebus/imessagehandler.h @@ -0,0 +1,29 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <memory> +#include "message.h" + +namespace mbus { + +/** + * This interface is implemented by application components that want + * to handle incoming messages received from either an + * IntermediateSession or a DestinationSession. + **/ +class IMessageHandler +{ +public: + virtual ~IMessageHandler() {} + + /** + * This method is invoked by messagebus to deliver a Message. + * + * @param message the Message being delivered + **/ + virtual void handleMessage(Message::UP message) = 0; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/intermediatesession.cpp b/messagebus/src/vespa/messagebus/intermediatesession.cpp new file mode 100644 index 00000000000..88fd6069a81 --- /dev/null +++ b/messagebus/src/vespa/messagebus/intermediatesession.cpp @@ -0,0 +1,80 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".intermediatesession"); + +#include <vespa/vespalib/util/vstringfmt.h> +#include "intermediatesession.h" +#include "messagebus.h" +#include "replygate.h" + +namespace mbus { + +IntermediateSession::IntermediateSession(MessageBus &mbus, const IntermediateSessionParams ¶ms) : + _mbus(mbus), + _name(params.getName()), + _msgHandler(params.getMessageHandler()), + _replyHandler(params.getReplyHandler()), + _gate(new ReplyGate(_mbus)) +{ + // empty +} + +IntermediateSession::~IntermediateSession() +{ + _gate->close(); + close(); + _gate->subRef(); +} + +void +IntermediateSession::close() +{ + _mbus.unregisterSession(_name); + _mbus.sync(); +} + +void +IntermediateSession::forward(Routable::UP routable) +{ + if (routable->isReply()) { + forward(Reply::UP(static_cast<Reply*>(routable.release()))); + } else { + forward(Message::UP(static_cast<Message*>(routable.release()))); + } +} + +void +IntermediateSession::forward(Reply::UP reply) +{ + IReplyHandler &handler = reply->getCallStack().pop(*reply); + handler.handleReply(std::move(reply)); +} + +void +IntermediateSession::forward(Message::UP msg) +{ + msg->pushHandler(*this); + _gate->handleMessage(std::move(msg)); +} + +void +IntermediateSession::handleMessage(Message::UP msg) +{ + _msgHandler.handleMessage(std::move(msg)); +} + +void +IntermediateSession::handleReply(Reply::UP reply) +{ + _replyHandler.handleReply(std::move(reply)); +} + +const string +IntermediateSession::getConnectionSpec() const +{ + return _mbus.getConnectionSpec() + "/" + _name; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/intermediatesession.h b/messagebus/src/vespa/messagebus/intermediatesession.h new file mode 100644 index 00000000000..7f190c2ea4a --- /dev/null +++ b/messagebus/src/vespa/messagebus/intermediatesession.h @@ -0,0 +1,100 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <boost/utility.hpp> +#include <memory> +#include <string> +#include "reply.h" +#include "imessagehandler.h" +#include "intermediatesessionparams.h" + +namespace mbus { + +class MessageBus; +class ReplyGate; + +/** + * An IntermediateSession is used to process Message and Reply objects + * on the way along a route. + **/ +class IntermediateSession : public boost::noncopyable, + public IMessageHandler, + public IReplyHandler +{ +private: + friend class MessageBus; + + MessageBus &_mbus; + string _name; + IMessageHandler &_msgHandler; + IReplyHandler &_replyHandler; + ReplyGate *_gate; + + /** + * This constructor is declared package private since only MessageBus is supposed to instantiate it. + * + * @param mbus The message bus that created this instance. + * @param params The parameter object for this session. + */ + IntermediateSession(MessageBus &mbus, const IntermediateSessionParams ¶ms); + +public: + /** + * Convenience typedefs. + */ + typedef std::unique_ptr<IntermediateSession> UP; + + /** + * The destructor untangles from messagebus. After this method returns, messagebus will not invoke any + * handlers associated with this session. + */ + virtual ~IntermediateSession(); + + /** + * This method unregisters this session from message bus, effectively disabling any more messages from + * being delivered to the message handler. After unregistering, this method calls {@link + * com.yahoo.messagebus.MessageBus#sync()} as to ensure that there are no threads currently entangled in + * the handler. + * + * This method will deadlock if you call it from the message or reply handler. + */ + void close(); + + /** + * Forwards a routable to the next hop in its route. This method will never block. + * + * @param routable The routable to forward. + */ + void forward(Routable::UP routable); + + /** + * Convenience method to call {@link #forward(Routable)}. + * + * @param msg The message to forward. + */ + void forward(Message::UP msg); + + /** + * Convenience method to call {@link #forward(Routable)}. + * + * @param reply The reply to forward. + */ + void forward(Reply::UP reply); + + /** + * Returns the connection spec string for this session. This returns a combination of the owning message + * bus' own spec string and the name of this session. + * + * @return The connection string. + */ + const string getConnectionSpec() const; + + // Implements IMessageHandler. + void handleMessage(Message::UP message); + + // Implements IReplyHandler. + void handleReply(Reply::UP reply); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/intermediatesessionparams.cpp b/messagebus/src/vespa/messagebus/intermediatesessionparams.cpp new file mode 100644 index 00000000000..2d8944b8f26 --- /dev/null +++ b/messagebus/src/vespa/messagebus/intermediatesessionparams.cpp @@ -0,0 +1,16 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include "intermediatesessionparams.h" + +namespace mbus { + +IntermediateSessionParams::IntermediateSessionParams() : + _name("intermediate"), + _broadcastName(true), + _msgHandler(NULL), + _replyHandler(NULL) +{ + // empty +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/intermediatesessionparams.h b/messagebus/src/vespa/messagebus/intermediatesessionparams.h new file mode 100644 index 00000000000..84224b8803b --- /dev/null +++ b/messagebus/src/vespa/messagebus/intermediatesessionparams.h @@ -0,0 +1,95 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <string> +#include "imessagehandler.h" +#include "ireplyhandler.h" + +namespace mbus { + +/** + * To facilitate several configuration parameters to the {@link MessageBus#createIntermediateSession(MessageHandler, + * ReplyHandler, IntermediateSessionParams)}, all parameters are held by this class. This class has reasonable default + * values for each parameter. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @version $Id$ + */ +class IntermediateSessionParams { +private: + string _name; + bool _broadcastName; + IMessageHandler *_msgHandler; + IReplyHandler *_replyHandler; + +public: + /** + * Constructs a new instance of this class with default values. + */ + IntermediateSessionParams(); + + /** + * Returns the name to register with message bus. + * + * @return The name. + */ + const string &getName() const { return _name; } + + /** + * Sets the name to register with message bus. + * + * @param name The name to set. + * @return This, to allow chaining. + */ + IntermediateSessionParams &setName(const string &name) { _name = name; return *this; } + + /** + * Returns whether or not to broadcast the name of this session on the network. + * + * @return True to broadcast, false otherwise. + */ + bool getBroadcastName() const { return _broadcastName; } + + /** + * Sets whether or not to broadcast the name of this session on the network. + * + * @param broadcastName True to broadcast, false otherwise. + * @return This, to allow chaining. + */ + IntermediateSessionParams &setBroadcastName(bool broadcastName) { _broadcastName = broadcastName; return *this; } + + /** + * Returns the handler to receive incoming replies. If you call this method without first assigning a + * reply handler to this object, you wil de-ref null. + * + * @return The handler. + */ + IReplyHandler &getReplyHandler() const { return *_replyHandler; } + + /** + * Sets the handler to receive incoming replies. + * + * @param handler The handler to set. + * @return This, to allow chaining. + */ + IntermediateSessionParams &setReplyHandler(IReplyHandler &handler) { _replyHandler = &handler; return *this; } + + /** + * Returns the handler to receive incoming messages. If you call this method without first assigning a + * message handler to this object, you wil de-ref null. + * + * @return The handler. + */ + IMessageHandler &getMessageHandler() const { return *_msgHandler; } + + /** + * Sets the handler to receive incoming messages. + * + * @param handler The handler to set. + * @return This, to allow chaining. + */ + IntermediateSessionParams &setMessageHandler(IMessageHandler &handler) { _msgHandler = &handler; return *this; } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/iprotocol.h b/messagebus/src/vespa/messagebus/iprotocol.h new file mode 100644 index 00000000000..c0af967f33b --- /dev/null +++ b/messagebus/src/vespa/messagebus/iprotocol.h @@ -0,0 +1,83 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <vespa/messagebus/routing/iroutingpolicy.h> +#include <string> +#include <vespa/vespalib/component/version.h> +#include "blobref.h" +#include "routable.h" + +namespace mbus { + +/** + * A protocol has support for decoding raw data into routable objects and for + * instantiating routing policy objects. Each protocol has a name. The name of + * a protocol is global across implementations. Protocols with the same name + * are expected to know how to encode and decode the same set of routables and + * also have support for the same set of routing policies. + */ +class IProtocol { +public: + virtual ~IProtocol() {} + + /** + * Convenience typedef for an auto pointer to an IProtocol object. + */ + typedef std::unique_ptr<IProtocol> UP; + + /** + * Convenience typedef for a shared pointer to a IProtocol object. + */ + typedef std::shared_ptr<IProtocol> SP; + + /** + * Obtain the name of this protocol. + * + * @return Protocol name. + */ + virtual const string & getName() const = 0; + + /** + * Instantiate a routing policy based on its name and parameter. Routing + * policies are created my messagebus based on the selector string. A + * selector path element using a custom routing policy is on the form + * '[name:param]'. The semantics of the parameter is up to the routing + * policy. It could be a simple value or even a config id. + * + * @param name Routing policy name (local to this protocol). + * @param param Ppolicy specific parameter. + * @return A newly created routing policy. + */ + virtual IRoutingPolicy::UP createPolicy(const string &name, + const string ¶m) const = 0; + + /** + * Encodes the protocol specific data of a routable into a byte array. + * + * Errors should be catched and logged by the encode implementation and + * an empty blob should be returned. This will make messagebus generate + * a reply to send back to the client. + * + * @param version The version to encode for. + * @param routable The routable to encode. + * @return The encoded data. + */ + virtual Blob encode(const vespalib::Version &version, const Routable &routable) const = 0; // throw() + + /** + * Decodes the protocol specific data into a routable of the correct type. + * + * Errors should be catched and logged by the decode implementation, and + * a null pointer should be returned. This will make messagebus generate + * a reply to send back to the client. + * + * @param version The version of the serialized routable. + * @param payload The payload to decode from. + * @return The decoded routable. + */ + virtual Routable::UP decode(const vespalib::Version &version, BlobRef data) const = 0; // throw() +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/ireplyhandler.h b/messagebus/src/vespa/messagebus/ireplyhandler.h new file mode 100644 index 00000000000..c30ca63c519 --- /dev/null +++ b/messagebus/src/vespa/messagebus/ireplyhandler.h @@ -0,0 +1,28 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <memory> +#include "reply.h" + +namespace mbus { + +/** + * This interface is implemented by application components that want + * to handle incoming replies received from either an + * IntermediateSession or a SourceSession. + **/ +class IReplyHandler +{ +public: + virtual ~IReplyHandler() {} + + /** + * This method is invoked by messagebus to deliver a Reply. + * + * @param reply the Reply being delivered + **/ + virtual void handleReply(Reply::UP reply) = 0; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/ithrottlepolicy.h b/messagebus/src/vespa/messagebus/ithrottlepolicy.h new file mode 100644 index 00000000000..1ac6d2c28df --- /dev/null +++ b/messagebus/src/vespa/messagebus/ithrottlepolicy.h @@ -0,0 +1,53 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "reply.h" + +namespace mbus { + +/** + * An implementation of this interface is used by {@link SourceSession} to throttle output. Every message + * entering {@link SourceSession#send(Message)} needs to be accepted by this interface's {@link + * #canSend(Message, int)} method. All messages accepted are passed through the {@link + * #processMessage(Message)} method, and the corresponding replies are passed through the {@link + * #processReply(Reply)} method. + */ +class IThrottlePolicy { +public: + /** + * Convenience typedefs. + */ + typedef std::unique_ptr<IThrottlePolicy> UP; + typedef std::shared_ptr<IThrottlePolicy> SP; + + /** + * Virtual destructor required for inheritance. + */ + virtual ~IThrottlePolicy() { /* empty */ } + + /** + * Returns whether or not the given message can be sent according to the current state of this policy. + * + * @param msg The message to evaluate. + * @param pendingCount The current number of pending messages. + * @return True to send the message. + */ + virtual bool canSend(const Message &msg, uint32_t pendingCount) = 0; + + /** + * This method is called once for every message that was accepted by {@link #canSend(Message, int)} and sent. + * + * @param msg The message beint sent. + */ + virtual void processMessage(Message &msg) = 0; + + /** + * This method is called once for every reply that is received. + * + * @param reply The reply received. + */ + virtual void processReply(Reply &reply) = 0; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/itimer.h b/messagebus/src/vespa/messagebus/itimer.h new file mode 100644 index 00000000000..6d03a0c45a2 --- /dev/null +++ b/messagebus/src/vespa/messagebus/itimer.h @@ -0,0 +1,36 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <memory> + +namespace mbus { + +/** + * This interface wraps access to some timer that can be used to measure elapsed + * time, in milliseconds. This abstraction allows for unit testing the behavior + * of time-based constructs. + */ +class ITimer { +public: + /** + * Convenience typedefs. + */ + typedef std::unique_ptr<ITimer> UP; + + /** + * Virtual destructor required for inheritance. + */ + virtual ~ITimer() { /* empty */ } + + /** + * Returns the current value of some arbitrary timer, in milliseconds. This + * method can only be used to measure elapsed time and is not related to any + * other notion of system or wall-clock time. + * + * @return The current value of the timer, in milliseconds. + */ + virtual uint64_t getMilliTime() const = 0; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/message.cpp b/messagebus/src/vespa/messagebus/message.cpp new file mode 100644 index 00000000000..cdf290f2be1 --- /dev/null +++ b/messagebus/src/vespa/messagebus/message.cpp @@ -0,0 +1,77 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".message"); + +#include <vespa/vespalib/util/backtrace.h> +#include "message.h" +#include "reply.h" +#include "ireplyhandler.h" +#include "emptyreply.h" +#include "error.h" +#include "errorcode.h" + +namespace mbus { + +Message::Message() : + _route(), + _timeReceived(), + _timeRemaining(0), + _retryEnabled(true), + _retry(0) +{ + // empty +} + +Message::~Message() +{ + if (getCallStack().size() > 0) { + string backtrace = vespalib::getStackTrace(0); + LOG(warning, "Deleted message %p with non-empty call-stack. Deleted at:\n%s", + this, backtrace.c_str()); + Reply::UP reply(new EmptyReply()); + swapState(*reply); + reply->addError(Error(ErrorCode::TRANSIENT_ERROR, + "The message object was deleted while containing state information; " + "generating an auto-reply.")); + IReplyHandler &handler = reply->getCallStack().pop(*reply); + handler.handleReply(std::move(reply)); + } +} + +void +Message::swapState(Routable &rhs) +{ + Routable::swapState(rhs); + if (!rhs.isReply()) { + Message &msg = static_cast<Message&>(rhs); + + std::swap(_route, msg._route); + std::swap(_retryEnabled, msg._retryEnabled); + std::swap(_retry, msg._retry); + std::swap(_timeReceived, msg._timeReceived); + std::swap(_timeRemaining, msg._timeRemaining); + } +} + +Message & +Message::setTimeReceived(uint64_t timeReceived) +{ + _timeReceived.SetMilliSecs(timeReceived); + return *this; +} + +Message & +Message::setTimeReceivedNow() +{ + _timeReceived.SetNow(); + return *this; +} + +uint64_t +Message::getTimeRemainingNow() const +{ + return (uint64_t)std::max(0.0, _timeRemaining - _timeReceived.MilliSecsToNow()); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/message.h b/messagebus/src/vespa/messagebus/message.h new file mode 100644 index 00000000000..6afe305389a --- /dev/null +++ b/messagebus/src/vespa/messagebus/message.h @@ -0,0 +1,229 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vespa/fastos/time.h> +#include <memory> +#include <vespa/messagebus/routing/route.h> +#include "routable.h" + +namespace mbus { + +/** + * A Message is a question, a Reply is the answer. + */ +class Message : public Routable { +private: + Route _route; + FastOS_Time _timeReceived; + uint64_t _timeRemaining; + bool _retryEnabled; + uint32_t _retry; + +public: + /** + * Convenience typedef for an auto pointer to a Message object. + */ + typedef std::unique_ptr<Message> UP; + + /** + * Constructs a new instance of this class. + */ + Message(); + + /** + * If a message is deleted with elements on the callstack, this destructor + * will log an error and generate an auto-reply to avoid having the sender + * wait indefinetly for a reply. + */ + virtual ~Message(); + + // Overrides Routable. + virtual void swapState(Routable &rhs); + + /** + * Returns the timestamp for when this message was last seen by message + * bus. If you are using this to determine message expiration, you should + * use {@link #isExpired()} instead. + * + * @return The timestamp this was last seen. + */ + uint64_t getTimeReceived() const { return (uint64_t)_timeReceived.MilliSecs(); } + + /** + * Sets the timestamp for when this message was last seen by message bus to + * the given time in milliseconds since epoch. Please see comment on {@link + * #isExpired()} for more information on how to determine whether or not a + * message has expired. You should never need to call this method yourself, + * as it is touched automatically whenever message bus encounters a new + * message. + * + * @param timeReceived The time received in milliseconds. + * @return This, to allow chaining. + */ + Message &setTimeReceived(uint64_t timeReceived); + + /** + * This is a convenience method to call {@link #setTimeReceived(uint64_t)} + * passing the current time as argument. + * + * @return This, to allow chaining. + */ + Message &setTimeReceivedNow(); + + /** + * Returns the number of milliseconds that remain before this message times + * out. This value is only updated by the network layer, and is therefore + * not current. If you are trying to determine message expiration, use + * {@link this#isExpired()} instead. + * + * @return The remaining time in milliseconds. + */ + uint64_t getTimeRemaining() const { return _timeRemaining; } + + /** + * Sets the numer of milliseconds that remain before this message times + * out. Please see comment on {@link this#isExpired()} for more information + * on how to determine whether or not a message has expired. + * + * @param timeRemaining The number of milliseconds until expiration. + * @return This, to allow chaining. + */ + Message &setTimeRemaining(uint64_t timeRemaining) { _timeRemaining = timeRemaining; return *this; } + + /** + * Returns the number of milliseconds that remain right now before this + * message times out. This is a function of {@link this#getTimeReceived()}, + * {@link this#getTimeRemaining()} and current time. Whenever a message is + * transmitted by message bus, a new remaining time is calculated and + * serialized as <code>timeRemaining = timeRemaining - (currentTime - + * timeReceived)</code>. This means that we are doing an over-estimate of + * remaining time, as we are only factoring in the time used by the + * application above message bus. + * + * @return The remaining time in milliseconds. + */ + uint64_t getTimeRemainingNow() const; + + /** + * Returns whether or not this message has expired. + * + * @return True if {@link this#getTimeRemainingNow()} is less than or equal + * to zero. + */ + bool isExpired() { return getTimeRemainingNow() == 0; } + + /** + * Access the route associated with this message. + * + * @return reference to internal route object + */ + Route &getRoute() { return _route; } + + /** + * Access the route associated with this message. + * + * @return reference to internal route object + */ + const Route &getRoute() const { return _route; } + + /** + * Set a new route for this routable. + * + * @param route The new route. + * @return This, to allow chaining. + */ + Message &setRoute(const Route &route) { _route = route; return *this; } + + /** + * Inherited from Routable. Classifies this object as 'not a reply'. + * + * @return false + */ + virtual bool isReply() const { return false; } + + /** + * Returns whether or not this message contains a sequence identifier that + * should be respected, i.e. whether or not this message requires + * sequencing. + * + * @return True to enable sequencing. + */ + virtual bool hasSequenceId() const { return false; } + + /** + * Returns the identifier used to order messages. Any two messages that have + * the same sequence id are ensured to arrive at the recipient in the order + * they were sent by the client. This value is only respected if the {@link + * #hasSequenceId()} method returns true. + * + * @return The sequence identifier. + */ + virtual uint64_t getSequenceId() const { return 0; } + + /** + * Returns whether or not this message contains a sequence bucket that + * should be respected, i.e. whether or not this message requires + * bucket-level sequencing. + * + * @return True to enable bucket sequencing. + */ + virtual bool hasBucketSequence() { return false; } + + /** + * Returns the identifier used to order message buckets. Any two messages + * that have the same bucket sequence are ensured to arrive at the NEXT peer + * in the order they were sent by THIS peer. This value is only respected if + * the {@link #hasBucketSequence()} method returns true. + * + * @return The bucket sequence. + */ + virtual uint64_t getBucketSequence() { return 0; } + + /** + * Obtain the approximate size of this message object in bytes. This enables + * messagebus to track the size of the send queue in both memory usage and + * item count. This method returns 1 by default, and must be overridden to + * enable message size tracking. + * + * @return 1 + */ + virtual uint32_t getApproxSize() const { return 1; } + + /** + * Sets whether or not this message can be resent. + * + * @param enabled Resendable flag. + */ + void setRetryEnabled(bool enabled) { _retryEnabled = enabled; } + + /** + * Returns whether or not this message can be resent. + * + * @return True if this can be resent. + */ + bool getRetryEnabled() const { return _retryEnabled; } + + /** + * Returns the number of times the sending of this message has been + * retried. This is available for inspection so that clients may implement + * logic to control resending. + * + * @see Reply#setRetry This method can be used to request resending that + * differs from the default. + * @return The retry count. + */ + uint32_t getRetry() const { return _retry; } + + /** + * Sets the number of times the sending of this message has been + * retried. This method only makes sense to modify BEFORE sending it, since + * its value is not serialized back into any reply that it may create. + * + * @param retry The retry count. + * @return This, to allow chaining. + */ + Message &setRetry(uint32_t retry) { _retry = retry; return *this; } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/messagebus.cpp b/messagebus/src/vespa/messagebus/messagebus.cpp new file mode 100644 index 00000000000..87247a90102 --- /dev/null +++ b/messagebus/src/vespa/messagebus/messagebus.cpp @@ -0,0 +1,424 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".messagebus"); + +#include <vespa/messagebus/routing/routingnode.h> +#include <vespa/messagebus/routing/routingspec.h> +#include <vespa/vespalib/util/exceptions.h> +#include <vespa/vespalib/util/vstringfmt.h> +#include "messagebus.h" +#include "imessagehandler.h" +#include "emptyreply.h" +#include "errorcode.h" +#include "sendproxy.h" + +using vespalib::LockGuard; + +namespace { + +/** + * Implements a task for running the resender in the messenger thread. This task + * acts as a proxy for the resender, allowing the task to be deleted without + * affecting the resender itself. + */ +class ResenderTask : public mbus::Messenger::ITask { +private: + mbus::Resender *_resender; + +public: + ResenderTask(mbus::Resender &resender) + : _resender(&resender) + { + // empty + } + + void run() { + _resender->resendScheduled(); + } + + uint8_t priority() const { + return 255; + } +}; + +/** + * Implements a task for monitoring shutdown of the messenger thread. This task + * helps to determine whether or not there is any work left in either the + * messenger or network thread. + */ +class ShutdownTask : public mbus::Messenger::ITask { +private: + mbus::INetwork &_net; + mbus::Messenger &_msn; + bool &_done; + vespalib::Gate &_gate; + +public: + ShutdownTask(mbus::INetwork &net, mbus::Messenger &msn, + bool &done, vespalib::Gate &gate) + : _net(net), + _msn(msn), + _done(done), + _gate(gate) + { + // empty + } + + ~ShutdownTask() { + _gate.countDown(); + } + + void run() { + _net.postShutdownHook(); + _done = _msn.isEmpty(); + } + + uint8_t priority() const { + return 255; + } +}; + +} // anonymous + +namespace mbus { + +MessageBus::MessageBus(INetwork &net, ProtocolSet protocols) : + _network(net), + _lock("mbus::MessageBus::_lock", false), + _routingTables(), + _sessions(), + _protocolRepository(), + _msn(), + _resender(), + _maxPendingCount(0), + _maxPendingSize(0), + _pendingCount(0), + _pendingSize(0) +{ + MessageBusParams params; + while (!protocols.empty()) { + IProtocol::SP protocol = protocols.extract(); + if (protocol.get() != NULL) { + params.addProtocol(protocol); + } + } + setup(params); +} + +MessageBus::MessageBus(INetwork &net, const MessageBusParams ¶ms) : + _network(net), + _lock("mbus::MessageBus::_lock", false), + _routingTables(), + _sessions(), + _protocolRepository(), + _msn(), + _resender(), + _maxPendingCount(params.getMaxPendingCount()), + _maxPendingSize(params.getMaxPendingSize()), + _pendingCount(0), + _pendingSize(0) +{ + setup(params); +} + +MessageBus::~MessageBus() +{ + // all sessions must have been destroyed prior to this, + // so no more traffic from clients + _msn.discardRecurrentTasks(); // no more traffic from recurrent tasks + _network.shutdown(); // no more traffic from network + + bool done = false; + while (!done) { + vespalib::Gate gate; + Messenger::ITask::UP task(new ShutdownTask(_network, _msn, done, gate)); + _msn.enqueue(std::move(task)); + gate.await(); + } +} + +void +MessageBus::setup(const MessageBusParams ¶ms) +{ + // Add all known protocols to the repository. + for (uint32_t i = 0, len = params.getNumProtocols(); i < len; ++i) { + _protocolRepository.putProtocol(params.getProtocol(i)); + } + + // Attach and start network. + _network.attach(*this); + if (!_network.start()) { + throw vespalib::NetworkSetupFailureException("Failed to start network."); + } + if (!_network.waitUntilReady(120)) { + throw vespalib::NetworkSetupFailureException("Network failed to become ready in time."); + } + + // Start messenger. + IRetryPolicy::SP retryPolicy = params.getRetryPolicy(); + if (retryPolicy.get() != NULL) { + _resender.reset(new Resender(retryPolicy)); + + Messenger::ITask::UP task(new ResenderTask(*_resender)); + _msn.addRecurrentTask(std::move(task)); + } + if (!_msn.start()) { + throw vespalib::NetworkSetupFailureException("Failed to start messenger."); + } +} + +SourceSession::UP +MessageBus::createSourceSession(IReplyHandler &handler) +{ + return createSourceSession(SourceSessionParams().setReplyHandler(handler)); +} + +SourceSession::UP +MessageBus::createSourceSession(IReplyHandler &handler, + const SourceSessionParams ¶ms) +{ + return createSourceSession(SourceSessionParams(params).setReplyHandler(handler)); +} + +SourceSession::UP +MessageBus::createSourceSession(const SourceSessionParams ¶ms) +{ + return SourceSession::UP(new SourceSession(*this, params)); +} + +IntermediateSession::UP +MessageBus::createIntermediateSession(const string &name, + bool broadcastName, + IMessageHandler &msgHandler, + IReplyHandler &replyHandler) +{ + return createIntermediateSession(IntermediateSessionParams() + .setName(name) + .setBroadcastName(broadcastName) + .setMessageHandler(msgHandler) + .setReplyHandler(replyHandler)); +} + +IntermediateSession::UP +MessageBus::createIntermediateSession(const IntermediateSessionParams ¶ms) +{ + LockGuard guard(_lock); + IntermediateSession::UP ret(new IntermediateSession(*this, params)); + _sessions[params.getName()] = ret.get(); + if (params.getBroadcastName()) { + _network.registerSession(params.getName()); + } + return ret; +} + +DestinationSession::UP +MessageBus::createDestinationSession(const string &name, + bool broadcastName, + IMessageHandler &handler) +{ + return createDestinationSession(DestinationSessionParams() + .setName(name) + .setBroadcastName(broadcastName) + .setMessageHandler(handler)); +} + +DestinationSession::UP +MessageBus::createDestinationSession(const DestinationSessionParams ¶ms) +{ + LockGuard guard(_lock); + DestinationSession::UP ret(new DestinationSession(*this, params)); + _sessions[params.getName()] = ret.get(); + if (params.getBroadcastName()) { + _network.registerSession(params.getName()); + } + return ret; +} + +void +MessageBus::unregisterSession(const string &sessionName) +{ + LockGuard guard(_lock); + _network.unregisterSession(sessionName); + _sessions.erase(sessionName); +} + +RoutingTable::SP +MessageBus::getRoutingTable(const string &protocol) +{ + typedef std::map<string, RoutingTable::SP>::iterator ITR; + LockGuard guard(_lock); + ITR itr = _routingTables.find(protocol); + if (itr == _routingTables.end()) { + return RoutingTable::SP(); // not found + } + return itr->second; +} + +IRoutingPolicy::SP +MessageBus::getRoutingPolicy(const string &protocolName, + const string &policyName, + const string &policyParam) +{ + return _protocolRepository.getRoutingPolicy(protocolName, policyName, policyParam); +} + +void +MessageBus::sync() +{ + _msn.sync(); + _network.sync(); // should not be necessary, as msn is intermediate +} + +void +MessageBus::handleMessage(Message::UP msg) +{ + if (_resender.get() != NULL && msg->hasBucketSequence()) { + deliverError(std::move(msg), ErrorCode::SEQUENCE_ERROR, + "Bucket sequences not supported when resender is enabled."); + return; + } + SendProxy &proxy = *(new SendProxy(*this, _network, _resender.get())); // deletes self + _msn.deliverMessage(std::move(msg), proxy); +} + +bool +MessageBus::setupRouting(const RoutingSpec &spec) +{ + std::map<string, RoutingTable::SP> rtm; + for (uint32_t i = 0; i < spec.getNumTables(); ++i) { + const RoutingTableSpec &cfg = spec.getTable(i); + IProtocol::SP protocol = getProtocol(cfg.getProtocol()); + if (protocol.get() == 0) { // protocol not found + LOG(info, "Protocol '%s' is not supported, ignoring routing table.", + cfg.getProtocol().c_str()); + continue; + } + RoutingTable::SP rt(new RoutingTable(cfg)); + rtm[cfg.getProtocol()] = rt; + } + { + LockGuard guard(_lock); + std::swap(_routingTables, rtm); + } + _protocolRepository.clearPolicyCache(); + return true; +} + +IProtocol::SP +MessageBus::getProtocol(const string &name) +{ + return _protocolRepository.getProtocol(name); +} + +IProtocol::SP +MessageBus::putProtocol(const IProtocol::SP & protocol) +{ + return _protocolRepository.putProtocol(protocol); +} + +bool +MessageBus::checkPending(Message &msg) +{ + bool busy = false; + const uint32_t size = msg.getApproxSize(); + { + constexpr auto relaxed = std::memory_order_relaxed; + const uint32_t maxCount = _maxPendingCount.load(relaxed); + const uint32_t maxSize = _maxPendingSize.load(relaxed); + if (maxCount > 0 || maxSize > 0) { + busy = ((maxCount > 0 && _pendingCount.load(relaxed) >= maxCount) || + (maxSize > 0 && _pendingSize.load(relaxed) >= maxSize)); + if (!busy) { + _pendingCount.fetch_add(1, relaxed); + _pendingSize.fetch_add(size, relaxed); + } + } + } + if (busy) { + return false; + } + msg.setContext(Context(static_cast<uint64_t>(size))); + msg.pushHandler(*this, *this); + return true; +} + +void +MessageBus::handleReply(Reply::UP reply) +{ + _pendingCount.fetch_sub(1, std::memory_order_relaxed); + _pendingSize.fetch_sub(reply->getContext().value.UINT64, + std::memory_order_relaxed); + IReplyHandler &handler = reply->getCallStack().pop(*reply); + deliverReply(std::move(reply), handler); +} + +void +MessageBus::handleDiscard(Context ctx) +{ + _pendingCount.fetch_sub(1, std::memory_order_relaxed); + _pendingSize.fetch_sub(ctx.value.UINT64, std::memory_order_relaxed); +} + +void +MessageBus::deliverMessage(Message::UP msg, const string &session) +{ + IMessageHandler *msgHandler = NULL; + { + LockGuard guard(_lock); + std::map<string, IMessageHandler*>::iterator it = _sessions.find(session); + if (it != _sessions.end()) { + msgHandler = it->second; + } + } + if (msgHandler == NULL) { + deliverError(std::move(msg), ErrorCode::UNKNOWN_SESSION, + vespalib::make_vespa_string( + "Session '%s' does not exist.", + session.c_str())); + } else if (!checkPending(*msg)) { + deliverError(std::move(msg), ErrorCode::SESSION_BUSY, + vespalib::make_vespa_string( + "Session '%s' is busy, try again later.", + session.c_str())); + } else { + _msn.deliverMessage(std::move(msg), *msgHandler); + } +} + +void +MessageBus::deliverError(Message::UP msg, uint32_t errCode, const string &errMsg) +{ + Reply::UP reply(new EmptyReply()); + reply->swapState(*msg); + reply->addError(Error(errCode, errMsg)); + + IReplyHandler &replyHandler = reply->getCallStack().pop(*reply); + deliverReply(std::move(reply), replyHandler); +} + +void +MessageBus::deliverReply(Reply::UP reply, IReplyHandler &handler) +{ + _msn.deliverReply(std::move(reply), handler); +} + +const string +MessageBus::getConnectionSpec() const +{ + return _network.getConnectionSpec(); +} + +void +MessageBus::setMaxPendingCount(uint32_t maxCount) +{ + _maxPendingCount.store(maxCount, std::memory_order_relaxed); +} + +void +MessageBus::setMaxPendingSize(uint32_t maxSize) +{ + _maxPendingSize.store(maxSize, std::memory_order_relaxed); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/messagebus.h b/messagebus/src/vespa/messagebus/messagebus.h new file mode 100644 index 00000000000..5bcfdef20a1 --- /dev/null +++ b/messagebus/src/vespa/messagebus/messagebus.h @@ -0,0 +1,308 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <boost/utility.hpp> +#include <map> +#include <vespa/messagebus/network/inetworkowner.h> +#include <vespa/messagebus/routing/resender.h> +#include <vespa/messagebus/routing/routingspec.h> +#include <vespa/messagebus/routing/routingtable.h> +#include <vespa/vespalib/util/sync.h> +#include "destinationsession.h" +#include "iconfighandler.h" +#include "idiscardhandler.h" +#include "intermediatesession.h" +#include "messagebusparams.h" +#include "messenger.h" +#include "protocolset.h" +#include "protocolrepository.h" +#include "sourcesession.h" +#include <string> +#include <atomic> + +namespace mbus { + +class SendProxy; + +/** + * A MessageBus object combined with an INetwork implementation makes up the central part of a messagebus setup. It is + * important that the application destructs all sessions before destructing the MessageBus object. Also, the INetwork + * object should be destructed after the MessageBus object. + */ +class MessageBus : public IMessageHandler, + public IConfigHandler, + public IDiscardHandler, + public INetworkOwner, + public IReplyHandler +{ +private: + INetwork &_network; + vespalib::Lock _lock; + std::map<string, RoutingTable::SP> _routingTables; + std::map<string, IMessageHandler*> _sessions; + ProtocolRepository _protocolRepository; + Messenger _msn; + Resender::UP _resender; + std::atomic<uint32_t> _maxPendingCount; + std::atomic<uint32_t> _maxPendingSize; + std::atomic<uint32_t> _pendingCount; + std::atomic<uint32_t> _pendingSize; + + /** + * This method performs the common constructor tasks. + * + * @param params The parameters to base setup on. + */ + void setup(const MessageBusParams ¶ms); + + /** + * This method handles choking input data so that message bus does not blindly accept everything. This prevents an + * application running out-of-memory in case it fail to choke input data itself. If this method returns false, it + * means that it should be rejected. + * + * @param msg The message to count. + * @return True if the message was accepted. + */ + bool checkPending(Message &msg); + + /** + * Constructs and schedules a Reply containing an error to the handler of the given Message. + * + * @param msg The message to reply to. + * @param errCode The code of the error to set. + * @param errMsg The message of the error to set. + */ + void deliverError(Message::UP msg, uint32_t errCode, const string &errMsg); + +public: + /** + * Convenience constructor that proxies {@link this#MessageBus(Network, MessageBusParams)} by adding the given + * protocols to a default {@link MessageBusParams} object. + * + * @param network The network to associate with. + * @param protocols An array of protocols to register. + */ + MessageBus(INetwork &net, ProtocolSet protocols); + + /** + * Constructs an instance of message bus. This requires a network object that it will associate with. This + * assignment may not change during the lifetime of this message bus. + * + * @param network The network to associate with. + * @param params The parameters that controls this bus. + */ + MessageBus(INetwork &net, const MessageBusParams ¶ms); + + /** + * Destruct. The destructor will shut down the underlying INetwork object. + **/ + virtual ~MessageBus(); + + /** + * This is a convenience method to call {@link this#createSourceSession(SourceSessionParams)} with default + * values for the {@link SourceSessionParams} object. + * + * @param handler The reply handler to receive the replies for the session. + * @return The created session. + */ + SourceSession::UP createSourceSession(IReplyHandler &handler); + + /** + * This is a convenience method to call {@link this#createSourceSession(SourceSessionParams)} by first + * assigning the reply handler to the parameter object. + * + * @param handler The reply handler to receive the replies for the session. + * @param params The parameters to control the session. + * @return The created session. + */ + SourceSession::UP createSourceSession(IReplyHandler &handler, + const SourceSessionParams ¶ms); + + /** + * Creates a source session on top of this message bus. + * + * @param params The parameters to control the session. + * @return The created session. + */ + SourceSession::UP createSourceSession(const SourceSessionParams ¶ms); + + /** + * This is a convenience method to call {@link this#createIntermediateSession(IntermediateSessionParams)} with + * default values for the {@link IntermediateSessionParams} object. + * + * @param name The local unique name for the created session. + * @param broadcastName Whether or not to broadcast this session's name on the network. + * @param msgHandler The handler to receive the messages for the session. + * @param replyHandler The handler to received the replies for the session. + * @return The created session. + */ + IntermediateSession::UP createIntermediateSession(const string &name, + bool broadcastName, + IMessageHandler &msgHandler, + IReplyHandler &replyHandler); + + /** + * Creates an intermediate session on top of this message bus using the given handlers and parameter object. + * + * @param params The parameters to control the session. + * @return The created session. + */ + IntermediateSession::UP createIntermediateSession(const IntermediateSessionParams ¶ms); + + /** + * This is a convenience method to call {@link this#createDestinationSession(DestinationSessionParams)} with default + * values for the {@link DestinationSessionParams} object. + * + * @param name The local unique name for the created session. + * @param broadcastName Whether or not to broadcast this session's name on the network. + * @param handler The handler to receive the messages for the session. + * @return The created session. + */ + DestinationSession::UP createDestinationSession(const string &name, + bool broadcastName, + IMessageHandler &handler); + + /** + * Creates a destination session on top of this message bus using the given handlers and parameter object. + * + * @param params The parameters to control the session. + * @return The created session. + */ + DestinationSession::UP createDestinationSession(const DestinationSessionParams ¶ms); + + /** + * Unregister a session. This method is invoked by session destructors to ensure that no more Message objects are + * delivered and that the session name is removed from the network naming service. The sync method can be invoked + * after invoking this one to ensure that no callbacks are active. + * + * @param sessionName name of the session to unregister + **/ + void unregisterSession(const string &sessionName); + + /** + * Obtain the routing table for the given protocol. If the appropriate routing table could not be found, a shared + * pointer to 0 is returned. + * + * @return shared pointer to routing table + * @param protocol the protocol name + **/ + RoutingTable::SP getRoutingTable(const string &protocol); + + /** + * Returns a routing policy that corresponds to the argument protocol name, policy name and policy parameter. This + * will cache reuse all policies as soon as they are first requested. + * + * @param protocol The name of the protocol to invoke {@link Protocol#createPolicy(String,String)} on. + * @param policyName The name of the routing policy to retrieve. + * @param policyParam The parameter for the routing policy to retrieve. + * @return A corresponding routing policy, or null. + */ + IRoutingPolicy::SP getRoutingPolicy(const string &protocol, const string &policyName, + const string &policyParam); + + /** + * Synchronize with internal threads. This method will handshake with all internal threads. This has the implicit + * effect of waiting for all active callbacks. Note that this method should never be invoked from a callback since + * that would make the thread wait for itself... forever. This method is typically used to untangle during session + * destruction. + **/ + void sync(); + + /** + * Returns the resender that is running within this message bus. + * + * @return The resender. + */ + Resender *getResender() { return _resender.get(); } + + /** + * Returns the number of messages received that have not been replied to yet. + * + * @return The pending count. + */ + uint32_t getPendingCount() const { return _pendingCount; } + + /** + * Returns the size of messages received that have not been replied to yet. + * + * @return The pending size. + */ + uint32_t getPendingSize() const { return _pendingSize; } + + /** + * Sets the maximum number of messages that can be received without being replied to yet. + * + * @param maxCount The max count. + */ + void setMaxPendingCount(uint32_t maxCount); + + /** + * Gets maximum number of messages that can be received without being + * replied to yet. + */ + uint32_t getMaxPendingCount() const noexcept { + return _maxPendingCount.load(std::memory_order_relaxed); + } + + /** + * Sets the maximum size of messages that can be received without being replied to yet. + * + * @param maxSize The max size. + */ + void setMaxPendingSize(uint32_t maxSize); + + /** + * Gets maximum combined size of messages that can be received without + * being replied to yet. + */ + uint32_t getMaxPendingSize() const noexcept { + return _maxPendingSize.load(std::memory_order_relaxed); + } + + /** + * Adds a protocol to the internal repository of protocols, replacing any previous instance of the + * protocol and clearing the associated routing policy cache. + * + * @param protocol The protocol to add. + */ + IProtocol::SP putProtocol(const IProtocol::SP & protocol); + + /** + * Returns the connection spec string for the network layer of this message bus. This is merely a proxy of + * the same function in the network layer. + * + * @return The connection string. + */ + const string getConnectionSpec() const; + + /** + * Provide access to the underlying {@link Messenger} object. + * + * @return The underlying {@link Messenger} object. + */ + Messenger & getMessenger() { return _msn; } + + // Implements IReplyHandler. + void handleReply(Reply::UP reply); + + // Implements IDiscardHandler. + void handleDiscard(Context ctx); + + // Implements IMessageHandler. + void handleMessage(Message::UP msg); + + // Implements IConfigHandler. + bool setupRouting(const RoutingSpec &spec); + + // Implements INetworkOwner. + IProtocol::SP getProtocol(const string &name); + + // Implements INetworkOwner. + void deliverMessage(Message::UP msg, const string &session); + + // Implements INetworkOwner. + void deliverReply(Reply::UP reply, IReplyHandler &handler); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/messagebusparams.cpp b/messagebus/src/vespa/messagebus/messagebusparams.cpp new file mode 100644 index 00000000000..f1a609086d6 --- /dev/null +++ b/messagebus/src/vespa/messagebus/messagebusparams.cpp @@ -0,0 +1,36 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/messagebus/routing/retrytransienterrorspolicy.h> +#include "messagebus.h" + +namespace mbus { + +MessageBusParams::MessageBusParams() : + _protocols(), + _retryPolicy(new RetryTransientErrorsPolicy()), + _maxPendingCount(1024), + _maxPendingSize(128 * 1024 * 1024) +{ + // empty +} + +uint32_t +MessageBusParams::getNumProtocols() const +{ + return _protocols.size(); +} + +IProtocol::SP +MessageBusParams::getProtocol(uint32_t i) const +{ + return _protocols[i]; +} + +MessageBusParams & +MessageBusParams::addProtocol(IProtocol::SP protocol) +{ + _protocols.push_back(protocol); + return *this; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/messagebusparams.h b/messagebus/src/vespa/messagebus/messagebusparams.h new file mode 100644 index 00000000000..6dbcfb8781f --- /dev/null +++ b/messagebus/src/vespa/messagebus/messagebusparams.h @@ -0,0 +1,103 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vespa/messagebus/routing/iretrypolicy.h> +#include <string> +#include <vector> +#include "iprotocol.h" + +namespace mbus { + +class MessageBus; + +/** + * To facilitate several configuration parameters to the {@link MessageBus} constructor, all parameters are held by this + * class. This class has reasonable default values for each parameter. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @version $Id$ + */ +class MessageBusParams { +private: + std::vector<IProtocol::SP> _protocols; + IRetryPolicy::SP _retryPolicy; + uint32_t _maxPendingCount; + uint32_t _maxPendingSize; + +public: + /** + * Constructs a new instance of this parameter object with default values for all members. + */ + MessageBusParams(); + + /** + * Returns the retry policy for the resender. + * + * @return The policy. + */ + IRetryPolicy::SP getRetryPolicy() const { return _retryPolicy; } + + /** + * Sets the retry policy for the resender. + * + * @param retryPolicy The policy to set. + * @return This, to allow chaining. + */ + MessageBusParams &setRetryPolicy(IRetryPolicy::SP retryPolicy) { _retryPolicy = retryPolicy; return *this; } + + /** + * Registers a protocol under the name given by {@link com.yahoo.messagebus.Protocol#getName()}. + * + * @param protocol The protocol to register. + * @return This, to allow chaining. + */ + MessageBusParams &addProtocol(IProtocol::SP protocol); + + /** + * Returns the number of protocols that are contained in this. + * + * @return The number of protocols. + */ + uint32_t getNumProtocols() const; + + /** + * Returns the protocol at the given index. + * + * @param i The index of the protocol to return. + * @return The protocol object. + */ + IProtocol::SP getProtocol(uint32_t i) const; + + /** + * Returns the maximum number of pending messages. + * + * @return The count limit. + */ + uint32_t getMaxPendingCount() const { return _maxPendingCount; } + + /** + * Sets the maximum number of allowed pending messages. + * + * @param maxCount The count limit to set. + * @return This, to allow chaining. + */ + MessageBusParams &setMaxPendingCount(uint32_t maxCount) { _maxPendingCount = maxCount; return *this; } + + /** + * Returns the maximum number of bytes allowed for pending messages. + * + * @return The size limit. + */ + uint32_t getMaxPendingSize() const { return _maxPendingSize; } + + /** + * Sets the maximum number of bytes allowed for pending messages. + * + * @param maxSize The size limit to set. + * @return This, to allow chaining. + */ + MessageBusParams &setMaxPendingSize(int maxSize) { _maxPendingSize = maxSize; return *this; } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/messenger.cpp b/messagebus/src/vespa/messagebus/messenger.cpp new file mode 100644 index 00000000000..3727b258a89 --- /dev/null +++ b/messagebus/src/vespa/messagebus/messenger.cpp @@ -0,0 +1,286 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".messenger"); + +#include <vespa/vespalib/util/sync.h> +#include "messenger.h" + +namespace { + +template<class T> +struct DeleteFunctor +{ + bool operator()(T *ptr) const + { + delete ptr; + return true; + } +}; + +class MessageTask : public mbus::Messenger::ITask { +private: + mbus::Message::UP _msg; + mbus::IMessageHandler &_handler; + +public: + MessageTask(mbus::Message::UP msg, mbus::IMessageHandler &handler) + : _msg(std::move(msg)), + _handler(handler) + { + // empty + } + + ~MessageTask() { + if (_msg.get() != NULL) { + _msg->discard(); + } + } + + void run() { + _handler.handleMessage(std::move(_msg)); + } + + uint8_t priority() const { + if (_msg.get() != NULL) { + return _msg->priority(); + } + + return 255; + } +}; + +class ReplyTask : public mbus::Messenger::ITask { +private: + mbus::Reply::UP _reply; + mbus::IReplyHandler &_handler; + +public: + ReplyTask(mbus::Reply::UP reply, mbus::IReplyHandler &handler) + : _reply(std::move(reply)), + _handler(handler) + { + // empty + } + + ~ReplyTask() { + if (_reply.get() != NULL) { + _reply->discard(); + } + } + + void run() { + _handler.handleReply(std::move(_reply)); + } + + uint8_t priority() const { + if (_reply.get() != NULL) { + return _reply->priority(); + } + + return 255; + } +}; + +class SyncTask : public mbus::Messenger::ITask { +private: + vespalib::Gate &_gate; + +public: + SyncTask(vespalib::Gate &gate) + : _gate(gate) + { + // empty + } + + ~SyncTask() { + _gate.countDown(); + } + + void run() { + // empty + } + + uint8_t priority() const { + return 255; + } +}; + +class AddRecurrentTask : public mbus::Messenger::ITask { +private: + std::vector<ITask*> &_tasks; + mbus::Messenger::ITask::UP _task; + +public: + AddRecurrentTask(std::vector<ITask*> &tasks, mbus::Messenger::ITask::UP task) + : _tasks(tasks), + _task(std::move(task)) + { + // empty + } + + void run() { + _tasks.push_back(_task.release()); + } + + uint8_t priority() const { + return 255; + } +}; + +class DiscardRecurrentTasks : public SyncTask { +private: + std::vector<ITask*> &_tasks; + +public: + DiscardRecurrentTasks(vespalib::Gate &gate, std::vector<ITask*> &tasks) + : SyncTask(gate), + _tasks(tasks) + { + // empty + } + + void run() { + std::for_each(_tasks.begin(), _tasks.end(), DeleteFunctor<ITask>()); + _tasks.clear(); + SyncTask::run(); + } + + uint8_t priority() const { + return 255; + } +}; + +} // anonymous + +namespace mbus { + +Messenger::Messenger() : + _monitor(), + _pool(128000), + _children(), + _queue(), + _closed(false) +{ + // empty +} + +Messenger::~Messenger() +{ + { + vespalib::MonitorGuard guard(_monitor); + _closed = true; + guard.broadcast(); + } + _pool.Close(); + std::for_each(_children.begin(), _children.end(), DeleteFunctor<ITask>()); + if ( ! _queue.empty()) { + LOG(warning, + "Messenger shut down with pending tasks, " + "please review shutdown logic."); + while (!_queue.empty()) { + delete _queue.front(); + _queue.pop(); + } + } +} + +void +Messenger::Run(FastOS_ThreadInterface *thread, void *arg) +{ + (void)thread; + (void)arg; + while (true) { + ITask::UP task; + { + vespalib::MonitorGuard guard(_monitor); + if (_closed) { + break; + } + if (_queue.empty()) { + guard.wait(100); + } + if (!_queue.empty()) { + task.reset(_queue.front()); + _queue.pop(); + } + } + if (task.get() != NULL) { + try { + task->run(); + } catch (const std::exception &e) { + LOG(warning, "An exception was thrown while running " + "a task; %s", e.what()); + } + } + for (ITask * itask : _children) { + itask->run(); + } + } +} + +void +Messenger::addRecurrentTask(ITask::UP task) +{ + ITask::UP add(new AddRecurrentTask(_children, std::move(task))); + enqueue(std::move(add)); +} + +void +Messenger::discardRecurrentTasks() +{ + vespalib::Gate gate; + ITask::UP task(new DiscardRecurrentTasks(gate, _children)); + enqueue(std::move(task)); + gate.await(); +} + +bool +Messenger::start() +{ + if (_pool.NewThread(this) == 0) { + return false; + } + return true; +} + +void +Messenger::deliverMessage(Message::UP msg, IMessageHandler &handler) +{ + enqueue(ITask::UP(new MessageTask(std::move(msg), handler))); +} + +void +Messenger::deliverReply(Reply::UP reply, IReplyHandler &handler) +{ + enqueue(ITask::UP(new ReplyTask(std::move(reply), handler))); +} + +void +Messenger::enqueue(ITask::UP task) +{ + vespalib::MonitorGuard guard(_monitor); + if (!_closed) { + _queue.push(task.release()); + if (_queue.size() == 1) { + guard.signal(); + } + } +} + +void +Messenger::sync() +{ + vespalib::Gate gate; + enqueue(ITask::UP(new SyncTask(gate))); + gate.await(); +} + +bool +Messenger::isEmpty() const +{ + vespalib::MonitorGuard guard(_monitor); + return _queue.empty(); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/messenger.h b/messagebus/src/vespa/messagebus/messenger.h new file mode 100644 index 00000000000..34c02ed1cad --- /dev/null +++ b/messagebus/src/vespa/messagebus/messenger.h @@ -0,0 +1,128 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vespa/fastos/fastos.h> +#include <vespa/vespalib/util/executor.h> +#include <vespa/vespalib/util/sync.h> +#include <vespa/vespalib/util/arrayqueue.hpp> +#include "imessagehandler.h" +#include "ireplyhandler.h" +#include "message.h" +#include "reply.h" + +namespace mbus { + +/** + * This class implements a single thread that is able to process arbitrary + * tasks. Tasks are enqueued using the synchronized {@link #enqueue(Task)} + * method, and are run in the order they were enqueued. + */ +class Messenger : public boost::noncopyable, + public FastOS_Runnable { +public: + /** + * Defines the required interface for tasks to be posted to this worker. + */ + class ITask : public boost::noncopyable, + public vespalib::Executor::Task { + public: + /** + * Convenience typedefs. + */ + typedef std::unique_ptr<ITask> UP; + + /** + * Returns the priority of this task. + */ + virtual uint8_t priority() const = 0; + }; + +private: + vespalib::Monitor _monitor; + FastOS_ThreadPool _pool; + std::vector<ITask*> _children; + vespalib::ArrayQueue<ITask*> _queue; + bool _closed; + +protected: + // Implements FastOS_Runnable. + void Run(FastOS_ThreadInterface *thread, void *arg); + +public: + /** + * Constructs a new messenger object. + */ + Messenger(); + + /** + * Frees any allocated resources. Also destroys all queued tasks. + */ + ~Messenger(); + + /** + * Adds a recurrent task to this that is to be run for every iteration of + * the main loop. This task must be very light-weight as to not block the + * messenger. This method is thread-safe. + * + * @param task The task to add. + */ + void addRecurrentTask(ITask::UP task); + + /** + * Discards all the recurrent tasks previously added to using the {@link + * #addRecurrentTask(ITask)} method. This method is thread-safe. + */ + void discardRecurrentTasks(); + + /** + * Starts the internal thread. This must be done AFTER all recurrent tasks + * have been added. + * + * @return True if the thread was started. + * @see #addRecurrentTask(ITask) + */ + bool start(); + + /** + * Handshakes with the internal thread. If this method is called using the + * messenger thread, this will deadlock. + */ + void sync(); + + /** + * Convenience method to post a {@link MessageTask} to the queue of tasks to + * be executed. + * + * @param msg The message to send. + * @param handler The handler to send to. + */ + void deliverMessage(Message::UP msg, IMessageHandler &handler); + + /** + * Convenience method to post a {@link ReplyTask} to the queue of tasks to + * be executed. + * + * @param reply The reply to return. + * @param handler The handler to return to. + */ + void deliverReply(Reply::UP reply, IReplyHandler &handler); + + /** + * Enqueues the given task in the list of tasks that this worker is to + * process. If this thread has been destroyed previously, this method + * invokes {@link Messenger.Task#destroy()}. + * + * @param task The task to enqueue. + */ + void enqueue(ITask::UP task); + + /** + * Returns whether or not there are any tasks queued for execution. + * + * @return True if there are no tasks. + */ + bool isEmpty() const; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/network/.gitignore b/messagebus/src/vespa/messagebus/network/.gitignore new file mode 100644 index 00000000000..5dae353d999 --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/.gitignore @@ -0,0 +1,2 @@ +.depend +Makefile diff --git a/messagebus/src/vespa/messagebus/network/CMakeLists.txt b/messagebus/src/vespa/messagebus/network/CMakeLists.txt new file mode 100644 index 00000000000..33074a33b9d --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/CMakeLists.txt @@ -0,0 +1,16 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_library(messagebus_network OBJECT + SOURCES + identity.cpp + oosclient.cpp + oosmanager.cpp + rpcnetwork.cpp + rpcnetworkparams.cpp + rpcsendv1.cpp + rpcservice.cpp + rpcserviceaddress.cpp + rpcservicepool.cpp + rpctarget.cpp + rpctargetpool.cpp + DEPENDS +) diff --git a/messagebus/src/vespa/messagebus/network/identity.cpp b/messagebus/src/vespa/messagebus/network/identity.cpp new file mode 100644 index 00000000000..adddbc0c6ec --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/identity.cpp @@ -0,0 +1,32 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".identity"); +#include "identity.h" +#include <vespa/vespalib/util/host_name.h> + +namespace mbus { + +Identity::Identity(const string &configId) : + _hostname(), + _servicePrefix(configId) +{ + _hostname = vespalib::HostName::get(); +} + +std::vector<string> +Identity::split(const string &name) +{ + std::vector<string> ret; + string::size_type pos = 0; + string::size_type split = name.find_first_of('/'); + while (split != string::npos) { + ret.push_back(string(name, pos, split - pos)); + pos = split + 1; + split = name.find_first_of('/', pos); + } + ret.push_back(string(name, pos)); + return ret; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/network/identity.h b/messagebus/src/vespa/messagebus/network/identity.h new file mode 100644 index 00000000000..f93a68bd781 --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/identity.h @@ -0,0 +1,58 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vespa/messagebus/common.h> +#include <vector> + +namespace mbus { + +/** + * An Identity object is a simple value object containing information + * about the identity of a Network object within the vespa cluster. It + * contains the service name prefix used when registering + * sessions. This class also has some static utility methods for + * general service name manipulation. + **/ +class Identity +{ +private: + string _hostname; + string _servicePrefix; + +public: + /** + * Use config to resolve the identity for the given config + * id. This is intended to be done once at program + * startup. Changing the identity of a service requires + * restart. This method will not mask config exceptions. + * + * @return identity for the given config id + * @param configId application config id + **/ + Identity(const string &configId); + + /** + * Obtain the hostname held by this object. + * + * @return hostname + **/ + const string &getHostname() const { return _hostname; } + + /** + * Obtain the service prefix held by this object. + * + * @return service prefix + **/ + const string &getServicePrefix() const { return _servicePrefix; } + + /** + * Split a service name into its path elements. + * + * @return service name path elements + * @param name the service name + **/ + static std::vector<string> split(const string &name); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/network/inetwork.h b/messagebus/src/vespa/messagebus/network/inetwork.h new file mode 100644 index 00000000000..cf98711bacd --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/inetwork.h @@ -0,0 +1,145 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <memory> +#include <vespa/messagebus/routing/routingnode.h> +#include <vespa/slobrok/sbmirror.h> +#include "inetworkowner.h" + +namespace mbus { + +/** + * This interface is used to hide away the implementation details of the network + * code from the rest of the messagebus implementation. The methods defined in + * this interface is intended to be invoked by MessageBus and not by the + * application. The only responsibility of the application is to instantiate an + * INetwork implementing object, give it to the MessageBus constructor and make + * sure it outlives the MessageBus object. + */ +class INetwork { +public: + /** + * Destructor. Frees any allocated resources. + */ + virtual ~INetwork() { } + + /** + * Attach the network layer to the given owner. This method should be + * invoked before starting the network. This method is invoked by the + * MessageBus constructor. + * + * @param owner owner of the network + */ + virtual void attach(INetworkOwner &owner) = 0; + + /** + * Returns a string that represents the connection specs of this network. It + * is in not a complete address since it know nothing of the sessions that + * run on it. + * + * @return The connection string. + */ + virtual const string getConnectionSpec() const = 0; + + /** + * Start this network. This method should be invoked after the attach method + * and before starting to use the network. This method is invoked by the + * MessageBus constructor. + * + * @return true if the network could be started + */ + virtual bool start() = 0; + + /** + * Waits for at most the given number of seconds for all dependencies to + * become ready. + * + * @param seconds The timeout. + * @return True if ready. + */ + virtual bool waitUntilReady(double seconds) const = 0; + + /** + * Register a session name with the network layer. This will make the + * session visible to other nodes. + * + * @param session the session name + */ + virtual void registerSession(const string &session) = 0; + + /** + * Unregister a session name with the network layer. This will make the + * session unavailable for other nodes. + * + * @param session session name + */ + virtual void unregisterSession(const string &session) = 0; + + /** + * Resolves the service address of the recipient referenced by the given + * routing node. If a recipient can not be resolved, this method tags the + * node with an error. If this method succeeds, you need to invoke {@link + * #freeServiceAddress(RoutingNode)} once you are done with the service + * address. + * + * @param recipient The node whose service address to allocate. + * @return True if a service address was allocated. + */ + virtual bool allocServiceAddress(RoutingNode &recipient) = 0; + + /** + * Frees the service address from the given routing node. This allows the + * network layer to track and close connections as required. + * + * @param recipient The node whose service address to free. + */ + virtual void freeServiceAddress(RoutingNode &recipient) = 0; + + /** + * Send a message to the given recipients. A {@link RoutingNode} contains + * all the necessary context for sending. + * + * @param msg The message to send. + * @param recipients A list of routing leaf nodes resolved for the message. + */ + virtual void send(const Message &msg, const std::vector<RoutingNode*> &recipients) = 0; + + /** + * Synchronize with internal threads. This method will handshake with all + * internal threads. This has the implicit effect of waiting for all active + * callbacks. Note that this method should never be invoked from a callback + * since that would make the thread wait for itself... forever. This method + * is typically used to untangle during session destruction. If this method + * is invoked after the shutdown method is invoked it will never return. + */ + virtual void sync() = 0; + + /** + * Shut down this network. This method will block until the network has been + * properly shut down. After the network has been shut down, this method + * will also flush out the ghosts in the system. In this case, the ghosts + * are the replies waiting to be delivered in a separate thread context, but + * having no real end-points since all sessions must be destructed before + * destructing the MessageBus object. This method is invoked by the + * MessageBus destructor. + */ + virtual void shutdown() = 0; + + /** + * If anything is posted to the network after {@link #shutdown()} is called, + * there is no thread alive that can generate a reply. By calling this method + * you are effectively flushing all ghosts from the network back to their + * respective owners. + */ + virtual void postShutdownHook() = 0; + + /** + * Returns a reference to a name server mirror. + * + * @return The mirror object. + */ + virtual const slobrok::api::IMirrorAPI &getMirror() const = 0; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/network/inetworkowner.h b/messagebus/src/vespa/messagebus/network/inetworkowner.h new file mode 100644 index 00000000000..493f121783f --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/inetworkowner.h @@ -0,0 +1,49 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <memory> +#include <vespa/messagebus/iprotocol.h> +#include <vespa/messagebus/ireplyhandler.h> + +namespace mbus { + +/** + * A network owner is the object that instantiates and uses a network. The API to send messages + * across the network is part of the Network interface, whereas this interface exposes the required + * functionality of a network owner to be able to decode and deliver incoming messages. + */ +class INetworkOwner { +public: + /** + * Required for inheritance. + */ + virtual ~INetworkOwner() { } + + /** + * All messages are sent across the network with its accompanying protocol name so that it can be decoded at the + * receiving end. The network queries its owner through this function to resolve the protocol from its name. + * + * @param name The name of the protocol to return. + * @return The named protocol. + */ + virtual IProtocol::SP getProtocol(const string &name) = 0; + + /** + * All messages that arrive in the network layer is passed to its owner through this function. + * + * @param message The message that just arrived from the network. + * @param session The name of the session that is the recipient of the request. + */ + virtual void deliverMessage(Message::UP message, const string &session) = 0; + + /** + * All replies that arrive in the network layer is passed through this to unentangle it from the network thread. + * + * @param reply The reply that just arrived from the network. + * @param handler The handler that is to receive the reply. + */ + virtual void deliverReply(Reply::UP reply, IReplyHandler &handler) = 0; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/network/iserviceaddress.h b/messagebus/src/vespa/messagebus/network/iserviceaddress.h new file mode 100644 index 00000000000..78f487cc942 --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/iserviceaddress.h @@ -0,0 +1,27 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +namespace mbus { + +/** + * This interface represents an abstract network service; i.e. somewhere to send messages. An instance of this is + * retrieved by calling {@link Network#lookup(String)}. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @version $Id$ + */ +class IServiceAddress { +public: + /** + * Convenience typedefs. + */ + typedef std::unique_ptr<IServiceAddress> UP; + + /** + * Virtual destructor required for inheritance. + */ + virtual ~IServiceAddress() { } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/network/oosclient.cpp b/messagebus/src/vespa/messagebus/network/oosclient.cpp new file mode 100644 index 00000000000..b25668c1481 --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/oosclient.cpp @@ -0,0 +1,111 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".oosclient"); +#include "oosclient.h" + +namespace mbus { + +void +OOSClient::handleReply() +{ + if (!_req->CheckReturnTypes("Si")) { + _target->SubRef(); + _target = 0; + Schedule(1.0); + return; + } + FRT_Values &ret = *(_req->GetReturn()); + uint32_t retGen = ret[1]._intval32; + if (_reqGen != retGen) { + StringList oos; + uint32_t numNames = ret[0]._string_array._len; + FRT_StringValue *names = ret[0]._string_array._pt; + for (uint32_t idx = 0; idx < numNames; ++idx) { + oos.push_back(string(names[idx]._str)); + } + _oosList.swap(oos); + _reqGen = retGen; + _listGen = retGen; + } + Schedule(0.1); +} + +void +OOSClient::handleConnect() +{ + if (_target == 0) { + _target = _orb.GetTarget(_spec.c_str()); + _reqGen = 0; + } +} + +void +OOSClient::handleInvoke() +{ + LOG_ASSERT(_target != 0); + _req = _orb.AllocRPCRequest(_req); + _req->SetMethodName("fleet.getOOSList"); + _req->GetParams()->AddInt32(_reqGen); // gencnt + _req->GetParams()->AddInt32(60000); // mstimeout + _target->InvokeAsync(_req, 70.0, this); +} + +void +OOSClient::PerformTask() +{ + if (_reqDone) { + _reqDone = false; + handleReply(); + return; + } + handleConnect(); + handleInvoke(); +} + +void +OOSClient::RequestDone(FRT_RPCRequest *req) +{ + LOG_ASSERT(req == _req && !_reqDone); + (void) req; + _reqDone = true; + ScheduleNow(); +} + +OOSClient::OOSClient(FRT_Supervisor &orb, + const string &mySpec) + : FNET_Task(orb.GetScheduler()), + _orb(orb), + _spec(mySpec), + _oosList(), + _reqGen(0), + _listGen(0), + _dumpGen(0), + _reqDone(false), + _target(0), + _req(0) +{ + ScheduleNow(); +} + +OOSClient::~OOSClient() +{ + Kill(); + if (_req != 0) { + _req->Abort(); + _req->SubRef(); + } + if (_target != 0) { + _target->SubRef(); + } +} + +void +OOSClient::dumpState(StringSet &dst) +{ + dst.insert(_oosList.begin(), _oosList.end()); + _dumpGen = _listGen; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/network/oosclient.h b/messagebus/src/vespa/messagebus/network/oosclient.h new file mode 100644 index 00000000000..62f799740ec --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/oosclient.h @@ -0,0 +1,125 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <vespa/fnet/frt/frt.h> +#include <vespa/messagebus/common.h> +#include <vector> +#include <set> + +namespace mbus { + +/** + * This class keeps track of OOS information obtained from a single + * server. This class is used by the OOSManager class. Note that since + * this class is only used inside the transport thread it has no + * synchronization. Using it directly will lead to race conditions and + * possible crashes. + **/ +class OOSClient : public FNET_Task, + public FRT_IRequestWait +{ +private: + typedef std::vector<string> StringList; + + FRT_Supervisor &_orb; + string _spec; + StringList _oosList; + uint32_t _reqGen; // server gen used for request + uint32_t _listGen; // server gen of the oosList + uint32_t _dumpGen; // server gen used for the last dump + bool _reqDone; + FRT_Target *_target; + FRT_RPCRequest *_req; + + OOSClient(const OOSClient &); + OOSClient &operator=(const OOSClient &); + + /** + * Handle a server reply. + **/ + void handleReply(); + + /** + * Handle server (re)connect. + **/ + void handleConnect(); + + /** + * Handle server invocation. + **/ + void handleInvoke(); + + /** + * From FNET_Task, performs overall server poll logic. + **/ + void PerformTask(); + + /** + * From FRT_IRequestWait, picks up server replies. + * + * @param req the request that has completed + **/ + void RequestDone(FRT_RPCRequest *req); + +public: + /** + * Data structure used to aggregate OOS information + **/ + typedef std::set<string> StringSet; + + /** + * Convenience typedef for a shared pointer to a OOSClient object. + **/ + typedef std::shared_ptr<OOSClient> SP; + + /** + * Create a new OOSClient polling oos information from the given + * server. + * + * @param orb object used for RPC operations + * @param spec fnet connect spec for oos server + **/ + OOSClient(FRT_Supervisor &orb, const string &spec); + + /** + * Destructor. + **/ + virtual ~OOSClient(); + + /** + * Obtain the connect spec of the OOS server this client is + * talking to. + * + * @return OOS server connect spec + **/ + const string &getSpec() const { return _spec; } + + /** + * Check if this client has changed. A client has changed if it + * has obtain now information after the dumpState method was last + * invoked. + * + * @return true is this client has changed + **/ + bool isChanged() const { return (_listGen != _dumpGen); } + + /** + * Returns whether or not this client has receieved any reply + * at all from the server it is connected to. + * + * @return True if initial request has returned. + */ + bool isReady() const { return _listGen != 0; } + + /** + * Dump the current oos information known by this client into the + * given string set. + * + * @param dst object used to aggregate oos information + **/ + void dumpState(StringSet &dst); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/network/oosmanager.cpp b/messagebus/src/vespa/messagebus/network/oosmanager.cpp new file mode 100644 index 00000000000..15f76c0668f --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/oosmanager.cpp @@ -0,0 +1,105 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".oosmanager"); + +#include <algorithm> +#include <vespa/fnet/frt/frt.h> +#include "oosmanager.h" +#include "rpcnetwork.h" + +namespace mbus { + +OOSClient::SP +OOSManager::getClient(const string &spec) +{ + for (uint32_t i = 0; i < _clients.size(); ++i) { + if (_clients[i]->getSpec() == spec) { + return _clients[i]; + } + } + return OOSClient::SP(new OOSClient(_orb, spec)); +} + +void +OOSManager::PerformTask() +{ + bool changed = false; + if (_slobrokGen != _mirror.updates()) { + _slobrokGen = _mirror.updates(); + SpecList newServices = _mirror.lookup(_servicePattern); + std::sort(newServices.begin(), newServices.end()); + if (newServices != _services) { + ClientList newClients; + for (uint32_t i = 0; i < newServices.size(); ++i) { + newClients.push_back(getClient(newServices[i].second)); + } + _services.swap(newServices); + _clients.swap(newClients); + changed = true; + } + } + bool allOk = _mirror.ready(); + for (uint32_t i = 0; i < _clients.size(); ++i) { + if (_clients[i]->isChanged()) { + changed = true; + } + if (!_clients[i]->isReady()) { + allOk = false; + } + } + if (changed) { + OOSSet oos(new StringSet()); + for (uint32_t i = 0; i < _clients.size(); ++i) { + _clients[i]->dumpState(*oos); + } + vespalib::LockGuard guard(_lock); + _oosSet.swap(oos); + } + if (allOk && !_ready) { + _ready = true; + } + Schedule(_ready ? 1.0 : 0.1); +} + +OOSManager::OOSManager(FRT_Supervisor &orb, + slobrok::api::MirrorAPI &mirror, + const string &servicePattern) + : FNET_Task(orb.GetScheduler()), + _orb(orb), + _mirror(mirror), + _disabled(servicePattern.empty()), + _ready(_disabled), + _lock("mbus::OOSManager::_lock", false), + _servicePattern(servicePattern), + _slobrokGen(0), + _clients(), + _oosSet() +{ + if (!_disabled) { + ScheduleNow(); + } +} + +OOSManager::~OOSManager() +{ + Kill(); +} + +bool +OOSManager::isOOS(const string &service) +{ + if (_disabled) { + return false; + } + vespalib::LockGuard guard(_lock); + if (_oosSet.get() == NULL) { + return false; + } + if (_oosSet->find(service) == _oosSet->end()) { + return false; + } + return true; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/network/oosmanager.h b/messagebus/src/vespa/messagebus/network/oosmanager.h new file mode 100644 index 00000000000..15a6fc48623 --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/oosmanager.h @@ -0,0 +1,93 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <boost/utility.hpp> +#include <vespa/fnet/fnet.h> +#include <set> +#include <vespa/slobrok/sbmirror.h> +#include <string> +#include <vespa/vespalib/util/sync.h> +#include "oosclient.h" + +namespace mbus { + +class RPCNetwork; + +/** + * This class keeps track of OOS information. A set of servers having OOS information are identified by looking up a + * service pattern in the slobrok. These servers are then polled for information. The information is compiled into a + * local repository for fast lookup. + */ +class OOSManager : public boost::noncopyable, public FNET_Task { +public: + /** + * Convenience typedefs. + */ + typedef slobrok::api::MirrorAPI MirrorAPI; + typedef MirrorAPI::SpecList SpecList; + typedef std::vector<OOSClient::SP> ClientList; + typedef std::set<string> StringSet; + typedef std::shared_ptr<StringSet> OOSSet; + +private: + FRT_Supervisor &_orb; + MirrorAPI &_mirror; + bool _disabled; + bool _ready; + vespalib::Lock _lock; + string _servicePattern; + uint32_t _slobrokGen; + SpecList _services; + ClientList _clients; + OOSSet _oosSet; + + /** + * Reuse or create a client against the given server. + * + * @param spec The connection spec of the OOS server we want to talk to. + * @return A shared oosclient object. + */ + OOSClient::SP getClient(const string &spec); + + /** + * Method invoked when this object is run as a task. This method will update the oos information held by + * this object. + */ + void PerformTask(); + +public: + /** + * Create a new OOSManager. The given service pattern will be looked up in the given slobrok mirror. The + * resulting set of services will be polled for oos information. + * + * @param orb The supervisor used for RPC operations. + * @param mirror The slobrok mirror. + * @param servicePattern The service pattern for oos servers. + */ + OOSManager(FRT_Supervisor &orb, + slobrok::api::MirrorAPI &mirror, + const string &servicePattern); + + /** + * Destructor. + */ + virtual ~OOSManager(); + + /** + * Returns whether or not some initial state has been returned. + * + * @return True, if initial state has been found. + */ + bool isReady() const { return _ready; } + + /** + * Returns whether or not the given service has been marked as out of service. + * + * @param service The service to check. + * @return True if the service is out of service. + */ + bool isOOS(const string &service); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/network/rpcnetwork.cpp b/messagebus/src/vespa/messagebus/network/rpcnetwork.cpp new file mode 100644 index 00000000000..434cdb0e3c3 --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/rpcnetwork.cpp @@ -0,0 +1,381 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".rpcnetwork"); + +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/iprotocol.h> +#include <vespa/messagebus/tracelevel.h> +#include <vespa/messagebus/vtag.h> +#include <vespa/vespalib/util/vstringfmt.h> +#include <vespa/vespalib/util/sync.h> +#include "inetworkowner.h" +#include "rpcnetwork.h" +#include "rpcsendv1.h" +#include "rpcservice.h" + +namespace { + +/** + * Implements a helper class for {@link RPCNetwork#sync()}. It provides a + * blocking method {@link #await()} that will wait until the internal state + * of this object is set to 'done'. By scheduling this task in the network + * thread and then calling this method, we achieve handshaking with the network + * thread. + */ +class SyncTask : public FNET_Task { +private: + vespalib::Gate _gate; + +public: + SyncTask(FNET_Scheduler &s) : + FNET_Task(&s), + _gate() { + ScheduleNow(); + } + + void await() { + _gate.await(); + } + + void PerformTask() { + _gate.countDown(); + } +}; + +} // namespace <unnamed> + +namespace mbus { + +RPCNetwork::SendContext::SendContext(RPCNetwork &net, const Message &msg, + const std::vector<RoutingNode*> &recipients) : + _net(net), + _msg(msg), + _traceLevel(msg.getTrace().getLevel()), + _recipients(recipients), + _hasError(false), + _pending(_recipients.size()), + _version(_net.getVersion()) +{ + // empty +} + +void +RPCNetwork::SendContext::handleVersion(const vespalib::Version *version) +{ + bool shouldSend = false; + { + vespalib::LockGuard guard(_lock); + if (version == NULL) { + _hasError = true; + } else if (*version < _version) { + _version = *version; + } + if (--_pending == 0) { + shouldSend = true; + } + } + if (shouldSend) { + _net.send(*this); + delete this; + } +} + +RPCNetwork::TargetPoolTask::TargetPoolTask( + FNET_Scheduler &scheduler, + RPCTargetPool &pool) : + FNET_Task(&scheduler), + _pool(pool) +{ + ScheduleNow(); +} + +void +RPCNetwork::TargetPoolTask::PerformTask() +{ + _pool.flushTargets(false); + Schedule(1.0); +} + +RPCNetwork::RPCNetwork(const RPCNetworkParams ¶ms) : + _owner(0), + _ident(params.getIdentity()), + _threadPool(128000, 0), + _transport(), + _orb(&_transport, NULL), + _scheduler(*_transport.GetScheduler()), + _targetPool(params.getConnectionExpireSecs()), + _targetPoolTask(_scheduler, _targetPool), + _servicePool(*this, 4096), + _mirror(_orb, slobrok::ConfiguratorFactory(params.getSlobrokConfig())), + _regAPI(_orb, slobrok::ConfiguratorFactory(params.getSlobrokConfig())), + _oosManager(_orb, _mirror, params.getOOSServerPattern()), + _requestedPort(params.getListenPort()), + _sendV1(), + _sendAdapters() +{ + _transport.SetDirectWrite(false); + _transport.SetMaxInputBufferSize(params.getMaxInputBufferSize()); + _transport.SetMaxOutputBufferSize(params.getMaxOutputBufferSize()); +} + +RPCNetwork::~RPCNetwork() +{ + shutdown(); +} + +FRT_RPCRequest * +RPCNetwork::allocRequest() +{ + return _orb.AllocRPCRequest(); +} + +RPCTarget::SP +RPCNetwork::getTarget(const RPCServiceAddress &address) +{ + return _targetPool.getTarget(_orb, address); +} + +void +RPCNetwork::replyError(const SendContext &ctx, uint32_t errCode, + const string &errMsg) +{ + for (std::vector<RoutingNode*>::const_iterator it = ctx._recipients.begin(); + it != ctx._recipients.end(); ++it) + { + Reply::UP reply(new EmptyReply()); + reply->setTrace(Trace(ctx._traceLevel)); + reply->addError(Error(errCode, errMsg)); + _owner->deliverReply(std::move(reply), **it); + } +} + +void +RPCNetwork::flushTargetPool() +{ + _targetPool.flushTargets(true); +} + +const vespalib::Version & +RPCNetwork::getVersion() const +{ + return Vtag::currentVersion; +} + +void +RPCNetwork::attach(INetworkOwner &owner) +{ + LOG_ASSERT(_owner == 0); + _owner = &owner; + + _sendV1.attach(*this); + _sendAdapters.insert(SendAdapterMap::value_type(vespalib::VersionSpecification(5), &_sendV1)); + _sendAdapters.insert(SendAdapterMap::value_type(vespalib::VersionSpecification(6), &_sendV1)); + + FRT_ReflectionBuilder builder(&_orb); + builder.DefineMethod("mbus.getVersion", "", "s", true, + FRT_METHOD(RPCNetwork::invoke), this); + builder.MethodDesc("Retrieves the message bus version."); + builder.ReturnDesc("version", "The message bus version."); +} + +void +RPCNetwork::invoke(FRT_RPCRequest *req) +{ + req->GetReturn()->AddString(getVersion().toString().c_str()); +} + +const string +RPCNetwork::getConnectionSpec() const +{ + return vespalib::make_vespa_string("tcp/%s:%d", _ident.getHostname().c_str(), _orb.GetListenPort()); +} + +RPCSendAdapter * +RPCNetwork::getSendAdapter(const vespalib::Version &version) +{ + for (SendAdapterMap::iterator it = _sendAdapters.begin(); + it != _sendAdapters.end(); ++it) + { + if (it->first.matches(version)) { + return it->second; + } + } + return NULL; +} + +bool +RPCNetwork::start() +{ + if (!_orb.Listen(_requestedPort)) { + return false; + } + if (!_transport.Start(&_threadPool)) { + return false; + } + return true; +} + +bool +RPCNetwork::waitUntilReady(double seconds) const +{ + for (uint32_t i = 0; i < seconds * 100; ++i) { + if (_mirror.ready() && _oosManager.isReady()) { + return true; + } + FastOS_Thread::Sleep(10); + } + return false; +} + +void +RPCNetwork::registerSession(const string &session) +{ + if (_ident.getServicePrefix().size() == 0) { + LOG(warning, "The session (%s) will not be registered" + "in the Slobrok since this network has no identity.", + session.c_str()); + return; + } + string name = _ident.getServicePrefix(); + name += "/"; + name += session; + _regAPI.registerName(name); +} + +void +RPCNetwork::unregisterSession(const string &session) +{ + if (_ident.getServicePrefix().size() == 0) { + return; + } + string name = _ident.getServicePrefix(); + name += "/"; + name += session; + _regAPI.unregisterName(name); +} + +bool +RPCNetwork::allocServiceAddress(RoutingNode &recipient) +{ + const Hop &hop = recipient.getRoute().getHop(0); + string service = hop.getServiceName(); + Error error = resolveServiceAddress(recipient, service); + if (error.getCode() == ErrorCode::NONE) { + return true; // service address resolved + } + recipient.setError(error); + return false; // service adddress not resolved +} + +Error +RPCNetwork::resolveServiceAddress(RoutingNode &recipient, const string &serviceName) +{ + if (_oosManager.isOOS(serviceName)) { + return Error(ErrorCode::SERVICE_OOS, + vespalib::make_vespa_string("The service '%s' has been marked as out of service.", + serviceName.c_str())); + } + RPCServiceAddress::UP ret = _servicePool.resolve(serviceName); + if (ret.get() == NULL) { + return Error(ErrorCode::NO_ADDRESS_FOR_SERVICE, + vespalib::make_vespa_string("The address of service '%s' could not be resolved. It is not currently " + "registered with the Vespa name server. " + "The service must be having problems, or the routing configuration is wrong.", + serviceName.c_str())); + } + RPCTarget::SP target = _targetPool.getTarget(_orb, *ret); + if (target.get() == NULL) { + return Error(ErrorCode::CONNECTION_ERROR, + vespalib::make_vespa_string("Failed to connect to service '%s'.", + serviceName.c_str())); + } + ret->setTarget(target); // free by freeServiceAddress() + recipient.setServiceAddress(IServiceAddress::UP(ret.release())); + return Error(); +} + +void +RPCNetwork::freeServiceAddress(RoutingNode &recipient) +{ + recipient.setServiceAddress(IServiceAddress::UP()); +} + +void +RPCNetwork::send(const Message &msg, const std::vector<RoutingNode*> &recipients) +{ + SendContext &ctx = *(new SendContext(*this, msg, recipients)); // deletes self + double timeout = ctx._msg.getTimeRemainingNow() / 1000.0; + for (uint32_t i = 0, len = ctx._recipients.size(); i < len; ++i) { + RoutingNode *&recipient = ctx._recipients[i]; + LOG_ASSERT(recipient != NULL); + + RPCServiceAddress &address = static_cast<RPCServiceAddress&>(recipient->getServiceAddress()); + LOG_ASSERT(address.hasTarget()); + + address.getTarget().resolveVersion(timeout, ctx); + } +} + +void +RPCNetwork::send(RPCNetwork::SendContext &ctx) +{ + if (ctx._hasError) { + replyError(ctx, ErrorCode::HANDSHAKE_FAILED, + "An error occured while resolving version."); + } else { + uint64_t timeRemaining = ctx._msg.getTimeRemainingNow(); + Blob payload = _owner->getProtocol(ctx._msg.getProtocol())->encode(ctx._version, ctx._msg); + RPCSendAdapter *adapter = getSendAdapter(ctx._version); + if (adapter == NULL) { + replyError(ctx, ErrorCode::INCOMPATIBLE_VERSION, + vespalib::make_vespa_string( + "Can not send to version '%s' recipient.", + ctx._version.toString().c_str())); + } else if (timeRemaining == 0) { + replyError(ctx, ErrorCode::TIMEOUT, + "Aborting transmission because zero time remains."); + } else if (payload.size() == 0) { + replyError(ctx, ErrorCode::ENCODE_ERROR, + vespalib::make_vespa_string( + "Protocol '%s' failed to encode message.", + ctx._msg.getProtocol().c_str())); + } else if (ctx._recipients.size() == 1) { + adapter->sendByHandover(*ctx._recipients.front(), ctx._version, std::move(payload), timeRemaining); + } else { + for (auto & recipient : ctx._recipients) { + adapter->send(*recipient, ctx._version, payload, timeRemaining); + } + } + } +} + +void +RPCNetwork::sync() +{ + SyncTask task(_scheduler); + task.await(); +} + +void +RPCNetwork::shutdown() +{ + _transport.ShutDown(false); + _threadPool.Close(); +} + +void +RPCNetwork::postShutdownHook() +{ + _scheduler.CheckTasks(); +} + +const slobrok::api::IMirrorAPI & +RPCNetwork::getMirror() const +{ + return _mirror; +} + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/network/rpcnetwork.h b/messagebus/src/vespa/messagebus/network/rpcnetwork.h new file mode 100644 index 00000000000..5585ff06cec --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/rpcnetwork.h @@ -0,0 +1,253 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vespa/messagebus/blob.h> +#include <vespa/messagebus/blobref.h> +#include <vespa/messagebus/message.h> +#include <vespa/messagebus/reply.h> +#include <vespa/slobrok/sbmirror.h> +#include <vespa/slobrok/sbregister.h> +#include <vespa/vespalib/component/versionspecification.h> +#include "inetwork.h" +#include "oosmanager.h" +#include "rpcnetworkparams.h" +#include "rpcsendv1.h" +#include "rpcservicepool.h" +#include "rpctargetpool.h" + +namespace mbus { + +/** + * Network implementation based on RPC. This class is responsible for + * keeping track of services and for sending messages to services. + **/ +class RPCNetwork : public boost::noncopyable, + public INetwork, + public FRT_Invokable { +private: + struct SendContext : public RPCTarget::IVersionHandler { + vespalib::Lock _lock; + RPCNetwork &_net; + const Message &_msg; + uint32_t _traceLevel; + std::vector<RoutingNode*> _recipients; + bool _hasError; + uint32_t _pending; + vespalib::Version _version; + + SendContext(RPCNetwork &net, const Message &msg, const std::vector<RoutingNode*> &recipients); + void handleVersion(const vespalib::Version *version); + }; + + struct TargetPoolTask : public FNET_Task { + RPCTargetPool &_pool; + + TargetPoolTask(FNET_Scheduler &scheduler, RPCTargetPool &pool); + void PerformTask(); + }; + + typedef std::map<vespalib::VersionSpecification, RPCSendAdapter*> SendAdapterMap; + + INetworkOwner *_owner; + Identity _ident; + FastOS_ThreadPool _threadPool; + FNET_Transport _transport; + FRT_Supervisor _orb; + FNET_Scheduler &_scheduler; + RPCTargetPool _targetPool; + TargetPoolTask _targetPoolTask; + RPCServicePool _servicePool; + slobrok::api::MirrorAPI _mirror; + slobrok::api::RegisterAPI _regAPI; + OOSManager _oosManager; + int _requestedPort; + RPCSendV1 _sendV1; + SendAdapterMap _sendAdapters; + + /** + * Resolves and assigns a service address for the given recipient using the + * given address. This is called by the {@link + * #allocServiceAddress(RoutingNode)} method. The target allocated here is + * released when the routing node calls {@link + * #freeServiceAddress(RoutingNode)}. + * + * @param recipient The recipient to assign the service address to. + * @param serviceName The name of the service to resolve. + * @return Any error encountered, or ErrorCode::NONE. + */ + Error resolveServiceAddress(RoutingNode &recipient, const string &serviceName); + + /** + * Determines and returns the send adapter that is compatible with the given + * version. If no adapter can be found, this method returns null. + * + * @param version The version for which to return an adapter. + * @return The compatible adapter. + */ + RPCSendAdapter *getSendAdapter(const vespalib::Version &version); + + /** + * This method is a callback invoked after {@link #send(Message, List)} once + * the version of all recipients have been resolved. If all versions were + * resolved ahead of time, this method is invoked by the same thread as the + * former. If not, this method is invoked by the network thread during the + * version callback. + * + * @param ctx All the required send-data. + */ + void send(SendContext &ctx); + +protected: + /** + * Returns the version of this network. This gets called when the + * "mbus.getVersion" method is invoked on this network, and is separated + * into its own function so that unit tests can override it to simulate + * other versions than current. + * + * @return The version to claim to be. + */ + virtual const vespalib::Version &getVersion() const; + + /** + * The network uses a cache of RPC targets (see {@link RPCTargetPool}) that + * allows it to save time by reusing open connections. It works by keeping a + * set of the most recently used targets open. Calling this method forces + * all unused connections to close immediately. + */ + void flushTargetPool(); + +public: + /** + * Create an RPCNetwork. The servicePrefix is combined with session names to + * create service names. If the service prefix is 'a/b' and the session name + * is 'c', the resulting service name that identifies the session on the + * message bus will be 'a/b/c' + * + * @param params A complete set of parameters. + */ + RPCNetwork(const RPCNetworkParams ¶ms); + + /** + * Destruct + **/ + virtual ~RPCNetwork(); + + /** + * Obtain the owner of this network. This method may only be invoked after + * the network has been attached to its owner. + * + * @return network owner + **/ + INetworkOwner &getOwner() { return *_owner; } + + /** + * Returns the identity of this network. + * + * @return The identity. + */ + const Identity &getIdentity() const { return _ident; } + + /** + * Obtain the port number this network is listening to. This method will + * return 0 until the start method has been invoked. + * + * @return port number + **/ + int getPort() const { return _orb.GetListenPort(); } + + /** + * Allocate a new rpc request object. The caller of this method gets the + * ownership of the returned request. + * + * @return a new rpc request + **/ + FRT_RPCRequest *allocRequest(); + + /** + * Returns an RPC target for the given service address. + * + * @param address The address for which to return a target. + * @return The target to send to. + */ + RPCTarget::SP getTarget(const RPCServiceAddress &address); + + /** + * Obtain a reference to the internal scheduler. This will be mostly used + * for testing. + * + * @return internal scheduler + **/ + FNET_Scheduler &getScheduler() { return _scheduler; } + + /** + * Obtain a reference to the internal OOS manager object. This will be + * mostly used for testing. + * + * @return internal OOS manager + **/ + OOSManager &getOOSManager() { return _oosManager; } + + /** + * Obtain a reference to the internal supervisor. This is used by + * the request adapters to register FRT methods. + * + * @return The supervisor. + */ + FRT_Supervisor &getSupervisor() { return _orb; } + + /** + * Deliver an error reply to the recipients of a {@link SendContext} in a + * way that avoids entanglement. + * + * @param ctx The send context that contains the recipient data. + * @param errCode The error code to return. + * @param errMsg The error string to return. + */ + void replyError(const SendContext &ctx, uint32_t errCode, + const string &errMsg); + + // Implements INetwork. + void attach(INetworkOwner &owner); + + // Implements INetwork. + const string getConnectionSpec() const; + + // Implements INetwork. + bool start(); + + // Implements INetwork. + bool waitUntilReady(double seconds) const; + + // Implements INetwork. + void registerSession(const string &session); + + // Implements INetwork. + void unregisterSession(const string &session); + + // Implements INetwork. + bool allocServiceAddress(RoutingNode &recipient); + + // Implements INetwork. + void freeServiceAddress(RoutingNode &recipient); + + // Implements INetwork. + void send(const Message &msg, const std::vector<RoutingNode*> &recipients); + + // Implements INetwork. + void sync(); + + // Implements INetwork. + void shutdown(); + + // Implements INetwork. + void postShutdownHook(); + + // Implements INetwork. + const slobrok::api::IMirrorAPI &getMirror() const; + + // Implements FRT_Invokable. + void invoke(FRT_RPCRequest *req); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/network/rpcnetworkparams.cpp b/messagebus/src/vespa/messagebus/network/rpcnetworkparams.cpp new file mode 100644 index 00000000000..fe0327a5c6e --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/rpcnetworkparams.cpp @@ -0,0 +1,25 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".rpcnetworkparams"); + +#include "rpcnetworkparams.h" + +#include <vespa/slobrok/cfg.h> + +namespace mbus { + +RPCNetworkParams::RPCNetworkParams() : + _identity(Identity("")), + _slobrokConfig("admin/slobrok.0"), + _oosServerPattern(""), + _listenPort(0), + _maxInputBufferSize(256*1024), + _maxOutputBufferSize(256*1024), + _connectionExpireSecs(30) +{ + // empty +} + +} + diff --git a/messagebus/src/vespa/messagebus/network/rpcnetworkparams.h b/messagebus/src/vespa/messagebus/network/rpcnetworkparams.h new file mode 100644 index 00000000000..8d9d4066177 --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/rpcnetworkparams.h @@ -0,0 +1,186 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <stdint.h> +#include "identity.h" +#include <vespa/slobrok/cfg.h> + +namespace mbus { + +/** + * To facilitate several configuration parameters to the {@link RPCNetwork} constructor, all parameters are + * held by this class. This class has reasonable default values for each parameter. + */ +class RPCNetworkParams { +private: + Identity _identity; + config::ConfigUri _slobrokConfig; + string _oosServerPattern; + int _listenPort; + uint32_t _maxInputBufferSize; + uint32_t _maxOutputBufferSize; + double _connectionExpireSecs; + +public: + /** + * Constructs a new parameter object with default values. + */ + RPCNetworkParams(); + + /** + * Returns the identity to use for the network. + * + * @return The identity. + */ + const Identity &getIdentity() const { + return _identity; + } + + /** + * Sets the identity to use for the network. + * + * @param identity The new identity. + * @return This, to allow chaining. + */ + RPCNetworkParams &setIdentity(const Identity &identity) { + _identity = identity; + return *this; + } + + /** + * Sets the identity to use for the network. + * + * @param identity A string representation of the identity. + * @return This, to allow chaining. + */ + RPCNetworkParams &setIdentity(const string &identity) { + return setIdentity(Identity(identity)); + } + + /** + * Returns the config id of the slobrok config. + * + * @return The config id. + */ + const config::ConfigUri & getSlobrokConfig() const { + return _slobrokConfig; + } + + /** + * Sets of the slobrok config. + * + * @param slobrokConfigId The new config. + * @return This, to allow chaining. + */ + RPCNetworkParams &setSlobrokConfig(const config::ConfigUri & slobrokConfig) { + _slobrokConfig = slobrokConfig; + return *this; + } + + /** + * Returns the config id pattern used to lookup OOS servers. + * + * @return The config id. + */ + const string &getOOSServerPattern() const { + return _oosServerPattern; + } + + /** + * Sets the config id pattern used to lookup OOS servers. + * + * @param oosServerPattern The server pattern. + * @return This, to allow chaining. + */ + RPCNetworkParams &setOOSServerPattern(const string &oosServerPattern) { + _oosServerPattern = oosServerPattern; + return *this; + } + + /** + * Returns the port to listen to. + * + * @return The port. + */ + int getListenPort() const { + return _listenPort; + } + + /** + * Sets the port to listen to. + * + * @param listenPort The new port. + * @return This, to allow chaining. + */ + RPCNetworkParams &setListenPort(int listenPort) { + _listenPort = listenPort; + return *this; + } + + /** + * Returns the number of seconds before an idle network connection expires. + * + * @return The number of seconds. + */ + double getConnectionExpireSecs() const{ + return _connectionExpireSecs; + } + + /** + * Sets the number of seconds before an idle network connection expires. + * + * @param secs The number of seconds. + * @return This, to allow chaining. + */ + RPCNetworkParams &setConnectionExpireSecs(double secs) { + _connectionExpireSecs = secs; + return *this; + } + + /** + * Returns the maximum input buffer size allowed for the underlying FNET connection. + * + * @return The maximum number of bytes. + */ + uint32_t getMaxInputBufferSize() const { + return _maxInputBufferSize; + } + + /** + * Sets the maximum input buffer size allowed for the underlying FNET connection. Using the value 0 means that there + * is no limit; the connection will not free any allocated memory until it is cleaned up. This might potentially + * save alot of allocation time. + * + * @param maxInputBufferSize The maximum number of bytes. + * @return This, to allow chaining. + */ + RPCNetworkParams &setMaxInputBufferSize(uint32_t maxInputBufferSize) { + _maxInputBufferSize = maxInputBufferSize; + return *this; + } + + /** + * Returns the maximum output buffer size allowed for the underlying FNET connection. + * + * @return The maximum number of bytes. + */ + uint32_t getMaxOutputBufferSize() const { + return _maxOutputBufferSize; + } + + /** + * Sets the maximum output buffer size allowed for the underlying FNET connection. Using the value 0 means that there + * is no limit; the connection will not free any allocated memory until it is cleaned up. This might potentially + * save alot of allocation time. + * + * @param maxOutputBufferSize The maximum number of bytes. + * @return This, to allow chaining. + */ + RPCNetworkParams &setMaxOutputBufferSize(uint32_t maxOutputBufferSize) { + _maxOutputBufferSize = maxOutputBufferSize; + return *this; + } +}; + +} + diff --git a/messagebus/src/vespa/messagebus/network/rpcsendadapter.h b/messagebus/src/vespa/messagebus/network/rpcsendadapter.h new file mode 100644 index 00000000000..91ba5de24b7 --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/rpcsendadapter.h @@ -0,0 +1,57 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <boost/utility.hpp> +#include <memory> +#include <vespa/vespalib/util/referencecounter.h> + +namespace mbus { + +class RoutingNode; +class RPCNetwork; + +/** + * This interface defines the necessary methods to process incoming and send + * outgoing RPC sends. The {@link RPCNetwork} maintains a list of supported RPC + * signatures, and dispatches sends to the corresponding adapter. + */ +class RPCSendAdapter : public boost::noncopyable +{ +public: + /** + * Required for inheritance. + */ + virtual ~RPCSendAdapter() { } + + /** + * Attaches this adapter to the given network. + * + * @param net The network to attach to. + */ + virtual void attach(RPCNetwork &net) = 0; + + /** + * Performs the actual sending to the given recipient. + * + * @param recipient The recipient to send to. + * @param version The version for which the payload is serialized. + * @param payload The already serialized payload of the message to send. + * @param timeRemaining The time remaining until the message expires. + */ + virtual void send(RoutingNode &recipient, const vespalib::Version &version, + BlobRef payload, uint64_t timeRemaining) = 0; + + /** + * Performs the actual sending to the given recipient. + * + * @param recipient The recipient to send to. + * @param version The version for which the payload is serialized. + * @param payload The already serialized payload of the message to send. + * @param timeRemaining The time remaining until the message expires. + */ + virtual void sendByHandover(RoutingNode &recipient, const vespalib::Version &version, + Blob payload, uint64_t timeRemaining) = 0; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/network/rpcsendv1.cpp b/messagebus/src/vespa/messagebus/network/rpcsendv1.cpp new file mode 100644 index 00000000000..642e8c64089 --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/rpcsendv1.cpp @@ -0,0 +1,410 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".rpcsendv1"); + +#include <vespa/messagebus/routing/routingnode.h> +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/error.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/tracelevel.h> +#include <vespa/vespalib/util/vstringfmt.h> +#include "rpcnetwork.h" +#include "rpcsendv1.h" +#include "rpctarget.h" + +namespace { + +/** + * Implements a helper class to hold the necessary context to create a reply from + * an rpc return value. This object is held as the context of an FRT_RPCRequest. + */ +class SendContext : public boost::noncopyable { +private: + mbus::RoutingNode &_recipient; + mbus::Trace _trace; + double _timeout; + +public: + typedef std::unique_ptr<SendContext> UP; + + SendContext(mbus::RoutingNode &recipient, uint64_t timeRemaining) + : _recipient(recipient), + _trace(recipient.getTrace().getLevel()), + _timeout(timeRemaining * 0.001) { } + mbus::RoutingNode &getRecipient() { return _recipient; } + mbus::Trace &getTrace() { return _trace; } + double getTimeout() { return _timeout; } +}; + +/** + * Implements a helper class to hold the necessary context to send a reply as an + * rpc return value. This object is held in the callstack of the reply. + */ +class ReplyContext : public boost::noncopyable { +private: + FRT_RPCRequest &_request; + vespalib::Version _version; + +public: + typedef std::unique_ptr<ReplyContext> UP; + + ReplyContext(FRT_RPCRequest &request, const vespalib::Version &version) + : _request(request), _version(version) { } + FRT_RPCRequest &getRequest() { return _request; } + const vespalib::Version &getVersion() { return _version; } +}; + +} + +namespace mbus { + +const char *RPCSendV1::METHOD_NAME = "mbus.send1"; +const char *RPCSendV1::METHOD_PARAMS = "sssbilsxi"; +const char *RPCSendV1::METHOD_RETURN = "sdISSsxs"; + +RPCSendV1::RPCSendV1() : + _net(NULL), + _clientIdent("client"), + _serverIdent("server") +{ + // empty +} + +void +RPCSendV1::attach(RPCNetwork &net) +{ + _net = &net; + const string &prefix = _net->getIdentity().getServicePrefix(); + if (!prefix.empty()) { + _clientIdent = vespalib::make_vespa_string("'%s'", prefix.c_str()); + _serverIdent = _clientIdent; + } + + FRT_ReflectionBuilder builder(&_net->getSupervisor()); + builder.DefineMethod(METHOD_NAME, METHOD_PARAMS, METHOD_RETURN, true, + FRT_METHOD(RPCSendV1::invoke), this); + builder.MethodDesc("Send a message bus request and get a reply back."); + builder.ParamDesc("version", "The version of the message."); + builder.ParamDesc("route", "Names of additional hops to visit."); + builder.ParamDesc("session", "The local session that should receive this message."); + builder.ParamDesc("retryEnabled", "Whether or not this message can be resent."); + builder.ParamDesc("retry", "The number of times the sending of this message has been retried."); + builder.ParamDesc("timeRemaining", "The number of milliseconds until timeout."); + builder.ParamDesc("protocol", "The name of the protocol that knows how to decode this message."); + builder.ParamDesc("payload", "The protocol specific message payload."); + builder.ParamDesc("level", "The trace level of the message."); + builder.ReturnDesc("version", "The lowest version the message was serialized as."); + builder.ReturnDesc("retry", "The retry request of the reply."); + builder.ReturnDesc("errorCodes", "The reply error codes."); + builder.ReturnDesc("errorMessages", "The reply error messages."); + builder.ReturnDesc("errorServices", "The reply error service names."); + builder.ReturnDesc("protocol", "The name of the protocol that knows how to decode this reply."); + builder.ReturnDesc("payload", "The protocol specific reply payload."); + builder.ReturnDesc("trace", "A string representation of the trace."); +} + +namespace { + +class FillByCopy : public PayLoadFiller +{ +public: + FillByCopy(BlobRef payload) : _payload(payload) { } + void fill(FRT_Values & v) const override { + v.AddData(_payload.data(), _payload.size()); + } +private: + BlobRef _payload; +}; + +class FillByHandover : public PayLoadFiller +{ +public: + FillByHandover(Blob payload) : _payload(std::move(payload)) { } + void fill(FRT_Values & v) const override { + v.AddData(std::move(_payload.payload()), _payload.size()); + } +private: + mutable Blob _payload; +}; + +} + +void +RPCSendV1::send(RoutingNode &recipient, const vespalib::Version &version, + BlobRef payload, uint64_t timeRemaining) +{ + send(recipient, version, FillByCopy(payload), timeRemaining); +} + +void +RPCSendV1::sendByHandover(RoutingNode &recipient, const vespalib::Version &version, + Blob payload, uint64_t timeRemaining) +{ + send(recipient, version, FillByHandover(std::move(payload)), timeRemaining); +} + +void +RPCSendV1::send(RoutingNode &recipient, const vespalib::Version &version, + const PayLoadFiller & payload, uint64_t timeRemaining) +{ + SendContext::UP ctx(new SendContext(recipient, timeRemaining)); + RPCServiceAddress &address = static_cast<RPCServiceAddress&>(recipient.getServiceAddress()); + const Message &msg = recipient.getMessage(); + Route route = recipient.getRoute(); + Hop hop = route.removeHop(0); + + FRT_RPCRequest *req = _net->allocRequest(); + FRT_Values &args = *req->GetParams(); + req->SetMethodName(METHOD_NAME); + args.AddString(version.toString().c_str()); + args.AddString(route.toString().c_str()); + args.AddString(address.getSessionName().c_str()); + args.AddInt8(msg.getRetryEnabled() ? 1 : 0); + args.AddInt32(msg.getRetry()); + args.AddInt64(timeRemaining); + args.AddString(msg.getProtocol().c_str()); + payload.fill(args); + args.AddInt32(recipient.getTrace().getLevel()); + + if (ctx->getTrace().shouldTrace(TraceLevel::SEND_RECEIVE)) { + ctx->getTrace().trace(TraceLevel::SEND_RECEIVE, + vespalib::make_vespa_string( + "Sending message (version %s) from %s to '%s' with %.2f seconds timeout.", + version.toString().c_str(), _clientIdent.c_str(), + address.getServiceName().c_str(), ctx->getTimeout())); + } + + if (hop.getIgnoreResult()) { + address.getTarget().getFRTTarget().InvokeVoid(req); + if (ctx->getTrace().shouldTrace(TraceLevel::SEND_RECEIVE)) { + ctx->getTrace().trace(TraceLevel::SEND_RECEIVE, + vespalib::make_vespa_string("Not waiting for a reply from '%s'.", + address.getServiceName().c_str())); + } + Reply::UP reply(new EmptyReply()); + reply->getTrace().swap(ctx->getTrace()); + _net->getOwner().deliverReply(std::move(reply), recipient); + } else { + SendContext *ptr = ctx.release(); + req->SetContext(FNET_Context(ptr)); + address.getTarget().getFRTTarget().InvokeAsync(req, ptr->getTimeout(), this); + } +} + +void +RPCSendV1::RequestDone(FRT_RPCRequest *req) +{ + SendContext::UP ctx(static_cast<SendContext*>(req->GetContext()._value.VOIDP)); + const string &serviceName = static_cast<RPCServiceAddress&>( + ctx->getRecipient().getServiceAddress()).getServiceName(); + Reply::UP reply; + Error error; + if (!req->CheckReturnTypes(METHOD_RETURN)) { + reply.reset(new EmptyReply()); + switch (req->GetErrorCode()) { + case FRTE_RPC_TIMEOUT: + error = Error(ErrorCode::TIMEOUT, + vespalib::make_vespa_string("A timeout occured while waiting for '%s' (%g seconds expired); %s", + serviceName.c_str(), ctx->getTimeout(), req->GetErrorMessage())); + break; + case FRTE_RPC_CONNECTION: + error = Error(ErrorCode::CONNECTION_ERROR, + vespalib::make_vespa_string("A connection error occured for '%s'; %s", + serviceName.c_str(), req->GetErrorMessage())); + break; + default: + error = Error(ErrorCode::NETWORK_ERROR, + vespalib::make_vespa_string("A network error occured for '%s'; %s", + serviceName.c_str(), req->GetErrorMessage())); + } + } else { + FRT_Values &ret = *req->GetReturn(); + + vespalib::Version version = vespalib::Version(ret[0]._string._str); + double retryDelay = ret[1]._double; + uint32_t *errorCodes = ret[2]._int32_array._pt; + uint32_t errorCodesLen = ret[2]._int32_array._len; + FRT_StringValue *errorMessages = ret[3]._string_array._pt; + uint32_t errorMessagesLen = ret[3]._string_array._len; + FRT_StringValue *errorServices = ret[4]._string_array._pt; + uint32_t errorServicesLen = ret[4]._string_array._len; + const char *protocolName = ret[5]._string._str; + const char *payload = ret[6]._data._buf; + uint32_t payloadLen = ret[6]._data._len; + const char *trace = ret[7]._string._str; + + if (payloadLen > 0) { + IProtocol::SP protocol = _net->getOwner().getProtocol(protocolName); + if (protocol.get() != NULL) { + Routable::UP routable = protocol->decode(version, + BlobRef(payload, payloadLen)); + if (routable.get() != NULL) { + if (routable->isReply()) { + reply.reset(static_cast<Reply*>(routable.release())); + } else { + error = Error(ErrorCode::DECODE_ERROR, + "Payload decoded to a message when expecting a reply."); + } + } else { + error = Error(ErrorCode::DECODE_ERROR, + vespalib::make_vespa_string("Protocol '%s' failed to decode routable.", + protocolName)); + } + + } else { + error = Error(ErrorCode::UNKNOWN_PROTOCOL, + vespalib::make_vespa_string("Protocol '%s' is not known by %s.", + protocolName, _serverIdent.c_str())); + } + } + if (reply.get() == NULL) { + reply.reset(new EmptyReply()); + } + reply->setRetryDelay(retryDelay); + for (uint32_t i = 0; i < errorCodesLen && i < errorMessagesLen && i < errorServicesLen; ++i) { + reply->addError(Error(errorCodes[i], + errorMessages[i]._str, + errorServices[i]._len > 0 ? errorServices[i]._str : serviceName.c_str())); + } + ctx->getTrace().getRoot().addChild(TraceNode::decode(trace)); + } + if (ctx->getTrace().shouldTrace(TraceLevel::SEND_RECEIVE)) { + ctx->getTrace().trace(TraceLevel::SEND_RECEIVE, + vespalib::make_vespa_string("Reply (type %d) received at %s.", + reply->getType(), _clientIdent.c_str())); + } + reply->getTrace().swap(ctx->getTrace()); + if (error.getCode() != ErrorCode::NONE) { + reply->addError(error); + } + _net->getOwner().deliverReply(std::move(reply), ctx->getRecipient()); + req->SubRef(); +} + +void +RPCSendV1::invoke(FRT_RPCRequest *req) +{ + req->Detach(); + + FRT_Values &args = *req->GetParams(); + vespalib::Version version = vespalib::Version(args[0]._string._str); + const char *route = args[1]._string._str; + const char *session = args[2]._string._str; + bool retryEnabled = args[3]._intval8 != 0; + uint32_t retry = args[4]._intval32; + uint64_t timeRemaining = args[5]._intval64; + const char *protocolName = args[6]._string._str; + const char *payload = args[7]._data._buf; + uint32_t payloadLen = args[7]._data._len; + uint32_t traceLevel = args[8]._intval32; + + IProtocol::SP protocol = _net->getOwner().getProtocol(protocolName); + if (protocol.get() == NULL) { + replyError(req, version, traceLevel, + Error(ErrorCode::UNKNOWN_PROTOCOL, + vespalib::make_vespa_string("Protocol '%s' is not known by %s.", + protocolName, _serverIdent.c_str()))); + return; + } + Routable::UP routable = protocol->decode(version, BlobRef(payload, payloadLen)); + req->DiscardBlobs(); + if (routable.get() == NULL) { + replyError(req, version, traceLevel, + Error(ErrorCode::DECODE_ERROR, + vespalib::make_vespa_string("Protocol '%s' failed to decode routable.", + protocolName))); + return; + } + if (routable->isReply()) { + replyError(req, version, traceLevel, + Error(ErrorCode::DECODE_ERROR, + "Payload decoded to a reply when expecting a mesage.")); + return; + } + Message::UP msg(static_cast<Message*>(routable.release())); + if (strlen(route) > 0) { + msg->setRoute(Route::parse(route)); + } + msg->setContext(Context(new ReplyContext(*req, version))); + msg->pushHandler(*this, *this); + msg->setRetryEnabled(retryEnabled); + msg->setRetry(retry); + msg->setTimeReceivedNow(); + msg->setTimeRemaining(timeRemaining); + msg->getTrace().setLevel(traceLevel); + if (msg->getTrace().shouldTrace(TraceLevel::SEND_RECEIVE)) { + msg->getTrace().trace(TraceLevel::SEND_RECEIVE, + vespalib::make_vespa_string("Message (type %d) received at %s for session '%s'.", + msg->getType(), _serverIdent.c_str(), session)); + } + _net->getOwner().deliverMessage(std::move(msg), session); +} + +void +RPCSendV1::handleReply(Reply::UP reply) +{ + ReplyContext::UP ctx(static_cast<ReplyContext*>(reply->getContext().value.PTR)); + FRT_RPCRequest &req = ctx->getRequest(); + string version = ctx->getVersion().toString(); + if (reply->getTrace().shouldTrace(TraceLevel::SEND_RECEIVE)) { + reply->getTrace().trace(TraceLevel::SEND_RECEIVE, + vespalib::make_vespa_string("Sending reply (version %s) from %s.", + version.c_str(), _serverIdent.c_str())); + } + Blob payload(0); + if (reply->getType() != 0) { + payload = _net->getOwner().getProtocol(reply->getProtocol())->encode(ctx->getVersion(), *reply); + if (payload.size() == 0) { + reply->addError(Error(ErrorCode::ENCODE_ERROR, + "An error occured while encoding the reply, see log.")); + } + } + FRT_Values &ret = *req.GetReturn(); + ret.AddString(version.c_str()); + ret.AddDouble(reply->getRetryDelay()); + + uint32_t errorCount = reply->getNumErrors(); + uint32_t *errorCodes = ret.AddInt32Array(errorCount); + FRT_StringValue *errorMessages = ret.AddStringArray(errorCount); + FRT_StringValue *errorServices = ret.AddStringArray(errorCount); + for (uint32_t i = 0; i < errorCount; ++i) { + errorCodes[i] = reply->getError(i).getCode(); + ret.SetString(errorMessages + i, + reply->getError(i).getMessage().c_str()); + ret.SetString(errorServices + i, + reply->getError(i).getService().c_str()); + } + + ret.AddString(reply->getProtocol().c_str()); + ret.AddData(std::move(payload.payload()), payload.size()); + if (reply->getTrace().getLevel() > 0) { + ret.AddString(reply->getTrace().getRoot().encode().c_str()); + } else { + ret.AddString(""); + } + req.Return(); +} + +void +RPCSendV1::handleDiscard(Context ctx) +{ + ReplyContext::UP tmp(static_cast<ReplyContext*>(ctx.value.PTR)); + FRT_RPCRequest &req = tmp->getRequest(); + FNET_Channel *chn = req.GetContext()._value.CHANNEL; + req.SubRef(); + chn->Free(); +} + +void +RPCSendV1::replyError(FRT_RPCRequest *req, const vespalib::Version &version, + uint32_t traceLevel, const Error &err) +{ + Reply::UP reply(new EmptyReply()); + reply->setContext(Context(new ReplyContext(*req, version))); + reply->getTrace().setLevel(traceLevel); + reply->addError(err); + handleReply(std::move(reply)); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/network/rpcsendv1.h b/messagebus/src/vespa/messagebus/network/rpcsendv1.h new file mode 100644 index 00000000000..570b3daff82 --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/rpcsendv1.h @@ -0,0 +1,83 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vespa/fnet/frt/frt.h> +#include <vespa/messagebus/idiscardhandler.h> +#include <vespa/messagebus/ireplyhandler.h> +#include "rpcsendadapter.h" + +namespace mbus { + +class PayLoadFiller +{ +public: + virtual ~PayLoadFiller() { } + virtual void fill(FRT_Values & v) const = 0; +}; + +/** + * Implements the send adapter for method "mbus.send". + */ +class RPCSendV1 : public RPCSendAdapter, + public FRT_Invokable, + public FRT_IRequestWait, + public IDiscardHandler, + public IReplyHandler { +private: + RPCNetwork *_net; + string _clientIdent; + string _serverIdent; + + /** + * Send an error reply for a given request. + * + * @param request The FRT request to reply to. + * @param version The version to serialize for. + * @param traceLevel The trace level to set in the reply. + * @param err The error to reply with. + */ + void replyError(FRT_RPCRequest *req, const vespalib::Version &version, + uint32_t traceLevel, const Error &err); + + void send(RoutingNode &recipient, const vespalib::Version &version, + const PayLoadFiller & filler, uint64_t timeRemaining); +public: + /** The name of the rpc method that this adapter registers. */ + static const char *METHOD_NAME; + + /** The parameter string of the rpc method. */ + static const char *METHOD_PARAMS; + + /** The return string of the rpc method. */ + static const char *METHOD_RETURN; + + /** + * Constructs a new instance of this adapter. This object is unusable until + * its attach() method has been called. + */ + RPCSendV1(); + + // Implements RPCSendAdapter. + void attach(RPCNetwork &net) override; + + // Implements RPCSendAdapter. + void send(RoutingNode &recipient, const vespalib::Version &version, + BlobRef payload, uint64_t timeRemaining) override; + void sendByHandover(RoutingNode &recipient, const vespalib::Version &version, + Blob payload, uint64_t timeRemaining) override; + + // Implements IReplyHandler. + void handleReply(Reply::UP reply) override; + + // Implements IDiscardHandler. + void handleDiscard(Context ctx) override; + + // Implements FRT_Invokable. + void invoke(FRT_RPCRequest *req); + + // Implements FRT_IRequestWait. + void RequestDone(FRT_RPCRequest *req) override; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/network/rpcservice.cpp b/messagebus/src/vespa/messagebus/network/rpcservice.cpp new file mode 100644 index 00000000000..c420f817726 --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/rpcservice.cpp @@ -0,0 +1,54 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".rpcservice"); + +#include "rpcservice.h" +#include "rpcserviceaddress.h" +#include "rpcnetwork.h" + +namespace mbus { + +RPCService::RPCService(const slobrok::api::IMirrorAPI &mirror, + const string &pattern) : + _mirror(mirror), + _pattern(pattern), + _addressIdx(random()), + _addressGen(0), + _addressList() +{ + // empty +} + +RPCServiceAddress::UP +RPCService::resolve() +{ + if (_pattern.find("tcp/") == 0) { + size_t pos = _pattern.find_last_of('/'); + if (pos != string::npos && pos < _pattern.size() - 1) { + RPCServiceAddress::UP ret(new RPCServiceAddress( + _pattern, + _pattern.substr(0, pos))); + if (!ret->isMalformed()) { + return ret; + } + } + } else { + if (_addressGen != _mirror.updates()) { + _addressGen = _mirror.updates(); + _addressList = _mirror.lookup(_pattern); + } + if (!_addressList.empty()) { + _addressIdx = (_addressIdx + 1) % _addressList.size(); + const AddressList::value_type &entry = _addressList[_addressIdx]; + return RPCServiceAddress::UP(new RPCServiceAddress( + entry.first, + entry.second)); + } + } + return RPCServiceAddress::UP(); +} + + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/network/rpcservice.h b/messagebus/src/vespa/messagebus/network/rpcservice.h new file mode 100644 index 00000000000..95e2bfaa377 --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/rpcservice.h @@ -0,0 +1,59 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <boost/utility.hpp> +#include <vespa/slobrok/sbmirror.h> +#include <vespa/slobrok/sbregister.h> +#include <vespa/vespalib/util/linkedptr.h> +#include "rpcserviceaddress.h" + +namespace mbus { + +class RPCNetwork; + +/** + * An RPCService represents a set of remote sessions matching a service pattern. + * The sessions are monitored using the slobrok. If multiple sessions are + * available, round robin is used to balance load between them. + */ +class RPCService : public boost::noncopyable { +private: + typedef slobrok::api::IMirrorAPI Mirror; + typedef slobrok::api::MirrorAPI::SpecList AddressList; + + const Mirror &_mirror; + string _pattern; + uint32_t _addressIdx; + uint32_t _addressGen; + AddressList _addressList; + +public: + typedef vespalib::LinkedPtr<RPCService> LP; + /** + * Create a new RPCService backed by the given network and using + * the given service pattern. + * + * @param mirror The naming server to send queries to. + * @param pattern The pattern to use when querying. + */ + RPCService(const slobrok::api::IMirrorAPI &mirror, const string &pattern); + + /** + * Resolve a concrete address from this service. This service may represent + * multiple remote sessions, so this will select one that is online. + * + * @return A concrete service address. + */ + RPCServiceAddress::UP resolve(); + + /** + * Returns the pattern used when querying for the naming server for + * addresses. This is given at construtor time. + * + * @return The service pattern. + */ + const string &getPattern() const { return _pattern; } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/network/rpcserviceaddress.cpp b/messagebus/src/vespa/messagebus/network/rpcserviceaddress.cpp new file mode 100644 index 00000000000..bae53313852 --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/rpcserviceaddress.cpp @@ -0,0 +1,52 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".rpcserviceaddress"); + +#include "rpcserviceaddress.h" + +namespace mbus { + +RPCServiceAddress::RPCServiceAddress(const string &serviceName, + const string &connectionSpec) : + _serviceName(serviceName), + _sessionName(""), + _connectionSpec(connectionSpec), + _target() +{ + size_t pos = serviceName.find_last_of('/'); + if (pos != string::npos) { + _sessionName = serviceName.substr(pos + 1); + } +} + +bool +RPCServiceAddress::isMalformed() +{ + if (_serviceName.empty()) { + return true; // no service + } + if (_sessionName.empty()) { + return true; // no session + } + if (_connectionSpec.empty()) { + return true; // no spec + } + size_t prefixPos = _connectionSpec.find("tcp/"); + if (prefixPos != 0) { + return true; // no prefix + } + size_t portPos = _connectionSpec.find_first_of(':', prefixPos); + if (portPos == string::npos) { + return true; // not colon + } + if (portPos == prefixPos + 4) { + return true; // no address + } + if (portPos == _connectionSpec.size() - 1) { + return true; // no port + } + return false; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/network/rpcserviceaddress.h b/messagebus/src/vespa/messagebus/network/rpcserviceaddress.h new file mode 100644 index 00000000000..0a923eda0c1 --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/rpcserviceaddress.h @@ -0,0 +1,90 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <string> +#include "iserviceaddress.h" +#include "rpctarget.h" + +namespace mbus { + +/** + * An RPCServiceAddress contains the service name, connection spec and + * session name of a concrete remote rpc service. + **/ +class RPCServiceAddress : public IServiceAddress { +private: + string _serviceName; + string _sessionName; + string _connectionSpec; + RPCTarget::SP _target; + +public: + /** + * Convenience typedefs. + */ + typedef std::unique_ptr<RPCServiceAddress> UP; + + /** + * Constructs a service address from the given specifications. The last component of the service is stored + * as the session name. + * + * @param serviceName The full service name of the address. + * @param connectionSpec The connection specification. + */ + RPCServiceAddress(const string &serviceName, + const string &connectionSpec); + + /** + * Returns whether or not this service address is malformed. + * + * @return True if malformed. + */ + bool isMalformed(); + + /** + * Returns the name of the remove service. + * + * @return The service name. + */ + const string &getServiceName() const { return _serviceName; } + + /** + * Returns the name of the remote session. + * + * @return The session name. + */ + const string &getSessionName() const { return _sessionName; } + + /** + * Returns the connection spec for the remote service. + * + * @return The connection spec. + */ + const string &getConnectionSpec() const { return _connectionSpec; } + + /** + * Sets the RPC target to be used when communicating with the remove service. + * + * @param target The target to set. + */ + void setTarget(RPCTarget::SP target) { _target = target; } + + /** + * Returns the RPC target to be used when communicating with the remove service. Make sure that {@link + * hasTarget()} returns true before calling this method, or you will be deref'ing null. + * + * @return The target to use. + */ + RPCTarget &getTarget() { return *_target; } + + /** + * Returns whether or not this has an RPC target set. + * + * @return True if target is set. + */ + bool hasTarget() const { return _target.get() != NULL; } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/network/rpcservicepool.cpp b/messagebus/src/vespa/messagebus/network/rpcservicepool.cpp new file mode 100644 index 00000000000..9ea0b6fec6a --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/rpcservicepool.cpp @@ -0,0 +1,49 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".rpcservicepool"); + +#include <algorithm> +#include "rpcservicepool.h" +#include "rpcnetwork.h" + +namespace mbus { + +RPCServicePool::RPCServicePool(RPCNetwork &net, uint32_t maxSize) : + _net(net), + _lru(maxSize) +{ + _lru.reserve(maxSize); + LOG_ASSERT(maxSize > 0); +} + +RPCServicePool::~RPCServicePool() +{ +} + +RPCServiceAddress::UP +RPCServicePool::resolve(const string &pattern) +{ + if (_lru.hasKey(pattern)) { + return _lru[pattern]->resolve(); + } else { + RPCService::LP service(new RPCService(_net.getMirror(), pattern)); + _lru[pattern] = service; + return service->resolve(); + } +} + +uint32_t +RPCServicePool::getSize() const +{ + return _lru.size(); +} + +bool +RPCServicePool::hasService(const string &pattern) const +{ + return _lru.hasKey(pattern); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/network/rpcservicepool.h b/messagebus/src/vespa/messagebus/network/rpcservicepool.h new file mode 100644 index 00000000000..e25cfbb4903 --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/rpcservicepool.h @@ -0,0 +1,66 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <boost/utility.hpp> +#include "rpcservice.h" +#include <vespa/vespalib/stllike/lrucache_map.h> + +namespace mbus { + +class RPCNetwork; + +/** + * Class used to reuse services for the same pattern when sending messages over + * the rpc network. + */ +class RPCServicePool : public boost::noncopyable { +private: + typedef vespalib::lrucache_map< vespalib::LruParam<string, RPCService::LP> > ServiceCache; + + RPCNetwork &_net; + ServiceCache _lru; + +public: + /** + * Create a new service pool for the given network. + * + * @param net The underlying RPC network. + * @param maxSize The max number of services to cache. + */ + RPCServicePool(RPCNetwork &net, uint32_t maxSize); + + /** + * Destructor. Frees any allocated resources. + */ + ~RPCServicePool(); + + /** + * Returns the RPCServiceAddress that corresponds to a given pattern. This + * reuses the RPCService object for matching pattern so that load balancing + * is possible on the network level. + * + * @param pattern The pattern for the service we require. + * @return A service address for the given pattern. + */ + RPCServiceAddress::UP resolve(const string &pattern); + + /** + * Returns the number of services available in the pool. This number will + * never exceed the limit given at construction time. + * + * @return The current size of this pool. + */ + uint32_t getSize() const; + + /** + * Returns whether or not there is a service available in the pool the + * corresponds to the given pattern. + * + * @param pattern The pattern to check for. + * @return True if a corresponding service is in the pool. + */ + bool hasService(const string &pattern) const; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/network/rpctarget.cpp b/messagebus/src/vespa/messagebus/network/rpctarget.cpp new file mode 100644 index 00000000000..4fce35c8d16 --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/rpctarget.cpp @@ -0,0 +1,100 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/vespalib/util/exceptions.h> +#include "rpctarget.h" + +namespace mbus { + +RPCTarget::RPCTarget(const string &spec, FRT_Supervisor &orb) : + _lock(), + _orb(orb), + _name(spec), + _target(*_orb.GetTarget(spec.c_str())), + _state(VERSION_NOT_RESOLVED), + _version(), + _versionHandlers() +{ + // empty +} + +RPCTarget::~RPCTarget() +{ + _target.SubRef(); +} + +void +RPCTarget::resolveVersion(double timeout, RPCTarget::IVersionHandler &handler) +{ + bool hasVersion = false; + bool shouldInvoke = false; + { + vespalib::MonitorGuard guard(_lock); + if (_state == VERSION_RESOLVED || _state == PROCESSING_HANDLERS) { + while (_state == PROCESSING_HANDLERS) { + guard.wait(); + } + hasVersion = true; + } else { + _versionHandlers.push_back(&handler); + if (_state != TARGET_INVOKED) { + _state = TARGET_INVOKED; + shouldInvoke = true; + } + } + } + if (hasVersion) { + handler.handleVersion(_version.get()); + } else if (shouldInvoke) { + FRT_RPCRequest *req = _orb.AllocRPCRequest(); + req->SetMethodName("mbus.getVersion"); + _target.InvokeAsync(req, timeout, this); + } +} + +bool +RPCTarget::isValid() const +{ + vespalib::MonitorGuard guard(_lock); + if (_target.IsValid()) { + return true; + } + if (_state == TARGET_INVOKED || _state == PROCESSING_HANDLERS) { + return true; // keep alive until RequestDone() is called + } + return false; +} + +void +RPCTarget::RequestDone(FRT_RPCRequest *req) +{ + HandlerList handlers; + { + vespalib::MonitorGuard guard(_lock); + assert(_state == TARGET_INVOKED); + if (req->CheckReturnTypes("s")) { + FRT_Values &val = *req->GetReturn(); + try { + _version.reset(new vespalib::Version(val[0]._string._str)); + } catch (vespalib::IllegalArgumentException &e) { + (void)e; + } + } else if (req->GetErrorCode() == FRTE_RPC_NO_SUCH_METHOD) { + _version.reset(new vespalib::Version("4.1")); + } + _versionHandlers.swap(handlers); + _state = PROCESSING_HANDLERS; + } + for (HandlerList::iterator it = handlers.begin(); + it != handlers.end(); ++it) + { + (*it)->handleVersion(_version.get()); + } + { + vespalib::MonitorGuard guard(_lock); + _state = (_version.get() ? VERSION_RESOLVED : VERSION_NOT_RESOLVED); + guard.broadcast(); + } + req->SubRef(); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/network/rpctarget.h b/messagebus/src/vespa/messagebus/network/rpctarget.h new file mode 100644 index 00000000000..89212c783b0 --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/rpctarget.h @@ -0,0 +1,120 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vespa/fnet/frt/frt.h> +#include <vector> +#include <vespa/vespalib/component/version.h> +#include <vespa/vespalib/util/sync.h> +#include <vespa/messagebus/common.h> + +namespace mbus { + +/** + * Implements a target object that encapsulates the FRT connection + * target. Instances of this class are returned by {@link RPCService}, and + * cached by {@link RPCTargetPool}. + */ +class RPCTarget : public FRT_IRequestWait { +public: + /** + * Declares a version handler used when resolving the version of a target. + * An instance of this is passed to {@link RPCTarget#resolveVersion(double, + * VersionHandler)}, and invoked either synchronously or asynchronously, + * depending on whether or not the version is already available. + */ + class IVersionHandler { + public: + /** + * Virtual destructor required for inheritance. + */ + virtual ~IVersionHandler() { } + + /** + * This method is invoked once the version of the corresponding {@link + * RPCTarget} becomes available. If a problem occured while retrieving + * the version, this method is invoked with a null argument. + * + * @param ver The version of corresponding target, or null. + */ + virtual void handleVersion(const vespalib::Version *ver) = 0; + }; + +private: + typedef std::vector<IVersionHandler*> HandlerList; + + enum ResolveState { + VERSION_NOT_RESOLVED, + TARGET_INVOKED, + PROCESSING_HANDLERS, + VERSION_RESOLVED, + }; + typedef std::unique_ptr<vespalib::Version> Version_UP; + + vespalib::Monitor _lock; + FRT_Supervisor &_orb; + string _name; + FRT_Target &_target; + ResolveState _state; + Version_UP _version; + HandlerList _versionHandlers; + +public: + /** + * Convenience typedefs. + */ + typedef std::shared_ptr<RPCTarget> SP; + + /** + * Constructs a new instance of this class. This object creates and + * takes ownership of a corresponding FRT target, and will deref it + * upon destruction. + * + * @param spec The connection spec of this target. + * @param orb The FRT supervisor to use when connecting to target. + */ + RPCTarget(const string &name, FRT_Supervisor &orb); + + /** + * Destructor. Subrefs the contained FRT target. + */ + ~RPCTarget(); + + /** + * Requests the version of this target be passed to the given {@link + * VersionHandler}. If the version is available, the handler is called + * synchronously; if not, the handler is called by the network thread once + * the target responds to the version query. + * + * @param timeout The timeout for the request in milliseconds. + * @param handler The handler to be called once the version is available. + */ + void resolveVersion(double timeout, IVersionHandler &handler); + + /** + * @return true if the FRT target is valid or has been invoked (which + * means we cannot destroy it). + */ + bool isValid() const; + + /** + * Returns the encapsulated FRT target. + * + * @return The target. + */ + FRT_Target &getFRTTarget() { return _target; } + + /** + * Returns the version to use when communicating with this target. + * Version must have been successfully resolved before calling this + * function. + * + * @return The negotiated version. + */ + const vespalib::Version &getVersion() const { return *_version; } + + // Implements FRT_IRequestWait. + void RequestDone(FRT_RPCRequest *req); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/network/rpctargetpool.cpp b/messagebus/src/vespa/messagebus/network/rpctargetpool.cpp new file mode 100644 index 00000000000..037e261ad7b --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/rpctargetpool.cpp @@ -0,0 +1,92 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".rpctargetpool"); + +#include <vespa/messagebus/systemtimer.h> +#include "rpctargetpool.h" + +namespace mbus { + +RPCTargetPool::Entry::Entry(RPCTarget::SP target, uint64_t lastUse) : + _target(target), + _lastUse(lastUse) +{ + // empty +} + +RPCTargetPool::RPCTargetPool(double expireSecs) : + _lock(), + _targets(), + _timer(new SystemTimer()), + _expireMillis(static_cast<uint64_t>(expireSecs * 1000)) +{ + // empty +} + +RPCTargetPool::RPCTargetPool(ITimer::UP timer, double expireSecs) : + _lock(), + _targets(), + _timer(std::move(timer)), + _expireMillis(static_cast<uint64_t>(expireSecs * 1000)) +{ + // empty +} + +RPCTargetPool::~RPCTargetPool() +{ + flushTargets(true); +} + +void +RPCTargetPool::flushTargets(bool force) +{ + uint64_t currentTime = _timer->getMilliTime(); + vespalib::LockGuard guard(_lock); + TargetMap::iterator it = _targets.begin(); + while (it != _targets.end()) { + Entry &entry = it->second; + if (entry._target.get() != NULL) { + if (entry._target.use_count() > 1) { + entry._lastUse = currentTime; + ++it; + continue; // someone is using this + } + if (!force) { + if (--entry._lastUse + _expireMillis > currentTime) { + ++it; + continue; // not sufficiently idle + } + } + } + _targets.erase(it++); // postfix increment to move the iterator + } +} + +size_t +RPCTargetPool::size() +{ + vespalib::LockGuard guard(_lock); + return _targets.size(); +} + +RPCTarget::SP +RPCTargetPool::getTarget(FRT_Supervisor &orb, const RPCServiceAddress &address) +{ + vespalib::LockGuard guard(_lock); + string spec = address.getConnectionSpec(); + TargetMap::iterator it = _targets.find(spec); + if (it != _targets.end()) { + Entry &entry = it->second; + if (entry._target->isValid()) { + entry._lastUse = _timer->getMilliTime(); + return entry._target; + } + _targets.erase(it); + } + RPCTarget::SP ret(new RPCTarget(spec, orb)); + _targets.insert(TargetMap::value_type(spec, Entry(ret, _timer->getMilliTime()))); + return ret; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/network/rpctargetpool.h b/messagebus/src/vespa/messagebus/network/rpctargetpool.h new file mode 100644 index 00000000000..3e4fa953bb5 --- /dev/null +++ b/messagebus/src/vespa/messagebus/network/rpctargetpool.h @@ -0,0 +1,97 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <boost/utility.hpp> +#include <map> +#include <vespa/messagebus/itimer.h> +#include <vespa/vespalib/util/sync.h> +#include "rpcserviceaddress.h" +#include "rpctarget.h" + +class FRT_Supervisor; + +namespace mbus { + +/** + * Class used to reuse targets for the same address when sending messages over + * the rpc network. + */ +class RPCTargetPool : public boost::noncopyable { +private: + /** + * Implements a helper class holds the necessary reference and token counter + * for a JRT target to keep connections open as long as they get used from + * time to time. + */ + struct Entry { + RPCTarget::SP _target; + uint64_t _lastUse; + + Entry(RPCTarget::SP target, uint64_t lastUse); + }; + typedef std::map<string, Entry> TargetMap; + + vespalib::Lock _lock; + TargetMap _targets; + ITimer::UP _timer; + uint64_t _expireMillis; + +public: + /** + * Constructs a new instance of this class, and registers the {@link + * SystemTimer} for detecting and closing connections that have expired + * according to the given parameter. + * + * @param expireSecs The number of seconds until an idle connection is + * closed. + */ + RPCTargetPool(double expireSecs); + + /** + * Constructs a new instance of this class, using the given {@link Timer} + * for detecting and closing connections that have expired according to the + * second paramter. + * + * @param timer The timer to use for connection expiration. + * @param expireSecs The number of seconds until an idle connection is + * closed. + */ + RPCTargetPool(ITimer::UP timer, double expireSecs); + + /** + * Destructor. Frees any allocated resources. + */ + ~RPCTargetPool(); + + /** + * This method will return a target for the given address. If a target does + * not currently exist for the given address, it will be created and added + * to the internal map. Each target is also reference counted so that the + * tokens of targets that are currently active is never decremented. + * + * @param orb The supervisor to use to connect to the target. + * @param address The address to resolve to a target. + * @return A target for the given address. + */ + RPCTarget::SP getTarget(FRT_Supervisor &orb, const RPCServiceAddress &address); + + /** + * Closes all unused target connections. Unless the force argument is true, + * this method will allow a grace period for all connections after last use + * before it starts closing them. This allows the most recently used + * connections to stay open. + * + * @param force Whether or not to force flush. + */ + void flushTargets(bool force); + + /** + * Returns the number of targets currently contained in this. + * + * @return The size of the internal map. + */ + size_t size(); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/protocolrepository.cpp b/messagebus/src/vespa/messagebus/protocolrepository.cpp new file mode 100644 index 00000000000..a8435c6a460 --- /dev/null +++ b/messagebus/src/vespa/messagebus/protocolrepository.cpp @@ -0,0 +1,82 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".protocolrepository"); + +#include "protocolrepository.h" + +namespace mbus { + +void +ProtocolRepository::clearPolicyCache() +{ + vespalib::LockGuard guard(_lock); + _routingPolicyCache.clear(); +} + +IProtocol::SP +ProtocolRepository::putProtocol(const IProtocol::SP & protocol) +{ + vespalib::LockGuard guard(_lock); + const string &name = protocol->getName(); + if (_protocols.find(name) != _protocols.end()) { + _routingPolicyCache.clear(); + } + IProtocol::SP prev = _protocols[name]; + _protocols[name] = protocol; + return prev; +} + +bool +ProtocolRepository::hasProtocol(const string &name) const +{ + vespalib::LockGuard guard(_lock); + return _protocols.find(name) != _protocols.end(); +} + +IProtocol::SP +ProtocolRepository::getProtocol(const string &name) +{ + vespalib::LockGuard guard(_lock); + ProtocolMap::iterator it = _protocols.find(name); + if (it != _protocols.end()) { + return it->second; + } + return IProtocol::SP(); +} + +IRoutingPolicy::SP +ProtocolRepository::getRoutingPolicy(const string &protocolName, + const string &policyName, + const string &policyParam) +{ + vespalib::LockGuard guard(_lock); + string cacheKey = protocolName + "." + policyName + "." + policyParam; + RoutingPolicyCache::iterator cit = _routingPolicyCache.find(cacheKey); + if (cit != _routingPolicyCache.end()) { + return cit->second; + } + ProtocolMap::iterator pit = _protocols.find(protocolName); + if (pit == _protocols.end()) { + LOG(error, "Protocol '%s' not supported.", protocolName.c_str()); + return IRoutingPolicy::SP(); + } + IRoutingPolicy::UP policy; + try { + policy = pit->second->createPolicy(policyName, policyParam); + } catch (const std::exception &e) { + LOG(error, "Protocol '%s' threw an exception; %s", + protocolName.c_str(), e.what()); + } + if (policy.get() == NULL) { + LOG(error, "Protocol '%s' failed to create routing policy '%s' " + "with parameter '%s'.", + protocolName.c_str(), policyName.c_str(), policyParam.c_str()); + return IRoutingPolicy::SP(); + } + IRoutingPolicy::SP ret(policy.release()); + _routingPolicyCache[cacheKey] = ret; + return ret; +} + +} diff --git a/messagebus/src/vespa/messagebus/protocolrepository.h b/messagebus/src/vespa/messagebus/protocolrepository.h new file mode 100644 index 00000000000..05917a8d5c0 --- /dev/null +++ b/messagebus/src/vespa/messagebus/protocolrepository.h @@ -0,0 +1,76 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <map> +#include <vespa/vespalib/util/sync.h> +#include "iprotocol.h" + +namespace mbus { + +/** + * Implements a thread-safe repository for protocols and their routing policies. This manages an internal cache of + * routing policies so that similarly referenced policy directives share the same instance of a policy. + */ +class ProtocolRepository : public boost::noncopyable { +private: + typedef std::map<string, IProtocol::SP> ProtocolMap; + typedef std::map<string, IRoutingPolicy::SP> RoutingPolicyCache; + + vespalib::Lock _lock; + ProtocolMap _protocols; + RoutingPolicyCache _routingPolicyCache; + +public: + + /** + * Registers a protocol with this repository. This will overwrite any protocol that was registered earlier + * that has the same name. If this method detects a protocol replacement, it will clear its internal + * routing policy cache. + * + * @param protocol The protocol to register. + * @return The previous protocol registered under this name. + */ + IProtocol::SP putProtocol(const IProtocol::SP & protocol); + + /** + * Returns whether or not this repository contains a protocol with the given name. Given the concurrent + * nature of things, one should not invoke this method followed by {@link #getProtocol(String)} and expect + * the return value to be non-null. Instead just get the protocol and compare it to null. + * + * @param name The name to check for. + * @return True if the named protocol is registered. + */ + bool hasProtocol(const string &name) const; + + /** + * Returns the protocol whose name matches the given argument. This method will return null if no such + * protocol has been registered. + * + * @param name The name of the protocol to return. + * @return The protocol registered, or null. + */ + IProtocol::SP getProtocol(const string &name); + + /** + * Creates and returns a routing policy that matches the given arguments. If a routing policy has been + * created previously using the exact same parameters, this method will returned that cached instance + * instead of creating another. Not that when you replace a protocol using {@link #putProtocol(Protocol)} + * the policy cache is cleared. + * + * @param protocolName The name of the protocol whose routing policy to create. + * @param policyName The name of the routing policy to create. + * @param policyParam The parameter to pass to the routing policy constructor. + * @return The created routing policy. + */ + IRoutingPolicy::SP getRoutingPolicy(const string &protocolName, + const string &policyName, + const string &policyParam); + + /** + * Clears the internal cache of routing policies. + */ + void clearPolicyCache(); +}; + +} + diff --git a/messagebus/src/vespa/messagebus/protocolset.cpp b/messagebus/src/vespa/messagebus/protocolset.cpp new file mode 100644 index 00000000000..c43e8366bb7 --- /dev/null +++ b/messagebus/src/vespa/messagebus/protocolset.cpp @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".protocolset"); +#include "protocolset.h" + +namespace mbus { + +ProtocolSet::ProtocolSet() + : _vector() +{ +} + +ProtocolSet & +ProtocolSet::add(IProtocol::SP protocol) +{ + _vector.push_back(protocol); + return *this; +} + +bool +ProtocolSet::empty() const +{ + return _vector.empty(); +} + +IProtocol::SP +ProtocolSet::extract() +{ + if (_vector.empty()) { + return IProtocol::SP(); + } + IProtocol::SP ret = _vector.back(); + _vector.pop_back(); + return ret; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/protocolset.h b/messagebus/src/vespa/messagebus/protocolset.h new file mode 100644 index 00000000000..bb3c29f7397 --- /dev/null +++ b/messagebus/src/vespa/messagebus/protocolset.h @@ -0,0 +1,52 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <memory> +#include <vector> +#include "iprotocol.h" + +namespace mbus { + +/** + * This class is used to bundle a set of IProtocol objects to be + * supported by a MessageBus instance. + **/ +class ProtocolSet +{ +private: + std::vector<IProtocol::SP> _vector; + +public: + /** + * Create an empty ProtocolSet. + **/ + ProtocolSet(); + + /** + * Add a Protocol to this set. + * + * @return this object, to allow chaining + * @param protocol the IProtocol we want to add + **/ + ProtocolSet &add(IProtocol::SP protocol); + + /** + * Check if this set is empty + * + * @return true if this set is empty + **/ + bool empty() const; + + /** + * Extract a single protocol from this set. This will remove the + * protocol from the set. If the set is empty, a shared pointer to + * 0 will be returned. + * + * @return the extracted IProtocol + **/ + IProtocol::SP extract(); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/queue.h b/messagebus/src/vespa/messagebus/queue.h new file mode 100644 index 00000000000..e65dcd2801a --- /dev/null +++ b/messagebus/src/vespa/messagebus/queue.h @@ -0,0 +1,64 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <queue> + +namespace mbus { + +/** + * A simple generic queue implementation that is not thread-safe. The + * API is similar to that of std::queue. + **/ +template <class T> +class Queue +{ +private: + std::queue<T> _queue; + +public: + /** + * Create an empty Queue. + **/ + Queue() : _queue() {} + + /** + * Obtain the size of this queue. The size denotes the number of + * elements currently on the queue. + * + * @return size current queue size + **/ + uint32_t size() const { + return _queue.size(); + } + + /** + * Access the element located at the front of the queue. This + * method yields undefined behavior if the queue is empty. + * + * @return element at the front of this queue + **/ + T &front() { + return _queue.front(); + } + + /** + * Push an element to the back of this queue. + * + * @param val the element value + **/ + void push(const T &val) { + _queue.push(val); + } + + /** + * Pop the front element from this queue. This method yields + * undefined behavior if the queue is empty. + **/ + void pop() { + _queue.pop(); + } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/reply.cpp b/messagebus/src/vespa/messagebus/reply.cpp new file mode 100644 index 00000000000..b911ab5f8e9 --- /dev/null +++ b/messagebus/src/vespa/messagebus/reply.cpp @@ -0,0 +1,87 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".reply"); + +#include <vespa/vespalib/util/vstringfmt.h> +#include <vespa/vespalib/util/backtrace.h> +#include "emptyreply.h" +#include "error.h" +#include "errorcode.h" +#include "ireplyhandler.h" +#include "message.h" +#include "reply.h" +#include "tracelevel.h" + +namespace mbus { + +Reply::Reply() : + _errors(), + _msg(), + _retryDelay(-1.0) +{ + // empty +} + +Reply::~Reply() +{ + if (getCallStack().size() > 0) { + string backtrace = vespalib::getStackTrace(0); + LOG(warning, "Deleted reply %p with non-empty call-stack. Deleted at:\n%s", + this, backtrace.c_str()); + Reply::UP reply(new EmptyReply()); + swapState(*reply); + reply->addError(Error(ErrorCode::FATAL_ERROR, + "The reply object was deleted while containing state information; " + "generating an auto-reply.")); + IReplyHandler &handler = reply->getCallStack().pop(*reply); + handler.handleReply(std::move(reply)); + } +} + +void +Reply::swapState(Routable &rhs) +{ + Routable::swapState(rhs); + if (rhs.isReply()) { + Reply &reply = static_cast<Reply&>(rhs); + + std::swap(_retryDelay, reply._retryDelay); + + Message::UP msg = std::move(_msg); + _msg = std::move(reply._msg); + reply._msg = std::move(msg); + + reply._errors.swap(_errors); + } +} + +bool +Reply::isReply() const +{ + return true; +} + +void +Reply::addError(const Error &e) +{ + if (getTrace().shouldTrace(TraceLevel::ERROR)) { + getTrace().trace(TraceLevel::ERROR, e.toString()); + } + _errors.push_back(e); +} + +bool +Reply::hasFatalErrors() const +{ + for (std::vector<Error>::const_iterator it = _errors.begin(); + it != _errors.end(); ++it) + { + if (it->getCode() >= ErrorCode::FATAL_ERROR) { + return true; + } + } + return false; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/reply.h b/messagebus/src/vespa/messagebus/reply.h new file mode 100644 index 00000000000..41c9bb2c3e0 --- /dev/null +++ b/messagebus/src/vespa/messagebus/reply.h @@ -0,0 +1,129 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vector> +#include "error.h" +#include "message.h" + +namespace mbus { + +class Message; + +/** + * A reply is a response to a message that has been sent throught the + * messagebus. No reply will ever exist without a corresponding message. There + * are no error-replies defined, as errors can instead piggyback any reply by + * the {@link #errors} member variable. + */ +class Reply : public Routable { +private: + std::vector<Error> _errors; // A list of errors that have occured during the lifetime of this reply. + Message::UP _msg; // The message to which this is a reply. + double _retryDelay; // How to perform resending of this. + +public: + /** + * Convenience typedef for an auto pointer to a Reply object. + */ + typedef std::unique_ptr<Reply> UP; + + /** + * Constructs a new instance of this class. This object is useless until the + * state of a real message is swapped with this using {@link + * #swapState(Routable)}. + */ + Reply(); + + /** + * If a reply is deleted with elements on the callstack, this destructor + * will log an error and generate an auto-reply to avoid having the sender + * wait indefinetly for a reply. + */ + virtual ~Reply(); + + // Inherit doc from Routable. + virtual void swapState(Routable &rhs); + + /** + * Inherited from Routable. Classifies this object as 'a reply'. + * + * @return true + */ + virtual bool isReply() const; + + /** + * Add an Error to this Reply + * + * @param error the error to add + */ + void addError(const Error &error); + + /** + * Returns whether or not this reply contains at least one error. + * + * @return True if this contains errors. + */ + bool hasErrors() const { return ! _errors.empty(); } + + /** + * Returns whether or not this reply contains any fatal errors. + * + * @return True if it contains fatal errors. + */ + bool hasFatalErrors() const; + + /** + * Returns the error at the given position. + * + * @param i The index of the error to return. + * @return The error at the given index. + */ + const Error &getError(uint32_t i) const { return _errors[i]; } + + /** + * Returns the number of errors that this reply contains. + * + * @return The number of replies. + */ + uint32_t getNumErrors() const { return _errors.size(); } + + /** + * Attach a Message to this Reply. If a Reply contains errors, messagebus + * will attach the original Message to the Reply before giving it to the + * application. + * + * @param msg the Message to attach + */ + void setMessage(Message::UP msg) { _msg = std::move(msg); } + + /** + * Detach the Message attached to this Reply. If a Reply contains errors, + * messagebus will attach the original Message to the Reply before giving it + * to the application. + * + * @return the detached Message + */ + Message::UP getMessage() { return std::move(_msg); } + + /** + * Returns the retry request of this reply. This can be set using {@link + * #setRetryDelay} and is an instruction to the resender logic of message + * bus on how to perform the retry. If this value is anything other than a + * negative number, it instructs the resender to disregard all configured + * resending attributes and instead act according to this value. + * + * @return The retry request. + */ + double getRetryDelay() const { return _retryDelay; } + + /** + * Sets the retry delay request of this reply. If this is a negative number, + * it will use the defaults configured in the source session. + * + * @param retryDelay The retry request. + */ + void setRetryDelay(double retryDelay) { _retryDelay = retryDelay; } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/replygate.cpp b/messagebus/src/vespa/messagebus/replygate.cpp new file mode 100644 index 00000000000..8363d17fd89 --- /dev/null +++ b/messagebus/src/vespa/messagebus/replygate.cpp @@ -0,0 +1,51 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".replygate"); + +#include "replygate.h" + +namespace mbus { + +ReplyGate::ReplyGate(IMessageHandler &sender) : + vespalib::ReferenceCounter(), + _sender(sender), + _open(true) +{ + // empty +} + +void +ReplyGate::handleMessage(Message::UP msg) +{ + addRef(); + msg->pushHandler(*this, *this); + _sender.handleMessage(std::move(msg)); +} + +void +ReplyGate::close() +{ + _open = false; +} + +void +ReplyGate::handleReply(Reply::UP reply) +{ + if (_open) { + IReplyHandler &handler = reply->getCallStack().pop(*reply); + handler.handleReply(std::move(reply)); + } else { + reply->discard(); + } + subRef(); +} + +void +ReplyGate::handleDiscard(Context ctx) +{ + (void)ctx; + subRef(); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/replygate.h b/messagebus/src/vespa/messagebus/replygate.h new file mode 100644 index 00000000000..121a66edd3f --- /dev/null +++ b/messagebus/src/vespa/messagebus/replygate.h @@ -0,0 +1,69 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <boost/utility.hpp> +#include <vespa/vespalib/util/referencecounter.h> +#include "idiscardhandler.h" +#include "imessagehandler.h" +#include "ireplyhandler.h" +#include "message.h" + +namespace mbus { + +/** + * A ReplyGate will forward replies until it is closed. After being closed, the + * gate will silently delete all replies. The ReplyGate class has external + * reference counting. This class is used by session objects to perform safe + * untangling from messagebus when being destructed while having pending + * messages. The reference counting is needed to ensure that the object is alive + * until all pending replies have been correctly ignored. Thread synchronization + * is handled outside this class. Note that this class is only intended for + * internal use. + */ +class ReplyGate : public boost::noncopyable, + public vespalib::ReferenceCounter, + public IDiscardHandler, + public IMessageHandler, + public IReplyHandler +{ +private: + IMessageHandler &_sender; + bool _open; + +public: + /** + * Create a new ReplyGate. + * + * @param sender The underlying IMessageHandler object. + */ + ReplyGate(IMessageHandler &sender); + + /** + * Send a Message to the underlying IMessageHandler. This method will + * increase the reference counter to ensure that this object is alive until + * the matching Reply has been obtained. In order to obtain the matching + * Reply, this method will push this object on the CallStack of the Message. + */ + void handleMessage(Message::UP msg); + + /** + * Forward or discard Reply. If the gate is still open, it will forward the + * Reply to the next IReplyHandler on the CallStack. If the gate is closed, + * the Reply will be discarded. This method also decreases the reference + * counter of this object. + */ + void handleReply(Reply::UP reply); + + // Implements IDiscardHandler. + void handleDiscard(Context ctx); + + /** + * Close this gate. After this has been invoked, the gate will start to + * discard Reply objects. A closed gate can never be re-opened. + */ + void close(); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/result.cpp b/messagebus/src/vespa/messagebus/result.cpp new file mode 100644 index 00000000000..81a55da1276 --- /dev/null +++ b/messagebus/src/vespa/messagebus/result.cpp @@ -0,0 +1,86 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".result"); +#include "result.h" + +namespace mbus { + +Result::Handover::Handover(bool a, const Error &e, Message *m) + : _accepted(a), + _error(e), + _msg(m) +{ +} + +Result::Result() + : _accepted(true), + _error(), + _msg() +{ +} + +Result::Result(const Error &err, Message::UP msg) + : _accepted(false), + _error(err), + _msg(std::move(msg)) +{ +} + +Result::Result(Result &&rhs) + : _accepted(rhs._accepted), + _error(rhs._error), + _msg(std::move(rhs._msg)) +{ +} + +Result::Result(const Handover &rhs) + : _accepted(rhs._accepted), + _error(rhs._error), + _msg(rhs._msg) +{ +} + +bool +Result::isAccepted() const +{ + return _accepted; +} + +const Error & +Result::getError() const +{ + return _error; +} + +Message::UP +Result::getMessage() +{ + return std::move(_msg); +} + +Result::operator Handover() +{ + return Handover(_accepted, _error, _msg.release()); +} + +Result & +Result::operator=(Result &&rhs) +{ + _accepted = rhs._accepted; + _error = rhs._error; + _msg = std::move(rhs._msg); + return *this; +} + +Result & +Result::operator=(const Handover &rhs) +{ + _accepted = rhs._accepted; + _error = rhs._error; + _msg.reset(rhs._msg); + return *this; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/result.h b/messagebus/src/vespa/messagebus/result.h new file mode 100644 index 00000000000..02f4ba1e838 --- /dev/null +++ b/messagebus/src/vespa/messagebus/result.h @@ -0,0 +1,122 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <memory> +#include "error.h" +#include "message.h" + +namespace mbus { + +/** + * A Result object is used as return value when trying to send a + * Message on a SourceSession. It says whether messagebus has accepted + * the message or not. If messagebus accepts the message an + * asynchronous reply will be delivered at a later time. If messagebus + * does not accept the message, the returned Result will indicate why + * the message was not accepted. A Result indication an error will + * also contain the message that did not get accepted, passing it back + * to the application. Note that Result objects have destructive copy + * of the pointer to the Message that is handed back to the + * application. + **/ +class Result +{ +private: + bool _accepted; + Error _error; + Message::UP _msg; + +public: + /** + * This inner class is used to implement destructive copy for + * return values. + **/ + class Handover { + friend class Result; + bool _accepted; + Error _error; + Message *_msg; + Handover(bool a, const Error &e, Message *m); + Handover(const Handover &); // not implemented + Handover &operator=(const Handover &); // not implemented + }; + + /** + * Create a Result indicating that messagebus has accepted the + * Message. + **/ + Result(); + + /** + * Create a Result indicating that messagebus has not accepted the + * Message. + * + * @param err the reason for not accepting the Message + * @param msg the message that did not get accepted + **/ + Result(const Error &err, Message::UP msg); + + /** + * Move constructor + * + * @param rhs the original object + **/ + Result(Result &&rhs); + + /** + * Construct a new Result from an internal Handover object that + * has destructed the original Result. + * + * @param rhs handover object + **/ + Result(const Handover &rhs); + + /** + * Check if the message was accepted. + * + * @return true if the Message was accepted + **/ + bool isAccepted() const; + + /** + * Obtain the error causing the message not to be accepted. + * + * @return error + **/ + const Error &getError() const; + + /** + * If the message was not accepted, this method may be used to get + * the Message back out. Note that this method hands the Message + * over to the caller. Also note that copying the Result will + * transfer the ownership of the Message to the new copy. + * + * @return the Message that was not accepted + **/ + Message::UP getMessage(); + + /** + * Perform an implicit typecast to support destructive copy of + * return values. + **/ + operator Handover(); + + /** + * Moving assignment operator + * + * @param rhs the original object + **/ + Result &operator=(Result &&rhs); + + /** + * Assign a Result from an internal Handover object that has + * destructed the original Result. + * + * @param rhs handover object + **/ + Result &operator=(const Handover &rhs); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routable.cpp b/messagebus/src/vespa/messagebus/routable.cpp new file mode 100644 index 00000000000..95d56e09696 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routable.cpp @@ -0,0 +1,35 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".routable"); +#include "emptyreply.h" +#include "errorcode.h" +#include "ireplyhandler.h" + +namespace mbus { + +Routable::Routable() : + _context(), + _stack(), + _trace() +{ + // empty +} + +void +Routable::discard() +{ + _context = Context(); + _stack.discard(); + _trace.clear(); +} + +void +Routable::swapState(Routable &rhs) +{ + std::swap(_context, rhs._context); + _stack.swap(rhs._stack); + _trace.swap(rhs._trace); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/routable.h b/messagebus/src/vespa/messagebus/routable.h new file mode 100644 index 00000000000..d40c80a736b --- /dev/null +++ b/messagebus/src/vespa/messagebus/routable.h @@ -0,0 +1,175 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <memory> +#include <boost/utility.hpp> +#include <vespa/messagebus/blob.h> +#include <vespa/messagebus/callstack.h> +#include <vespa/messagebus/context.h> +#include <vespa/messagebus/trace.h> + +namespace mbus { + +/** + * Superclass for objects that can be either explicitly (Message) or implicitly + * (Reply) routed. Note that protocol implementors should never subclass this + * directly, but rather through the {@link Message} and {@link Reply} classes. + * + * A routable can be regarded as a protocol-defined value with additional + * message bus related state. The state is what differentiates two Routables + * that carry the same value. This includes the application context attached to + * the routable and the {@link CallStack} used to track the path of the routable + * within messagebus. When a routable is copied (if the protocol supports it) + * only the value part is copied. The state must be explicitly transfered by + * invoking the {@link #swapState(Routable)} method. That method is used to + * transfer the state from a message to the corresponding reply, or to a + * different message if the application decides to replace it. + */ +class Routable : public boost::noncopyable { +private: + Context _context; + CallStack _stack; + Trace _trace; + +public: + /** + * Convenience typedef for an auto pointer to a Routable object. + */ + typedef std::unique_ptr<Routable> UP; + + /** + * Constructs a new instance of this class. + */ + Routable(); + + /** + * Required for inheritance. + */ + virtual ~Routable() { /* empty */ } + + /** + * Discards this routable. Invoking this prevents the auto-generation of + * replies if you later discard the routable. This is a required step to + * ensure safe shutdown if you need destroy a message bus instance while + * there are still messages and replies alive in your application. + */ + void discard(); + + /** + * Access the CallStack of this routable. The CallStack is intended for + * internal messagebus use. + * + * @return reference to internal CallStack + */ + CallStack &getCallStack() { return _stack; } + + /** + * Pushes the given reply handler onto the call stack of this routable, also + * storing the current context. + * + * @param handler The handler to push. + */ + void pushHandler(IReplyHandler &handler) { _stack.push(handler, _context); } + + /** + * Pushes the given reply- and discard handler onto the call stack of this + * routable, also storing the current context. + * + * @param replyHandler The handler called if the reply arrives. + * @param discardHandler The handler called if the reply is dicarded. + */ + void pushHandler(IReplyHandler &replyHandler, IDiscardHandler &discardHandler) { + _stack.push(replyHandler, _context, &discardHandler); + } + + /** + * Access the Trace object for this Routable. The Trace is part of the + * object state and will not be copied along with the value part of a + * Routable. The swapState method will transfer the Trace from one Routable + * to another. + * + * @return Trace object + */ + Trace &getTrace() { return _trace; } + + /** + * Access the Trace object for this Routable. The Trace is part of the + * object state and will not be copied along with the value part of a + * Routable. The swapState method will transfer the Trace from one Routable + * to another. + * + * @return Trace object + */ + const Trace &getTrace() const { return _trace; } + + /** + * Sets the Trace object for this Routable. + * + * @param trace The trace to set. + */ + void setTrace(const Trace &trace) { _trace = trace; } + + /** + * Swaps the state that makes this routable unique to another routable. The + * state is what identifies a routable for message bus, so only one message + * can ever have the same state. This function must be called explicitly + * when cloning and copying messages. + * + * @param rhs The routable to swap state with. + */ + virtual void swapState(Routable &rhs); + + /** + * Get the context of this routable. + * + * @return the context + */ + Context getContext() const { return _context; } + + /** + * Set the context of this routable. When setting a context on a Message, + * the Reply for that Message will have the same context as the original + * Message when received. + * + * @param ctx the context + */ + void setContext(Context ctx) { _context = ctx; } + + /** + * Check whether this routable is a reply. + * + * @return true if this is a reply + */ + virtual bool isReply() const = 0; + + /** + * Obtain the name of the protocol for this routable. This method must be + * implemented by all routable classes part of a protocol. + */ + virtual const string &getProtocol() const = 0; + + /** + * Return the type of this routable. The type is protocol specific with the + * exception that the value 0 is reserved for the EmptyReply class. This + * value is typically intended to be used by the application to identify + * what kind of routable object it is looking at. + * + * @return routable type id + */ + virtual uint32_t getType() const = 0; + + /** + * Returns the priority of this message. 0 is most highly + * prioritized, and messages with higher priority will be sent + * earlier than other messages. + */ + virtual uint8_t priority() const = 0; + + /** + * Returns a string representation of this message. + */ + virtual string toString() const { return ""; } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routablequeue.cpp b/messagebus/src/vespa/messagebus/routablequeue.cpp new file mode 100644 index 00000000000..8e255f8b393 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routablequeue.cpp @@ -0,0 +1,79 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".routablequeue"); +#include "routablequeue.h" + +namespace mbus { + +RoutableQueue::RoutableQueue() + : _monitor("mbus::RoutableQueue::_monitor", true), + _queue() +{ +} + +RoutableQueue::~RoutableQueue() +{ + while (_queue.size() > 0) { + Routable *r = _queue.front(); + _queue.pop(); + delete r; + } +} + +uint32_t +RoutableQueue::size() +{ + vespalib::MonitorGuard guard(_monitor); + return _queue.size(); +} + +void +RoutableQueue::enqueue(Routable::UP r) +{ + vespalib::MonitorGuard guard(_monitor); + _queue.push(r.get()); + r.release(); + if (_queue.size() == 1) { + guard.broadcast(); // support multiple readers + } +} + +Routable::UP +RoutableQueue::dequeue(uint32_t msTimeout) +{ + FastOS_Time t; + t.SetNow(); + uint32_t msLeft = msTimeout; + vespalib::MonitorGuard guard(_monitor); + while (_queue.size() == 0 && msLeft > 0) { + if (!guard.wait(msLeft) || _queue.size() > 0) { + break; + } + uint32_t elapsed = (uint32_t)t.MilliSecsToNow(); + msLeft = (elapsed > msTimeout) ? 0 : msTimeout - elapsed; + } + if (_queue.size() == 0) { + return Routable::UP(); + } + Routable::UP ret(_queue.front()); + _queue.pop(); + return ret; +} + +void +RoutableQueue::handleMessage(Message::UP msg) +{ + Routable::UP r(msg.release()); + enqueue(std::move(r)); +} + +void +RoutableQueue::handleReply(Reply::UP reply) +{ + Routable::UP r(reply.release()); + enqueue(std::move(r)); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/routablequeue.h b/messagebus/src/vespa/messagebus/routablequeue.h new file mode 100644 index 00000000000..f3174abf5f5 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routablequeue.h @@ -0,0 +1,90 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <vespa/vespalib/util/sync.h> +#include "imessagehandler.h" +#include "ireplyhandler.h" +#include "queue.h" +#include "routable.h" +#include "message.h" +#include "reply.h" + +namespace mbus { + +/** + * A RoutableQueue is a queue of Routable objects that also implements + * the IMessageHandler and IReplyHandler APIs. This class is included + * as a convenience for application developers that does not want to + * write their own Message and Reply handlers for use with session + * objects. Note that a RoutableQueue cannot be copied and that it + * owns all objects currently on the queue. The RoutableQueue class is + * thread-safe. + **/ +class RoutableQueue : public IMessageHandler, + public IReplyHandler +{ +private: + vespalib::Monitor _monitor; + Queue<Routable*> _queue; + + RoutableQueue(const RoutableQueue &); + RoutableQueue &operator=(const RoutableQueue &); + +public: + /** + * Create an empty queue. + **/ + RoutableQueue(); + + /** + * The destructor will delete any objects still on the queue. + **/ + virtual ~RoutableQueue(); + + /** + * Obtain the number of elements currently in this queue. Note + * that the return value of this method may become invalid really + * fast if the queue is used by multiple threads. + * + * @return current queue size + **/ + uint32_t size(); + + /** + * Enqueue a routable on this queue. + * + * @param r the Routable to enqueue + **/ + void enqueue(Routable::UP r); + + /** + * Dequeue a routable from this queue. This method will block if + * the queue is currently empty. The 'msTimeout' parameter + * indicate how long to wait for something to be enqueued. If 0 is + * given as timeout, this method will not block at all, making it + * perform a simple poll. If the dequeue operation times out, the + * returned auto-pointer will point to 0. + * + * @return the dequeued routable + * @param msTimeout how long to wait if the queue is empty + **/ + Routable::UP dequeue(uint32_t msTimeout); + + /** + * Handle a Message by enqueuing it. + * + * @param msg the Message to handle + **/ + virtual void handleMessage(Message::UP msg); + + /** + * Handle a Reply by enqueuing it. + * + * @param reply the Reply to handle + **/ + virtual void handleReply(Reply::UP reply); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routing/.gitignore b/messagebus/src/vespa/messagebus/routing/.gitignore new file mode 100644 index 00000000000..1465a0826cb --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/.gitignore @@ -0,0 +1,5 @@ +.depend +Makefile +lex.yy.cpp +parser.tab.cpp +parser.tab.h diff --git a/messagebus/src/vespa/messagebus/routing/CMakeLists.txt b/messagebus/src/vespa/messagebus/routing/CMakeLists.txt new file mode 100644 index 00000000000..2edb990809e --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/CMakeLists.txt @@ -0,0 +1,24 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_library(messagebus_routing OBJECT + SOURCES + errordirective.cpp + hop.cpp + hopblueprint.cpp + hopspec.cpp + policydirective.cpp + resender.cpp + retrytransienterrorspolicy.cpp + route.cpp + routedirective.cpp + routeparser.cpp + routespec.cpp + routingcontext.cpp + routingnode.cpp + routingnodeiterator.cpp + routingspec.cpp + routingtable.cpp + routingtablespec.cpp + tcpdirective.cpp + verbatimdirective.cpp + DEPENDS +) diff --git a/messagebus/src/vespa/messagebus/routing/errordirective.cpp b/messagebus/src/vespa/messagebus/routing/errordirective.cpp new file mode 100644 index 00000000000..f87698da1ba --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/errordirective.cpp @@ -0,0 +1,26 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/vespalib/util/vstringfmt.h> +#include "errordirective.h" + +namespace mbus { + +ErrorDirective::ErrorDirective(const vespalib::stringref &msg) : + _msg(msg) +{ + // empty +} + +string +ErrorDirective::toString() const +{ + return vespalib::make_vespa_string("(%s)", _msg.c_str()); +} + +string +ErrorDirective::toDebugString() const +{ + return vespalib::make_vespa_string("ErrorDirective(msg = '%s')", _msg.c_str()); +} + +} // mbus diff --git a/messagebus/src/vespa/messagebus/routing/errordirective.h b/messagebus/src/vespa/messagebus/routing/errordirective.h new file mode 100644 index 00000000000..d55596e2871 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/errordirective.h @@ -0,0 +1,41 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "ihopdirective.h" + +namespace mbus { + +/** + * This class represents an error directive within a {@link Hop}'s selector. This means to stop whatever is being + * resolved, and instead return a reply containing a specified error. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @version $Id$ + */ +class ErrorDirective : public IHopDirective { +private: + string _msg; + +public: + /** + * Constructs a new error directive. + * + * @param msg The error message. + */ + ErrorDirective(const vespalib::stringref &msg); + + /** + * Returns the error string that is to be assigned to the reply. + * + * @return The error string. + */ + const string &getMessage() const { return _msg; } + + virtual Type getType() const { return TYPE_ERROR; } + virtual bool matches(const IHopDirective &) const { return false; } + virtual string toString() const; + virtual string toDebugString() const; +}; + +} // mbus + diff --git a/messagebus/src/vespa/messagebus/routing/hop.cpp b/messagebus/src/vespa/messagebus/routing/hop.cpp new file mode 100644 index 00000000000..5a38eba907c --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/hop.cpp @@ -0,0 +1,148 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".hop"); + +#include "hop.h" +#include "routeparser.h" + +namespace mbus { + +Hop::Hop() : + _selector(), + _ignoreResult(false) +{ + // empty +} + +Hop::Hop(const string &selector) : + _selector(), + _ignoreResult(false) +{ + Hop hop = parse(selector); + _selector.swap(hop._selector); + _ignoreResult = hop._ignoreResult; +} + +Hop::Hop(const std::vector<IHopDirective::SP> &selector, + bool ignoreResult) : + _selector(selector), + _ignoreResult(ignoreResult) +{ + // empty +} + +Hop & +Hop::addDirective(IHopDirective::SP dir) +{ + _selector.push_back(dir); + return *this; +} + +Hop & +Hop::setDirective(uint32_t i, IHopDirective::SP dir) +{ + _selector[i] = dir; + return *this; +} + +IHopDirective::SP +Hop::removeDirective(uint32_t i) +{ + IHopDirective::SP ret = _selector[i]; + _selector.erase(_selector.begin() + i); + return ret; +} + +Hop & +Hop::clearDirectives() +{ + _selector.clear(); + return *this; +} + +Hop & +Hop::setIgnoreResult(bool ignoreResult) +{ + _ignoreResult = ignoreResult; + return *this; +} + +Hop +Hop::parse(const string &hop) +{ + return RouteParser::createHop(hop); +} + +bool +Hop::matches(const Hop &hop) const +{ + if (_selector.size() != hop.getNumDirectives()) { + return false; + } + for (uint32_t i = 0; i < hop.getNumDirectives(); ++i) { + if (!_selector[i]->matches(*hop.getDirective(i))) { + return false; + } + } + return true; +} + +string +Hop::toDebugString() const +{ + string ret = "Hop(selector = { "; + for (uint32_t i = 0; i < _selector.size(); ++i) { + ret.append(_selector[i]->toDebugString()); + if (i < _selector.size() - 1) { + ret.append(", "); + } + } + ret.append(" }, ignoreResult = "); + ret.append(_ignoreResult ? "true" : "false"); + ret.append(")"); + return ret; +} + +string +Hop::toString() const +{ + string ret = _ignoreResult ? "?" : ""; + ret.append(toString(0, _selector.size())); + return ret; +} + +string +Hop::toString(uint32_t fromIncluding, uint32_t toNotIncluding) const +{ + string ret = ""; + for (uint32_t i = fromIncluding; i < toNotIncluding; ++i) { + ret.append(_selector[i]->toString()); + if (i < toNotIncluding - 1) { + ret.append("/"); + } + } + return ret; +} + +string +Hop::getPrefix(uint32_t toNotIncluding) const +{ + if (toNotIncluding > 0) { + return toString(0, toNotIncluding) + "/"; + } + return ""; +} + +string +Hop::getSuffix(uint32_t fromNotIncluding) const +{ + if (fromNotIncluding < _selector.size() - 1) { + string ret = "/"; + ret.append(toString(fromNotIncluding + 1, _selector.size())); + return ret; + } + return ""; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/routing/hop.h b/messagebus/src/vespa/messagebus/routing/hop.h new file mode 100644 index 00000000000..6d23cda729c --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/hop.h @@ -0,0 +1,180 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <stdint.h> +#include <vector> +#include "ihopdirective.h" + +namespace mbus { + +/** + * A hop is basically an instantiated {@link HopBlueprint}, but it can also be contructed using the factory method + * {@link this#parse(String)}. It is a set of primitives, either a string primitive that is to be matched verbatim to a + * service address, or a {@link RoutingPolicy} directive. + */ +class Hop { +private: + std::vector<IHopDirective::SP> _selector; + bool _ignoreResult; + +public: + /** + * Convenience typedef for an auto-pointer to a hop. + */ + typedef std::unique_ptr<Hop> UP; + + /** + * Constructs an empty hop. + */ + Hop(); + + /** + * Constructs a new hop based on a selector string. + * + * @param selector The selector string for this hop. + */ + Hop(const string &selector); + + /** + * Constructs a fully populated hop. + * + * @param selector The selector to copy. + * @param ignoreResult Whether or not to ignore the result of this hop. + */ + Hop(const std::vector<IHopDirective::SP> &selector, bool ignoreResult); + + /** + * Adds a new directive to this hop. + * + * @param directive The directive to add. + * @return This, to allow chaining. + */ + Hop &addDirective(IHopDirective::SP dir); + + /** + * Returns whether or not there are any directives contained in this hop. + * + * @return True if there is at least one directive. + */ + bool hasDirectives() const { return !_selector.empty(); } + + /** + * Returns the number of directives contained in this hop. + * + * @return The number of directives. + */ + uint32_t getNumDirectives() const { return _selector.size(); } + + /** + * Returns the directive at the given index. + * + * @param i The index of the directive to return. + * @return The item. + */ + IHopDirective::SP getDirective(uint32_t i) const { return _selector[i]; } + + /** + * Sets the directive at a given index. + * + * @param i The index at which to set the directive. + * @param dir The directive to set. + * @return This, to allow chaining. + */ + Hop &setDirective(uint32_t i, IHopDirective::SP dir); + + /** + * Removes the directive at the given index. + * + * @param i The index of the directive to remove. + * @return The removed directive. + */ + IHopDirective::SP removeDirective(uint32_t i); + + /** + * Clears all directives from this hop. + * + * @return This, to allow chaining. + */ + Hop &clearDirectives(); + + /** + * Returns the service name referenced by this hop. This is the concatenation of all selector primitives, + * but with no ignore-result prefix. + * + * @return The service name. + */ + string getServiceName() const { return toString(0, _selector.size()); } + + /** + * Returns whether or not to ignore the result when routing through this hop. + * + * @return True to ignore the result. + */ + bool getIgnoreResult() const { return _ignoreResult; } + + /** + * Sets whether or not to ignore the result when routing through this hop. + * + * @param ignoreResult Whether or not to ignore the result. + * @return This, to allow chaining. + */ + Hop &setIgnoreResult(bool ignoreResult); + + /** + * Parses the given string as a single hop. The {@link this#toString()} method is compatible with this parser. + * + * @param hop The string to parse. + * @return A hop that corresponds to the string. + */ + static Hop parse(const string &hop); + + /** + * Returns true whether this hop matches another. This respects policy directives matching any other. + * + * @param hop The hop to compare to. + * @return True if this matches the argument, false otherwise. + */ + bool matches(const Hop &hop) const; + + /** + * Returns a string representation of this that can be debugged but not parsed. + * + * @return The debug string. + */ + string toDebugString() const; + + /** + * Returns a string representation of this that can be parsed. + * + * @return The parseable string. + */ + string toString() const; + + /** + * Returns a string concatenation of a subset of the selector primitives contained in this. + * + * @param fromIncluding The index of the first primitive to include. + * @param toNotIncluding The index after the last primitive to include. + * @return The string concatenation. + */ + string toString(uint32_t fromIncluding, uint32_t toNotIncluding) const; + + /** + * Returns the prefix of this hop's selector to, but not including, the given index. + * + * @param toNotIncluding The index to which to generate prefix. + * @return The prefix before the index. + */ + string getPrefix(uint32_t toNotIncluding) const; + + /** + * Returns the suffix of this hop's selector from, but not including, the given index. + * + * @param fromNotIncluding The index from which to generate suffix. + * @return The suffix after the index. + */ + string getSuffix(uint32_t fromNotIncluding) const; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routing/hopblueprint.cpp b/messagebus/src/vespa/messagebus/routing/hopblueprint.cpp new file mode 100644 index 00000000000..efcea51f58e --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/hopblueprint.cpp @@ -0,0 +1,65 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".hopblueprint"); + +#include "hopblueprint.h" +#include "hopspec.h" + +namespace mbus { + +HopBlueprint::HopBlueprint(const HopSpec &spec) : + _selector(), + _recipients(), + _ignoreResult(spec.getIgnoreResult()) +{ + Hop hop = Hop::parse(spec.getSelector()); + for (uint32_t i = 0; i < hop.getNumDirectives(); ++i) { + _selector.push_back(hop.getDirective(i)); + } + std::vector<string> lst; + for (uint32_t i = 0; i < spec.getNumRecipients(); ++i) { + lst.push_back(spec.getRecipient(i)); + } + for (std::vector<string>::iterator it = lst.begin(); + it != lst.end(); ++it) + { + _recipients.push_back(Hop::parse(*it)); + } +} + +HopBlueprint & +HopBlueprint::setIgnoreResult(bool ignoreResult) +{ + _ignoreResult = ignoreResult; + return *this; +} + +string +HopBlueprint::toString() const +{ + string ret = "HopBlueprint(selector = { "; + for (uint32_t i = 0; i < _selector.size(); ++i) { + ret.append("'"); + ret.append(_selector[i]->toString()); + ret.append("'"); + if (i < _selector.size() - 1) { + ret.append(", "); + } + } + ret.append(" }, recipients = { "); + for (uint32_t i = 0; i < _recipients.size(); ++i) { + ret.append("'"); + ret.append(_recipients[i].toString()); + ret.append("'"); + if (i < _recipients.size() - 1) { + ret.append(", "); + } + } + ret.append(" }, ignoreResult = "); + ret.append(_ignoreResult ? "true" : "false"); + ret.append(")"); + return ret; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/routing/hopblueprint.h b/messagebus/src/vespa/messagebus/routing/hopblueprint.h new file mode 100644 index 00000000000..b37796fba5d --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/hopblueprint.h @@ -0,0 +1,103 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "hop.h" + +namespace mbus { + +class HopSpec; + +/** + * A hop blueprint is a stored prototype of a hop that has been created from a {@link HopSpec} object. A map of these + * are stored in a {@link RoutingTable}. + */ +class HopBlueprint { +private: + std::vector<IHopDirective::SP> _selector; + std::vector<Hop> _recipients; + bool _ignoreResult; + +public: + /** + * Create a new blueprint from a specification object. + * + * @param spec The specification to base instantiation on. + */ + HopBlueprint(const HopSpec &spec); + + /** + * Creates a hop instance from thie blueprint. + * + * @return The created hop. + */ + Hop::UP create() const { return Hop::UP(new Hop(_selector, _ignoreResult)); } + + /** + * Returns whether or not there are any directives contained in this hop. + * + * @return True if there is at least one directive. + */ + bool hasDirectives() const { return !_selector.empty(); } + + /** + * Returns the number of directives contained in this hop. + * + * @return The number of directives. + */ + uint32_t getNumDirectives() const { return _selector.size(); } + + /** + * Returns the directive at the given index. + * + * @param i The index of the directive to return. + * @return The item. + */ + IHopDirective::SP getDirective(uint32_t i) const { return _selector[i]; } + + /** + * Returns whether or not there are any recipients that the selector can choose from. + * + * @return True if there is at least one recipient. + */ + bool hasRecipients() const { return !_recipients.empty(); } + + /** + * Returns the number of recipients that the selector can choose from. + * + * @return The number of recipients. + */ + uint32_t getNumRecipients() const { return _recipients.size(); } + + /** + * Returns the recipient at the given index. + * + * @param i The index of the recipient to return. + * @return The recipient at the given index. + */ + const Hop &getRecipient(uint32_t i) const { return _recipients[i]; } + + /** + * Returns whether or not to ignore the result when routing through this hop. + * + * @return True to ignore the result. + */ + bool getIgnoreResult() const { return _ignoreResult; } + + /** + * Sets whether or not to ignore the result when routing through this hop. + * + * @param ignoreResult Whether or not to ignore the result. + * @return This, to allow chaining. + */ + HopBlueprint &setIgnoreResult(bool ignoreResult); + + /** + * Returns a string representation of this. + * + * @return The string. + */ + string toString() const; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routing/hopspec.cpp b/messagebus/src/vespa/messagebus/routing/hopspec.cpp new file mode 100644 index 00000000000..40c523ca1b6 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/hopspec.cpp @@ -0,0 +1,89 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".hopspec"); + +#include <vespa/vespalib/util/vstringfmt.h> +#include "routingspec.h" + +namespace mbus { + +HopSpec::HopSpec(const string &name, const string &selector) : + _name(name), + _selector(selector), + _recipients(), + _ignoreResult(false) +{ + // empty +} + +HopSpec & +HopSpec::addRecipients(const std::vector<string> &recipients) +{ + _recipients.insert(_recipients.end(), recipients.begin(), recipients.end()); + return *this; +} + +string +HopSpec::removeRecipient(uint32_t i) +{ + string ret = _recipients[i]; + _recipients.erase(_recipients.begin() + i); + return ret; +} + +HopSpec & +HopSpec::setIgnoreResult(bool ignoreResult) +{ + _ignoreResult = ignoreResult; + return *this; +} + +void +HopSpec::toConfig(string &cfg, const string &prefix) const +{ + cfg.append(prefix).append("name ").append(RoutingSpec::toConfigString(_name)).append("\n"); + cfg.append(prefix).append("selector ").append(RoutingSpec::toConfigString(_selector)).append("\n"); + if (_ignoreResult) { + cfg.append(prefix).append("ignoreresult true\n"); + } + uint32_t numRecipients = _recipients.size(); + if (numRecipients > 0) { + cfg.append(prefix).append("recipient[").append(vespalib::make_vespa_string("%d", numRecipients)).append("]\n"); + for (uint32_t i = 0; i < numRecipients; ++i) { + cfg.append(prefix).append("recipient[").append(vespalib::make_vespa_string("%d", i)).append("] "); + cfg.append(RoutingSpec::toConfigString(_recipients[i])).append("\n"); + } + } +} + +string +HopSpec::toString() const +{ + string ret = ""; + toConfig(ret, ""); + return ret; +} + +bool +HopSpec::operator==(const HopSpec &rhs) const +{ + if (_name != rhs._name) { + return false; + } + if (_selector != rhs._selector) { + return false; + } + if (_recipients.size() != rhs._recipients.size()) { + return false; + } + for (uint32_t i = 0, len = _recipients.size(); i < len; ++i) { + if (_recipients[i] != rhs._recipients[i]) { + return false; + } + } + return true; +} + + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/routing/hopspec.h b/messagebus/src/vespa/messagebus/routing/hopspec.h new file mode 100644 index 00000000000..3bb40847569 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/hopspec.h @@ -0,0 +1,159 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vector> +#include <vespa/messagebus/common.h> + +namespace mbus { + +/** + * Along with the {@link RoutingSpec}, {@link RoutingTableSpec} and {@link RouteSpec}, this holds the routing + * specifications for all protocols. The only way a client can configure or alter the settings of a message bus instance + * is through these classes. + * + * This class contains the spec for a single hop. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @version $Id$ + */ +class HopSpec { +private: + string _name; + string _selector; + std::vector<string> _recipients; + bool _ignoreResult; + +public: + /** + * The default constructor requires both the name and the selector. + * + * @param name A protocol unique name for this hop. + * @param selector A string that represents the selector for this hop. + */ + HopSpec(const string &name, const string &selector); + + /** + * Returns the protocol-unique name of this hop. + * + * @return The name. + */ + const string &getName() const { return _name; } + + /** + * Returns the string selector that resolves the recipients of this hop. + * + * @return The selector. + */ + const string &getSelector() const { return _selector; } + + /** + * Returns whether or not there are any recipients that the selector can choose from. + * + * @return True if there is at least one recipient. + */ + bool hasRecipients() const { return !_recipients.empty(); } + + /** + * Returns the number of recipients that the selector can choose from. + * + * @return The number of recipients. + */ + uint32_t getNumRecipients() const { return _recipients.size(); } + + /** + * Returns the recipients at the given index. + * + * @param i The index of the recipient to return. + * @return The recipient at the given index. + */ + const string &getRecipient(uint32_t i) const { return _recipients[i]; } + + /** + * Adds the given recipient to this. + * + * @param recipient The recipient to add. + * @return This, to allow chaining. + */ + HopSpec &addRecipient(const string &recipient) { _recipients.push_back(recipient); return *this; } + + /** + * Adds the given recipients to this. + * + * @param recipients The recipients to add. + * @return This, to allow chaining. + */ + HopSpec &addRecipients(const std::vector<string> &recipients); + + /** + * Sets the recipient at the given index. + * + * @param i The index at which to set the recipient. + * @param recipient The recipient to set. + * @return This, to allow chaining. + */ + HopSpec &setRecipient(uint32_t i, const string &recipient) { _recipients[i] = recipient; return *this; } + + /** + * Removes the recipient at the given index. + * + * @param i The index of the recipient to remove. + * @return The removed recipient. + */ + string removeRecipient(uint32_t i); + + /** + * Clears the list of recipients that the selector may choose from. + * + * @return This, to allow chaining. + */ + HopSpec &clearRecipients() { _recipients.clear(); return *this; } + + /** + * Returns whether or not to ignore the result when routing through this hop. + * + * @return True to ignore the result. + */ + bool getIgnoreResult() const { return _ignoreResult; } + + /** + * Sets whether or not to ignore the result when routing through this hop. + * + * @param ignoreResult Whether or not to ignore the result. + * @return This, to allow chaining. + */ + HopSpec &setIgnoreResult(bool ignoreResult); + + /** + * Appends the content of this to the given config string. + * + * @param cfg The config to add to. + * @param prefix The prefix to use for each add. + */ + void toConfig(string &cfg, const string &prefix) const; + + /** + * Returns a string representation of this. + * + * @return The string. + */ + string toString() const; + + /** + * Implements the equality operator. + * + * @param rhs The object to compare to. + * @return True if this equals the other. + */ + bool operator==(const HopSpec &rhs) const; + + /** + * Implements the inequality operator. + * + * @param rhs The object to compare to. + * @return True if this does not equals the other. + */ + bool operator!=(const HopSpec &rhs) const { return !(*this == rhs); } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routing/ihopdirective.h b/messagebus/src/vespa/messagebus/routing/ihopdirective.h new file mode 100644 index 00000000000..6d6c59509d9 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/ihopdirective.h @@ -0,0 +1,71 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vespa/messagebus/common.h> + +namespace mbus { + +/** + * This class is the base class for the primitives that make up a {@link Hop}'s selector. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @version $Id$ + */ +class IHopDirective { +public: + /** + * Defines the various polymorphic variants of a hop directive. + */ + enum Type { + TYPE_ERROR, + TYPE_POLICY, + TYPE_ROUTE, + TYPE_TCP, + TYPE_VERBATIM + }; + + /** + * Convenience typedefs. + */ + typedef std::unique_ptr<IHopDirective> UP; + typedef std::shared_ptr<IHopDirective> SP; + + /** + * Implements a virtual destructor to allow virtual methods. + */ + virtual ~IHopDirective() { + // empty + } + + /** + * Returns the type of directive that this is. + * + * @return The type. + */ + virtual Type getType() const = 0; + + /** + * Returns true if this directive matches another. + * + * @param dir The directive to compare this to. + * @return True if this matches the argument. + */ + virtual bool matches(const IHopDirective &dir) const = 0; + + /** + * Returns a string representation of this that can be parsed. + * + * @return The string. + */ + virtual string toString() const = 0; + + /** + * Returns a string representation of this that can be debugged but not parsed. + * + * @return The debug string. + */ + virtual string toDebugString() const = 0; +}; + +} // mbus + diff --git a/messagebus/src/vespa/messagebus/routing/iretrypolicy.h b/messagebus/src/vespa/messagebus/routing/iretrypolicy.h new file mode 100644 index 00000000000..3727724500c --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/iretrypolicy.h @@ -0,0 +1,49 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <memory> +#include <stdint.h> + +namespace mbus { + +/** + * When a {@link com.yahoo.messagebus.Reply} containing errors is returned to a {@link + * com.yahoo.messagebus.MessageBus}, an object implementing this interface is consulted on whether or not to + * resend the corresponding {@link com.yahoo.messagebus.Message}. The policy is passed to the message bus at + * creation time using the {@link com.yahoo.messagebus.MessageBusParams#setRetryPolicy(RetryPolicy::UP)} + * method. + */ +class IRetryPolicy { +public: + /** + * Convenience typedefs. + */ + typedef std::unique_ptr<IRetryPolicy> UP; + typedef std::shared_ptr<IRetryPolicy> SP; + + /** + * Virtual destructor required for inheritance. + */ + virtual ~IRetryPolicy() { /* empty */ } + + /** + * Returns whether or not a {@link com.yahoo.messagebus.Reply} containing an {@link + * com.yahoo.messagebus.Error} with the given error code can be retried. This method is invoked once for + * each error in a reply. + * + * @param errorCode The code to check. + * @return True if the message can be resent. + */ + virtual bool canRetry(uint32_t errorCode) const = 0; + + /** + * Returns the number of seconds to delay resending a message. + * + * @param retry The retry attempt. + * @return The delay in seconds. + */ + virtual double getRetryDelay(uint32_t retry) const = 0; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routing/iroutingpolicy.h b/messagebus/src/vespa/messagebus/routing/iroutingpolicy.h new file mode 100644 index 00000000000..8fabd5210c3 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/iroutingpolicy.h @@ -0,0 +1,53 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <memory> + +namespace mbus { + +class RoutingContext; + +/** + * A routing policy selects who should get a message and merges the replies that are returned from those + * recipients. Note that recipient selection is done separately for each path element. This means that a routing policy + * is not selecting recipients directly, but rather the set of strings that should be used for the next service name + * path element. This process is done recursively until a set of actual service names are produced. The merging process + * is similar, but in reverse order. + */ +class IRoutingPolicy { +public: + /** + * Convenience typedefs. + */ + typedef std::unique_ptr<IRoutingPolicy> UP; + typedef std::shared_ptr<IRoutingPolicy> SP; + +public: + /** + * Destructor. Frees any allocated resources. + */ + virtual ~IRoutingPolicy() { + // empty + } + + /** + * This function must choose a set of services that is to receive the given message from a list of possible + * recipients. This is done by adding child routing contexts to the argument object. These children can then be + * iterated and manipulated even before selection pass is concluded. + * + * @param context The complete context for the invokation of this policy. Contains all available data. + */ + virtual void select(RoutingContext &context) = 0; + + /** + * This function is called when all replies have arrived for some message. The implementation is responsible for + * merging multiple replies into a single sensible reply. The replies is contained in the child context objects of + * the argument context, and then response must be set in that context. + * + * @param context The complete context for the invokation of this policy. Contains all available data. + */ + virtual void merge(RoutingContext &context) = 0; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routing/policydirective.cpp b/messagebus/src/vespa/messagebus/routing/policydirective.cpp new file mode 100644 index 00000000000..da278315631 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/policydirective.cpp @@ -0,0 +1,31 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/vespalib/util/vstringfmt.h> +#include "policydirective.h" + +namespace mbus { + +PolicyDirective::PolicyDirective(const vespalib::stringref &name, const vespalib::stringref ¶m) : + _name(name), + _param(param) +{ + // empty +} + +string +PolicyDirective::toString() const +{ + if (_param.empty()) { + return vespalib::make_vespa_string("[%s]", _name.c_str()); + } + return vespalib::make_vespa_string("[%s:%s]", _name.c_str(), _param.c_str()); +} + +string +PolicyDirective::toDebugString() const +{ + return vespalib::make_vespa_string("PolicyDirective(name = '%s', param = '%s')", + _name.c_str(), _param.c_str()); +} + +} // mbus diff --git a/messagebus/src/vespa/messagebus/routing/policydirective.h b/messagebus/src/vespa/messagebus/routing/policydirective.h new file mode 100644 index 00000000000..3b25bd76197 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/policydirective.h @@ -0,0 +1,51 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "ihopdirective.h" + +namespace mbus { + +/** + * This class represents a policy directive within a {@link Hop}'s selector. This means to create the named protocol + * using the given parameter string, and the running that protocol within the context of this directive. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @version $Id$ + */ +class PolicyDirective : public IHopDirective { +private: + string _name; + string _param; + +public: + + /** + * Constructs a new policy selector item. + * + * @param name The name of the policy to invoke. + * @param param The parameter to pass to the name constructor. + */ + PolicyDirective(const vespalib::stringref &name, const vespalib::stringref ¶m); + + /** + * Returns the name of the policy that this item is to invoke. + * + * @return The name name. + */ + const string &getName() const { return _name; } + + /** + * Returns the parameter string for this policy directive. + * + * @return The parameter. + */ + const string &getParam() const { return _param; } + + virtual Type getType() const { return TYPE_POLICY; } + virtual bool matches(const IHopDirective &) const { return true; } + virtual string toString() const; + virtual string toDebugString() const; +}; + +} // mbus + diff --git a/messagebus/src/vespa/messagebus/routing/resender.cpp b/messagebus/src/vespa/messagebus/routing/resender.cpp new file mode 100644 index 00000000000..b767252f39f --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/resender.cpp @@ -0,0 +1,99 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".resender"); + +#include <vespa/messagebus/error.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/tracelevel.h> +#include <vespa/vespalib/util/vstringfmt.h> +#include "resender.h" +#include "routingnode.h" + +namespace mbus { + +Resender::Resender(IRetryPolicy::SP retryPolicy) : + _queue(), + _retryPolicy(retryPolicy), + _time() +{ + _time.SetNow(); +} + +Resender::~Resender() +{ + while (!_queue.empty()) { + _queue.top().second->discard(); + _queue.pop(); + } +} + +void +Resender::resendScheduled() +{ + typedef std::vector<RoutingNode*> NodeList; + NodeList sendList; + + double now = _time.MilliSecsToNow(); + while (!_queue.empty() && _queue.top().first <= now) { + sendList.push_back(_queue.top().second); + _queue.pop(); + } + + for (NodeList::iterator it = sendList.begin(); + it != sendList.end(); ++it) + { + (*it)->getTrace().trace(mbus::TraceLevel::COMPONENT, + "Resender resending message."); + (*it)->send(); + } +} + +bool +Resender::canRetry(uint32_t errorCode) const +{ + return _retryPolicy->canRetry(errorCode); +} + +bool +Resender::shouldRetry(const Reply &reply) const +{ + uint32_t numErrors = reply.getNumErrors(); + if (numErrors == 0) { + return false; + } + for (uint32_t i = 0; i < numErrors; ++i) { + if (!_retryPolicy->canRetry(reply.getError(i).getCode())) { + return false; + } + } + return true; +} + +bool +Resender::scheduleRetry(RoutingNode &node) +{ + Message &msg = node.getMessage(); + if (!msg.getRetryEnabled()) { + return false; + } + uint32_t retry = msg.getRetry() + 1; + double delay = node.getReplyRef().getRetryDelay(); + if (delay < 0) { + delay = _retryPolicy->getRetryDelay(retry); + } + if (msg.getTimeRemainingNow() * 0.001 - delay <= 0) { + node.addError(ErrorCode::TIMEOUT, "Timeout exceeded by resender, giving up."); + return false; + } + node.prepareForRetry(); // consumes the reply + node.getTrace().trace( + TraceLevel::COMPONENT, + vespalib::make_vespa_string("Message scheduled for retry %u in %.2f seconds.", + retry, delay)); + msg.setRetry(retry); + _queue.push(Entry((uint64_t)(_time.MilliSecsToNow() + delay * 1000), &node)); + return true; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/routing/resender.h b/messagebus/src/vespa/messagebus/routing/resender.h new file mode 100644 index 00000000000..43f195dae09 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/resender.h @@ -0,0 +1,92 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <boost/utility.hpp> +#include <vespa/messagebus/queue.h> +#include <vespa/messagebus/reply.h> +#include <queue> +#include <vector> +#include <vespa/vespalib/util/sync.h> +#include "iretrypolicy.h" + +namespace mbus { + +class RoutingNode; + +/** + * The resender handles scheduling and execution of sending instances of {@link + * RoutingNode}. An instance of this class is owned by {@link + * com.yahoo.messagebus.MessageBus}. Because this class does not have any + * internal thread, it depends on message bus to keep polling it whenever it has + * time. + */ +class Resender : public boost::noncopyable +{ +private: + typedef std::pair<uint64_t, RoutingNode*> Entry; + struct Cmp { + bool operator()(const Entry &a, const Entry &b) { + return (b.first < a.first); + } + }; + typedef std::priority_queue<Entry, std::vector<Entry>, Cmp> PriorityQueue; + + PriorityQueue _queue; + IRetryPolicy::SP _retryPolicy; + FastOS_Time _time; + +public: + /** + * Convenience typedefs. + */ + typedef std::unique_ptr<Resender> UP; + + /** + * Constructs a new resender. + * + * @param retryPolicy The retry policy to use. + */ + Resender(IRetryPolicy::SP retryPolicy); + + /** + * Empties the retry queue. + */ + ~Resender(); + + /** + * Returns whether or not the current {@link RetryPolicy} supports resending + * a {@link Reply} that contains an error with the given error code. + * + * @param errorCode The code to check. + * @return True if the message can be resent. + */ + bool canRetry(uint32_t errorCode) const; + + /** + * Returns whether or not the given reply should be retried. + * + * @param reply The reply to check. + * @return True if retry is required. + */ + bool shouldRetry(const Reply &reply) const; + + /** + * Schedules the given node for resending, if enabled by message. This will + * invoke {@link RoutingNode#prepareForRetry()} if the node was queued. This + * method is NOT thread-safe, and should only be called by the messenger + * thread. + * + * @param node The node to resend. + * @return True if the node was queued. + */ + bool scheduleRetry(RoutingNode &node); + + /** + * Invokes {@link RoutingNode#send()} on all routing nodes that are + * applicable for sending at the current time. + */ + void resendScheduled(); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routing/retrytransienterrorspolicy.cpp b/messagebus/src/vespa/messagebus/routing/retrytransienterrorspolicy.cpp new file mode 100644 index 00000000000..d38a22c5893 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/retrytransienterrorspolicy.cpp @@ -0,0 +1,42 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/messagebus/errorcode.h> +#include "retrytransienterrorspolicy.h" + +namespace mbus { + +RetryTransientErrorsPolicy::RetryTransientErrorsPolicy() : + _enabled(true), + _baseDelay(1) +{ + // empty +} + +RetryTransientErrorsPolicy & +RetryTransientErrorsPolicy::setEnabled(bool enabled) +{ + _enabled = enabled; + return *this; +} + +RetryTransientErrorsPolicy & +RetryTransientErrorsPolicy::setBaseDelay(double baseDelay) +{ + _baseDelay = baseDelay; + return *this; +} + +bool +RetryTransientErrorsPolicy::canRetry(uint32_t errorCode) const +{ + return _enabled && errorCode < ErrorCode::FATAL_ERROR; +} + +double +RetryTransientErrorsPolicy::getRetryDelay(uint32_t retry) const +{ + return _baseDelay * retry; +} + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routing/retrytransienterrorspolicy.h b/messagebus/src/vespa/messagebus/routing/retrytransienterrorspolicy.h new file mode 100644 index 00000000000..98efe3de08c --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/retrytransienterrorspolicy.h @@ -0,0 +1,53 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "iretrypolicy.h" + +namespace mbus { + +/** + * Implements a retry policy that allows resending of any error that is not fatal. It also does progressive + * back-off, delaying each attempt by the given time multiplied by the retry attempt. + */ +class RetryTransientErrorsPolicy : public IRetryPolicy { +private: + volatile bool _enabled; + volatile double _baseDelay; + +public: + /** + * Convenience typedefs. + */ + typedef std::unique_ptr<RetryTransientErrorsPolicy> UP; + typedef std::shared_ptr<RetryTransientErrorsPolicy> SP; + + /** + * Constructs a new instance of this policy. By default retries are enabled with a 1.0 second base delay. + */ + RetryTransientErrorsPolicy(); + + /** + * Sets whether or not this policy should allow retries or not. + * + * @param enabled True to allow retries. + * @return This, to allow chaining. + */ + RetryTransientErrorsPolicy &setEnabled(bool enabled); + + /** + * Sets the base delay in seconds to wait between retries. This amount is multiplied by the retry number. + * + * @param baseDelay The time in seconds. + * @return This, to allow chaining. + */ + RetryTransientErrorsPolicy &setBaseDelay(double baseDelay); + + // Implements IRetryPolicy. + bool canRetry(uint32_t errorCode) const; + + // Implements IRetryPolicy. + double getRetryDelay(uint32_t retry) const; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routing/route.cpp b/messagebus/src/vespa/messagebus/routing/route.cpp new file mode 100644 index 00000000000..593a2a3f0ef --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/route.cpp @@ -0,0 +1,83 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".route"); + +#include "route.h" +#include "routeparser.h" + +namespace mbus { + +Route::Route() : + _hops() +{ + // empty +} + +Route::Route(const std::vector<Hop> &lst) : + _hops(lst) +{ + // empty +} + +Route & +Route::addHop(const Hop &hop) +{ + _hops.push_back(hop); + return *this; +} + +Route & +Route::setHop(uint32_t i, const Hop &hop) +{ + _hops[i] = hop; + return *this; +} + +Hop +Route::removeHop(uint32_t i) +{ + Hop ret = _hops[i]; + _hops.erase(_hops.begin() + i); + return ret; +} + +Route & +Route::clearHops() +{ + _hops.clear(); + return *this; +} + +string +Route::toString() const { + string ret = ""; + for (uint32_t i = 0; i < _hops.size(); ++i) { + ret.append(_hops[i].toString()); + if (i < _hops.size() - 1) { + ret.append(" "); + } + } + return ret; +} + +string +Route::toDebugString() const { + string ret = "Route(hops = { "; + for (uint32_t i = 0; i < _hops.size(); ++i) { + ret.append(_hops[i].toDebugString()); + if (i < _hops.size() - 1) { + ret.append(", "); + } + } + ret.append(" })"); + return ret; +} + +Route +Route::parse(const string &route) +{ + return RouteParser::createRoute(route); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/routing/route.h b/messagebus/src/vespa/messagebus/routing/route.h new file mode 100644 index 00000000000..d027f28302c --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/route.h @@ -0,0 +1,128 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vector> +#include "hop.h" + +namespace mbus { + +class Hop; + +/** + * A route is a list of {@link Hop hops} that are resolved from first to last as a routable moves from source to + * destination. A route may be changed at any time be either application logic or an invoked {@link RoutingPolicy}, so + * no guarantees on actual path can be given without the full knowledge of all that logic. + * + * To construct a route you may either use the factory method {@link this#parse(String)} to produce a route instance + * from a string representation, or you may build one programatically through the hop accessors. + */ +class Route { +private: + std::vector<Hop> _hops; + +public: + /** + * Convenience typedef for an auto-pointer to a route. + */ + typedef std::unique_ptr<Route> UP; + + /** + * Parses the given string as a list of space-separated hops. The {@link this#toString()} method is compatible with + * this parser. + * + * @param route The string to parse. + * @return A route that corresponds to the string. + */ + static Route parse(const string &route); + + /** + * Create a Route that contains no hops + */ + Route(); + + /** + * Constructs a route that contains the given hops. + * + * @param hops The hops to instantiate with. + */ + Route(const std::vector<Hop> &hops); + + /** + * Returns whether or not there are any hops in this route. + * + * @return True if there is at least one hop. + */ + bool hasHops() const { return !_hops.empty(); } + + /** + * Returns the number of hops that make up this route. + * + * @return The number of hops. + */ + uint32_t getNumHops() const { return _hops.size(); } + + /** + * Returns the hop at the given index. + * + * @param i The index of the hop to return. + * @return The hop. + */ + Hop &getHop(uint32_t i) { return _hops[i]; } + + /** + * Returns a const reference to the hop at the given index. + * + * @param i The index of the hop to return. + * @return The hop. + */ + const Hop &getHop(uint32_t i) const { return _hops[i]; } + + /** + * Adds a hop to the list of hops that make up this route. + * + * @param hop The hop to add. + * @return This, to allow chaining. + */ + Route &addHop(const Hop &hop); + + /** + * Sets the hop at a given index. + * + * @param i The index at which to set the hop. + * @param hop The hop to set. + * @return This, to allow chaining. + */ + Route &setHop(uint32_t i, const Hop &hop); + + /** + * Removes the hop at a given index. + * + * @param i The index of the hop to remove. + * @return The hop removed. + */ + Hop removeHop(uint32_t i); + + /** + * Clears the list of hops that make up this route. + * + * @return This, to allow chaining. + */ + Route &clearHops(); + + /** + * Returns a string representation of this route. + * + * @return A string representation. + */ + string toString() const; + + /** + * Returns a string representation of this that can be debugged but not parsed. + * + * @return The debug string. + */ + string toDebugString() const; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routing/routedirective.cpp b/messagebus/src/vespa/messagebus/routing/routedirective.cpp new file mode 100644 index 00000000000..b0389904a9a --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/routedirective.cpp @@ -0,0 +1,36 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/vespalib/util/vstringfmt.h> +#include "routedirective.h" + +namespace mbus { + +RouteDirective::RouteDirective(const vespalib::stringref & name) : + _name(name) +{ + // empty +} + +bool +RouteDirective::matches(const IHopDirective &dir) const +{ + if (dir.getType() != TYPE_ROUTE) { + return false; + } + return _name == static_cast<const RouteDirective&>(dir).getName(); +} + +string +RouteDirective::toString() const +{ + return vespalib::make_vespa_string("route:%s", _name.c_str()); +} + +string +RouteDirective::toDebugString() const +{ + return vespalib::make_vespa_string("RouteDirective(name = '%s')", + _name.c_str()); +} + +} // mbus diff --git a/messagebus/src/vespa/messagebus/routing/routedirective.h b/messagebus/src/vespa/messagebus/routing/routedirective.h new file mode 100644 index 00000000000..3fe50e2efd9 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/routedirective.h @@ -0,0 +1,42 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "ihopdirective.h" + +namespace mbus { + +/** + * This class represents a route directive within a {@link Hop}'s selector. This will be replaced by the named route + * when evaluated. If the route is not present in the running protocol's routing table, routing will fail. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @version $Id$ + */ +class RouteDirective : public IHopDirective { +private: + string _name; + +public: + + /** + * Constructs a new directive to insert a route. + * + * @param name The name of the route to insert. + */ + RouteDirective(const vespalib::stringref &name); + + /** + * Returns the name of the route to insert. + * + * @return The name name. + */ + const string &getName() const { return _name; } + + virtual Type getType() const { return TYPE_ROUTE; } + virtual bool matches(const IHopDirective &dir) const; + virtual string toString() const; + virtual string toDebugString() const; +}; + +} // mbus + diff --git a/messagebus/src/vespa/messagebus/routing/routeparser.cpp b/messagebus/src/vespa/messagebus/routing/routeparser.cpp new file mode 100644 index 00000000000..c1c0dba871e --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/routeparser.cpp @@ -0,0 +1,153 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".routeparser"); + +#include <vespa/messagebus/common.h> +#include <stdio.h> +#include <vespa/vespalib/util/stringfmt.h> +#include <vespa/vespalib/util/vstringfmt.h> +#include "errordirective.h" +#include "policydirective.h" +#include "routedirective.h" +#include "routeparser.h" +#include "tcpdirective.h" +#include "verbatimdirective.h" + +using vespalib::stringref; + +namespace mbus { + +bool +RouteParser::isWhitespace(char c) +{ + return c == ' ' || c == '\f' || c == '\n' || c == '\r' || c == '\t'; +} + +IHopDirective::SP +RouteParser::createRouteDirective(const stringref &str) +{ + return IHopDirective::SP(new RouteDirective(str)); +} + +IHopDirective::SP +RouteParser::createTcpDirective(const stringref &str) +{ + size_t posP = str.find(":"); + if (posP == string::npos || posP == 0) { + return IHopDirective::SP(); // no host + } + size_t posS = str.find("/", posP); + if (posS == string::npos || posS == posP + 1) { + return IHopDirective::SP(); // no port + } + return IHopDirective::SP(new TcpDirective(str.substr(0, posP), + atoi(str.substr(posP + 1, posS - 1).c_str()), + str.substr(posS + 1))); +} + +IHopDirective::SP +RouteParser::createPolicyDirective(const stringref &str) +{ + size_t pos = str.find(":"); + if (pos == string::npos) { + return IHopDirective::SP(new PolicyDirective(str, "")); + } + return IHopDirective::SP(new PolicyDirective(str.substr(0, pos), str.substr(pos + 1))); +} + +IHopDirective::SP +RouteParser::createVerbatimDirective(const stringref &str) +{ + return IHopDirective::SP(new VerbatimDirective(str)); +} + +IHopDirective::SP +RouteParser::createErrorDirective(const stringref &str) +{ + return IHopDirective::SP(new ErrorDirective(str)); +} + +IHopDirective::SP +RouteParser::createDirective(const stringref &str) +{ + if (str.size() > 2 && str[0] == '[') { + return createPolicyDirective(str.substr(1, str.size() - 2)); + } + return createVerbatimDirective(str); +} + +Hop +RouteParser::createHop(const stringref &str) +{ + if (str.empty()) { + return Hop().addDirective(createErrorDirective("Failed to parse empty string.")); + } + size_t len = str.size(); + if (len > 1 && str[0] == '?') { + Hop hop = createHop(str.substr(1)); + hop.setIgnoreResult(true); + return hop; + } + if (len > 4 && str.substr(0, 4) == "tcp/") { + IHopDirective::SP tcp = createTcpDirective(str.substr(4)); + if (tcp.get() != NULL) { + return Hop().addDirective(tcp); + } + } + if (len > 6 && str.substr(0, 6) == "route:") { + return Hop().addDirective(createRouteDirective(str.substr(6))); + } + Hop ret; + for (size_t from = 0, at = 0, depth = 0; at <= len; ++at) { + if (at == len || (depth == 0 && str[at] == '/')) { + if (depth > 0) { + return Hop().addDirective(createErrorDirective( + "Unexpected token '': syntax error")); + } + ret.addDirective(createDirective(str.substr(from, at - from))); + from = at + 1; + } else if (isWhitespace(str[at]) && depth == 0) { + return Hop().addDirective(createErrorDirective( + vespalib::make_string( + "Failed to completely parse '%s'.", + str.c_str()))); + } else if (str[at] == '[') { + ++depth; + } else if (str[at] == ']') { + if (depth == 0) { + return Hop().addDirective(createErrorDirective( + "Unexpected token ']': syntax error")); + } + --depth; + } + } + return ret; +} + +Route +RouteParser::createRoute(const stringref &str) +{ + Route ret; + for (size_t from = 0, at = 0, depth = 0; at <= str.size(); ++at) { + if (at == str.size() || (depth == 0 && isWhitespace(str[at]))) { + if (from < at - 1) { + Hop hop = createHop(str.substr(from, at - from)); + if (hop.hasDirectives() && + hop.getDirective(0)->getType() == IHopDirective::TYPE_ERROR) + { + return Route().addHop(hop); + } + ret.addHop(hop); + } + from = at + 1; + } else if (str[at] == '[') { + ++depth; + } else if (str[at] == ']') { + --depth; + } + } + return ret; +} + +} // mbus diff --git a/messagebus/src/vespa/messagebus/routing/routeparser.h b/messagebus/src/vespa/messagebus/routing/routeparser.h new file mode 100644 index 00000000000..4ccd59cbdc4 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/routeparser.h @@ -0,0 +1,44 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vespa/vespalib/util/sync.h> +#include "hop.h" +#include "route.h" + +namespace mbus { + +/** + * This is a convenient entry point into creating a route or hop object. You can + * either build a routing object by hand, but using the factory method {@link + * #create} with a string is far simpler. + */ +class RouteParser { +private: + static bool isWhitespace(char c); + static IHopDirective::SP createDirective(const vespalib::stringref &str); + static IHopDirective::SP createErrorDirective(const vespalib::stringref &str); + static IHopDirective::SP createPolicyDirective(const vespalib::stringref &str); + static IHopDirective::SP createRouteDirective(const vespalib::stringref &str); + static IHopDirective::SP createTcpDirective(const vespalib::stringref &str); + static IHopDirective::SP createVerbatimDirective(const vespalib::stringref &str); + +public: + /** + * Creates a hop from a string representation. + * + * @param str The string to parse as a hop. + * @return The created hop. + */ + static Hop createHop(const vespalib::stringref &str); + + /** + * Creates a route from a string representation. + * + * @param str The string to parse as a route. + * @return The created route. + */ + static Route createRoute(const vespalib::stringref &str); +}; + +} // mbus + diff --git a/messagebus/src/vespa/messagebus/routing/routespec.cpp b/messagebus/src/vespa/messagebus/routing/routespec.cpp new file mode 100644 index 00000000000..848b553bf98 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/routespec.cpp @@ -0,0 +1,72 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".routespec"); + +#include <vespa/vespalib/util/vstringfmt.h> +#include "routingspec.h" + +namespace mbus { + +RouteSpec::RouteSpec(const string &name) : + _name(name), + _hops() +{ + // empty +} + +RouteSpec & +RouteSpec::addHops(const std::vector<string> &hops) +{ + _hops.insert(_hops.end(), hops.begin(), hops.end()); + return *this; +} + +string +RouteSpec::removeHop(uint32_t i) +{ + string ret = _hops[i]; + _hops.erase(_hops.begin() + i); + return ret; +} + +void +RouteSpec::toConfig(string &cfg, const string &prefix) const +{ + cfg.append(prefix).append("name ").append(RoutingSpec::toConfigString(_name)).append("\n"); + uint32_t numHops = _hops.size(); + if (numHops > 0) { + cfg.append(prefix).append("hop[").append(vespalib::make_vespa_string("%d", numHops)).append("]\n"); + for (uint32_t i = 0; i < numHops; ++i) { + cfg.append(prefix).append("hop[").append(vespalib::make_vespa_string("%d", i)).append("] "); + cfg.append(RoutingSpec::toConfigString(_hops[i])).append("\n"); + } + } +} + +string +RouteSpec::toString() const +{ + string ret = ""; + toConfig(ret, ""); + return ret; +} + +bool +RouteSpec::operator==(const RouteSpec &rhs) const +{ + if (_name != rhs._name) { + return false; + } + if (_hops.size() != rhs._hops.size()) { + return false; + } + for (uint32_t i = 0, len = _hops.size(); i < len; ++i) { + if (_hops[i] != rhs._hops[i]) { + return false; + } + } + return true; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/routing/routespec.h b/messagebus/src/vespa/messagebus/routing/routespec.h new file mode 100644 index 00000000000..1eaefb11958 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/routespec.h @@ -0,0 +1,134 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <string> +#include <vector> + +namespace mbus { + +/** + * Along with the {@link RoutingSpec}, {@link RoutingTableSpec} and {@link HopSpec}, this holds the routing + * specifications for all protocols. The only way a client can configure or alter the settings of a message bus instance + * is through these classes. + * + * This class contains the spec for a single route. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @version $Id$ + */ +class RouteSpec { +private: + string _name; + std::vector<string> _hops; + +public: + /** + * The default constructor assigns a value to the immutable name variable. + * + * @param name A protocol-unique name for this route. + */ + RouteSpec(const string &name); + + /** + * Returns the protocol-unique name of this route. + * + * @return The name. + */ + const string &getName() const { return _name; } + + /** + * Returns the hop name at the given index. + * + * @param i The index of the hop to return. + * @return The hop at the given index. + */ + const string &getHop(uint32_t i) const { return _hops[i]; } + + /** + * Returns whether or not there are any hops in this route. + * + * @return True if there is at least one hop. + */ + bool hasHops() const { return !_hops.empty(); } + + /** + * Returns the number of hops that make up this route. + * + * @return The number of hops. + */ + uint32_t getNumHops() const { return _hops.size(); } + + /** + * Adds the given hop name to this. + * + * @param hop The hop to add. + * @return This, to allow chaining. + */ + RouteSpec &addHop(const string &hop) { _hops.push_back(hop); return *this; } + + /** + * Adds the given hop names to this. + * + * @param hops The hops to add. + * @return This, to allow chaining. + */ + RouteSpec &addHops(const std::vector<string> &hops); + + /** + * Sets the hop name for a given index. + * + * @param i The index of the hop to set. + * @param hop The hop to set. + * @return This, to allow chaining. + */ + RouteSpec &setHop(uint32_t i, const string &hop) { _hops[i] = hop; return *this; } + + /** + * Removes the hop name at the given index. + * + * @param i The index of the hop to remove. + * @return The removed hop. + */ + string removeHop(uint32_t i); + + /** + * Clears the list of hops that make up this route. + * + * @return This, to allow chaining. + */ + RouteSpec &clearHops() { _hops.clear(); return *this; } + + /** + * Appends the content of this to the given config string. + * + * @param cfg The config to add to. + * @param prefix The prefix to use for each add. + */ + void toConfig(string &cfg, const string &prefix) const; + + /** + * Returns a string representation of this route specification. + * + * @return The string. + */ + string toString() const; + + /** + * Implements the equality operator. + * + * @param rhs The object to compare to. + * @return True if this equals the other. + */ + bool operator==(const RouteSpec &rhs) const; + + /** + * Implements the inequality operator. + * + * @param rhs The object to compare to. + * @return True if this does not equals the other. + */ + bool operator!=(const RouteSpec &rhs) const { return !(*this == rhs); } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routing/routingcontext.cpp b/messagebus/src/vespa/messagebus/routing/routingcontext.cpp new file mode 100644 index 00000000000..8e7f2739148 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/routingcontext.cpp @@ -0,0 +1,239 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".routingcontext"); + +#include "route.h" +#include "routingnode.h" + +namespace mbus { + +RoutingContext::RoutingContext(RoutingNode &node, uint32_t directive) : + _node(node), + _directive(directive), + _consumableErrors(), + _selectOnRetry(true), + _context() +{ + // empty +} + +bool +RoutingContext::hasRecipients() const +{ + return !_node.getRecipients().empty(); +} + +uint32_t +RoutingContext::getNumRecipients() const +{ + return _node.getRecipients().size(); +} + +const Route & +RoutingContext::getRecipient(uint32_t idx) const +{ + return _node.getRecipients()[idx]; +} + +const std::vector<Route> & +RoutingContext::getAllRecipients() const +{ + return _node.getRecipients(); +} + +void +RoutingContext::getMatchedRecipients(std::vector<Route> &ret) const +{ + std::set<string> done; + const std::vector<Route> &recipients = _node.getRecipients(); + const Hop &hop = getHop(); + for (std::vector<Route>::const_iterator it = recipients.begin(); + it != recipients.end(); ++it) + { + if (it->hasHops() && hop.matches(it->getHop(0))) { + IHopDirective::SP dir = it->getHop(0).getDirective(_directive); + string key = dir->toString(); + if (done.find(key) == done.end()) { + Route add = *it; + add.setHop(0, hop); + add.getHop(0).setDirective(_directive, dir); + ret.push_back(add); + done.insert(key); + } + } + } +} + +bool +RoutingContext::getSelectOnRetry() const +{ + return _selectOnRetry; +} + +RoutingContext & +RoutingContext::setSelectOnRetry(bool selectOnRetry) +{ + _selectOnRetry = selectOnRetry; + return *this; +} + +const Route & +RoutingContext::getRoute() const +{ + return _node.getRoute(); +} + +const Hop & +RoutingContext::getHop() const +{ + return _node.getRoute().getHop(0); +} + +uint32_t +RoutingContext::getDirectiveIndex() const +{ + return _directive; +} + +const PolicyDirective & +RoutingContext::getDirective() const +{ + return static_cast<const PolicyDirective&>(*getHop().getDirective(_directive)); +} + +string +RoutingContext::getHopPrefix() const +{ + return getHop().getPrefix(_directive); +} + +string +RoutingContext::getHopSuffix() const +{ + return getHop().getSuffix(_directive); +} + +Context & +RoutingContext::getContext() +{ + return _context; +} + +const Context & +RoutingContext::getContext() const +{ + return _context; +} + +RoutingContext & +RoutingContext::setContext(const Context &ctx) +{ + _context = ctx; + return *this; +} + +const Message & +RoutingContext::getMessage() const +{ + return _node.getMessage(); +} + +void +RoutingContext::trace(uint32_t level, const string ¬e) +{ + _node.getTrace().trace(level, note); +} + +bool +RoutingContext::hasReply() const +{ + return _node.hasReply(); +} + +const Reply & +RoutingContext::getReply() const +{ + return _node.getReplyRef(); +} + +RoutingContext & +RoutingContext::setReply(Reply::UP reply) +{ + _node.setReply(std::move(reply)); + return *this; +} + +RoutingContext & +RoutingContext::setError(uint32_t code, const string &msg) +{ + _node.setError(code, msg); + return *this; +} + +RoutingContext & +RoutingContext::setError(const Error &err) +{ + _node.setError(err); + return *this; +} + +MessageBus & +RoutingContext::getMessageBus() +{ + return _node.getMessageBus(); +} + +bool +RoutingContext::hasChildren() const +{ + return !_node.getChildren().empty(); +} + +uint32_t +RoutingContext::getNumChildren() const +{ + return _node.getChildren().size(); +} + +RoutingNodeIterator +RoutingContext::getChildIterator() +{ + return RoutingNodeIterator(_node.getChildren()); +} + +void +RoutingContext::addChild(const Route &route) +{ + _node.addChild(route); +} + +void +RoutingContext::addChildren(const std::vector<Route> &routes) +{ + for (std::vector<Route>::const_iterator it = routes.begin(); + it != routes.end(); ++it) + { + addChild(*it); + } +} + +const slobrok::api::IMirrorAPI & +RoutingContext::getMirror() const +{ + return _node.getNetwork().getMirror(); +} + +void +RoutingContext::addConsumableError(uint32_t errorCode) +{ + _consumableErrors.insert(errorCode); +} + +bool +RoutingContext::isConsumableError(uint32_t errorCode) +{ + return _consumableErrors.find(errorCode) != _consumableErrors.end(); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/routing/routingcontext.h b/messagebus/src/vespa/messagebus/routing/routingcontext.h new file mode 100644 index 00000000000..b9c26dc6224 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/routingcontext.h @@ -0,0 +1,291 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vespa/messagebus/context.h> +#include <vespa/messagebus/messagebus.h> +#include <vespa/slobrok/sbmirror.h> +#include <string> +#include <set> +#include "hop.h" +#include "policydirective.h" +#include "routingnodeiterator.h" + +namespace mbus { + +class RoutingNode; + +/** + * This context object is what is seen by {@link RoutingPolicy} when doing both select() and merge(). It + * contains the necessary accessors to everything a policy is expected to need. An instance of this is created + * for every {@link RoutingNode} that contains a policy. + */ +class RoutingContext { +private: + RoutingNode &_node; + uint32_t _directive; + std::set<uint32_t> _consumableErrors; + bool _selectOnRetry; + Context _context; + +public: + /** + * Convenience typedefs. + */ + typedef std::unique_ptr<RoutingContext> UP; + + /** + * Constructs a new routing context for a given routing node and hop. + * + * @param node The owning routing node. + * @param directive The index to the policy directive of the hop. + */ + RoutingContext(RoutingNode &node, uint32_t directive); + + /** + * Returns whether or not this hop has any configured recipients. + * + * @return True if there is at least one recipient. + */ + bool hasRecipients() const; + + /** + * Returns the number of configured recipients for this hop. + * + * @return The recipient count. + */ + uint32_t getNumRecipients() const; + + /** + * Returns the configured recipient at the given index. + * + * @param idx The index of the recipient to return. + * @return The reipient at the given index. + */ + const Route &getRecipient(uint32_t idx) const; + + /** + * Returns all configured recipients for this hop. + * + * @return An unmodifiable list of recipients. + */ + const std::vector<Route> &getAllRecipients() const; + + /** + * Returns a list of all configured recipients whose first hop matches this. + * + * @param ret The list to add matched recipients to. + */ + void getMatchedRecipients(std::vector<Route> &ret) const; + + /** + * Returns whether or not the policy is required to reselect if resending occurs. + * + * @return True to invoke {@link RoutingPolicy#select(RoutingContext)} on resend. + */ + bool getSelectOnRetry() const; + + /** + * Sets whether or not the policy is required to reselect if resending occurs. + * + * @param selectOnRetry The value to set. + * @return This, to allow chaining. + */ + RoutingContext &setSelectOnRetry(bool selectOnRetry); + + /** + * Returns the route that contains the routing policy that spawned this. + * + * @return The route. + */ + const Route &getRoute() const; + + /** + * Returns the hop that contains the routing policy that spawned this. + * + * @return The hop. + */ + const Hop &getHop() const; + + /** + * Returns the index of the hop directive that spawned this. + * + * @return The directive index. + */ + uint32_t getDirectiveIndex() const; + + /** + * Returns the policy directive that spawned this. + * + * @return The directive object. + */ + const PolicyDirective &getDirective() const; + + /** + * Returns the part of the route string that precedes the active policy directive. This is the same as calling + * {@link this#getHop()}.getPrefix({@link this#getDirectiveIndex()}). + * + * @return The hop prefix. + */ + string getHopPrefix() const; + + /** + * Returns the remainder of the route string immediately following the active policy directive. This is the same as + * calling {@link this#getHop()}.getSuffix({@link this#getDirectiveIndex()}). + * + * @return The hop suffix. + */ + string getHopSuffix() const; + + /** + * Returns the routing specific context object. + * + * @return The context. + */ + Context &getContext(); + + /** + * Returns a const reference to the routing specific context object. + * + * @return The context. + */ + const Context &getContext() const; + + /** + * Sets a routing specific context object that will be available at merge(). + * + * @param context An arbitrary object. + * @return This, to allow chaining. + */ + RoutingContext &setContext(const Context &ctx); + + /** + * Returns the message being routed. + * + * @return The message. + */ + const Message &getMessage() const; + + /** + * Adds a string to the trace of the message being routed. + * + * @param level The level of the trace note. + * @param note The note to add. + */ + void trace(uint32_t level, const string ¬e); + + /** + * Returns whether or not a reply is available. + * + * @return True if a reply is set. + */ + bool hasReply() const; + + /** + * Returns the reply generated by the associated routing policy. + * + * @return The reply. + */ + const Reply &getReply() const; + + /** + * Sets the reply generated by the associated routing policy. + * + * @param reply The reply to set. + * @return This, to allow chaining. + */ + RoutingContext &setReply(Reply::UP reply); + + /** + * This is a convenience method to call {@link #setError(Error)}. + * + * @param code The code of the error to set. + * @param msg The message of the error to set. + * @return This, to allow chaining. + */ + RoutingContext &setError(uint32_t code, const string &msg); + + /** + * This is a convenience method to assign an {@link EmptyReply} containing a single error to this. This also fiddles + * with the trace object so that the error gets written to it. + * + * @param err The error to set. + * @return This, to allow chaining. + * @see #setReply(Reply) + */ + RoutingContext &setError(const Error &err); + + /** + * Returns the message bus instance on which this is running. + * + * @return The message bus. + */ + MessageBus &getMessageBus(); + + /** + * Returns whether or not the owning routing node has any child nodes. + * + * @return True if there is at least one child. + */ + bool hasChildren() const; + + /** + * Returns the number of children the owning routing node has. + * + * @return The child count. + */ + uint32_t getNumChildren() const; + + /** + * Returns an iterator for the child routing nodes. + * + * @return The iterator. + */ + RoutingNodeIterator getChildIterator(); + + /** + * Adds a child routing context to this based on a given route. This is the typical entry point a policy will use to + * select recipients during a {@link RoutingPolicy#select(RoutingContext)} invokation. + * + * @param route The route to contain in the child context. + */ + void addChild(const Route &route); + + /** + * This is a convenience method to more easily add a list of children to this. It will simply call the {@link + * this#addChild} method for each element in the list. + * + * @param routes A list of routes to add as children. + */ + void addChildren(const std::vector<Route> &routes); + + /** + * Returns the local mirror of the system's name server. + * + * @return The mirror api. + */ + const slobrok::api::IMirrorAPI &getMirror() const; + + /** + * Adds the given error code to the list of codes that the associated routing policy <u>may</u> consume. This is + * used to verify whether or not a resolved routing tree can succeed if sent. Because verification is only done + * before sending, the error types that must be added here are only those that can be generated by message bus + * itself. + * + * @param errorCode The code that might be consumed. + * @see RoutingNode#hasUnconsumedErrors() + * @see com.yahoo.messagebus.ErrorCode + */ + void addConsumableError(uint32_t errorCode); + + /** + * Returns whether or not the given error code <u>may</u> be consumed by the associated routing policy. + * + * @param errorCode The code to check. + * @return True if the code may be consumed. + * @see this#addConsumableError(int) + */ + bool isConsumableError(uint32_t errorCode); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routing/routingnode.cpp b/messagebus/src/vespa/messagebus/routing/routingnode.cpp new file mode 100644 index 00000000000..55b96837098 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/routingnode.cpp @@ -0,0 +1,601 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".routingnode"); + +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/tracelevel.h> +#include <stack> +#include <vespa/vespalib/util/atomic.h> +#include <vespa/vespalib/util/vstringfmt.h> +#include "errordirective.h" +#include "policydirective.h" +#include "routedirective.h" +#include "routingnode.h" + +namespace mbus { + +RoutingNode::RoutingNode(MessageBus &mbus, INetwork &net, Resender *resender, + IReplyHandler &replyHandler, Message &msg, + IDiscardHandler *discardHandler) + : _mbus(mbus), + _net(net), + _resender(resender), + _parent(NULL), + _recipients(), + _children(), + _replyHandler(&replyHandler), + _discardHandler(discardHandler), + _trace(msg.getTrace().getLevel()), + _pending(0), + _msg(msg), + _reply(), + _route(msg.getRoute()), + _policy(), + _routingContext(), + _serviceAddress(), + _isActive(true), + _shouldRetry(false) +{ + // empty +} + +RoutingNode::RoutingNode(RoutingNode &parent, const Route &route) + : _mbus(parent._mbus), + _net(parent._net), + _resender(parent._resender), + _parent(&parent), + _recipients(parent._recipients), + _children(), + _replyHandler(NULL), + _discardHandler(NULL), + _trace(parent._trace.getLevel()), + _pending(0), + _msg(parent._msg), + _reply(), + _route(route), + _policy(), + _routingContext(), + _serviceAddress(), + _isActive(true), + _shouldRetry(false) +{ + // empty +} + +RoutingNode::~RoutingNode() +{ + clearChildren(); +} + +void +RoutingNode::clearChildren() +{ + for (std::vector<RoutingNode*>::iterator it = _children.begin(); + it != _children.end(); ++it) + { + delete *it; + } + _children.clear(); +} + +void +RoutingNode::discard() +{ + LOG_ASSERT(_parent == NULL); + if (_discardHandler != NULL) { + _discardHandler->handleDiscard(Context()); + } +} + +void +RoutingNode::send() +{ + if (!resolve(0)) { + notifyAbort("Route resolution failed."); + } else if (hasUnconsumedErrors()) { + notifyAbort("Errors found while resolving route."); + } else { + notifyTransmit(); + } +} + +void +RoutingNode::prepareForRetry() +{ + _shouldRetry = false; + _reply.reset(); + if (_routingContext.get() != NULL && _routingContext->getSelectOnRetry()) { + clearChildren(); + } else if (!_children.empty()) { + bool retryingSome = false; + for (std::vector<RoutingNode*>::iterator it = _children.begin(); + it != _children.end(); ++it) + { + RoutingNode *child= *it; + if (child->_shouldRetry || child->_reply.get() == NULL) { + child->prepareForRetry(); + retryingSome = true; + } + } + if (!retryingSome) { + // Entering here means we have no children that should be resent even + // though this node reports a transient error. The only thing we can + // do is to reselect from this. + clearChildren(); + } + } +} + +void +RoutingNode::notifyParent() +{ + if (_serviceAddress.get() != NULL) { + _net.freeServiceAddress(*this); + } + tryIgnoreResult(); + if (_parent != NULL) { + _parent->notifyMerge(); + return; + } + if (_shouldRetry && _resender->scheduleRetry(*this)) { + return; + } + notifySender(); +} + +void +RoutingNode::addChild(const Route &route) +{ + RoutingNode *child = new RoutingNode(*this, route); + if (shouldIgnoreResult()) { + child->_route.getHop(0).setIgnoreResult(true); + } + _children.push_back(child); +} + +void +RoutingNode::setError(uint32_t code, const string &msg) +{ + setError(Error(code, msg)); +} + +void +RoutingNode::setError(const Error &err) +{ + Reply::UP reply(new EmptyReply()); + reply->getTrace().setLevel(_trace.getLevel()); + reply->addError(err); + setReply(std::move(reply)); +} + +void +RoutingNode::addError(uint32_t code, const string &msg) +{ + addError(Error(code, msg)); +} + +void +RoutingNode::addError(const Error &err) +{ + if (_reply.get() != NULL) { + _reply->getTrace().swap(_trace); + _reply->addError(err); + _reply->getTrace().swap(_trace); + } else { + setError(err); + } +} + +void +RoutingNode::setReply(Reply::UP reply) +{ + if (reply.get() != NULL) { + _shouldRetry = _resender != NULL && _resender->shouldRetry(*reply); + _trace.getRoot().addChild(reply->getTrace().getRoot()); + reply->getTrace().clear(); + } + _reply = std::move(reply); +} + +void +RoutingNode::handleReply(Reply::UP reply) +{ + setReply(std::move(reply)); + notifyParent(); +} + +void +RoutingNode::notifyAbort(const string &msg) +{ + std::stack<RoutingNode*> mystack; + mystack.push(this); + while (!mystack.empty()) { + RoutingNode *node = mystack.top(); + mystack.pop(); + if (!node->_isActive) { + // reply not pending + } else if (node->_reply.get() != NULL) { + node->notifyParent(); + } else if (node->_children.empty()) { + node->setError(ErrorCode::SEND_ABORTED, msg); + node->notifyParent(); + } else { + for (std::vector<RoutingNode*>::iterator it = node->_children.begin(); + it != node->_children.end(); ++it) + { + mystack.push(*it); + } + } + } +} + +void +RoutingNode::notifyTransmit() +{ + std::vector<RoutingNode*> sendTo; + std::stack<RoutingNode*> mystack; + mystack.push(this); + while (!mystack.empty()) { + RoutingNode *node = mystack.top(); + mystack.pop(); + if (node->_isActive) { + if (node->_children.empty()) { + if (node->hasReply()) { + node->notifyParent(); + } else { + LOG_ASSERT(node->_serviceAddress.get() != NULL); + sendTo.push_back(node); + } + } else { + for (std::vector<RoutingNode*>::iterator it = node->_children.begin(); + it != node->_children.end(); ++it) + { + mystack.push(*it); + } + } + } + } + if (!sendTo.empty()) { + _net.send(_msg, sendTo); + } +} + +void +RoutingNode::notifySender() +{ + _reply->getTrace().swap(_trace); + _replyHandler->handleReply(std::move(_reply)); +} + +void +RoutingNode::notifyMerge() +{ + if (vespalib::Atomic::postDec(&_pending) > 1) { + return; + } + + // Merges the trace information from all children into this. This method takes care not to spend cycles + // manipulating the trace in case tracing is disabled. + if (_trace.getLevel() > 0) { + TraceNode tail; + for (std::vector<RoutingNode*>::iterator it = _children.begin(); + it != _children.end(); ++it) + { + TraceNode &root = (*it)->_trace.getRoot(); + tail.addChild(root); + root.clear(); + } + tail.setStrict(false); + _trace.getRoot().addChild(tail); + } + + // Execute the {@link RoutingPolicy#merge(RoutingContext)} method of the current routing policy. If a + // policy fails to produce a reply, this attaches an error reply to this node. + const PolicyDirective &dir = _routingContext->getDirective(); + _trace.trace(TraceLevel::SPLIT_MERGE, vespalib::make_vespa_string( + "Routing policy '%s' merging replies.", + dir.getName().c_str())); + try { + _policy->merge(*_routingContext); + } catch (const std::exception &e) { + setError(ErrorCode::POLICY_ERROR, vespalib::make_vespa_string( + "Policy '%s' threw an exception; %s", + dir.getName().c_str(), e.what())); + } + if (_reply.get() == NULL) { + setError(ErrorCode::APP_FATAL_ERROR, vespalib::make_vespa_string( + "Routing policy '%s' failed to merge replies.", + dir.getName().c_str())); + } + + // Notifies the parent node. + notifyParent(); +} + +bool +RoutingNode::hasUnconsumedErrors() +{ + bool hasError = false; + + std::stack<RoutingNode*> mystack; + mystack.push(this); + while (!mystack.empty()) { + RoutingNode *node = mystack.top(); + mystack.pop(); + if (node->_reply.get() != NULL) { + for (uint32_t i = 0; i < node->_reply->getNumErrors(); ++i) { + int errorCode = node->_reply->getError(i).getCode(); + RoutingNode *it = node; + while (it != NULL) { + if (it->_routingContext.get() != NULL && + it->_routingContext->isConsumableError(errorCode)) + { + errorCode = ErrorCode::NONE; + break; + } + it = it->_parent; + } + if (errorCode != ErrorCode::NONE) { + _shouldRetry = _resender != NULL && _resender->canRetry(errorCode); + if (!_shouldRetry) { + return true; // no need to continue + } + hasError = true; + } + } + } else { + for (std::vector<RoutingNode*>::iterator it = node->_children.begin(); + it != node->_children.end(); ++it) + { + mystack.push(*it); + } + } + } + + return hasError; +} + +bool +RoutingNode::resolve(uint32_t depth) +{ + if (!_route.hasHops()) { + setError(ErrorCode::ILLEGAL_ROUTE, "Route has no hops."); + return false; + } + if (!_children.empty()) { + return resolveChildren(depth + 1); + } + while (lookupHop() || lookupRoute()) { + if (++depth > 64) { + break; + } + } + if (depth > 64) { + setError(ErrorCode::ILLEGAL_ROUTE, "Depth limit exceeded."); + return false; + } + if (findErrorDirective()) { + return false; + } + if (findPolicyDirective()) { + if (executePolicySelect()) { + return resolveChildren(depth + 1); + } + return _reply.get() != NULL; + } + _net.allocServiceAddress(*this); + return _serviceAddress.get() != NULL || _reply.get() != NULL; +} + +bool +RoutingNode::lookupHop() +{ + RoutingTable::SP table = _mbus.getRoutingTable(_msg.getProtocol()); + if (table.get() != NULL) { + string name = _route.getHop(0).getServiceName(); + if (table->hasHop(name)) { + const HopBlueprint *hop = table->getHop(name); + configureFromBlueprint(*hop); + _trace.trace(TraceLevel::SPLIT_MERGE, + vespalib::make_vespa_string("Recognized '%s' as %s.", + name.c_str(), hop->toString().c_str())); + return true; + } + } + return false; +} + +bool +RoutingNode::lookupRoute() +{ + RoutingTable::SP table = _mbus.getRoutingTable(_msg.getProtocol()); + Hop &hop = _route.getHop(0); + if (hop.getDirective(0)->getType() == IHopDirective::TYPE_ROUTE) { + RouteDirective &dir = static_cast<RouteDirective&>(*hop.getDirective(0)); + if (table.get() == NULL || !table->hasRoute(dir.getName())) { + setError(ErrorCode::ILLEGAL_ROUTE, + vespalib::make_vespa_string("Route '%s' does not exist.", + dir.getName().c_str())); + return false; + } + insertRoute(*table->getRoute(dir.getName())); + _trace.trace(TraceLevel::SPLIT_MERGE, + vespalib::make_vespa_string("Route '%s' retrieved by directive; new route is '%s'.", + dir.getName().c_str(), _route.toString().c_str())); + return true; + } + if (table.get() != NULL) { + string name = hop.getServiceName(); + if (table->hasRoute(name)) { + insertRoute(*table->getRoute(name)); + _trace.trace(TraceLevel::SPLIT_MERGE, + vespalib::make_vespa_string("Recognized '%s' as route '%s'.", + name.c_str(), _route.toString().c_str())); + return true; + } + } + return false; +} + +void +RoutingNode::insertRoute(const Route &ins) +{ + Route route = ins; + if (shouldIgnoreResult()) { + route.getHop(0).setIgnoreResult(true); + } + for (uint32_t i = 1; i < _route.getNumHops(); ++i) { + route.addHop(_route.getHop(i)); + } + _route = route; +} + +bool +RoutingNode::findErrorDirective() +{ + Hop &hop = _route.getHop(0); + for (uint32_t i = 0; i < hop.getNumDirectives(); ++i) { + IHopDirective::SP dir = hop.getDirective(i); + if (dir->getType() == IHopDirective::TYPE_ERROR) { + setError(ErrorCode::ILLEGAL_ROUTE, + static_cast<ErrorDirective&>(*dir).getMessage()); + return true; + } + } + return false; +} + +bool +RoutingNode::findPolicyDirective() +{ + Hop &hop = _route.getHop(0); + for (uint32_t i = 0; i < hop.getNumDirectives(); ++i) { + IHopDirective::SP dir = hop.getDirective(i); + if (dir->getType() == IHopDirective::TYPE_POLICY) { + _routingContext.reset(new RoutingContext(*this, i)); + return true; + } + } + return false; +} + +bool +RoutingNode::executePolicySelect() +{ + const PolicyDirective &dir = _routingContext->getDirective(); + _policy = _mbus.getRoutingPolicy(_msg.getProtocol(), dir.getName(), dir.getParam()); + if (_policy.get() == NULL) { + setError(ErrorCode::UNKNOWN_POLICY, vespalib::make_vespa_string( + "Protocol '%s' could not create routing policy " + "'%s' with parameter '%s'.", + _msg.getProtocol().c_str(), dir.getName().c_str(), + dir.getParam().c_str())); + return false; + } + _trace.trace(TraceLevel::SPLIT_MERGE, vespalib::make_vespa_string( + "Running routing policy '%s'.", + dir.getName().c_str())); + try { + _policy->select(*_routingContext); + } catch (const std::exception &e) { + setError(ErrorCode::POLICY_ERROR, vespalib::make_vespa_string( + "Policy '%s' threw an exception; %s", + dir.getName().c_str(), e.what())); + return false; + } + if (_children.empty()) { + if (_reply.get() == NULL) { + setError(ErrorCode::NO_SERVICES_FOR_ROUTE, + vespalib::make_vespa_string( + "Policy '%s' selected no recipients for " + "route '%s'.", + dir.getName().c_str(), _route.toString().c_str())); + } else { + _trace.trace(TraceLevel::SPLIT_MERGE, + vespalib::make_vespa_string( + "Policy '%s' assigned a reply to this branch.", + dir.getName().c_str())); + } + return false; + } + for (std::vector<RoutingNode*>::iterator it = _children.begin(); + it != _children.end(); ++it) + { + RoutingNode *child = *it; + Hop &hop = child->_route.getHop(0); + child->_trace.trace(TraceLevel::SPLIT_MERGE, + vespalib::make_vespa_string("Component '%s' selected by policy '%s'.", + hop.toString().c_str(), dir.getName().c_str())); + } + return true; +} + +bool +RoutingNode::resolveChildren(uint32_t childDepth) +{ + int numActiveChildren = 0; + bool ret = true; + for (std::vector<RoutingNode*>::iterator it = _children.begin(); + it != _children.end(); ++it) + { + RoutingNode *child = *it; + child->_trace.trace(TraceLevel::SPLIT_MERGE, + vespalib::make_vespa_string("Resolving '%s'.", child->_route.toString().c_str())); + child->_isActive = (child->_reply.get() == NULL); + if (child->_isActive) { + ++numActiveChildren; + if (!child->resolve(childDepth)) { + ret = false; + break; + } + } else { + child->_trace.trace(TraceLevel::SPLIT_MERGE, "Already completed."); + } + } + _pending = numActiveChildren; + return ret; +} + +void +RoutingNode::configureFromBlueprint(const HopBlueprint &hop) +{ + bool ignoreResult = shouldIgnoreResult(); + _route.setHop(0, *hop.create()); + if (ignoreResult) { + _route.getHop(0).setIgnoreResult(true); + } + _recipients.clear(); + for (uint32_t r = 0; r < hop.getNumRecipients(); ++r) { + Route recipient; + recipient.addHop(hop.getRecipient(r)); + for (uint32_t h = 1; h < _route.getNumHops(); ++h) { + recipient.addHop(_route.getHop(h)); + } + _recipients.push_back(recipient); + } +} + +bool +RoutingNode::tryIgnoreResult() +{ + if (!shouldIgnoreResult()) { + return false; + } + if (_reply.get() == NULL || !_reply->hasErrors()) { + return false; + } + setReply(Reply::UP(new EmptyReply())); + _trace.trace(TraceLevel::SPLIT_MERGE, "Ignoring errors in reply."); + return true; +} + +bool +RoutingNode::shouldIgnoreResult() +{ + return _route.getNumHops() > 0 && _route.getHop(0).getIgnoreResult(); +} + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routing/routingnode.h b/messagebus/src/vespa/messagebus/routing/routingnode.h new file mode 100644 index 00000000000..92a4b835ba1 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/routingnode.h @@ -0,0 +1,446 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <map> +#include <vespa/messagebus/idiscardhandler.h> +#include <vespa/messagebus/ireplyhandler.h> +#include <vespa/messagebus/message.h> +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/network/inetwork.h> +#include <vespa/messagebus/network/iserviceaddress.h> +#include <vespa/messagebus/reply.h> +#include <string> +#include <vector> +#include <vespa/vespalib/util/sync.h> +#include "iroutingpolicy.h" +#include "resender.h" +#include "route.h" +#include "routingcontext.h" +#include "routingnodeiterator.h" + +namespace mbus { + +/** + * This class represents a node in the routing tree that is created when a route + * is resolved. There will be one node per modification of the route. For every + * {@link RoutingPolicy} there will be an instance of this that has its policy + * and {@link RoutingContext} member set. A policy is oblivious to this class, + * it can only access the context object. + */ +class RoutingNode : public IReplyHandler { +private: + MessageBus &_mbus; + INetwork &_net; + Resender *_resender; + RoutingNode *_parent; + std::vector<Route> _recipients; + std::vector<RoutingNode*> _children; + IReplyHandler *_replyHandler; + IDiscardHandler *_discardHandler; + Trace _trace; + volatile uint32_t _pending; + Message &_msg; + Reply::UP _reply; + Route _route; + IRoutingPolicy::SP _policy; + RoutingContext::UP _routingContext; + IServiceAddress::UP _serviceAddress; + bool _isActive; + bool _shouldRetry; + + /** + * Constructs a new instance of this class. This is the child node + * constructor, and is the constructor used when building the routing tree. + * + * @param parent The parent routing node. + * @param route The route to assign to this. + */ + RoutingNode(RoutingNode &parent, const Route &route); + + /** + * Clears the list of child routing node objects, and frees the memory used + * to allocate them. + */ + void clearChildren(); + + /** + * This method collects all unsent leaf nodes and passes them to {@link + * Network#send(com.yahoo.messagebus.Message, java.util.List)}. This is + * orthogonal to {@link #notifyAbort(String)} in that it ensures that a + * reply will return to sender. + */ + void notifyTransmit(); + + /** + * This method merges the content of all its children, and invokes itself on + * the parent node. If not all children are ready for merg, this method does + * nothing. The rationale for this is that the last child to receive a reply + * will propagate the merge upwards. Once this method reaches the root node, + * the reply is either scheduled for resending or passed to the owning reply + * handler. + */ + void notifyMerge(); + + /** + * Returns whether or not transmitting along this routing tree can possibly + * succeed. This evaluates to false if either a) there are no leaf nodes to + * send to, or b) some leaf node contains a fatal error that is not masked + * by a routing policy above it in the tree. If only transient errors would + * reach this, the resend flag is set to true. + * + * @return True if no error reaches this. + */ + bool hasUnconsumedErrors(); + + /** + * This method performs the necessary selection logic to resolve the next + * step of the current route. There is a hard limit to how deep the routing + * tree may resolve to, and if that depth is ever exceeded, this method + * returns false. This should only really happen if routing has been + * misconfigured. + * + * @param depth The current depth. + * @return False if selection failed. + */ + bool resolve(uint32_t depth); + + /** + * This method checks to see whether the string representation of the + * current hop is actually the name of another. If a hop is found, the + * first hop of the current route is replaced by this. + * + * @return True if a hop was found and added. + */ + bool lookupHop(); + + /** + * This method checks to see whether the current hop contains a {@link + * RouteDirective}, or if its string representation is actually the name of + * a configured route. If a route is found, the first hop of the current + * route is replaced by expanding the named route. If a route directive + * requests a non-existant route, this method creates an error-reply for + * this node. + * + * @return True if a route was found and added. + * @see #insertRoute(Route) + */ + bool lookupRoute(); + + /** + * This method replaces the first hop of the current route with the given + * route. + * + * @param ins The route to insert. + */ + void insertRoute(const Route &ins); + + /** + * This method traverses the current hop looking for an isntance of {@link + * ErrorDirective}. If one is found, this method assigns a corresponding + * error reply to this node. + * + * @return True if an error was found. + */ + bool findErrorDirective(); + + /** + * This method traverses the current hop looking for an instance of {@link + * PolicyDirective}. If one is found, this method creates and assigns a + * routing context to this. + * + * @return True if a policy was found. + */ + bool findPolicyDirective(); + + /** + * Creates the {@link RoutingPolicy} referenced by the current routing + * context, and executes its {@link RoutingPolicy#select(RoutingContext)} + * method. + * + * @return True if at least one child was added. + */ + bool executePolicySelect(); + + /** + * This method invokes the {@link #resolve(int)} method of all the child + * nodes of this. If any of these exceed the depth limit, this method + * returns false. + * + * @param childDepth The depth of the children. + * @return False if depth limit was exceeded. + */ + bool resolveChildren(uint32_t childDepth); + + /** + * Configures this node based on a hop blueprint. For each recipient in the + * blueprint it creates a copy of the current route, and sets the first hop + * of that route to be the configured recipient hop. In effect, this + * replaces the current hop and retains the rest of the route. + * + * @param hop The blueprint to use for configuration. + */ + void configureFromBlueprint(const HopBlueprint &hop); + + /** + * This method mergs this node as ready for merge. If it has a parent + * routing node, its pending member is decremented. If this causes the + * parent's pending count to reach zero, its {@link #notifyMerge()} method + * is invoked. A special flag is used to make sure that failed resending + * avoids notifying parents of previously resolved branches of the tree. + */ + void notifyParent(); + + /** + * If a reply has been set containing an error, and {@link + * #shouldIgnoreResult()} returns <tt>true</tt>, this method replaces that + * reply with one that has no error. + * + * @return Whether or not the reply was replaced. + */ + bool tryIgnoreResult(); + + /** + * Returns whether or not to ignore any errors that may occur on this node + * or any of its children. + * + * @return True to ignore the result. + */ + bool shouldIgnoreResult(); + +public: + /** + * Convenience typedefs. + */ + typedef std::unique_ptr<RoutingNode> UP; + + /** + * Constructs a new instance of this class. This is the root node + * constructor, and will be used by the different sessions for sending + * messages. + * + * @param mbus The message bus on which we are running. + * @param net The network layer we are to transmit through. + * @param resender The resender to schedule with. + * @param replyHandler The handler to receive the final reply. + * @param msg The message being sent. + * @param discardHandler The handler to notify when discarding this. + */ + RoutingNode(MessageBus &mbus, INetwork &net, Resender *resender, + IReplyHandler &replyHandler, Message &msg, + IDiscardHandler *discardHandler = NULL); + + /** + * Destructor. Frees up any allocated resources, namely all child nodes of + * this. + */ + ~RoutingNode(); + + /** + * Discards this routing node. Invoking this will notify the parent {@link + * SendProxy} to ensure that the corresponding message is discarded, and all + * allocated memory is freed. This is a required step to ensure safe + * shutdown if you need to destroy a message bus instance while there are + * still routing nodes alive in your application. + */ + void discard(); + + /** + * This is the single entry-point for sending a message along a route. This + * can only be invoked on the root node of a routing tree. It runs all the + * necessary selection, verification and transmission logic. Once this has + * been called, it guarantees that a reply is returned to the registered + * reply handler. + */ + void send(); + + /** + * This method is used to reset the internal state of routing nodes that + * will be resent. If a routing policy sets {@link + * RoutingContext#setSelectOnResend(boolean)} to true, this method will + * reroute everything from that node onwards. If that flag is not set, + * scheduling recurses into any child that got a reply with only transient + * errors. Finally, if neither this node or none of its children were + * scheduled for resending, force reroute from this. + */ + void prepareForRetry(); + + /** + * This method may only be invoked on a root node, as it passes the current + * reply to the member {@link ReplyHandler}. The actual call to the handler + * is done in a separate thread to avoid deadlocks. + */ + void notifySender(); + + /** + * This method assigns an error reply to all unsent leaf nodes, and invokes + * {@link #notifyParent()} on them. This has the effect of ensuring that a + * reply will return to sender. + * + * @param msg The error message to assign. + */ + void notifyAbort(const string &msg); + + /** + * Adds a child routing node to this based on a route. This is package + * private because client code should only access it through a {@link + * RoutingPolicy} and its {@link RoutingContext#addChild(Route)} method. + * + * @param route The route to store in the child node. + */ + void addChild(const Route &route); + + /** + * This is a convenience method to call {@link #setError(Error)}. + * + * @param code The code of the error to set. + * @param msg The message of the error to set. + */ + void setError(uint32_t code, const string &msg); + + /** + * This is a convenience method to assign an {@link EmptyReply} containing a + * single error to this. This also fiddles with the trace object so that the + * error gets written to it. + * + * @param err The error to set. + * @see #setReply(Reply) + */ + void setError(const Error &err); + + /** + * This is a convenience method to call {@link #addError(Error)}. + * + * @param code The code of the error to add. + * @param msg The message of the error to add. + */ + void addError(uint32_t code, const string &msg); + + /** + * This is a convenience method to add an error to this. If a reply has + * already been set, this method will add the error to it. If no reply is + * set, this method calls {@link #setError(Error)}. This method also fiddles + * with the trace object so that the error gets written to it. + * + * @param err The error to add. + */ + void addError(const Error &err); + + /** + * Returns the message bus being used to send the message. + * + * @return The message bus. + */ + MessageBus &getMessageBus() { return _mbus; } + + /** + * Returns the network being used to send the message. + * + * @return The network layer. + */ + INetwork &getNetwork() { return _net; } + + /** + * Returns the message being routed. You should NEVER modify a message that + * is retrieved from a routing node or context, as the result of doing so is + * undefined. + * + * @return The message being routed. + */ + Message &getMessage() { return _msg; } + + /** + * Returns the trace object for this node. Each node has a separate trace + * object so that merging can be done correctly. + * + * @return The trace object. + */ + Trace &getTrace() { return _trace; } + + /** + * Returns the route object as it exists at this point of the tree. + * + * @return The route at this point. + */ + const Route &getRoute() const { return _route; } + + /** + * Returns whether or not this node contains a reply. + * + * @return True if this node has a reply. + */ + bool hasReply() const { return _reply.get() != NULL; } + + /** + * Returns the reply of this node. + * + * @return The reply assigned to this node. + */ + Reply::UP getReply() { return std::move(_reply); } + + /** + * Returns a reference to the reply of this node. This should never be + * called unless {@link #hasReply()} is true, because you will be deref'ing + * null. + * + * @return The reply assigned to this node. + */ + Reply &getReplyRef() { return *_reply; } + + /** + * Sets the reply of this routing node. This method also updates the + * internal state of this node; it is tagged for resending if the reply has + * only transient errors, and the reply's {@link Trace} is copied. This + * method <u>does not</u> call the parent node's {@link #notifyMerge()}. + * + * @param reply The reply to set. + */ + void setReply(Reply::UP reply); + + /** + * Returns the list of configured recipient {@link Route routes}. This is + * accessed by client code through a more strict api in {@link + * RoutingContext}. + * + * @return The list of recipients. + */ + std::vector<Route> &getRecipients() { return _recipients; } + + /** + * Returns the list of current child nodes. This is accessed by client code + * through a more strict api in {@link RoutingContext}. + * + * @return The list of children. + */ + std::vector<RoutingNode*> &getChildren() { return _children; } + + /** + * Returns whether or not the service address of this node has been set. + * + * @return True if an address is set. + */ + bool hasServiceAddress() { return _serviceAddress.get() != NULL; } + + /** + * Returns the service address of this node. This is attached by the network + * layer, and should only ever be present in leaf nodes. Do not invoke this + * unless {@link #hasServiceAddress()} is true, or you will deref null. + * + * @return The recipient address. + */ + IServiceAddress &getServiceAddress() { return *_serviceAddress; } + + /** + * Sets the service address of this node. This is called by the network + * layer as this calls its {@link Network#resolveRecipient(RoutingNode)} + * method. + * + * @param serviceAddress The recipient address. + */ + void setServiceAddress(IServiceAddress::UP serviceAddress) { _serviceAddress = std::move(serviceAddress); } + + // Inherit doc from IReplyHandler. + virtual void handleReply(Reply::UP reply); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routing/routingnodeiterator.cpp b/messagebus/src/vespa/messagebus/routing/routingnodeiterator.cpp new file mode 100644 index 00000000000..9528a896736 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/routingnodeiterator.cpp @@ -0,0 +1,64 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include "routingnode.h" + +namespace mbus { + +RoutingNodeIterator::RoutingNodeIterator(std::vector<RoutingNode*> &children) : + _pos(children.begin()), + _end(children.end()) +{ + // empty +} + +bool +RoutingNodeIterator::isValid() +{ + return _pos != _end; +} + +RoutingNodeIterator & +RoutingNodeIterator::next() +{ + ++_pos; + return *this; +} + +RoutingNodeIterator & +RoutingNodeIterator::skip(uint32_t num) +{ + for (uint32_t i = 0; i < num && isValid(); ++i) { + next(); + } + return *this; +} + +const Route & +RoutingNodeIterator::getRoute() const +{ + return (*_pos)->getRoute(); +} + +bool +RoutingNodeIterator::hasReply() const +{ + return (*_pos)->hasReply(); +} + +Reply::UP +RoutingNodeIterator::removeReply() +{ + RoutingNode *node = *_pos; + Reply::UP ret = node->getReply(); + ret->getTrace().setLevel(node->getTrace().getLevel()); + ret->getTrace().swap(node->getTrace()); + return ret; +} + +const Reply & +RoutingNodeIterator::getReplyRef() const +{ + return (*_pos)->getReplyRef(); +} + +} // mbus diff --git a/messagebus/src/vespa/messagebus/routing/routingnodeiterator.h b/messagebus/src/vespa/messagebus/routing/routingnodeiterator.h new file mode 100644 index 00000000000..12ab0f63485 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/routingnodeiterator.h @@ -0,0 +1,80 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vespa/messagebus/reply.h> +#include "route.h" + +namespace mbus { + +class RoutingNode; + +/** + * Implements an iterator for the child routing contexts of this. Use {@link + * RoutingContext#getChildIterator()} to retrieve an instance of this. + */ +class RoutingNodeIterator { +private: + std::vector<RoutingNode*>::iterator _pos, _end; + +public: + /** + * Constructs a new iterator based on a given list. + * + * @param children The list to iterate through. + */ + RoutingNodeIterator(std::vector<RoutingNode*> &children); + + /** + * Returns whether or not this iterator is valid. + * + * @return True if we are still pointing to a valid entry. + */ + bool isValid(); + + /** + * Steps to the next child in the map. + * + * @return This, to allow chaining. + */ + RoutingNodeIterator &next(); + + /** + * Skips the given number of children. + * + * @param num The number of children to skip. + * @return This, to allow chaining. + */ + RoutingNodeIterator &skip(uint32_t num); + + /** + * Returns the route of the current child. + * + * @return The route. + */ + const Route &getRoute() const; + + /** + * Returns whether or not a reply is set in the current child. + * + * @return True if a reply is available. + */ + bool hasReply() const; + + /** + * Removes and returns the reply of the current child. This is the correct way of reusing a reply of a + * child node, the {@link #getReplyRef()} should be used when just inspecting a child reply. + * + * @return The reply. + */ + Reply::UP removeReply(); + + /** + * Returns the reply of the current child. + * + * @return The reply. + */ + const Reply &getReplyRef() const; +}; + +} // mbus + diff --git a/messagebus/src/vespa/messagebus/routing/routingspec.cpp b/messagebus/src/vespa/messagebus/routing/routingspec.cpp new file mode 100644 index 00000000000..7f4f605d3ff --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/routingspec.cpp @@ -0,0 +1,81 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".routingspec"); + +#include <vespa/vespalib/util/vstringfmt.h> +#include "routingspec.h" + +namespace mbus { + +RoutingSpec::RoutingSpec() : + _tables() +{ + // empty +} + +RoutingTableSpec +RoutingSpec::removeTable(uint32_t i) +{ + RoutingTableSpec ret = _tables[i]; + _tables.erase(_tables.begin() + i); + return ret; +} + +string +RoutingSpec::toConfigString(const string &input) +{ + string ret; + ret.append("\""); + for (uint32_t i = 0, len = input.size(); i < len; ++i) { + if (input[i] == '\\') { + ret.append("\\\\"); + } else if (input[i] == '"') { + ret.append("\\\""); + } else if (input[i] == '\n') { + ret.append("\\n"); + } else if (input[i] == 0) { + ret.append("\\x00"); + } else { + ret += input[i]; + } + } + ret.append("\""); + return ret; +} + +void +RoutingSpec::toConfig(string &cfg, const string &prefix) const +{ + uint32_t numTables = _tables.size(); + if (numTables > 0) { + cfg.append(prefix).append("routingtable[").append(vespalib::make_vespa_string("%d", numTables)).append("]\n"); + for (uint32_t i = 0; i < numTables; ++i) { + _tables[i].toConfig(cfg, vespalib::make_vespa_string("%sroutingtable[%d].", prefix.c_str(), i)); + } + } +} + +string +RoutingSpec::toString() const +{ + string ret = ""; + toConfig(ret, ""); + return ret; +} + +bool +RoutingSpec::operator==(const RoutingSpec &rhs) const +{ + if (_tables.size() != rhs._tables.size()) { + return false; + } + for (uint32_t i = 0, len = _tables.size(); i < len; ++i) { + if (_tables[i] != rhs._tables[i]) { + return false; + } + } + return true; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/routing/routingspec.h b/messagebus/src/vespa/messagebus/routing/routingspec.h new file mode 100644 index 00000000000..381c0531406 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/routingspec.h @@ -0,0 +1,136 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vector> +#include "routingtablespec.h" + +namespace mbus { + +/** + * Along with the {@link RoutingTableSpec}, {@link RouteSpec} and {@link HopSpec}, this holds the routing specifications + * for all protocols. The only way a client can configure or alter the settings of a message bus instance is through + * these classes. + * + * This class is the root spec class for configuring message bus routing. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @version $Id$ + */ +class RoutingSpec { +private: + std::vector<RoutingTableSpec> _tables; + +public: + /** + * Creates an empty specification. + */ + RoutingSpec(); + + /** + * Returns whether or not there are routing table specs contained in this. + * + * @return True if there is at least one table. + */ + bool hasTables() const { return !_tables.empty(); } + + /** + * Returns the number of routing table specs that are contained in this. + * + * @return The number of routing tables. + */ + uint32_t getNumTables() const { return _tables.size(); } + + /** + * Returns the routing table spec at the given index. + * + * @param i The index of the routing table to return. + * @return The routing table at the given index. + */ + RoutingTableSpec &getTable(uint32_t i) { return _tables[i]; } + + /** + * Returns a const reference to the routing table spec at the given index. + * + * @param i The index of the routing table to return. + * @return The routing table at the given index. + */ + const RoutingTableSpec &getTable(uint32_t i) const { return _tables[i]; } + + /** + * Sets the routing table spec at the given index. + * + * @param i The index at which to set the routing table. + * @param table The routing table to set. + * @return This, to allow chaining. + */ + RoutingSpec &setTable(uint32_t i, const RoutingTableSpec &table) { _tables[i] = table; return *this; } + + /** + * Adds a routing table spec to the list of tables. + * + * @param table The routing table to add. + * @return This, to allow chaining. + */ + RoutingSpec &addTable(const RoutingTableSpec &table) { _tables.push_back(table); return *this; } + + /** + * Returns the routing table spec at the given index. + * + * @param i The index of the routing table to remove. + * @return The removed routing table. + */ + RoutingTableSpec removeTable(uint32_t i); + + /** + * Clears the list of routing table specs contained in this. + * + * @return This, to allow chaining. + */ + RoutingSpec &clearTables() { _tables.clear(); return *this; } + + /** + * Appends the content of this to the given config string. + * + * @param cfg The config to add to. + * @param prefix The prefix to use for each add. + */ + void toConfig(string &cfg, const string &prefix) const; + + /** + * Convert a string value to a quoted value suitable for use in a config string. + * <p/> + * Adds double quotes before and after, and adds backslash-escapes to any double quotes that was contained in the + * string. A null pointer will produce the special unquoted string null that the config library will convert back + * to a null pointer. + * + * @param input the String to be escaped + * @return an escaped String + */ + static string toConfigString(const string &input); + + /** + * Returns a string representation of this. + * + * @return The string. + */ + string toString() const; + + /** + * Implements the equality operator. + * + * @param rhs The object to compare to. + * @return True if this equals the other. + */ + bool operator==(const RoutingSpec &rhs) const; + + /** + * Implements the inequality operator. + * + * @param rhs The object to compare to. + * @return True if this does not equals the other. + */ + bool operator!=(const RoutingSpec &rhs) const { return !(*this == rhs); } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routing/routingtable.cpp b/messagebus/src/vespa/messagebus/routing/routingtable.cpp new file mode 100644 index 00000000000..fa58570d2a5 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/routingtable.cpp @@ -0,0 +1,77 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".routingtable"); +#include <vespa/messagebus/iprotocol.h> +#include <vespa/messagebus/message.h> +#include <vector> +#include <vespa/vespalib/util/vstringfmt.h> +#include "hop.h" +#include "routingtable.h" +#include "routingtablespec.h" + +namespace mbus { + +RoutingTable::HopIterator::HopIterator(const std::map<string, HopBlueprint> &hops) : + _pos(hops.begin()), + _end(hops.end()) +{ + // empty +} + +RoutingTable::RouteIterator::RouteIterator(const std::map<string, Route> &routes) : + _pos(routes.begin()), + _end(routes.end()) +{ + // empty +} + +RoutingTable::RoutingTable(const RoutingTableSpec &spec) : + _name(spec.getProtocol()), + _hops(), + _routes() +{ + for (uint32_t i = 0; i < spec.getNumHops(); ++i) { + const HopSpec& hopSpec = spec.getHop(i); + _hops.insert(std::map<string, HopBlueprint>::value_type(hopSpec.getName(), + HopBlueprint(hopSpec))); + } + for (uint32_t i = 0; i < spec.getNumRoutes(); ++i) { + Route route; + const RouteSpec &routeSpec = spec.getRoute(i); + for (uint32_t j = 0; j < routeSpec.getNumHops(); ++j) { + route.addHop(Hop(routeSpec.getHop(j))); + } + _routes.insert(std::map<string, Route>::value_type(routeSpec.getName(), + route)); + } +} + +bool +RoutingTable::hasHop(const string &name) const +{ + return _hops.find(name) != _hops.end(); +} + +const HopBlueprint * +RoutingTable::getHop(const string &name) const +{ + std::map<string, HopBlueprint>::const_iterator it = _hops.find(name); + return it != _hops.end() ? &(it->second) : NULL; +} + +bool +RoutingTable::hasRoute(const string &name) const +{ + return _routes.find(name) != _routes.end(); +} + +const Route * +RoutingTable::getRoute(const string &name) const +{ + std::map<string, Route>::const_iterator it = _routes.find(name); + return it != _routes.end() ? &(it->second) : NULL; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/routing/routingtable.h b/messagebus/src/vespa/messagebus/routing/routingtable.h new file mode 100644 index 00000000000..84b371ea229 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/routingtable.h @@ -0,0 +1,152 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <boost/utility.hpp> +#include <map> +#include <string> +#include "hopblueprint.h" +#include "route.h" + +namespace mbus { + +class RoutingTableSpec; +class IProtocol; +class INetwork; +class IReplyHandler; +class Message; + +/** + * At any time there may only ever be zero or one routing table registered in message bus for each protocol. This class + * contains a list of named hops and routes that may be used to substitute references to these during route resolving. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @version $Id$ + */ +class RoutingTable : public boost::noncopyable { +private: + string _name; + std::map<string, HopBlueprint> _hops; + std::map<string, Route> _routes; + +public: + /** + * Implements an iterator for the hops contained in this table. + */ + class HopIterator { + private: + std::map<string, HopBlueprint>::const_iterator _pos, _end; + + public: + HopIterator(const std::map<string, HopBlueprint> &hops); + + bool isValid() { return _pos != _end; } + void next() { ++_pos; } + const string &getName() { return _pos->first; } + const HopBlueprint &getHop() { return _pos->second; } + }; + + /** + * Implements an iterator for the routes contained in this table. + */ + class RouteIterator { + private: + std::map<string, Route>::const_iterator _pos, _end; + + public: + RouteIterator(const std::map<string, Route> &hops); + + bool isValid() { return _pos != _end; } + void next() { ++_pos; } + const string &getName() { return _pos->first; } + const Route &getRoute() { return _pos->second; } + }; + + /** + * Convenience typedef for a shared pointer to a RoutingTable object. + */ + typedef std::shared_ptr<RoutingTable> SP; + + /** + * Creates a new routing table based on a given specification. This also verifies the integrity of the table. + * + * @param spec The specification to use. + */ + RoutingTable(const RoutingTableSpec &spec); + + /** + * Returns whether or not there are any hops in this routing table. + * + * @return True if there is at least one hop. + */ + bool hasHops() const { return !_hops.empty(); } + + /** + * Returns the number of hops that are contained in this. + * + * @return The number of hops. + */ + uint32_t getNumHops() const { return _hops.size(); } + + /** + * Returns whether or not there exists a named hop in this. + * + * @param name The name of the hop to look for. + * @return True if the named hop exists. + */ + bool hasHop(const string &name) const; + + /** + * Returns the named hop, may be null. + * + * @param name The name of the hop to return. + * @return The hop implementation object. + */ + const HopBlueprint *getHop(const string &name) const; + + /** + * Returns an iterator for the hops of this. + * + * @return An iterator. + */ + HopIterator getHopIterator() const { return HopIterator(_hops); } + + /** + * Returns whether or not there are any routes in this routing table. + * + * @return True if there is at least one route. + */ + bool hasRoutes() const { return !_routes.empty(); } + + /** + * Returns the number of routes that are contained in this. + * + * @return The number of routes. + */ + uint32_t getNumRoutes() const { return _routes.size(); } + + /** + * Returns whether or not there exists a named route in this. + * + * @param name The name of the route to look for. + * @return True if the named route exists. + */ + bool hasRoute(const string &name) const; + + /** + * Returns the named route, may be null. + * + * @param name The name of the route to return. + * @return The route implementation object. + */ + const Route *getRoute(const string &name) const; + + /** + * Returns an iterator for the routes of this. + * + * @return An iterator. + */ + RouteIterator getRouteIterator() const { return RouteIterator(_routes); } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routing/routingtablespec.cpp b/messagebus/src/vespa/messagebus/routing/routingtablespec.cpp new file mode 100644 index 00000000000..9b3ec6634e2 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/routingtablespec.cpp @@ -0,0 +1,89 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".routingtablespec"); + +#include <vespa/vespalib/util/vstringfmt.h> +#include "routingspec.h" + +namespace mbus { + +RoutingTableSpec::RoutingTableSpec(const string &protocol) : + _protocol(protocol), + _hops(), + _routes() +{ + // empty +} + +HopSpec +RoutingTableSpec::removeHop(uint32_t i) +{ + HopSpec ret = _hops[i]; + _hops.erase(_hops.begin() + i); + return ret; +} + +RouteSpec +RoutingTableSpec::removeRoute(uint32_t i) +{ + RouteSpec ret = _routes[i]; + _routes.erase(_routes.begin() + i); + return ret; +} + +void +RoutingTableSpec::toConfig(string &cfg, const string &prefix) const +{ + cfg.append(prefix).append("protocol ").append(RoutingSpec::toConfigString(_protocol)).append("\n"); + uint32_t numHops = _hops.size(); + if (numHops > 0) { + cfg.append(prefix).append("hop[").append(vespalib::make_vespa_string("%d", numHops)).append("]\n"); + for (uint32_t i = 0; i < numHops; ++i) { + _hops[i].toConfig(cfg, vespalib::make_vespa_string("%shop[%d].", prefix.c_str(), i)); + } + } + uint32_t numRoutes = _routes.size(); + if (numRoutes > 0) { + cfg.append(prefix).append("route[").append(vespalib::make_vespa_string("%d", numRoutes)).append("]\n"); + for (uint32_t i = 0; i < numRoutes; ++i) { + _routes[i].toConfig(cfg, vespalib::make_vespa_string("%sroute[%d].", prefix.c_str(), i)); + } + } +} + +string +RoutingTableSpec::toString() const +{ + string ret = ""; + toConfig(ret, ""); + return ret; +} + +bool +RoutingTableSpec::operator==(const RoutingTableSpec &rhs) const +{ + if (_protocol != rhs._protocol) { + return false; + } + if (_hops.size() != rhs._hops.size()) { + return false; + } + for (uint32_t i = 0, len = _hops.size(); i < len; ++i) { + if (_hops[i] != rhs._hops[i]) { + return false; + } + } + if (_routes.size() != rhs._routes.size()) { + return false; + } + for (uint32_t i = 0, len = _routes.size(); i < len; ++i) { + if (_routes[i] != rhs._routes[i]) { + return false; + } + } + return true; +} + + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/routing/routingtablespec.h b/messagebus/src/vespa/messagebus/routing/routingtablespec.h new file mode 100644 index 00000000000..993031a0adf --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/routingtablespec.h @@ -0,0 +1,192 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <string> +#include <vector> +#include "hopspec.h" +#include "routespec.h" + +namespace mbus { + +/** + * Along with the {@link RoutingSpec}, {@link RouteSpec} and {@link HopSpec}, this holds the routing specifications for + * all protocols. The only way a client can configure or alter the settings of a message bus instance is through these + * classes. + * + * This class contains the spec for a single routing table, which corresponds to exactly one protocol. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @version $Id$ + */ +class RoutingTableSpec { +private: + string _protocol; + std::vector<HopSpec> _hops; + std::vector<RouteSpec> _routes; + +public: + /** + * Creates a new routing table specification for a named protocol. + * + * @param protocol The name of the protocol that this belongs to. + */ + RoutingTableSpec(const string &protocol); + + /** + * Returns the name of the protocol that this is the routing table for. + * + * @return The protocol name. + */ + const string &getProtocol() const { return _protocol; } + + /** + * Returns whether or not there are any hop specs contained in this. + * + * @return True if there is at least one hop. + */ + bool hasHops() const { return !_hops.empty(); } + + /** + * Returns the number of hops that are contained in this table. + * + * @return The number of hops. + */ + uint32_t getNumHops() const { return _hops.size(); } + + /** + * Returns the hop spec at the given index. + * + * @param i The index of the hop to return. + * @return The hop at the given position. + */ + HopSpec &getHop(uint32_t i) { return _hops[i]; } + + /** + * Returns a const reference to the hop spec at the given index. + * + * @param i The index of the hop to return. + * @return The hop at the given position. + */ + const HopSpec &getHop(uint32_t i) const { return _hops[i]; } + + /** + * Adds the given hop spec to this. + * + * @param hop The hop to add. + * @return This, to allow chaining. + */ + RoutingTableSpec &addHop(const HopSpec &hop) { _hops.push_back(hop); return *this; } + + /** + * Sets the hop spec at the given index. + * + * @param i The index at which to set the hop. + * @param hop The hop to set. + * @return This, to allow chaining. + */ + RoutingTableSpec &setHop(uint32_t i, const HopSpec &hop) { _hops[i] = hop; return *this; } + + /** + * Removes the hop spec at the given index. + * + * @param i The index of the hop to remove. + * @return The removed hop. + */ + HopSpec removeHop(uint32_t i); + + /** + * Clears the list of hop specs contained in this. + * + * @return This, to allow chaining. + */ + RoutingTableSpec &clearHops() { _hops.clear(); return *this; } + + /** + * Returns whether or not there are any route specs contained in this. + * + * @return True if there is at least one route. + */ + bool hasRoutes() const { return !_routes.empty(); } + + /** + * Returns the number of route specs contained in this. + * + * @return The number of routes. + */ + uint32_t getNumRoutes() const { return _routes.size(); } + + /** + * Returns the route spec at the given index. + * + * @param i The index of the route to return. + * @return The route at the given index. + */ + RouteSpec &getRoute(uint32_t i) { return _routes[i]; } + + /** + * Returns a const reference to the route spec at the given index. + * + * @param i The index of the route to return. + * @return The route at the given index. + */ + const RouteSpec &getRoute(uint32_t i) const { return _routes[i]; } + + /** + * Adds a route spec to this. + * + * @param route The route to add. + * @return This, to allow chaining. + */ + RoutingTableSpec &addRoute(const RouteSpec &route) { _routes.push_back(route); return *this; } + + /** + * Sets the route spec at the given index. + * + * @param i The index at which to set the route. + * @param route The route to set. + * @return This, to allow chaining. + */ + RoutingTableSpec &setRoute(uint32_t i, const RouteSpec &route) { _routes[i] = route; return *this; } + + /** + * Removes a route spec at a given index. + * + * @param i The index of the route to remove. + * @return The removed route. + */ + RouteSpec removeRoute(uint32_t i); + + /** + * Appends the content of this to the given config string. + * + * @param cfg The config to add to. + * @param prefix The prefix to use for each add. + */ + void toConfig(string &cfg, const string &prefix) const; + + /** + * Returns a string representation of this. + * + * @return The string. + */ + string toString() const; + + /** + * Implements the equality operator. + * + * @param rhs The object to compare to. + * @return True if this equals the other. + */ + bool operator==(const RoutingTableSpec &rhs) const; + + /** + * Implements the inequality operator. + * + * @param rhs The object to compare to. + * @return True if this does not equals the other. + */ + bool operator!=(const RoutingTableSpec &rhs) const { return !(*this == rhs); } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/routing/tcpdirective.cpp b/messagebus/src/vespa/messagebus/routing/tcpdirective.cpp new file mode 100644 index 00000000000..041f089c472 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/tcpdirective.cpp @@ -0,0 +1,43 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/vespalib/stllike/asciistream.h> +#include "tcpdirective.h" + +namespace mbus { + +TcpDirective::TcpDirective(const vespalib::stringref &host, uint32_t port, const vespalib::stringref &session) : + _host(host), + _port(port), + _session(session) +{ + // empty +} + +bool +TcpDirective::matches(const IHopDirective &dir) const +{ + if (dir.getType() != TYPE_TCP) { + return false; + } + const TcpDirective &rhs = static_cast<const TcpDirective&>(dir); + return _host == rhs._host && _port == rhs._port && _session == rhs._session; +} + +string +TcpDirective::toString() const +{ + vespalib::asciistream os; + os << "tcp/" << _host << ':' << _port << '/' << _session; + return os.str(); +} + +string +TcpDirective::toDebugString() const +{ + vespalib::asciistream os; + os << "TcpDirective(host = '" << _host << "', port = " << _port << ", session = '" << _session << "')"; + return os.str(); +} + +} // mbus diff --git a/messagebus/src/vespa/messagebus/routing/tcpdirective.h b/messagebus/src/vespa/messagebus/routing/tcpdirective.h new file mode 100644 index 00000000000..ab8c422c387 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/tcpdirective.h @@ -0,0 +1,60 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <stdint.h> +#include "ihopdirective.h" + +namespace mbus { + +/** + * This class represents a tcp directive within a {@link Hop}'s selector. This is a connection string used to establish + * a direct connection to a host, bypassing service lookups through Slobrok. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @version $Id$ + */ +class TcpDirective : public IHopDirective { +private: + string _host; + uint32_t _port; + string _session; + +public: + /** + * Constructs a new directive to route directly to a tcp address. + * + * @param host The host name to connect to. + * @param port The port to connect to. + * @param session The session to route to. + */ + TcpDirective(const vespalib::stringref &host, uint32_t port, const vespalib::stringref &session); + + /** + * Returns the host to connect to. This may be an ip address or a name. + * + * @return The host. + */ + const string &getHost() const { return _host; } + + /** + * Returns the port to connect to on the remove host. + * + * @return The port number. + */ + uint32_t getPort() const { return _port; } + + /** + * Returns the name of the session to route to. + * + * @return The session name. + */ + const string &getSession() const { return _session; } + + virtual Type getType() const { return TYPE_TCP; } + virtual bool matches(const IHopDirective &dir) const; + virtual string toString() const; + virtual string toDebugString() const; +}; + +} // mbus + diff --git a/messagebus/src/vespa/messagebus/routing/verbatimdirective.cpp b/messagebus/src/vespa/messagebus/routing/verbatimdirective.cpp new file mode 100644 index 00000000000..0fc91209f3c --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/verbatimdirective.cpp @@ -0,0 +1,36 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/vespalib/util/vstringfmt.h> +#include "verbatimdirective.h" + +namespace mbus { + +VerbatimDirective::VerbatimDirective(const vespalib::stringref &image) : + _image(image) +{ + // empty +} + +bool +VerbatimDirective::matches(const IHopDirective &dir) const +{ + if (dir.getType() != TYPE_VERBATIM) { + return false; + } + return _image == static_cast<const VerbatimDirective&>(dir)._image; +} + +string +VerbatimDirective::toString() const +{ + return _image; +} + +string +VerbatimDirective::toDebugString() const +{ + return vespalib::make_vespa_string("VerbatimDirective(image = '%s')", + _image.c_str()); +} + +} // mbus diff --git a/messagebus/src/vespa/messagebus/routing/verbatimdirective.h b/messagebus/src/vespa/messagebus/routing/verbatimdirective.h new file mode 100644 index 00000000000..41ebae497f9 --- /dev/null +++ b/messagebus/src/vespa/messagebus/routing/verbatimdirective.h @@ -0,0 +1,41 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "ihopdirective.h" + +namespace mbus { + +/** + * This class represents a verbatim match within a {@link Hop}'s selector. This is nothing more than a string that will + * be used as-is when performing service name lookups. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @version $Id$ + */ +class VerbatimDirective : public IHopDirective { +private: + string _image; + +public: + /** + * Constructs a new verbatim selector item for a given image. + * + * @param image The image to assign to this. + */ + VerbatimDirective(const vespalib::stringref &image); + + /** + * Returns the image to which this is a verbatim match. + * + * @return The image. + */ + const string &getImage() const { return _image; } + + virtual Type getType() const { return TYPE_VERBATIM; } + virtual bool matches(const IHopDirective &dir) const; + virtual string toString() const; + virtual string toDebugString() const; +}; + +} // mbus + diff --git a/messagebus/src/vespa/messagebus/rpcmessagebus.cpp b/messagebus/src/vespa/messagebus/rpcmessagebus.cpp new file mode 100644 index 00000000000..fc966f896cb --- /dev/null +++ b/messagebus/src/vespa/messagebus/rpcmessagebus.cpp @@ -0,0 +1,38 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".mb"); +#include "rpcmessagebus.h" + +namespace mbus { + +RPCMessageBus::RPCMessageBus(const MessageBusParams &mbusParams, + const RPCNetworkParams &rpcParams, + const config::ConfigUri & routingCfgUri) : + _net(rpcParams), + _bus(_net, mbusParams), + _agent(_bus), + _subscriber(routingCfgUri.getContext()) +{ + _subscriber.subscribe(routingCfgUri.getConfigId(), &_agent); + _subscriber.start(); +} + +RPCMessageBus::RPCMessageBus(const ProtocolSet &protocols, + const RPCNetworkParams &rpcParams, + const config::ConfigUri &routingCfgUri) : + _net(rpcParams), + _bus(_net, protocols), + _agent(_bus), + _subscriber(routingCfgUri.getContext()) +{ + _subscriber.subscribe(routingCfgUri.getConfigId(), &_agent); + _subscriber.start(); +} + +RPCMessageBus::~RPCMessageBus() +{ + _subscriber.close(); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/rpcmessagebus.h b/messagebus/src/vespa/messagebus/rpcmessagebus.h new file mode 100644 index 00000000000..022a5825110 --- /dev/null +++ b/messagebus/src/vespa/messagebus/rpcmessagebus.h @@ -0,0 +1,99 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <boost/utility.hpp> +#include <vespa/messagebus/network/rpcnetwork.h> +#include <string> +#include <vespa/config/helper/legacysubscriber.h> +#include "messagebus.h" +#include "configagent.h" +#include "protocolset.h" + +namespace mbus { + +/** + * The RPCMessageBus class wraps a MessageBus with an RPCNetwork and handles + * reconfiguration. The RPCMessageBus constructor will perform setup, while the + * RPCMessageBus destructor will perform controlled shutdown and cleanup. Please + * note that according to the object delete order, you must delete all sessions + * before deleting the underlying MessageBus object. + */ +class RPCMessageBus : public boost::noncopyable { +private: + RPCNetwork _net; + MessageBus _bus; + ConfigAgent _agent; + config::ConfigFetcher _subscriber; + +public: + /** + * Convenience typedefs. + */ + typedef std::unique_ptr<RPCMessageBus> UP; + typedef std::shared_ptr<RPCMessageBus> SP; + + /** + * Constructs a new instance of this class. + * + * @param mbusParams A complete set of message bus parameters. + * @param rpcParams A complete set of network parameters. + * @param routingCfgId The config id for message bus routing specs. + */ + RPCMessageBus(const MessageBusParams &mbusParams, + const RPCNetworkParams &rpcParams = RPCNetworkParams(), + const config::ConfigUri & routingCfgId = config::ConfigUri("client")); + + + /** + * This constructor requires an array of protocols that it is to support, as + * well as the host application's config identifier. That identifier is + * necessary so that all created sessions can be uniquely identified on the + * network. + * + * @param protocols An array of known protocols. + * @param rpcParams A complete set of network parameters. + * @param routingCfgId The config id for messagebus routing specs. + */ + RPCMessageBus(const ProtocolSet &protocols, + const RPCNetworkParams &rpcParams = RPCNetworkParams(), + const config::ConfigUri & routingCfgId = config::ConfigUri("client")); + + /** + * Destruct. This will destruct the internal MessageBus and RPCNetwork + * objects, thus performing cleanup. Note that all sessions created from the + * internal MessageBus object should be destructed before deleting this + * object. + **/ + ~RPCMessageBus(); + + /** + * Returns a reference to the contained message bus object. + * + * @return The mbus object. + */ + MessageBus &getMessageBus() { return _bus; } + + /** + * Returns a const reference to the contained message bus object. + * + * @return The mbus object. + */ + const MessageBus &getMessageBus() const { return _bus; } + + /** + * Returns a reference to the contained rpc network object. + * + * @return The rpc network. + */ + RPCNetwork &getRPCNetwork() { return _net; } + + /** + * Returns a const reference to the contained rpc network object. + * + * @return The rpc network. + */ + const RPCNetwork &getRPCNetwork() const { return _net; } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/sendproxy.cpp b/messagebus/src/vespa/messagebus/sendproxy.cpp new file mode 100644 index 00000000000..be7b4822d0d --- /dev/null +++ b/messagebus/src/vespa/messagebus/sendproxy.cpp @@ -0,0 +1,72 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".sendproxy"); + +#include "sendproxy.h" + +namespace mbus { + +SendProxy::SendProxy(MessageBus &mbus, INetwork &net, Resender *resender) : + _mbus(mbus), + _net(net), + _resender(resender), + _msg(), + _logTrace(false), + _root() +{ + // empty +} + +void +SendProxy::handleMessage(Message::UP msg) +{ + Trace &trace = msg->getTrace(); + if (trace.getLevel() == 0) { + if (logger.wants(ns_log::Logger::spam)) { + trace.setLevel(9); + _logTrace = true; + } else if (logger.wants(ns_log::Logger::debug)) { + trace.setLevel(6); + _logTrace = true; + } + } + _msg = std::move(msg); + _root.reset(new RoutingNode(_mbus, _net, _resender, *this, *_msg, this)); + _root->send(); +} + +void +SendProxy::handleDiscard(Context ctx) +{ + (void)ctx; + _msg->discard(); + delete this; +} + +void +SendProxy::handleReply(Reply::UP reply) +{ + Trace &trace = _msg->getTrace(); + if (_logTrace) { + if (reply->hasErrors()) { + LOG(debug, "Trace for reply with error(s):\n%s", reply->getTrace().toString().c_str()); + } else if (logger.wants(ns_log::Logger::spam)) { + LOG(spam, "Trace for reply:\n%s", reply->getTrace().toString().c_str()); + } + Trace empty; + trace.swap(empty); + } else if (trace.getLevel() > 0) { + trace.getRoot().addChild(reply->getTrace().getRoot()); + trace.getRoot().normalize(); + } + reply->swapState(*_msg); + reply->setMessage(std::move(_msg)); + + IReplyHandler &handler = reply->getCallStack().pop(*reply); + handler.handleReply(std::move(reply)); + + delete this; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/sendproxy.h b/messagebus/src/vespa/messagebus/sendproxy.h new file mode 100644 index 00000000000..36de3ab8aff --- /dev/null +++ b/messagebus/src/vespa/messagebus/sendproxy.h @@ -0,0 +1,50 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <boost/utility.hpp> +#include <vespa/messagebus/routing/routingtable.h> +#include <vespa/messagebus/routing/routingnode.h> + +namespace mbus { + +class MessageBus; + +/** + * This class owns a message that is being sent by message bus. Once a reply is received, the message is + * attached to it and returned to the application. After the reply has been propagated upwards, this object + * deletes itself. This also implements the discard policy of {@link RoutingNode}. + */ +class SendProxy : public boost::noncopyable, + public IDiscardHandler, + public IMessageHandler, + public IReplyHandler { +private: + MessageBus &_mbus; + INetwork &_net; + Resender *_resender; + Message::UP _msg; + bool _logTrace; + RoutingNode::UP _root; + +public: + /** + * Constructs a new instance of this class to maintain sending of a single message. + * + * @param mbus The message bus that owns this. + * @param net The network layer to transmit through. + * @param resender The resender to use. + */ + SendProxy(MessageBus &mbus, INetwork &net, Resender *resender); + + // Implements IDiscardHandler. + void handleDiscard(Context ctx); + + // Implements IMessageHandler. + void handleMessage(Message::UP msg); + + // Implements IReplyHandler. + void handleReply(Reply::UP reply); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/sequencer.cpp b/messagebus/src/vespa/messagebus/sequencer.cpp new file mode 100644 index 00000000000..eca801d4b2e --- /dev/null +++ b/messagebus/src/vespa/messagebus/sequencer.cpp @@ -0,0 +1,111 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".sequencer"); + +#include <vespa/vespalib/util/vstringfmt.h> +#include "sequencer.h" +#include "tracelevel.h" + +namespace mbus { + +Sequencer::Sequencer(IMessageHandler &sender) : + _lock("mbus::Sequencer::_lock", false), + _sender(sender), + _seqMap() +{ + // empty +} + +Sequencer::~Sequencer() +{ + for (QueueMap::iterator it = _seqMap.begin(); it != _seqMap.end(); ++it) { + MessageQueue *queue = it->second; + if (queue != NULL) { + while (queue->size() > 0) { + Message *msg = queue->front(); + queue->pop(); + msg->discard(); + delete msg; + } + delete queue; + } + } +} + +Message::UP +Sequencer::filter(Message::UP msg) +{ + uint64_t seqId = msg->getSequenceId(); + msg->setContext(Context(seqId)); + { + vespalib::LockGuard guard(_lock); + QueueMap::iterator it = _seqMap.find(seqId); + if (it != _seqMap.end()) { + if (it->second == NULL) { + it->second = new MessageQueue(); + } + msg->getTrace().trace(TraceLevel::COMPONENT, + vespalib::make_vespa_string("Sequencer queued message with sequence id '%" PRIu64 "'.", seqId)); + it->second->push(msg.get()); + msg.release(); + return Message::UP(); + } + _seqMap[seqId] = NULL; // insert empty queue + } + return std::move(msg); +} + +void +Sequencer::sequencedSend(Message::UP msg) +{ + msg->getTrace().trace(TraceLevel::COMPONENT, + vespalib::make_vespa_string("Sequencer sending message with sequence id '%" PRIu64 "'.", + msg->getContext().value.UINT64)); + msg->pushHandler(*this); + _sender.handleMessage(std::move(msg)); +} + +void +Sequencer::handleMessage(Message::UP msg) +{ + if (msg->hasSequenceId()) { + msg = filter(std::move(msg)); + if (msg.get() != NULL) { + sequencedSend(std::move(msg)); + } + } else { + _sender.handleMessage(std::move(msg)); // unsequenced + } +} + +void +Sequencer::handleReply(Reply::UP reply) +{ + uint64_t seq = reply->getContext().value.UINT64; + reply->getTrace().trace(TraceLevel::COMPONENT, + vespalib::make_vespa_string("Sequencer received reply with sequence id '%" PRIu64 "'.", seq)); + Message::UP msg; + { + vespalib::LockGuard guard(_lock); + QueueMap::iterator it = _seqMap.find(seq); + MessageQueue *que = it->second; + LOG_ASSERT(it != _seqMap.end()); + if (que == NULL || que->size() == 0) { + if (que != NULL) { + delete que; + } + _seqMap.erase(it); + } else { + msg.reset(que->front()); + que->pop(); + } + } + if (msg.get() != NULL) { + sequencedSend(std::move(msg)); + } + IReplyHandler &handler = reply->getCallStack().pop(*reply); + handler.handleReply(std::move(reply)); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/sequencer.h b/messagebus/src/vespa/messagebus/sequencer.h new file mode 100644 index 00000000000..aee26c0a9ca --- /dev/null +++ b/messagebus/src/vespa/messagebus/sequencer.h @@ -0,0 +1,89 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <boost/utility.hpp> +#include <map> +#include <vespa/vespalib/util/sync.h> +#include "imessagehandler.h" +#include "ireplyhandler.h" +#include "message.h" +#include "reply.h" +#include "queue.h" + +namespace mbus { + +/** + * A Sequencer ensures correct sequencing of pending messages. When a Sequencer is created, it is given an + * object implementing the IMessageHandler API to use for sending messages. This class is used by the + * SourceSession class and is not intended for external use. + */ +class Sequencer : public boost::noncopyable, + public IMessageHandler, + public IReplyHandler +{ +private: + vespalib::Lock _lock; + IMessageHandler &_sender; + + typedef Queue<Message*> MessageQueue; + typedef std::map<uint64_t, MessageQueue*> QueueMap; + QueueMap _seqMap; + +private: + /** + * Filter a message against the current sequencing state. If the message is returned back out again, it + * has been cleared for sending and its sequencing information has been added to the state. If the message + * is not returned it has been queued for later sending due to sequencing restrictions. This method also + * sets the sequence id as message context. + * + * @param msg The message to filter. + * @return The argument message if it passed the filter. + */ + Message::UP filter(Message::UP msg); + + /** + * Internal method for forwarding a sequenced message to the underlying sender. + * + * @param msg The message to forward. + */ + void sequencedSend(Message::UP msg); + +public: + /** + * Convenience typedef for an auto pointer to a Sequencer object. + */ + typedef std::unique_ptr<Sequencer> UP; + + /** + * Create a new Sequencer using the given sender to send messages. + * + * @param sender The underlying sender. + */ + Sequencer(IMessageHandler &sender); + + /** + * Destruct. This will also destruct any Message objects held back due to sequencing collisions. + */ + virtual ~Sequencer(); + + /** + * All messages pass through this handler when being sent by the owning source session. In case the + * message has no sequencing-id, it is simply passed through to the next handler in the chain. Sequenced + * messages are sent only if there is no queue for their id, otherwise they are queued. + * + * @param msg The message to send. + */ + void handleMessage(Message::UP msg); + + /** + * Lookup the sequencing id of an incoming reply to pop the front of the corresponding queue, and then + * send the next message in line, if any. + * + * @param reply The reply received. + */ + void handleReply(Reply::UP reply); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/sourcesession.cpp b/messagebus/src/vespa/messagebus/sourcesession.cpp new file mode 100644 index 00000000000..a2cea9ddc8a --- /dev/null +++ b/messagebus/src/vespa/messagebus/sourcesession.cpp @@ -0,0 +1,166 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".sourcesession"); + +#include <vespa/messagebus/routing/routingtable.h> +#include <vespa/vespalib/util/vstringfmt.h> +#include "error.h" +#include "errorcode.h" +#include "messagebus.h" +#include "replygate.h" +#include "sourcesession.h" +#include "sourcesessionparams.h" +#include "tracelevel.h" +#include <algorithm> +#include <iostream> + +namespace mbus { + +SourceSession::SourceSession(MessageBus &mbus, const SourceSessionParams ¶ms) + : _monitor("mbus::SourceSession::_monitor", false), + _mbus(mbus), + _gate(new ReplyGate(_mbus)), + _sequencer(*_gate), + _replyHandler(params.getReplyHandler()), + _throttlePolicy(params.getThrottlePolicy()), + _timeout(params.getTimeout()), + _pendingCount(0), + _closed(false), + _done(false) +{ + LOG_ASSERT(params.hasReplyHandler()); +} + +SourceSession::~SourceSession() +{ + // Ensure that no more replies propagate from mbus. + _gate->close(); + _mbus.sync(); + + // Tell gate that we will no longer use it. + _gate->subRef(); +} + +Result +SourceSession::send(Message::UP msg, const string &routeName, bool parseIfNotFound) +{ + bool found = false; + RoutingTable::SP rt = _mbus.getRoutingTable(msg->getProtocol()); + if (rt.get() != NULL) { + const Route *route = rt->getRoute(routeName); + if (route != NULL) { + msg->setRoute(*route); + found = true; + } else if (!parseIfNotFound) { + string str = vespalib::make_vespa_string( + "Route '%s' not found.", + routeName.c_str()); + return Result(Error(ErrorCode::ILLEGAL_ROUTE, str), std::move(msg)); + } + } else if (!parseIfNotFound) { + string str = vespalib::make_vespa_string( + "No routing table available for protocol '%s'.", + msg->getProtocol().c_str()); + return Result(Error(ErrorCode::ILLEGAL_ROUTE, str), std::move(msg)); + } + if (!found) { + msg->setRoute(Route::parse(routeName)); + } + return send(std::move(msg)); +} + +Result +SourceSession::send(Message::UP msg, const Route &route) +{ + msg->setRoute(route); + return send(std::move(msg)); +} + +Result +SourceSession::send(Message::UP msg) +{ + msg->setTimeReceivedNow(); + if (msg->getTimeRemaining() == 0) { + msg->setTimeRemaining((uint64_t)(_timeout * 1000)); + } + { + vespalib::MonitorGuard guard(_monitor); + if (_closed) { + return Result(Error(ErrorCode::SEND_QUEUE_CLOSED, + "Source session is closed."), std::move(msg)); + } + if (_throttlePolicy.get() != NULL && !_throttlePolicy->canSend(*msg, _pendingCount)) { + return Result(Error(ErrorCode::SEND_QUEUE_FULL, + vespalib::make_vespa_string("Too much pending data (%d messages).", + _pendingCount)), std::move(msg)); + } + msg->pushHandler(_replyHandler); + if (_throttlePolicy.get() != NULL) { + _throttlePolicy->processMessage(*msg); + } + ++_pendingCount; + } + if (msg->getTrace().shouldTrace(TraceLevel::COMPONENT)) { + msg->getTrace().trace(TraceLevel::COMPONENT, + vespalib::make_vespa_string("Source session accepted a %d byte message. " + "%d message(s) now pending.", + msg->getApproxSize(), _pendingCount)); + } + msg->pushHandler(*this); + _sequencer.handleMessage(std::move(msg)); + return Result(); +} + +void +SourceSession::handleReply(Reply::UP reply) +{ + bool done; + { + vespalib::MonitorGuard guard(_monitor); + LOG_ASSERT(_pendingCount > 0); + --_pendingCount; + if (_throttlePolicy.get() != NULL) { + _throttlePolicy->processReply(*reply); + } + done = (_closed && _pendingCount == 0); + } + if (reply->getTrace().shouldTrace(TraceLevel::COMPONENT)) { + reply->getTrace().trace(TraceLevel::COMPONENT, + vespalib::make_vespa_string("Source session received reply. " + "%d message(s) now pending.", + _pendingCount)); + } + IReplyHandler &handler = reply->getCallStack().pop(*reply); + handler.handleReply(std::move(reply)); + if (done) { + vespalib::MonitorGuard guard(_monitor); + LOG_ASSERT(_pendingCount == 0); + LOG_ASSERT(_closed); + _done = true; + guard.broadcast(); + } +} + +void +SourceSession::close() +{ + vespalib::MonitorGuard guard(_monitor); + _closed = true; + if (_pendingCount == 0) { + _done = true; + } + while (!_done) { + guard.wait(); + } +} + +SourceSession & +SourceSession::setTimeout(double timeout) +{ + vespalib::MonitorGuard guard(_monitor); + _timeout = timeout; + return *this; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/sourcesession.h b/messagebus/src/vespa/messagebus/sourcesession.h new file mode 100644 index 00000000000..af2c4ff6c92 --- /dev/null +++ b/messagebus/src/vespa/messagebus/sourcesession.h @@ -0,0 +1,128 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <boost/utility.hpp> +#include <vespa/vespalib/util/sync.h> +#include "ireplyhandler.h" +#include "result.h" +#include "sequencer.h" +#include "sourcesessionparams.h" + +namespace mbus { + +class MessageBus; +class ReplyGate; + +/** + * A SourceSession is used to send Message objects along a named or explicitly defined route and get Reply + * objects back. A source session does not have a service name and can only receive replies to the messages + * sent on it. + **/ +class SourceSession : public boost::noncopyable, public IReplyHandler { +private: + friend class MessageBus; + + vespalib::Monitor _monitor; + MessageBus &_mbus; + ReplyGate *_gate; + Sequencer _sequencer; + IReplyHandler &_replyHandler; + IThrottlePolicy::SP _throttlePolicy; + double _timeout; + uint32_t _pendingCount; + bool _closed; + bool _done; + +private: + /** + * This is the private constructor used by mbus to create source sessions. It expects all arguments but + * the {@link SourceSessionParams} to be proper, so no checks are performed. + * + * @param mbus The message bus that created this instance. + * @param params A parameter object that holds configuration parameters. + */ + SourceSession(MessageBus &mbus, const SourceSessionParams ¶ms); + +public: + /** + * Convenience typedef for an auto pointer to a SourceSession object. + **/ + typedef std::unique_ptr<SourceSession> UP; + + /** + * The destructor untangles from messagebus. This is safe, but you will loose the replies of all pending + * messages. After this method returns, messagebus will not invoke any handlers associated with this + * session. + **/ + virtual ~SourceSession(); + + /** + * This is a convenience function to assign a named route to the given message, and then pass it to the + * other {@link #send(Message)} method of this session. If the route could not be found this methods + * returns with an appropriate error, unless the 'parseIfNotFound' argument is true. In that case, the + * route name is passed through to the Route factory method {@link Route#create}. + * + * @param msg The message to send. + * @param routeName The route to assign to the message. + * @param parseIfNotFound Whether or not to parse routeName as a route if it could not be found. + * @return The immediate result of the attempt to send this message. + */ + Result send(Message::UP msg, const string &routeName, bool parseIfNotFound = false); + + /** + * This is a convenience function to assign a given route to the given message, and then pass it to the + * other {@link #send(Message)} method of this session. + * + * @param msg The message to send. + * @param route The route to assign to the message. + * @return The immediate result of the attempt to send this message. + */ + Result send(Message::UP msg, const Route &route); + + /** + * Send a Message along a route that has already been specified in the message object. + * + * @return send result + * @param msg the message to send + */ + Result send(Message::UP msg); + + /** + * Handle a Reply obtained from messagebus. + * + * @param reply the Reply + **/ + void handleReply(Reply::UP reply); + + /** + * Close this session. This method will block until Reply objects have been obtained for all pending + * Message objects. Also, no more Message objects will be accepted by this session after closing has + * initiated. + **/ + void close(); + + /** + * Returns the reply handler of this session. + * + * @return The reply handler. + */ + IReplyHandler &getReplyHandler() { return _replyHandler; } + + /** + * Returns the number of messages sent that have not been replied to yet. + * + * @return The pending count. + */ + uint32_t getPendingCount() const { return _pendingCount; } + + /** + * Sets the number of seconds a message can be attempted sent until it times out. + * + * @param timeout The numer of seconds allowed. + * @return This, to allow chaining. + */ + SourceSession &setTimeout(double timeout); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/sourcesessionparams.cpp b/messagebus/src/vespa/messagebus/sourcesessionparams.cpp new file mode 100644 index 00000000000..ae73b462660 --- /dev/null +++ b/messagebus/src/vespa/messagebus/sourcesessionparams.cpp @@ -0,0 +1,65 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".sourcesessionparams"); + +#include "dynamicthrottlepolicy.h" +#include "sourcesessionparams.h" + +namespace mbus { + +SourceSessionParams::SourceSessionParams() : + _replyHandler(NULL), + _throttlePolicy(new DynamicThrottlePolicy()), + _timeout(180.0) +{ + // empty +} + +IThrottlePolicy::SP +SourceSessionParams::getThrottlePolicy() const +{ + return _throttlePolicy; +} + +SourceSessionParams & +SourceSessionParams::setThrottlePolicy(IThrottlePolicy::SP throttlePolicy) +{ + _throttlePolicy = throttlePolicy; + return *this; +} + +double +SourceSessionParams::getTimeout() const +{ + return _timeout; +} + +SourceSessionParams & +SourceSessionParams::setTimeout(double timeout) +{ + _timeout = timeout; + return *this; +} + +bool +SourceSessionParams::hasReplyHandler() const +{ + return _replyHandler != NULL; +} + +IReplyHandler & +SourceSessionParams::getReplyHandler() const +{ + return *_replyHandler; +} + +SourceSessionParams & +SourceSessionParams::setReplyHandler(IReplyHandler &handler) +{ + _replyHandler = &handler; + return *this; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/sourcesessionparams.h b/messagebus/src/vespa/messagebus/sourcesessionparams.h new file mode 100644 index 00000000000..3c45f68bd10 --- /dev/null +++ b/messagebus/src/vespa/messagebus/sourcesessionparams.h @@ -0,0 +1,83 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "ireplyhandler.h" +#include "ithrottlepolicy.h" + +namespace mbus { + +/** + * To facilitate several configuration parameters to the {@link MessageBus#createSourceSession(ReplyHandler, + * SourceSessionParams)}, all parameters are held by this class. This class has reasonable default values for each + * parameter. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @version $Id$ + */ +class SourceSessionParams { +private: + IReplyHandler *_replyHandler; + IThrottlePolicy::SP _throttlePolicy; + double _timeout; + +public: + /** + * This constructor will set default values for all parameters. + */ + SourceSessionParams(); + + /** + * Returns the policy to use for throttling output. + * + * @return The policy. + */ + IThrottlePolicy::SP getThrottlePolicy() const; + + /** + * Sets the policy to use for throttling output. + * + * @param throttlePolicy The policy to set. + * @return This, to allow chaining. + */ + SourceSessionParams &setThrottlePolicy(IThrottlePolicy::SP throttlePolicy); + + /** + * Returns the total timeout parameter. + * + * @return The total timeout parameter. + */ + double getTimeout() const; + + /** + * Returns the number of seconds a message can spend trying to succeed. + * + * @return The timeout in seconds. + */ + SourceSessionParams &setTimeout(double timeout); + + /** + * Returns whether or not a reply handler has been assigned to this. + * + * @return True if a handler is set. + */ + bool hasReplyHandler() const; + + /** + * Returns the handler to receive incoming replies. If you call this method without first assigning a + * reply handler to this object, you wil de-ref null. + * + * @return The handler. + */ + IReplyHandler &getReplyHandler() const; + + /** + * Sets the handler to receive incoming replies. + * + * @param handler The handler to set. + * @return This, to allow chaining. + */ + SourceSessionParams &setReplyHandler(IReplyHandler &handler); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/staticthrottlepolicy.cpp b/messagebus/src/vespa/messagebus/staticthrottlepolicy.cpp new file mode 100644 index 00000000000..d26362e9072 --- /dev/null +++ b/messagebus/src/vespa/messagebus/staticthrottlepolicy.cpp @@ -0,0 +1,75 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include "staticthrottlepolicy.h" + +namespace mbus { + +StaticThrottlePolicy::StaticThrottlePolicy() : + _maxPendingCount(0), + _maxPendingSize(0), + _pendingSize(0) +{ + // empty +} + +uint32_t +StaticThrottlePolicy::getMaxPendingCount() const +{ + return _maxPendingCount; +} + +StaticThrottlePolicy & +StaticThrottlePolicy::setMaxPendingCount(uint32_t maxCount) +{ + _maxPendingCount = maxCount; + return *this; +} + +uint64_t +StaticThrottlePolicy::getMaxPendingSize() const +{ + return _maxPendingSize; +} + +StaticThrottlePolicy & +StaticThrottlePolicy::setMaxPendingSize(uint64_t maxSize) +{ + _maxPendingSize = maxSize; + return *this; +} + +uint64_t +StaticThrottlePolicy::getPendingSize() const +{ + return _pendingSize; +} + +bool +StaticThrottlePolicy::canSend(const Message &msg, uint32_t pendingCount) +{ + if (_maxPendingCount > 0 && pendingCount >= _maxPendingCount) { + return false; + } + if (_maxPendingSize > 0 && _pendingSize >= _maxPendingSize) { + return false; + } + (void)msg; + return true; +} + +void +StaticThrottlePolicy::processMessage(Message &msg) +{ + uint32_t size = msg.getApproxSize(); + msg.setContext(Context((uint64_t)size)); + _pendingSize += size; +} + +void +StaticThrottlePolicy::processReply(Reply &reply) +{ + uint32_t size = reply.getContext().value.UINT64; + _pendingSize -= size; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/staticthrottlepolicy.h b/messagebus/src/vespa/messagebus/staticthrottlepolicy.h new file mode 100644 index 00000000000..e6d75dfd3c7 --- /dev/null +++ b/messagebus/src/vespa/messagebus/staticthrottlepolicy.h @@ -0,0 +1,84 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "ithrottlepolicy.h" + +namespace mbus { + +/** + * This is an implementatin of the {@link ThrottlePolicy} that offers static limits to the amount of pending + * data a {@link SourceSession} is allowed to have. You may choose to set a limit to the total number of + * pending messages (by way of {@link #setMaxPendingCount(int)}), the total size of pending messages (by way + * of {@link #setMaxPendingSize(long)}), or some combination thereof. + * + * <b>NOTE:</b> By context, "pending" is refering to the number of sent messages that have not been replied to + * yet. + */ +class StaticThrottlePolicy: public IThrottlePolicy { +private: + uint32_t _maxPendingCount; + uint64_t _maxPendingSize; + uint64_t _pendingSize; + +public: + /** + * Convenience typedefs. + */ + typedef std::unique_ptr<StaticThrottlePolicy> UP; + typedef std::shared_ptr<StaticThrottlePolicy> SP; + + /** + * Constructs a new instance of this policy and sets the appropriate default values of member data. + */ + StaticThrottlePolicy(); + + /** + * Returns the maximum number of pending messages allowed. + * + * @return The max limit. + */ + uint32_t getMaxPendingCount() const; + + /** + * Sets the maximum number of pending messages allowed. + * + * @param maxCount The max count. + * @return This, to allow chaining. + */ + StaticThrottlePolicy &setMaxPendingCount(uint32_t maxCount); + + /** + * Returns the maximum total size of pending messages allowed. + * + * @return The max limit. + */ + uint64_t getMaxPendingSize() const; + + /** + * Sets the maximum total size of pending messages allowed. This size is relative to the value returned by + * {@link com.yahoo.messagebus.Message#getApproxSize()}. + * + * @param maxSize The max size. + * @return This, to allow chaining. + */ + StaticThrottlePolicy &setMaxPendingSize(uint64_t maxSize); + + /** + * Returns the total size of pending messages. + * + * @return The size. + */ + uint64_t getPendingSize() const; + + // Implements IThrottlePolicy. + bool canSend(const Message &msg, uint32_t pendingCount); + + // Implements IThrottlePolicy. + void processMessage(Message &msg); + + // Implements IThrottlePolicy. + void processReply(Reply &reply); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/systemtimer.cpp b/messagebus/src/vespa/messagebus/systemtimer.cpp new file mode 100644 index 00000000000..35f8b22f1b8 --- /dev/null +++ b/messagebus/src/vespa/messagebus/systemtimer.cpp @@ -0,0 +1,15 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include "systemtimer.h" + +namespace mbus { + +uint64_t +SystemTimer::getMilliTime() const +{ + FastOS_Time time; + time.SetNow(); + return (uint64_t)time.MilliSecs(); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/systemtimer.h b/messagebus/src/vespa/messagebus/systemtimer.h new file mode 100644 index 00000000000..b03f847718b --- /dev/null +++ b/messagebus/src/vespa/messagebus/systemtimer.h @@ -0,0 +1,20 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "itimer.h" + +namespace mbus { + +/** + * This is the implementation of the {@link Timer} interface that all time-based + * constructs in message bus use by default. The only reason for replacing this + * is for writing unit tests. + */ +class SystemTimer : public ITimer { +public: + // Implements ITimer. + uint64_t getMilliTime() const; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/testlib/.gitignore b/messagebus/src/vespa/messagebus/testlib/.gitignore new file mode 100644 index 00000000000..ae41f308bef --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/.gitignore @@ -0,0 +1,3 @@ +.depend +Makefile +/libmessagebus-test.so.5.1 diff --git a/messagebus/src/vespa/messagebus/testlib/CMakeLists.txt b/messagebus/src/vespa/messagebus/testlib/CMakeLists.txt new file mode 100644 index 00000000000..4c930bfd9aa --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/CMakeLists.txt @@ -0,0 +1,17 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_library(messagebus_messagebus-test + SOURCES + custompolicy.cpp + oosserver.cpp + oosstate.cpp + receptor.cpp + simplemessage.cpp + simpleprotocol.cpp + simplereply.cpp + slobrok.cpp + slobrokstate.cpp + testserver.cpp + INSTALL lib64 + DEPENDS + slobrok_slobrokserver +) diff --git a/messagebus/src/vespa/messagebus/testlib/create-class-cpp.sh b/messagebus/src/vespa/messagebus/testlib/create-class-cpp.sh new file mode 100755 index 00000000000..f7c209427a8 --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/create-class-cpp.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +class=$1 +guard=`echo $class | tr 'a-z' 'A-Z'` +name=`echo $class | tr 'A-Z' 'a-z'` + +cat <<EOF +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/log/log.h> +LOG_SETUP(".$name"); +#include <vespa/fastos/fastos.h> +#include "$name.h" + +namespace mbus { + +$class::$class() +{ +} + +$class::~$class() +{ +} + +} // namespace mbus +EOF diff --git a/messagebus/src/vespa/messagebus/testlib/create-class-h.sh b/messagebus/src/vespa/messagebus/testlib/create-class-h.sh new file mode 100755 index 00000000000..c4df87c4f8a --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/create-class-h.sh @@ -0,0 +1,26 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once +#!/bin/sh + +class=$1 +guard=`echo $class | tr 'a-z' 'A-Z'` + +cat <<EOF +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + + +namespace mbus { + +class $class +{ +private: + $class(const $class &); + $class &operator=(const $class &); +public: + $class(); + virtual ~$class(); +}; + +} // namespace mbus + +EOF diff --git a/messagebus/src/vespa/messagebus/testlib/create-interface.sh b/messagebus/src/vespa/messagebus/testlib/create-interface.sh new file mode 100755 index 00000000000..e6f93b24355 --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/create-interface.sh @@ -0,0 +1,22 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once +#!/bin/sh + +class=$1 +guard=`echo $class | tr 'a-z' 'A-Z'` + +cat <<EOF +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + + +namespace mbus { + +class $class +{ +public: + virtual ~$class() {} +}; + +} // namespace mbus + +EOF diff --git a/messagebus/src/vespa/messagebus/testlib/custompolicy.cpp b/messagebus/src/vespa/messagebus/testlib/custompolicy.cpp new file mode 100644 index 00000000000..4d26732067d --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/custompolicy.cpp @@ -0,0 +1,143 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".custompolicy"); + +#include <boost/tokenizer.hpp> +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/routing/routingcontext.h> +#include <vespa/vespalib/util/vstringfmt.h> +#include "custompolicy.h" +#include "simpleprotocol.h" + +namespace mbus { + +CustomPolicy::CustomPolicy(bool selectOnRetry, + const std::vector<uint32_t> consumableErrors, + const std::vector<Route> &routes) : + _selectOnRetry(selectOnRetry), + _consumableErrors(consumableErrors), + _routes(routes) +{ + // empty +} + +void +CustomPolicy::select(RoutingContext &context) +{ + string str = "Selecting { "; + for (uint32_t i = 0; i < _routes.size(); ++i) { + str.append("'"); + str.append(_routes[i].toString()); + str.append("'"); + if (i < _routes.size() - 1) { + str.append(", "); + } + } + str.append(" }."); + context.trace(1, str); + context.setSelectOnRetry(_selectOnRetry); + for (std::vector<uint32_t>::iterator it = _consumableErrors.begin(); + it != _consumableErrors.end(); ++it) + { + context.addConsumableError(*it); + } + context.addChildren(_routes); +} + +void +CustomPolicy::merge(RoutingContext &context) +{ + Reply::UP ret(new EmptyReply()); + std::vector<string> routes; + for (RoutingNodeIterator it = context.getChildIterator(); + it.isValid(); it.next()) + { + routes.push_back(it.getRoute().toString()); + const Reply &reply = it.getReplyRef(); + for (uint32_t i = 0; i < reply.getNumErrors(); ++i) { + ret->addError(reply.getError(i)); + } + } + context.setReply(std::move(ret)); + string str = "Merged { "; + for (uint32_t i = 0; i < routes.size(); ++i) { + str.append("'"); + str.append(routes[i]); + str.append("'"); + if (i < _routes.size() - 1) { + str.append(", "); + } + } + str.append(" }."); + context.trace(1, str); +} + + +CustomPolicyFactory::CustomPolicyFactory() : + _selectOnRetry(true), + _consumableErrors() +{ + // empty +} + +CustomPolicyFactory::CustomPolicyFactory(bool selectOnRetry) : + _selectOnRetry(selectOnRetry), + _consumableErrors() +{ + // empty +} + +CustomPolicyFactory::CustomPolicyFactory(bool selectOnRetry, uint32_t consumableError) : + _selectOnRetry(selectOnRetry), + _consumableErrors() +{ + _consumableErrors.push_back(consumableError); +} + +CustomPolicyFactory::CustomPolicyFactory(bool selectOnRetry, const std::vector<uint32_t> consumableErrors) : + _selectOnRetry(selectOnRetry), + _consumableErrors(consumableErrors) +{ + // empty +} + +IRoutingPolicy::UP +CustomPolicyFactory::create(const string ¶m) +{ + string str = "{ "; + for (uint32_t i = 0; i < _consumableErrors.size(); ++i) { + str.append(ErrorCode::getName(_consumableErrors[i])); + if (i < _consumableErrors.size() - 1) { + str.append(", "); + } + } + str.append(" }"); + + std::vector<Route> routes; + parseRoutes(param, routes); + + LOG(info, "Creating custom policy; selectOnRetry = %d, consumableErrors = %s, param = '%s'.", + _selectOnRetry, str.c_str(), param.c_str()); + IRoutingPolicy::UP ret(new CustomPolicy(_selectOnRetry, _consumableErrors, routes)); + return ret; +} + + +void +CustomPolicyFactory::parseRoutes(const string &str, + std::vector<Route> &routes) +{ + typedef boost::char_separator<char> Separator; + typedef boost::tokenizer<Separator> Tokenizer; + Separator separator(","); + Tokenizer tokenizer(str, separator); + for (Tokenizer::iterator it = tokenizer.begin(); + it != tokenizer.end(); ++it) + { + routes.push_back(Route::parse(*it)); + } +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/testlib/custompolicy.h b/messagebus/src/vespa/messagebus/testlib/custompolicy.h new file mode 100644 index 00000000000..d054706312b --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/custompolicy.h @@ -0,0 +1,41 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <boost/utility.hpp> +#include <vespa/messagebus/routing/iroutingpolicy.h> +#include "simpleprotocol.h" + +namespace mbus { + +class CustomPolicy : public boost::noncopyable, public IRoutingPolicy { +private: + bool _selectOnRetry; + std::vector<uint32_t> _consumableErrors; + std::vector<Route> _routes; + +public: + CustomPolicy(bool selectOnRetry, + const std::vector<uint32_t> consumableErrors, + const std::vector<Route> &routes); + + virtual void select(RoutingContext &context); + virtual void merge(RoutingContext &context); +}; + +class CustomPolicyFactory : public SimpleProtocol::IPolicyFactory { +private: + bool _selectOnRetry; + std::vector<uint32_t> _consumableErrors; + +public: + CustomPolicyFactory(); + CustomPolicyFactory(bool selectOnRetry); + CustomPolicyFactory(bool selectOnRetry, uint32_t consumableError); + CustomPolicyFactory(bool selectOnRetry, const std::vector<uint32_t> consumableErrors); + + IRoutingPolicy::UP create(const string ¶m); + static void parseRoutes(const string &str, std::vector<Route> &routes); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/testlib/oosserver.cpp b/messagebus/src/vespa/messagebus/testlib/oosserver.cpp new file mode 100644 index 00000000000..bf7eefbb84a --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/oosserver.cpp @@ -0,0 +1,83 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".oosserver"); +#include "oosserver.h" +#include "slobrok.h" + +namespace mbus { + +OOSServer::OOSServer(const Slobrok &slobrok, const string service, + const OOSState &state) + : _lock("mbus::OOSServer::_lock", false), + _orb(), + _port(0), + _regAPI(_orb, slobrok::ConfiguratorFactory(slobrok.config())), + _genCnt(1), + _state() +{ + setState(state); + { + FRT_ReflectionBuilder rb(&_orb); + //------------------------------------------------------------------- + rb.DefineMethod("fleet.getOOSList", "ii", "Si", true, + FRT_METHOD(OOSServer::rpc_poll), this); + rb.MethodDesc("fetch OOS information"); + rb.ParamDesc("gencnt", "generation already known by client"); + rb.ParamDesc("timeout", "How many milliseconds to wait for changes " + "before returning if nothing has changed (max=10000)"); + rb.ReturnDesc("names", "list of services that are OOS " + "(empty if generation has not changed)"); + rb.ReturnDesc("newgen", "generation of the returned list"); + //------------------------------------------------------------------- + } + _orb.Listen(0); + _port = _orb.GetListenPort(); + _orb.Start(); + _regAPI.registerName(service); +} + +OOSServer::~OOSServer() +{ + _orb.ShutDown(true); +} + +int +OOSServer::port() const +{ + return _port; +} + +void +OOSServer::rpc_poll(FRT_RPCRequest *req) +{ + vespalib::LockGuard guard(_lock); + FRT_Values &dst = *req->GetReturn(); + FRT_StringValue *names = dst.AddStringArray(_state.size()); + for (uint32_t i = 0; i < _state.size(); ++i) { + dst.SetString(&names[i], _state[i].c_str()); + } + dst.AddInt32(_genCnt); +} + +void +OOSServer::setState(const OOSState &state) +{ + std::vector<string> newState; + for (OOSState::ITR itr = state.begin(); + itr != state.end(); ++itr) + { + if (itr->second) { + newState.push_back(itr->first); + } + } + vespalib::LockGuard guard(_lock); + _state = newState; + ++_genCnt; + if (_genCnt == 0) { + ++_genCnt; + } +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/testlib/oosserver.h b/messagebus/src/vespa/messagebus/testlib/oosserver.h new file mode 100644 index 00000000000..a9ab128aa5d --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/oosserver.h @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <vespa/vespalib/util/sync.h> +#include <vespa/fnet/frt/frt.h> +#include <vespa/slobrok/sbregister.h> +#include <string> +#include <vector> +#include "oosstate.h" + +namespace mbus { + +class Slobrok; + +class OOSServer : public FRT_Invokable +{ +private: + OOSServer(const OOSServer &); + OOSServer &operator=(const OOSServer &); + + vespalib::Lock _lock; + FRT_Supervisor _orb; + int _port; + slobrok::api::RegisterAPI _regAPI; + uint32_t _genCnt; + std::vector<string> _state; + +public: + OOSServer(const Slobrok &slobrok, const string service, + const OOSState &state = OOSState()); + ~OOSServer(); + int port() const; + void rpc_poll(FRT_RPCRequest *req); + void setState(const OOSState &state); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/testlib/oosstate.cpp b/messagebus/src/vespa/messagebus/testlib/oosstate.cpp new file mode 100644 index 00000000000..c4ad1fcafcd --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/oosstate.cpp @@ -0,0 +1,34 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".oosstate"); +#include "oosstate.h" + +namespace mbus { + +OOSState::OOSState() + : _data() +{ +} + +OOSState & +OOSState::add(const string &service, bool oos) +{ + _data.push_back(std::make_pair(service, oos)); + return *this; +} + +OOSState::ITR +OOSState::begin() const +{ + return _data.begin(); +} + +OOSState::ITR +OOSState::end() const +{ + return _data.end(); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/testlib/oosstate.h b/messagebus/src/vespa/messagebus/testlib/oosstate.h new file mode 100644 index 00000000000..b083d1fc8af --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/oosstate.h @@ -0,0 +1,27 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <vector> +#include <vespa/messagebus/common.h> + +namespace mbus { + +class OOSState +{ +public: + typedef std::vector<std::pair<string, bool> > TYPE; + typedef TYPE::const_iterator ITR; + +private: + TYPE _data; + +public: + OOSState(); + OOSState &add(const string &service, bool oos = true); + ITR begin() const; + ITR end() const; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/testlib/receptor.cpp b/messagebus/src/vespa/messagebus/testlib/receptor.cpp new file mode 100644 index 00000000000..2e45975cac6 --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/receptor.cpp @@ -0,0 +1,67 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".receptor"); +#include "receptor.h" + +namespace mbus { + +Receptor::Receptor() + : IMessageHandler(), + IReplyHandler(), + _mon("mbus::Receptor::_mon", true), + _msg(), + _reply() +{ +} + +void +Receptor::handleMessage(Message::UP msg) +{ + vespalib::MonitorGuard guard(_mon); + _msg = std::move(msg); + guard.broadcast(); +} + +void +Receptor::handleReply(Reply::UP reply) +{ + vespalib::MonitorGuard guard(_mon); + _reply = std::move(reply); + guard.broadcast(); +} + +Message::UP +Receptor::getMessage(double maxWait) +{ + int ms = (int)(maxWait * 1000); + FastOS_Time startTime; + startTime.SetNow(); + vespalib::MonitorGuard guard(_mon); + while (_msg.get() == 0) { + int w = ms - (int)startTime.MilliSecsToNow(); + if (w <= 0 || !guard.wait(w)) { + break; + } + } + return std::move(_msg); +} + +Reply::UP +Receptor::getReply(double maxWait) +{ + int ms = (int)(maxWait * 1000); + FastOS_Time startTime; + startTime.SetNow(); + vespalib::MonitorGuard guard(_mon); + while (_reply.get() == 0) { + int w = ms - (int)startTime.MilliSecsToNow(); + if (w <= 0 || !guard.wait(w)) { + break; + } + } + return std::move(_reply); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/testlib/receptor.h b/messagebus/src/vespa/messagebus/testlib/receptor.h new file mode 100644 index 00000000000..d7a0376596e --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/receptor.h @@ -0,0 +1,30 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <vespa/vespalib/util/sync.h> +#include <vespa/messagebus/imessagehandler.h> +#include <vespa/messagebus/ireplyhandler.h> + +namespace mbus { + +class Receptor : public IMessageHandler, + public IReplyHandler +{ +private: + vespalib::Monitor _mon; + Message::UP _msg; + Reply::UP _reply; + + Receptor(const Receptor &); + Receptor &operator=(const Receptor &); +public: + Receptor(); + virtual void handleMessage(Message::UP msg); + virtual void handleReply(Reply::UP reply); + Message::UP getMessage(double maxWait = 120.0); + Reply::UP getReply(double maxWait = 120.0); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/testlib/simplemessage.cpp b/messagebus/src/vespa/messagebus/testlib/simplemessage.cpp new file mode 100644 index 00000000000..947df5100af --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/simplemessage.cpp @@ -0,0 +1,87 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".simplemessage"); +#include "simplemessage.h" +#include "simpleprotocol.h" + +namespace mbus { + +SimpleMessage::SimpleMessage(const string &str) : + Message(), + _value(str), + _hasSeqId(false), + _seqId(0) +{ + // empty +} + +SimpleMessage::SimpleMessage(const string &str, bool hasSeqId, uint64_t seqId) : + Message(), + _value(str), + _hasSeqId(hasSeqId), + _seqId(seqId) +{ + // empty +} + +SimpleMessage::~SimpleMessage() +{ + // empty +} + +void +SimpleMessage::setValue(const string &value) +{ + _value = value; +} + +const string & +SimpleMessage::getValue() const +{ + return _value; +} + +int +SimpleMessage::getHash() const +{ + int hash = 0; + string str = _value; + for (uint32_t i = 0; i < str.size(); ++i) { + hash += (hash << 9) + (hash >> 7) + (str[i] << 5) + (str[i] >> 3); + } + return hash; +} + +const string & +SimpleMessage::getProtocol() const +{ + return SimpleProtocol::NAME; +} + +uint32_t +SimpleMessage::getType() const +{ + return SimpleProtocol::MESSAGE; +} + +bool +SimpleMessage::hasSequenceId() const +{ + return _hasSeqId; +} + +uint64_t +SimpleMessage::getSequenceId() const +{ + return _seqId; +} + +uint32_t +SimpleMessage::getApproxSize() const +{ + return _value.size(); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/testlib/simplemessage.h b/messagebus/src/vespa/messagebus/testlib/simplemessage.h new file mode 100644 index 00000000000..c709397a89f --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/simplemessage.h @@ -0,0 +1,35 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <vespa/messagebus/message.h> + +namespace mbus { + +class SimpleMessage : public Message { +private: + string _value; + bool _hasSeqId; + uint64_t _seqId; + +public: + SimpleMessage(const string &str); + SimpleMessage(const string &str, bool hasSeqId, uint64_t seqId); + ~SimpleMessage(); + + void setValue(const string &value); + const string &getValue() const; + int getHash() const; + const string & getProtocol() const; + uint32_t getType() const; + bool hasSequenceId() const; + uint64_t getSequenceId() const; + uint32_t getApproxSize() const; + + uint8_t priority() const { return 8; } + + string toString() const { return _value; } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/testlib/simpleprotocol.cpp b/messagebus/src/vespa/messagebus/testlib/simpleprotocol.cpp new file mode 100644 index 00000000000..07a4321f05b --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/simpleprotocol.cpp @@ -0,0 +1,155 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".simpleprotocol"); + +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/routing/routingcontext.h> +#include <vespa/messagebus/routing/routingnodeiterator.h> +#include "simplemessage.h" +#include "simpleprotocol.h" +#include "simplereply.h" + +namespace mbus { + +const string SimpleProtocol::NAME("Simple"); +const uint32_t SimpleProtocol::MESSAGE(1); +const uint32_t SimpleProtocol::REPLY(2); + +class AllPolicy : public IRoutingPolicy { +public: + void select(RoutingContext &ctx) { + std::vector<Route> recipients; + ctx.getMatchedRecipients(recipients); + ctx.addChildren(recipients); + } + + void merge(RoutingContext &ctx) { + SimpleProtocol::simpleMerge(ctx); + } +}; + +class AllPolicyFactory : public SimpleProtocol::IPolicyFactory { +public: + IRoutingPolicy::UP create(const string &) { + return IRoutingPolicy::UP(new AllPolicy()); + } +}; + +class HashPolicy : public IRoutingPolicy { +public: + void select(RoutingContext &ctx) { + std::vector<Route> recipients; + ctx.getMatchedRecipients(recipients); + if (!recipients.empty()) { + int i = static_cast<const SimpleMessage&>(ctx.getMessage()).getHash(); + ctx.addChild(recipients[std::abs(i) % recipients.size()]); + } + } + + void merge(RoutingContext &ctx) { + SimpleProtocol::simpleMerge(ctx); + } +}; + +class HashPolicyFactory : public SimpleProtocol::IPolicyFactory { +public: + IRoutingPolicy::UP create(const string &) { + return IRoutingPolicy::UP(new HashPolicy()); + } +}; + +SimpleProtocol::SimpleProtocol() : + _policies() +{ + addPolicyFactory("All", IPolicyFactory::SP(new AllPolicyFactory)); + addPolicyFactory("Hash", IPolicyFactory::SP(new HashPolicyFactory)); +} + +SimpleProtocol::~SimpleProtocol() +{ + // empty +} + +void +SimpleProtocol::addPolicyFactory(const string &name, + IPolicyFactory::SP factory) +{ + _policies.insert(FactoryMap::value_type(name, factory)); +} + +const string & +SimpleProtocol::getName() const +{ + return NAME; +} + +IRoutingPolicy::UP +SimpleProtocol::createPolicy(const string &name, + const string ¶m) const +{ + FactoryMap::const_iterator it = _policies.find(name); + if (it != _policies.end()) { + return it->second->create(param); + } + return IRoutingPolicy::UP(); +} + +Blob +SimpleProtocol::encode(const vespalib::Version &version, const Routable &routable) const +{ + (void)version; + if (routable.getType() == MESSAGE) { + string str = "M"; + str.append(static_cast<const SimpleMessage&>(routable).getValue()); + Blob ret(str.size()); + memcpy(ret.data(), str.data(), str.size()); + return ret; + } else if (routable.getType() == REPLY) { + string str = "R"; + str.append(static_cast<const SimpleReply&>(routable).getValue()); + Blob ret(str.size()); + memcpy(ret.data(), str.data(), str.size()); + return ret; + } else { + return Blob(0); + } +} + +Routable::UP +SimpleProtocol::decode(const vespalib::Version &version, BlobRef data) const +{ + (void)version; + + const char *d = data.data(); + uint32_t s = data.size(); + if (s < 1) { + return Routable::UP(); // too short + } + string str(d + 1, s - 1); + if (*d == 'M') { + return Routable::UP(new SimpleMessage(str)); + } else if (*d == 'R') { + return Routable::UP(new SimpleReply(str)); + } else { + return Routable::UP(); // unknown type + } +} + +void +SimpleProtocol::simpleMerge(RoutingContext &ctx) +{ + Reply::UP ret(new EmptyReply()); + for (RoutingNodeIterator it = ctx.getChildIterator(); + it.isValid(); it.next()) + { + const Reply &reply = it.getReplyRef(); + for (uint32_t i = 0; i < reply.getNumErrors(); ++i) { + ret->addError(reply.getError(i)); + } + } + ctx.setReply(std::move(ret)); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/testlib/simpleprotocol.h b/messagebus/src/vespa/messagebus/testlib/simpleprotocol.h new file mode 100644 index 00000000000..4a57d86f4a7 --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/simpleprotocol.h @@ -0,0 +1,91 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <boost/utility.hpp> +#include <map> +#include <string> +#include <vespa/messagebus/iprotocol.h> + +namespace mbus { + +class SimpleProtocol : public boost::noncopyable, public IProtocol { +public: + /** + * Defines a policy factory interface that tests can use to register arbitrary policies with this protocol. + */ + class IPolicyFactory { + public: + /** + * Convenience typedefs. + */ + typedef std::shared_ptr<IPolicyFactory> SP; + + /** + * Required for inheritance. + */ + virtual ~IPolicyFactory() { } + + /** + * Creates a new isntance of the routing policy that this factory encapsulates. + * + * @param param The param for the policy constructor. + * @return The routing policy created. + */ + virtual IRoutingPolicy::UP create(const string ¶m) = 0; + }; + +private: + typedef std::map<string, IPolicyFactory::SP> FactoryMap; + FactoryMap _policies; + +public: + static const string NAME; + static const uint32_t MESSAGE; + static const uint32_t REPLY; + + /** + * Constructs a new simple protocol. This registers policy factories for both {@link SimpleAllPolicy} and + * {@link SimpleHashPolicy}. + */ + SimpleProtocol(); + + /** + * Frees up any allocated resources. + */ + virtual ~SimpleProtocol(); + + /** + * Registers a policy factory with this protocol under a given name. Whenever a policy is requested that + * matches this name, the factory is invoked. + * + * @param name The name of the policy. + * @param factory The policy factory. + */ + void addPolicyFactory(const string &name, + IPolicyFactory::SP factory); + + /** + * Common merge logic that can be used for any simple policy. It all errors across all replies into + * a new {@link EmptyReply}. + * + * @param ctx The routing context whose children to merge. + */ + static void simpleMerge(RoutingContext &ctx); + + // Implements IProtocol. + const string & getName() const; + + // Implements IProtocol. + IRoutingPolicy::UP createPolicy(const string &name, + const string ¶m) const; + + // Implements IProtocol. + Blob encode(const vespalib::Version &version, const Routable &routable) const; + + // Implements IProtocol. + Routable::UP decode(const vespalib::Version &version, BlobRef data) const; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/testlib/simplereply.cpp b/messagebus/src/vespa/messagebus/testlib/simplereply.cpp new file mode 100644 index 00000000000..1559a157860 --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/simplereply.cpp @@ -0,0 +1,47 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".simplereply"); +#include "simplereply.h" +#include "simpleprotocol.h" + +namespace mbus { + +SimpleReply::SimpleReply(const string &str) : + Reply(), + _value(str) +{ + // empty +} + +SimpleReply::~SimpleReply() +{ + // empty +} + +void +SimpleReply::setValue(const string &value) +{ + _value = value; +} + +const string & +SimpleReply::getValue() const +{ + return _value; +} + +const string & +SimpleReply::getProtocol() const +{ + return SimpleProtocol::NAME; +} + +uint32_t +SimpleReply::getType() const +{ + return SimpleProtocol::REPLY; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/testlib/simplereply.h b/messagebus/src/vespa/messagebus/testlib/simplereply.h new file mode 100644 index 00000000000..6a33855976c --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/simplereply.h @@ -0,0 +1,29 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <string> +#include <vespa/messagebus/reply.h> +#include "simplemessage.h" + +namespace mbus { + +class SimpleReply : public Reply +{ +private: + string _value; + SimpleReply &operator=(const SimpleReply &); +public: + typedef std::unique_ptr<SimpleReply> UP; + SimpleReply(const string &str); + virtual ~SimpleReply(); + void setValue(const string &value); + const string &getValue() const; + virtual const string & getProtocol() const; + virtual uint32_t getType() const; + + uint8_t priority() const { return 8; } +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/testlib/slobrok.cpp b/messagebus/src/vespa/messagebus/testlib/slobrok.cpp new file mode 100644 index 00000000000..924391c0f8d --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/slobrok.cpp @@ -0,0 +1,107 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".slobrok"); +#include "slobrok.h" +#include <vespa/slobrok/cfg.h> +#include <vespa/slobrok/server/sbenv.h> +#include <vespa/vespalib/util/sync.h> +#include <vespa/vespalib/util/vstringfmt.h> + +namespace { +class WaitTask : public FNET_Task +{ +private: + bool _done; + vespalib::Monitor _mon; +public: + WaitTask(FNET_Scheduler *s) : FNET_Task(s), _done(false), _mon() {} + void wait() { + vespalib::MonitorGuard guard(_mon); + while (!_done) { + guard.wait(); + } + } + virtual void PerformTask() { + vespalib::MonitorGuard guard(_mon); + _done = true; + guard.signal(); + } +}; +} // namespace <unnamed> + +namespace mbus { + +void +Slobrok::Thread::setEnv(slobrok::SBEnv *env) +{ + _env = env; +} + +void +Slobrok::Thread::Run(FastOS_ThreadInterface *, void *) +{ + if (_env->MainLoop() != 0) { + LOG_ABORT("Slobrok main failed"); + } +} + +void +Slobrok::init() +{ + slobrok::ConfigShim shim(_port); + _env.reset(new slobrok::SBEnv(shim)); + _thread.setEnv(_env.get()); + WaitTask wt(_env->getTransport()->GetScheduler()); + wt.ScheduleNow(); + if (_pool.NewThread(&_thread, 0) == 0) { + LOG_ABORT("Could not spawn thread"); + } + wt.wait(); + int p = _env->getSupervisor()->GetListenPort(); + LOG_ASSERT(p != 0 && (p == _port || _port == 0)); + _port = p; +} + +Slobrok::Slobrok() + : _pool(128000, 0), + _env(), + _port(0), + _thread() +{ + init(); +} + +Slobrok::Slobrok(int p) + : _pool(128000, 0), + _env(), + _port(p), + _thread() +{ + init(); +} + +Slobrok::~Slobrok() +{ + _env->getTransport()->ShutDown(true); + _pool.Close(); +} + +int +Slobrok::port() const +{ + return _port; +} + +config::ConfigUri +Slobrok::config() const +{ + cloud::config::SlobroksConfigBuilder builder; + cloud::config::SlobroksConfig::Slobrok sb; + sb.connectionspec = vespalib::make_string("tcp/localhost:%d", port()); + builder.slobrok.push_back(sb); + return config::ConfigUri::createFromInstance(builder); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/testlib/slobrok.h b/messagebus/src/vespa/messagebus/testlib/slobrok.h new file mode 100644 index 00000000000..28053e7b506 --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/slobrok.h @@ -0,0 +1,45 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <memory> +#include <vespa/messagebus/common.h> +#include <vespa/slobrok/cfg.h> + +namespace slobrok { +class SBEnv; +} // namespace slobrok + +namespace mbus { + +class Slobrok +{ +private: + class Thread : public FastOS_Runnable { + private: + slobrok::SBEnv *_env; + public: + void setEnv(slobrok::SBEnv *env); + void Run(FastOS_ThreadInterface *, void *); + }; + FastOS_ThreadPool _pool; + std::unique_ptr<slobrok::SBEnv> _env; + int _port; + Thread _thread; + + Slobrok(const Slobrok &); + Slobrok &operator=(const Slobrok &); + + void init(); + +public: + typedef std::unique_ptr<Slobrok> UP; + Slobrok(); + Slobrok(int port); + ~Slobrok(); + int port() const; + config::ConfigUri config() const; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/testlib/slobrokstate.cpp b/messagebus/src/vespa/messagebus/testlib/slobrokstate.cpp new file mode 100644 index 00000000000..4f825f8dba1 --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/slobrokstate.cpp @@ -0,0 +1,34 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".slobrokstate"); +#include "slobrokstate.h" + +namespace mbus { + +SlobrokState::SlobrokState() + : _data() +{ +} + +SlobrokState & +SlobrokState::add(const string &pattern, uint32_t cnt) +{ + _data.push_back(std::make_pair(pattern, cnt)); + return *this; +} + +SlobrokState::ITR +SlobrokState::begin() const +{ + return _data.begin(); +} + +SlobrokState::ITR +SlobrokState::end() const +{ + return _data.end(); +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/testlib/slobrokstate.h b/messagebus/src/vespa/messagebus/testlib/slobrokstate.h new file mode 100644 index 00000000000..f8af060e4e6 --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/slobrokstate.h @@ -0,0 +1,27 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <vector> +#include <vespa/messagebus/common.h> + +namespace mbus { + +class SlobrokState +{ +public: + typedef std::vector<std::pair<string, uint32_t> > TYPE; + typedef TYPE::const_iterator ITR; + +private: + TYPE _data; + +public: + SlobrokState(); + SlobrokState &add(const string &pattern, uint32_t cnt = 1); + ITR begin() const; + ITR end() const; +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/testlib/testserver.cpp b/messagebus/src/vespa/messagebus/testlib/testserver.cpp new file mode 100644 index 00000000000..c0ef5f9fbf9 --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/testserver.cpp @@ -0,0 +1,106 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP(".testserver"); + +#include <vespa/vespalib/util/vstringfmt.h> +#include <vespa/messagebus/network/rpcnetworkparams.h> +#include <vespa/messagebus/vtag.h> +#include "oosstate.h" +#include "simpleprotocol.h" +#include "slobrok.h" +#include "slobrokstate.h" +#include "testserver.h" + +namespace mbus { + +VersionedRPCNetwork::VersionedRPCNetwork(const RPCNetworkParams ¶ms) : + RPCNetwork(params), + _version(Vtag::currentVersion) +{ + // empty +} + +void +VersionedRPCNetwork::setVersion(const vespalib::Version &version) +{ + _version = version; + flushTargetPool(); +} + +TestServer::TestServer(const Identity &ident, + const RoutingSpec &spec, + const Slobrok &slobrok, + const string &oosServerPattern, + IProtocol::SP protocol) : + net(RPCNetworkParams() + .setIdentity(ident) + .setSlobrokConfig(slobrok.config()) + .setOOSServerPattern(oosServerPattern)), + mb(net, ProtocolSet().add(IProtocol::SP(new SimpleProtocol())).add(protocol)) +{ + mb.setupRouting(spec); +} + +TestServer::TestServer(const MessageBusParams &mbusParams, + const RPCNetworkParams &netParams) : + net(netParams), + mb(net, mbusParams) +{ + // empty +} + +bool +TestServer::waitSlobrok(const string &pattern, uint32_t cnt) +{ + return waitState(SlobrokState().add(pattern, cnt)); +} + +bool +TestServer::waitOOS(const string &service) +{ + return waitState(OOSState().add(service, true)); +} + +bool +TestServer::waitState(const SlobrokState &slobrokState) +{ + for (uint32_t i = 0; i < 12000; ++i) { + bool done = true; + for (SlobrokState::ITR itr = slobrokState.begin(); + itr != slobrokState.end(); ++itr) + { + slobrok::api::MirrorAPI::SpecList res = net.getMirror().lookup(itr->first); + if (res.size() != itr->second) { + done = false; + } + } + if (done) { + return true; + } + FastOS_Thread::Sleep(10); + } + return false; +} + +bool +TestServer::waitState(const OOSState &oosState) +{ + for (uint32_t i = 0; i < 12000; ++i) { + bool done = true; + for (OOSState::ITR itr = oosState.begin(); + itr != oosState.end(); ++itr) + { + if (net.getOOSManager().isOOS(itr->first) != itr->second) { + done = false; + } + } + if (done) { + return true; + } + FastOS_Thread::Sleep(10); + } + return false; +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/testlib/testserver.h b/messagebus/src/vespa/messagebus/testlib/testserver.h new file mode 100644 index 00000000000..ed0720eef1a --- /dev/null +++ b/messagebus/src/vespa/messagebus/testlib/testserver.h @@ -0,0 +1,53 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <boost/utility.hpp> +#include <memory> +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/network/rpcnetwork.h> + +namespace mbus { + +class Identity; +class RoutingTableSpec; +class Slobrok; +class SlobrokState; +class OOSState; + +class VersionedRPCNetwork : public RPCNetwork { +private: + vespalib::Version _version; + +protected: + const vespalib::Version &getVersion() const { return _version; } + +public: + VersionedRPCNetwork(const RPCNetworkParams ¶ms); + void setVersion(const vespalib::Version &version); +}; + +class TestServer : public boost::noncopyable { +public: + typedef std::unique_ptr<TestServer> UP; + + VersionedRPCNetwork net; + MessageBus mb; + + TestServer(const Identity &ident, + const RoutingSpec &spec, + const Slobrok &slobrok, + const string &oosServerPattern = "", + IProtocol::SP protocol = IProtocol::SP()); + TestServer(const MessageBusParams &mbusParams, + const RPCNetworkParams &netParams); + + bool waitSlobrok(const string &pattern, uint32_t cnt = 1); + bool waitOOS(const string &service); + + bool waitState(const SlobrokState &slobrokState); + bool waitState(const OOSState &oosState); +}; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/trace.h b/messagebus/src/vespa/messagebus/trace.h new file mode 100644 index 00000000000..98356fb7b51 --- /dev/null +++ b/messagebus/src/vespa/messagebus/trace.h @@ -0,0 +1,18 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <vespa/vespalib/trace/trace.h> +#include <vespa/messagebus/tracenode.h> + +namespace mbus { + + typedef vespalib::Trace Trace; + +#define MBUS_TRACE2(ttrace, level, note, addTime) \ + VESPALIB_TRACE2(ttrace, level, note, addTime) + +#define MBUS_TRACE(trace, level, note) VESPALIB_TRACE2(trace, level, note, true) + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/tracelevel.h b/messagebus/src/vespa/messagebus/tracelevel.h new file mode 100644 index 00000000000..8ec862eecab --- /dev/null +++ b/messagebus/src/vespa/messagebus/tracelevel.h @@ -0,0 +1,10 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vespa/vespalib/trace/tracelevel.h> +namespace mbus { + + typedef vespalib::TraceLevel TraceLevel; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/tracenode.h b/messagebus/src/vespa/messagebus/tracenode.h new file mode 100644 index 00000000000..1dddf39428b --- /dev/null +++ b/messagebus/src/vespa/messagebus/tracenode.h @@ -0,0 +1,12 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vespa/messagebus/common.h> +#include <vespa/vespalib/trace/tracenode.h> + +namespace mbus { + + typedef vespalib::TraceNode TraceNode; + +} // namespace mbus + diff --git a/messagebus/src/vespa/messagebus/vtag.cpp b/messagebus/src/vespa/messagebus/vtag.cpp new file mode 100644 index 00000000000..0a42b5e6ec0 --- /dev/null +++ b/messagebus/src/vespa/messagebus/vtag.cpp @@ -0,0 +1,81 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <string.h> +#include <stdio.h> +#include "vtag.h" +#include <vespa/vespalib/component/version.h> + +#ifndef V_TAG +#define V_TAG "NOTAG" +#define V_TAG_DATE "NOTAG" +#define V_TAG_SYSTEM "NOTAG" +#define V_TAG_SYSTEM_REV "NOTAG" +#define V_TAG_BUILDER "NOTAG" +#define V_TAG_VERSION "0" +#define V_TAG_ARCH "NOTAG" +#endif + +namespace mbus { + +char VersionTag[] = V_TAG; +char VersionTagDate[] = V_TAG_DATE; +char VersionTagSystem[] = V_TAG_SYSTEM; +char VersionTagSystemRev[] = V_TAG_SYSTEM_REV; +char VersionTagBuilder[] = V_TAG_BUILDER; +char VersionTagPkg[] = V_TAG_PKG; +char VersionTagComponent[] = V_TAG_COMPONENT; +char VersionTagArch[] = V_TAG_ARCH; + +vespalib::Version Vtag::currentVersion(VersionTagComponent); + +void +Vtag::printVersionNice() +{ + char *s = VersionTag; + bool needdate = true; + if (strncmp(VersionTag, "V_", 2) == 0) { + s += 2; + do { + while (strchr("0123456789", *s) != NULL) { + printf("%c", *s++); + } + if (strncmp(s, "_RELEASE", 8) == 0) { + needdate = false; + break; + } + if (strncmp(s, "_RC", 3) == 0) { + char *e = strchr(s, '-'); + if (e == NULL) { + printf("%s", s); + } else { + printf("%.*s", (int)(e-s), s); + } + needdate = false; + break; + } + if (*s == '_' && strchr("0123456789", *++s)) { + printf("."); + } else { + break; + } + } while (*s && *s != '-'); + } else { + char *e = strchr(s, '-'); + if (e == NULL) { + printf("%s", s); + } else { + printf("%.*s", (int)(e-s), s); + } + } + if (needdate) { + s = VersionTagDate; + char *e = strchr(s, '-'); + if (e == NULL) { + printf("-%s", s); + } else { + printf("-%.*s", (int)(e-s), s); + } + } +} + +} // namespace mbus diff --git a/messagebus/src/vespa/messagebus/vtag.h b/messagebus/src/vespa/messagebus/vtag.h new file mode 100644 index 00000000000..505ffb42161 --- /dev/null +++ b/messagebus/src/vespa/messagebus/vtag.h @@ -0,0 +1,23 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +namespace vespalib { +class Version; +} + +namespace mbus { + +extern char VersionTag[]; +extern char VersionTagDate[]; +extern char VersionTagSystem[]; +extern char VersionTagSystemRev[]; +extern char VersionTagBuilder[]; + +class Vtag { +public: + static vespalib::Version currentVersion; + static void printVersionNice(); +}; + +} // namespace messagebus + diff --git a/messagebus/test/CMakeLists.txt b/messagebus/test/CMakeLists.txt new file mode 100644 index 00000000000..a6d2fdcb8a0 --- /dev/null +++ b/messagebus/test/CMakeLists.txt @@ -0,0 +1,10 @@ +vespa_add_module_dependency(slobrok_slobrokserver) +vespa_add_module_dependency(messagebus_messagebus-test) +vespa_add_module_dependency(messagebus) +add_subdirectory(src/binref) +add_subdirectory(src/tests/compile-cpp) +add_subdirectory(src/tests/compile-java) +add_subdirectory(src/tests/error) +add_subdirectory(src/tests/errorcodes) +add_subdirectory(src/tests/speed) +add_subdirectory(src/tests/trace) diff --git a/messagebus/test/src/.gitignore b/messagebus/test/src/.gitignore new file mode 100644 index 00000000000..8689bfd3624 --- /dev/null +++ b/messagebus/test/src/.gitignore @@ -0,0 +1,8 @@ +Makefile.inc +Makefile.ini +config.cfg +config_command.sh +configure +project.dsw +versiontag.mak +/messagebus_test.mak diff --git a/messagebus/test/src/binref/.gitignore b/messagebus/test/src/binref/.gitignore new file mode 100644 index 00000000000..c1b83610972 --- /dev/null +++ b/messagebus/test/src/binref/.gitignore @@ -0,0 +1,5 @@ +.depend +Makefile +compilejava +env.sh +runjava diff --git a/messagebus/test/src/binref/CMakeLists.txt b/messagebus/test/src/binref/CMakeLists.txt new file mode 100644 index 00000000000..20594f98d02 --- /dev/null +++ b/messagebus/test/src/binref/CMakeLists.txt @@ -0,0 +1,5 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +configure_file(compilejava.in compilejava @ONLY) +configure_file(runjava.in runjava @ONLY) +configure_file(env.sh.in env.sh @ONLY) diff --git a/messagebus/test/src/binref/compilejava.in b/messagebus/test/src/binref/compilejava.in new file mode 100755 index 00000000000..bebe4374afb --- /dev/null +++ b/messagebus/test/src/binref/compilejava.in @@ -0,0 +1,11 @@ +#!/bin/sh +unset VESPA_LOG_TARGET +CLASSPATH=@PROJECT_BINARY_DIR@/messagebus/target/messagebus-jar-with-dependencies.jar +CLASSPATH=$CLASSPATH:@PROJECT_BINARY_DIR@/component/target/component.jar +CLASSPATH=$CLASSPATH:. + +if [ $# -lt 1 ]; then + echo "usage: compilejava file ..." + exit 1 +fi +exec javac -classpath $CLASSPATH "$@" diff --git a/messagebus/test/src/binref/env.sh.in b/messagebus/test/src/binref/env.sh.in new file mode 100644 index 00000000000..dda4234226f --- /dev/null +++ b/messagebus/test/src/binref/env.sh.in @@ -0,0 +1,2 @@ +BINREF=@CMAKE_CURRENT_BINARY_DIR@ +export BINREF diff --git a/messagebus/test/src/binref/progctl.sh b/messagebus/test/src/binref/progctl.sh new file mode 120000 index 00000000000..781d2058cf9 --- /dev/null +++ b/messagebus/test/src/binref/progctl.sh @@ -0,0 +1 @@ +../../../../vespalib/src/vespa/vespalib/testkit/progctl.sh
\ No newline at end of file diff --git a/messagebus/test/src/binref/runjava.in b/messagebus/test/src/binref/runjava.in new file mode 100755 index 00000000000..20d4de0a477 --- /dev/null +++ b/messagebus/test/src/binref/runjava.in @@ -0,0 +1,13 @@ +#!/bin/sh +unset VESPA_LOG_TARGET +unset LD_PRELOAD +CLASSPATH=@PROJECT_BINARY_DIR@/messagebus/target/messagebus-jar-with-dependencies.jar +CLASSPATH=$CLASSPATH:@PROJECT_BINARY_DIR@/component/target/component.jar +CLASSPATH=$CLASSPATH:. +if [ $# -lt 1 ]; then + echo "usage: runjava <class> [args]" + exit 1 +fi +CLASS=$1 +shift +exec java -cp $CLASSPATH $CLASS "$@" diff --git a/messagebus/test/src/binref/sbcmd b/messagebus/test/src/binref/sbcmd new file mode 120000 index 00000000000..49b9c735282 --- /dev/null +++ b/messagebus/test/src/binref/sbcmd @@ -0,0 +1 @@ +../../../../slobrok/src/apps/sbcmd/sbcmd
\ No newline at end of file diff --git a/messagebus/test/src/binref/slobrok b/messagebus/test/src/binref/slobrok new file mode 120000 index 00000000000..5bc8ae2a9f2 --- /dev/null +++ b/messagebus/test/src/binref/slobrok @@ -0,0 +1 @@ +../../../../slobrok/src/apps/slobrok/slobrok
\ No newline at end of file diff --git a/messagebus/test/src/binref/testrun.sh b/messagebus/test/src/binref/testrun.sh new file mode 120000 index 00000000000..457b9f75c5e --- /dev/null +++ b/messagebus/test/src/binref/testrun.sh @@ -0,0 +1 @@ +../../../../vespalib/src/vespa/vespalib/testkit/testrun.sh
\ No newline at end of file diff --git a/messagebus/test/src/setup.sh b/messagebus/test/src/setup.sh new file mode 100755 index 00000000000..2111dfb447c --- /dev/null +++ b/messagebus/test/src/setup.sh @@ -0,0 +1,11 @@ +#!/bin/sh -e +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +MYDIR=`pwd` +cp ../../src/cpp/Makefile.inc . +cp ../../src/cpp/versiontag.mak . +cp ../../src/cpp/config.cfg . +cp ../../src/cpp/config_command.sh . +sh config_command.sh +echo MODULEDEP_INCLUDES += -I$MYDIR/../../src/cpp >> Makefile.ini +echo LIBDIR_MESSAGEBUS=$MYDIR/../../src/cpp/messagebus >> Makefile.ini +echo LIBDIR_MESSAGEBUS-TEST=$MYDIR/../../src/cpp/messagebus/testlib >> Makefile.ini diff --git a/messagebus/test/src/test-report-index.html b/messagebus/test/src/test-report-index.html new file mode 100644 index 00000000000..4ddd6eab8f9 --- /dev/null +++ b/messagebus/test/src/test-report-index.html @@ -0,0 +1,17 @@ +<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> +<html> + <title>Messagebus</title> + <body> + <h1>Messagebus Test Reports</h1> + <ul> + <li><a href="../../../messagebus/target/site/surefire-report.html">Messagebus Java Test Report</a></li> + <li><a href="test-report-cpp/test-report.html">Messagebus C++ Test Report</a></li> + <li><a href="test-report-cross/test-report.html">Messagebus Cross-language Test Report</a></li> + </ul> + <h1>Messagebus API Documentation</h1> + <ul> + <li><a href="../../../messagebus/src/java/docs/javadoc/index.html">Messagebus Java API Documentation</a></li> + <li><a href="../../../messagebus/src/cpp/doxygen/html/index.html">Messagebus C++ API Documentation</a></li> + </ul> + </body> +</html> diff --git a/messagebus/test/src/testlist.txt b/messagebus/test/src/testlist.txt new file mode 100644 index 00000000000..3ff4369c385 --- /dev/null +++ b/messagebus/test/src/testlist.txt @@ -0,0 +1,6 @@ +tests/compile-java +tests/compile-cpp +tests/trace +tests/error +tests/errorcodes +tests/speed diff --git a/messagebus/test/src/tests/compile-cpp/.gitignore b/messagebus/test/src/tests/compile-cpp/.gitignore new file mode 100644 index 00000000000..14e4fb37c45 --- /dev/null +++ b/messagebus/test/src/tests/compile-cpp/.gitignore @@ -0,0 +1,4 @@ +.depend +Makefile +compile-cpp_test +messagebus_test_compile-cpp_test_app diff --git a/messagebus/test/src/tests/compile-cpp/CMakeLists.txt b/messagebus/test/src/tests/compile-cpp/CMakeLists.txt new file mode 100644 index 00000000000..5491399fce2 --- /dev/null +++ b/messagebus/test/src/tests/compile-cpp/CMakeLists.txt @@ -0,0 +1,7 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_test_compile-cpp_test_app + SOURCES + compile-cpp.cpp + DEPENDS +) +vespa_add_test(NAME messagebus_test_compile-cpp_test_app NO_VALGRIND COMMAND messagebus_test_compile-cpp_test_app) diff --git a/messagebus/test/src/tests/compile-cpp/DESC b/messagebus/test/src/tests/compile-cpp/DESC new file mode 100644 index 00000000000..465d625ca9e --- /dev/null +++ b/messagebus/test/src/tests/compile-cpp/DESC @@ -0,0 +1,2 @@ +simple compilation test to check dependencies. + diff --git a/messagebus/test/src/tests/compile-cpp/FILES b/messagebus/test/src/tests/compile-cpp/FILES new file mode 100644 index 00000000000..956ce16a56e --- /dev/null +++ b/messagebus/test/src/tests/compile-cpp/FILES @@ -0,0 +1 @@ +compile-cpp.cpp diff --git a/messagebus/test/src/tests/compile-cpp/compile-cpp.cpp b/messagebus/test/src/tests/compile-cpp/compile-cpp.cpp new file mode 100644 index 00000000000..e15b57859b7 --- /dev/null +++ b/messagebus/test/src/tests/compile-cpp/compile-cpp.cpp @@ -0,0 +1,16 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("compile-cpp_test"); +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/routing/route.h> + +TEST_SETUP(Test); + +int +Test::Main() +{ + TEST_INIT("compile-cpp_test"); + mbus::Route r; + TEST_DONE(); +} diff --git a/messagebus/test/src/tests/compile-java/.gitignore b/messagebus/test/src/tests/compile-java/.gitignore new file mode 100644 index 00000000000..d615ebbafe7 --- /dev/null +++ b/messagebus/test/src/tests/compile-java/.gitignore @@ -0,0 +1,4 @@ +*.class +.depend +Makefile +compile-java_test diff --git a/messagebus/test/src/tests/compile-java/CMakeLists.txt b/messagebus/test/src/tests/compile-java/CMakeLists.txt new file mode 100644 index 00000000000..3e7665f5fed --- /dev/null +++ b/messagebus/test/src/tests/compile-java/CMakeLists.txt @@ -0,0 +1,2 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_test(NAME messagebus_test_compile-java_test NO_VALGRIND COMMAND sh compile-java_test.sh) diff --git a/messagebus/test/src/tests/compile-java/DESC b/messagebus/test/src/tests/compile-java/DESC new file mode 100644 index 00000000000..465d625ca9e --- /dev/null +++ b/messagebus/test/src/tests/compile-java/DESC @@ -0,0 +1,2 @@ +simple compilation test to check dependencies. + diff --git a/messagebus/test/src/tests/compile-java/FILES b/messagebus/test/src/tests/compile-java/FILES new file mode 100644 index 00000000000..5b154bb1605 --- /dev/null +++ b/messagebus/test/src/tests/compile-java/FILES @@ -0,0 +1 @@ +TestCompile.java diff --git a/messagebus/test/src/tests/compile-java/TestCompile.java b/messagebus/test/src/tests/compile-java/TestCompile.java new file mode 100644 index 00000000000..443ae093794 --- /dev/null +++ b/messagebus/test/src/tests/compile-java/TestCompile.java @@ -0,0 +1,9 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +import com.yahoo.messagebus.EmptyReply; + +public class TestCompile { + public static void main(String[] args) { + EmptyReply er = new EmptyReply(); + } +} diff --git a/messagebus/test/src/tests/compile-java/compile-java_test.sh b/messagebus/test/src/tests/compile-java/compile-java_test.sh new file mode 100755 index 00000000000..f3da918eae1 --- /dev/null +++ b/messagebus/test/src/tests/compile-java/compile-java_test.sh @@ -0,0 +1,6 @@ +#!/bin/bash +. ../../binref/env.sh + +$BINREF/compilejava TestCompile.java +$BINREF/runjava TestCompile + diff --git a/messagebus/test/src/tests/create-test.sh b/messagebus/test/src/tests/create-test.sh new file mode 100755 index 00000000000..27277838a07 --- /dev/null +++ b/messagebus/test/src/tests/create-test.sh @@ -0,0 +1,71 @@ +#!/bin/sh +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +gen_ignore_file() { + echo "generating '$1' ..." + echo ".depend" > $1 + echo "Makefile" >> $1 + echo "${test}_test" >> $1 +} + +gen_project_file() { + echo "generating '$1' ..." + echo "APPLICATION ${test}_test" > $1 + echo "OBJS $test" >> $1 + echo "EXTERNALLIBS messagebus messagebus-test" >> $1 + echo "EXTERNALLIBS slobrokserver slobrok fnet vespalib config vespalog" >> $1 + echo "" >> $1 + echo "CUSTOMMAKE" >> $1 + echo "test: depend ${test}_test" >> $1 + echo -e "\t@./${test}_test" >> $1 +} + +gen_source() { + echo "generating '$1' ..." + echo "#include <vespa/log/log.h>" > $1 + echo "LOG_SETUP(\"${test}_test\");" >> $1 + echo "#include <vespa/fastos/fastos.h>" >> $1 + echo "#include <vespa/vespalib/testkit/testapp.h>" >> $1 + echo "" >> $1 + echo "TEST_SETUP(Test);" >> $1 + echo "" >> $1 + echo "int" >> $1 + echo "Test::Main()" >> $1 + echo "{" >> $1 + echo " TEST_INIT(\"${test}_test\");" >> $1 + echo " TEST_DONE();" >> $1 + echo "}" >> $1 +} + +gen_desc() { + echo "generating '$1' ..." + echo "$test test. Take a look at $test.cpp for details." > $1 +} + +gen_file_list() { + echo "generating '$1' ..." + echo "$test.cpp" > $1 +} + +if [ $# -ne 1 ]; then + echo "usage: $0 <name>" + echo " name: name of the test to create" + exit 1 +fi + +test=$1 +if [ -e $test ]; then + echo "$test already present, don't want to mess it up..." + exit 1 +fi + +echo "creating directory '$test' ..." +mkdir -p $test || exit 1 +cd $test || exit 1 +test=`basename $test` + +gen_ignore_file .cvsignore +gen_project_file fastos.project +gen_source $test.cpp +gen_desc DESC +gen_file_list FILES diff --git a/messagebus/test/src/tests/error/.gitignore b/messagebus/test/src/tests/error/.gitignore new file mode 100644 index 00000000000..20cb631e9e8 --- /dev/null +++ b/messagebus/test/src/tests/error/.gitignore @@ -0,0 +1,15 @@ +*.class +.depend +Makefile +cpp-client +cpp-server +error_test +out.* +pid.* +routing.cfg +slobrok.cfg +/cpp-client-error +/cpp-server-error +messagebus_test_error_test_app +messagebus_test_cpp-client-error_app +messagebus_test_cpp-server-error_app diff --git a/messagebus/test/src/tests/error/CMakeLists.txt b/messagebus/test/src/tests/error/CMakeLists.txt new file mode 100644 index 00000000000..abd089571f5 --- /dev/null +++ b/messagebus/test/src/tests/error/CMakeLists.txt @@ -0,0 +1,17 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_test_error_test_app + SOURCES + error.cpp + DEPENDS +) +vespa_add_test(NAME messagebus_test_error_test_app NO_VALGRIND COMMAND sh error_test.sh) +vespa_add_executable(messagebus_test_cpp-server-error_app + SOURCES + cpp-server.cpp + DEPENDS +) +vespa_add_executable(messagebus_test_cpp-client-error_app + SOURCES + cpp-client.cpp + DEPENDS +) diff --git a/messagebus/test/src/tests/error/DESC b/messagebus/test/src/tests/error/DESC new file mode 100644 index 00000000000..171966761ee --- /dev/null +++ b/messagebus/test/src/tests/error/DESC @@ -0,0 +1,2 @@ +Check that java and cpp messagebus components are able to pass errors +to each other and preserve meaning. diff --git a/messagebus/test/src/tests/error/FILES b/messagebus/test/src/tests/error/FILES new file mode 100644 index 00000000000..571002a917f --- /dev/null +++ b/messagebus/test/src/tests/error/FILES @@ -0,0 +1,8 @@ +error.cpp +out.server.cpp +out.server.java +cpp-client.cpp +cpp-server.cpp +JavaClient.java +JavaServer.java +routing-template.cfg diff --git a/messagebus/test/src/tests/error/JavaClient.java b/messagebus/test/src/tests/error/JavaClient.java new file mode 100644 index 00000000000..e263b3597da --- /dev/null +++ b/messagebus/test/src/tests/error/JavaClient.java @@ -0,0 +1,65 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.test.*; +import com.yahoo.config.*; +import com.yahoo.messagebus.routing.*; +import com.yahoo.messagebus.network.*; +import com.yahoo.messagebus.network.rpc.*; +import com.yahoo.messagebus.network.rpc.test.*; +import java.util.Arrays; +import java.util.logging.*; + +public class JavaClient { + + private static Logger log = Logger.getLogger(JavaClient.class.getName()); + + public static void main(String[] args) { + try { + RPCMessageBus mb = new RPCMessageBus( + Arrays.asList((Protocol)new SimpleProtocol()), + new RPCNetworkParams() + .setIdentity(new Identity("server/java")) + .setSlobrokConfigId("file:slobrok.cfg"), + "file:routing.cfg"); + + Receptor src = new Receptor(); + Message msg = null; + Reply reply = null; + + SourceSession session = mb.getMessageBus().createSourceSession(src, new SourceSessionParams().setTimeout(300)); + for (int i = 0; i < 10; i++) { + msg = new SimpleMessage("test"); + msg.getTrace().setLevel(9); + session.send(msg, "test"); + reply = src.getReply(60); + if (reply == null) { + System.err.println("JAVA-CLIENT: no reply"); + } else { + System.err.println("JAVA-CLIENT:\n" + reply.getTrace()); + if (reply.getNumErrors() == 2) { + break; + } + } + Thread.sleep(1000); + } + if (reply == null) { + System.err.println("JAVA-CLIENT: no reply"); + System.exit(1); + } + if (reply.getNumErrors() != 2 || + reply.getError(0).getCode() != ErrorCode.APP_FATAL_ERROR + 1 || + reply.getError(1).getCode() != ErrorCode.APP_FATAL_ERROR + 2 || + !reply.getError(0).getMessage().equals("ERR 1") || + !reply.getError(1).getMessage().equals("ERR 2")) + { + System.err.printf("JAVA-CLIENT: wrong errors\n"); + System.exit(1); + } + session.destroy(); + mb.destroy(); + } catch (Exception e) { + log.log(Level.SEVERE, "JAVA-CLIENT: Failed", e); + System.exit(1); + } + } +} diff --git a/messagebus/test/src/tests/error/JavaServer.java b/messagebus/test/src/tests/error/JavaServer.java new file mode 100644 index 00000000000..b5321f41fc3 --- /dev/null +++ b/messagebus/test/src/tests/error/JavaServer.java @@ -0,0 +1,47 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.test.*; +import com.yahoo.config.*; +import com.yahoo.messagebus.routing.*; +import com.yahoo.messagebus.network.*; +import com.yahoo.messagebus.network.rpc.*; +import java.util.Arrays; +import java.util.logging.*; + +public class JavaServer implements MessageHandler { + + private static Logger log = Logger.getLogger(JavaServer.class.getName()); + + private DestinationSession session; + + public JavaServer(RPCMessageBus mb) { + session = mb.getMessageBus().createDestinationSession("session", true, this); + } + + public void handleMessage(Message msg) { + Reply reply = new EmptyReply(); + msg.swapState(reply); + reply.addError(new com.yahoo.messagebus.Error(ErrorCode.APP_FATAL_ERROR + 1, "ERR 1")); + reply.addError(new com.yahoo.messagebus.Error(ErrorCode.APP_FATAL_ERROR + 2, "ERR 2")); + session.reply(reply); + } + + public static void main(String[] args) { + try { + RPCMessageBus mb = new RPCMessageBus( + Arrays.asList((Protocol)new SimpleProtocol()), + new RPCNetworkParams() + .setIdentity(new Identity("server/java")) + .setSlobrokConfigId("file:slobrok.cfg"), + "file:routing.cfg"); + JavaServer server = new JavaServer(mb); + System.out.println("java server started"); + while (true) { + Thread.sleep(1000); + } + } catch (Exception e) { + log.log(Level.SEVERE, "JAVA-SERVER: Failed", e); + System.exit(1); + } + } +} diff --git a/messagebus/test/src/tests/error/cpp-client.cpp b/messagebus/test/src/tests/error/cpp-client.cpp new file mode 100644 index 00000000000..4f94a13977c --- /dev/null +++ b/messagebus/test/src/tests/error/cpp-client.cpp @@ -0,0 +1,75 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("cpp-client"); +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/sourcesession.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/rpcmessagebus.h> +#include <vespa/messagebus/errorcode.h> +#include <vespa/messagebus/iprotocol.h> +#include <vespa/messagebus/protocolset.h> +#include <vespa/messagebus/sourcesessionparams.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/vespalib/util/sync.h> + +using namespace mbus; + +class App : public FastOS_Application +{ +public: + int Main(); +}; + +int +App::Main() +{ + RPCMessageBus mb(ProtocolSet().add(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams() + .setIdentity(Identity("server/cpp")) + .setSlobrokConfig("file:slobrok.cfg"), + "file:routing.cfg"); + + Receptor src; + Message::UP msg; + Reply::UP reply; + + SourceSession::UP ss = mb.getMessageBus().createSourceSession(src, SourceSessionParams().setTimeout(300)); + for (int i = 0; i < 10; ++i) { + msg.reset(new SimpleMessage("test")); + msg->getTrace().setLevel(9); + ss->send(std::move(msg), "test"); + reply = src.getReply(600); // 10 minutes timeout + if (reply.get() == 0) { + fprintf(stderr, "CPP-CLIENT: no reply\n"); + } else { + fprintf(stderr, "CPP-CLIENT:\n%s\n", + reply->getTrace().toString().c_str()); + if (reply->getNumErrors() == 2) { + break; + } + } + FastOS_Thread::Sleep(1000); + } + if (reply.get() == 0) { + fprintf(stderr, "CPP-CLIENT: no reply\n"); + return 1; + } + if (reply->getNumErrors() != 2 || + reply->getError(0).getCode() != (ErrorCode::APP_FATAL_ERROR + 1) || + reply->getError(1).getCode() != (ErrorCode::APP_FATAL_ERROR + 2) || + reply->getError(0).getMessage() != "ERR 1" || + reply->getError(1).getMessage() != "ERR 2") + { + fprintf(stderr, "CPP-CLIENT: wrong errors\n"); + return 1; + } + return 0; +} + +int main(int argc, char **argv) { + App app; + return app.Entry(argc, argv); +} diff --git a/messagebus/test/src/tests/error/cpp-server.cpp b/messagebus/test/src/tests/error/cpp-server.cpp new file mode 100644 index 00000000000..2eb929f6ca9 --- /dev/null +++ b/messagebus/test/src/tests/error/cpp-server.cpp @@ -0,0 +1,73 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("cpp-server"); +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/destinationsession.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/rpcmessagebus.h> +#include <vespa/messagebus/iprotocol.h> +#include <vespa/messagebus/protocolset.h> +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/error.h> +#include <vespa/messagebus/errorcode.h> + +using namespace mbus; + +class Server : public IMessageHandler +{ +private: + DestinationSession::UP _session; +public: + Server(MessageBus &bus); + ~Server(); + void handleMessage(Message::UP msg); +}; + +Server::Server(MessageBus &bus) + : _session(bus.createDestinationSession("session", true, *this)) +{ + fprintf(stderr, "cpp server started\n"); +} + +Server::~Server() +{ + _session.reset(); +} + +void +Server::handleMessage(Message::UP msg) { + Reply::UP reply(new EmptyReply()); + msg->swapState(*reply); + reply->addError(Error(ErrorCode::APP_FATAL_ERROR + 1, "ERR 1")); + reply->addError(Error(ErrorCode::APP_FATAL_ERROR + 2, "ERR 2")); + _session->reply(std::move(reply)); +} + +class App : public FastOS_Application +{ +public: + int Main(); +}; + +int +App::Main() +{ + RPCMessageBus mb(ProtocolSet().add(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams() + .setIdentity(Identity("server/cpp")) + .setSlobrokConfig("file:slobrok.cfg"), + "file:routing.cfg"); + Server server(mb.getMessageBus()); + while (true) { + FastOS_Thread::Sleep(1000); + } + return 0; +} + +int main(int argc, char **argv) { + App app; + return app.Entry(argc, argv); +} diff --git a/messagebus/test/src/tests/error/ctl.sh b/messagebus/test/src/tests/error/ctl.sh new file mode 100755 index 00000000000..ea969749808 --- /dev/null +++ b/messagebus/test/src/tests/error/ctl.sh @@ -0,0 +1,3 @@ +#!/bin/sh +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +sh ../../binref/progctl.sh progdefs.sh "$@" diff --git a/messagebus/test/src/tests/error/error.cpp b/messagebus/test/src/tests/error/error.cpp new file mode 100644 index 00000000000..9b01e5d61d0 --- /dev/null +++ b/messagebus/test/src/tests/error/error.cpp @@ -0,0 +1,45 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("error_test"); +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/vespalib/util/stringfmt.h> + +using namespace mbus; +using vespalib::make_string; + +TEST_SETUP(Test); + +int +Test::Main() +{ + TEST_INIT("error_test"); + Slobrok slobrok; + { // Make slobrok config + EXPECT_TRUE(system("echo slobrok[1] > slobrok.cfg") == 0); + EXPECT_TRUE(system(make_string("echo 'slobrok[0].connectionspec tcp/localhost:%d' " + ">> slobrok.cfg", slobrok.port()).c_str()) == 0); + } + { // CPP SERVER + { // Make routing config + EXPECT_TRUE(system("cat routing-template.cfg | sed 's#session#cpp/session#' > routing.cfg") == 0); + } + fprintf(stderr, "STARTING CPP-SERVER\n"); + EXPECT_TRUE(system("sh ctl.sh start server cpp") == 0); + EXPECT_TRUE(system("./messagebus_test_cpp-client-error_app") == 0); + EXPECT_TRUE(system("../../binref/runjava JavaClient") == 0); + EXPECT_TRUE(system("sh ctl.sh stop server cpp") == 0); + } + { // JAVA SERVER + { // Make routing config + EXPECT_TRUE(system("cat routing-template.cfg | sed 's#session#java/session#' > routing.cfg") == 0); + } + fprintf(stderr, "STARTING JAVA-SERVER\n"); + EXPECT_TRUE(system("sh ctl.sh start server java") == 0); + EXPECT_TRUE(system("./messagebus_test_cpp-client-error_app") == 0); + EXPECT_TRUE(system("../../binref/runjava JavaClient") == 0); + EXPECT_TRUE(system("sh ctl.sh stop server java") == 0); + } + TEST_DONE(); +} diff --git a/messagebus/test/src/tests/error/error_test.sh b/messagebus/test/src/tests/error/error_test.sh new file mode 100755 index 00000000000..bd9ea35643d --- /dev/null +++ b/messagebus/test/src/tests/error/error_test.sh @@ -0,0 +1,6 @@ +#!/bin/bash +. ../../binref/env.sh + +$BINREF/compilejava JavaServer.java +$BINREF/compilejava JavaClient.java +VESPA_LOG_LEVEL='all -spam' ./messagebus_test_error_test_app diff --git a/messagebus/test/src/tests/error/progdefs.sh b/messagebus/test/src/tests/error/progdefs.sh new file mode 100644 index 00000000000..2f6f37a9425 --- /dev/null +++ b/messagebus/test/src/tests/error/progdefs.sh @@ -0,0 +1,3 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +prog server cpp "" "./messagebus_test_cpp-server-error_app" +prog server java "" "../../binref/runjava JavaServer" diff --git a/messagebus/test/src/tests/error/routing-template.cfg b/messagebus/test/src/tests/error/routing-template.cfg new file mode 100644 index 00000000000..4b938c9cc82 --- /dev/null +++ b/messagebus/test/src/tests/error/routing-template.cfg @@ -0,0 +1,11 @@ +routingtable[1] +routingtable[0].protocol "Simple" +routingtable[0].hop[1] +routingtable[0].hop[0].name "server" +routingtable[0].hop[0].selector "server/session" +routingtable[0].hop[0].recipient[1] +routingtable[0].hop[0].recipient[0] "server/session" +routingtable[0].route[1] +routingtable[0].route[0].name "test" +routingtable[0].route[0].hop[1] +routingtable[0].route[0].hop[0] "server" diff --git a/messagebus/test/src/tests/errorcodes/.gitignore b/messagebus/test/src/tests/errorcodes/.gitignore new file mode 100644 index 00000000000..13957172a38 --- /dev/null +++ b/messagebus/test/src/tests/errorcodes/.gitignore @@ -0,0 +1,7 @@ +.depend +DumpCodes.class +Makefile +cpp-dump.txt +dumpcodes +java-dump.txt +messagebus_test_dumpcodes_app diff --git a/messagebus/test/src/tests/errorcodes/CMakeLists.txt b/messagebus/test/src/tests/errorcodes/CMakeLists.txt new file mode 100644 index 00000000000..3f08783b363 --- /dev/null +++ b/messagebus/test/src/tests/errorcodes/CMakeLists.txt @@ -0,0 +1,7 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_test_dumpcodes_app + SOURCES + dumpcodes.cpp + DEPENDS +) +vespa_add_test(NAME messagebus_test_dumpcodes_app NO_VALGRIND COMMAND sh errorcodes_test.sh) diff --git a/messagebus/test/src/tests/errorcodes/DESC b/messagebus/test/src/tests/errorcodes/DESC new file mode 100644 index 00000000000..103ebb4698f --- /dev/null +++ b/messagebus/test/src/tests/errorcodes/DESC @@ -0,0 +1,2 @@ +A small test to check that error codes are equal in the Java and C++ +implementations. diff --git a/messagebus/test/src/tests/errorcodes/DumpCodes.java b/messagebus/test/src/tests/errorcodes/DumpCodes.java new file mode 100644 index 00000000000..8eb97813404 --- /dev/null +++ b/messagebus/test/src/tests/errorcodes/DumpCodes.java @@ -0,0 +1,51 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +import com.yahoo.messagebus.ErrorCode; + +public class DumpCodes { + + private static void dump(String desc, int value) { + String name = ErrorCode.getName(value); + System.out.printf("%s => %d => \"%s\"\n", desc, value, + name != null ? name : ""); + } + + public static void main(String[] args) { + dump("NONE", ErrorCode.NONE); + + dump("SEND_QUEUE_FULL", ErrorCode.SEND_QUEUE_FULL); + dump("NO_ADDRESS_FOR_SERVICE", ErrorCode.NO_ADDRESS_FOR_SERVICE); + dump("CONNECTION_ERROR", ErrorCode.CONNECTION_ERROR); + dump("UNKNOWN_SESSION", ErrorCode.UNKNOWN_SESSION); + dump("SESSION_BUSY", ErrorCode.SESSION_BUSY); + dump("SEND_ABORTED", ErrorCode.SEND_ABORTED); + dump("HANDSHAKE_FAILED", ErrorCode.HANDSHAKE_FAILED); + dump("first unused TRANSIENT_ERROR", ErrorCode.TRANSIENT_ERROR + 8); + + dump("SEND_QUEUE_CLOSED", ErrorCode.SEND_QUEUE_CLOSED); + dump("ILLEGAL_ROUTE", ErrorCode.ILLEGAL_ROUTE); + dump("NO_SERVICES_FOR_ROUTE", ErrorCode.NO_SERVICES_FOR_ROUTE); + dump("SERVICE_OOS", ErrorCode.SERVICE_OOS); + dump("ENCODE_ERROR", ErrorCode.ENCODE_ERROR); + dump("NETWORK_ERROR", ErrorCode.NETWORK_ERROR); + dump("UNKNOWN_PROTOCOL", ErrorCode.UNKNOWN_PROTOCOL); + dump("DECODE_ERROR", ErrorCode.DECODE_ERROR); + dump("TIMEOUT", ErrorCode.TIMEOUT); + dump("INCOMPATIBLE_VERSION", ErrorCode.INCOMPATIBLE_VERSION); + dump("UNKNOWN_POLICY", ErrorCode.UNKNOWN_POLICY); + dump("NETWORK_SHUTDOWN", ErrorCode.NETWORK_SHUTDOWN); + dump("POLICY_ERROR", ErrorCode.POLICY_ERROR); + dump("SEQUENCE_ERROR", ErrorCode.SEQUENCE_ERROR); + dump("first unused FATAL_ERROR", ErrorCode.FATAL_ERROR + 15); + + dump("max UNKNOWN below", ErrorCode.TRANSIENT_ERROR - 1); + dump("min TRANSIENT_ERROR", ErrorCode.TRANSIENT_ERROR); + dump("max TRANSIENT_ERROR", ErrorCode.TRANSIENT_ERROR + 49999); + dump("min APP_TRANSIENT_ERROR", ErrorCode.APP_TRANSIENT_ERROR); + dump("max APP_TRANSIENT_ERROR", ErrorCode.APP_TRANSIENT_ERROR + 49999); + dump("min FATAL_ERROR", ErrorCode.FATAL_ERROR); + dump("max FATAL_ERROR", ErrorCode.FATAL_ERROR + 49999); + dump("min APP_FATAL_ERROR", ErrorCode.APP_FATAL_ERROR); + dump("max APP_FATAL_ERROR", ErrorCode.APP_FATAL_ERROR + 49999); + dump("min UNKNOWN above", ErrorCode.ERROR_LIMIT); + } +} diff --git a/messagebus/test/src/tests/errorcodes/FILES b/messagebus/test/src/tests/errorcodes/FILES new file mode 100644 index 00000000000..766402133fb --- /dev/null +++ b/messagebus/test/src/tests/errorcodes/FILES @@ -0,0 +1,5 @@ +dumpcodes.cpp +DumpCodes.java +ref-dump.txt +cpp-dump.txt +java-dump.txt diff --git a/messagebus/test/src/tests/errorcodes/dumpcodes.cpp b/messagebus/test/src/tests/errorcodes/dumpcodes.cpp new file mode 100644 index 00000000000..121d8585726 --- /dev/null +++ b/messagebus/test/src/tests/errorcodes/dumpcodes.cpp @@ -0,0 +1,70 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("dumpcodes"); +#include <vespa/messagebus/errorcode.h> +#include <string> + +using namespace mbus; + +class App : public FastOS_Application +{ +public: + void dump(const std::string &desc, uint32_t value); + int Main(); +}; + +void +App::dump(const std::string &desc, uint32_t value) +{ + fprintf(stdout, "%s => %u => \"%s\"\n", desc.c_str(), value, + ErrorCode::getName(value).c_str()); +} + +int +App::Main() +{ + dump("NONE", ErrorCode::NONE); + + dump("SEND_QUEUE_FULL", ErrorCode::SEND_QUEUE_FULL); + dump("NO_ADDRESS_FOR_SERVICE", ErrorCode::NO_ADDRESS_FOR_SERVICE); + dump("CONNECTION_ERROR", ErrorCode::CONNECTION_ERROR); + dump("UNKNOWN_SESSION", ErrorCode::UNKNOWN_SESSION); + dump("SESSION_BUSY", ErrorCode::SESSION_BUSY); + dump("SEND_ABORTED", ErrorCode::SEND_ABORTED); + dump("HANDSHAKE_FAILED", ErrorCode::HANDSHAKE_FAILED); + dump("first unused TRANSIENT_ERROR", ErrorCode::TRANSIENT_ERROR + 8); + + dump("SEND_QUEUE_CLOSED", ErrorCode::SEND_QUEUE_CLOSED); + dump("ILLEGAL_ROUTE", ErrorCode::ILLEGAL_ROUTE); + dump("NO_SERVICES_FOR_ROUTE", ErrorCode::NO_SERVICES_FOR_ROUTE); + dump("SERVICE_OOS", ErrorCode::SERVICE_OOS); + dump("ENCODE_ERROR", ErrorCode::ENCODE_ERROR); + dump("NETWORK_ERROR", ErrorCode::NETWORK_ERROR); + dump("UNKNOWN_PROTOCOL", ErrorCode::UNKNOWN_PROTOCOL); + dump("DECODE_ERROR", ErrorCode::DECODE_ERROR); + dump("TIMEOUT", ErrorCode::TIMEOUT); + dump("INCOMPATIBLE_VERSION", ErrorCode::INCOMPATIBLE_VERSION); + dump("UNKNOWN_POLICY", ErrorCode::UNKNOWN_POLICY); + dump("NETWORK_SHUTDOWN", ErrorCode::NETWORK_SHUTDOWN); + dump("POLICY_ERROR", ErrorCode::POLICY_ERROR); + dump("SEQUENCE_ERROR", ErrorCode::SEQUENCE_ERROR); + dump("first unused FATAL_ERROR", ErrorCode::FATAL_ERROR + 15); + + dump("max UNKNOWN below", ErrorCode::TRANSIENT_ERROR - 1); + dump("min TRANSIENT_ERROR", ErrorCode::TRANSIENT_ERROR); + dump("max TRANSIENT_ERROR", ErrorCode::TRANSIENT_ERROR + 49999); + dump("min APP_TRANSIENT_ERROR", ErrorCode::APP_TRANSIENT_ERROR); + dump("max APP_TRANSIENT_ERROR", ErrorCode::APP_TRANSIENT_ERROR + 49999); + dump("min FATAL_ERROR", ErrorCode::FATAL_ERROR); + dump("max FATAL_ERROR", ErrorCode::FATAL_ERROR + 49999); + dump("min APP_FATAL_ERROR", ErrorCode::APP_FATAL_ERROR); + dump("max APP_FATAL_ERROR", ErrorCode::APP_FATAL_ERROR + 49999); + dump("min UNKNOWN above", ErrorCode::ERROR_LIMIT); + return 0; +} + +int main(int argc, char **argv) { + App app; + return app.Entry(argc, argv); +} diff --git a/messagebus/test/src/tests/errorcodes/errorcodes_test.sh b/messagebus/test/src/tests/errorcodes/errorcodes_test.sh new file mode 100644 index 00000000000..186de0a5033 --- /dev/null +++ b/messagebus/test/src/tests/errorcodes/errorcodes_test.sh @@ -0,0 +1,9 @@ +#!/bin/bash +. ../../binref/env.sh + +$BINREF/compilejava DumpCodes.java + +./messagebus_test_dumpcodes_app > cpp-dump.txt +$BINREF/runjava DumpCodes > java-dump.txt +diff -u ref-dump.txt cpp-dump.txt +diff -u ref-dump.txt java-dump.txt diff --git a/messagebus/test/src/tests/errorcodes/ref-dump.txt b/messagebus/test/src/tests/errorcodes/ref-dump.txt new file mode 100644 index 00000000000..b8038816897 --- /dev/null +++ b/messagebus/test/src/tests/errorcodes/ref-dump.txt @@ -0,0 +1,34 @@ +NONE => 0 => "NONE" +SEND_QUEUE_FULL => 100001 => "SEND_QUEUE_FULL" +NO_ADDRESS_FOR_SERVICE => 100002 => "NO_ADDRESS_FOR_SERVICE" +CONNECTION_ERROR => 100003 => "CONNECTION_ERROR" +UNKNOWN_SESSION => 100004 => "UNKNOWN_SESSION" +SESSION_BUSY => 100005 => "SESSION_BUSY" +SEND_ABORTED => 100006 => "SEND_ABORTED" +HANDSHAKE_FAILED => 100007 => "HANDSHAKE_FAILED" +first unused TRANSIENT_ERROR => 100008 => "UNKNOWN(100008)" +SEND_QUEUE_CLOSED => 200001 => "SEND_QUEUE_CLOSED" +ILLEGAL_ROUTE => 200002 => "ILLEGAL_ROUTE" +NO_SERVICES_FOR_ROUTE => 200003 => "NO_SERVICES_FOR_ROUTE" +SERVICE_OOS => 200004 => "SERVICE_OOS" +ENCODE_ERROR => 200005 => "ENCODE_ERROR" +NETWORK_ERROR => 200006 => "NETWORK_ERROR" +UNKNOWN_PROTOCOL => 200007 => "UNKNOWN_PROTOCOL" +DECODE_ERROR => 200008 => "DECODE_ERROR" +TIMEOUT => 200009 => "TIMEOUT" +INCOMPATIBLE_VERSION => 200010 => "INCOMPATIBLE_VERSION" +UNKNOWN_POLICY => 200011 => "UNKNOWN_POLICY" +NETWORK_SHUTDOWN => 200012 => "NETWORK_SHUTDOWN" +POLICY_ERROR => 200013 => "POLICY_ERROR" +SEQUENCE_ERROR => 200014 => "SEQUENCE_ERROR" +first unused FATAL_ERROR => 200015 => "UNKNOWN(200015)" +max UNKNOWN below => 99999 => "UNKNOWN(99999)" +min TRANSIENT_ERROR => 100000 => "TRANSIENT_ERROR" +max TRANSIENT_ERROR => 149999 => "UNKNOWN(149999)" +min APP_TRANSIENT_ERROR => 150000 => "APP_TRANSIENT_ERROR" +max APP_TRANSIENT_ERROR => 199999 => "UNKNOWN(199999)" +min FATAL_ERROR => 200000 => "FATAL_ERROR" +max FATAL_ERROR => 249999 => "UNKNOWN(249999)" +min APP_FATAL_ERROR => 250000 => "APP_FATAL_ERROR" +max APP_FATAL_ERROR => 299999 => "UNKNOWN(299999)" +min UNKNOWN above => 300000 => "UNKNOWN(300000)" diff --git a/messagebus/test/src/tests/speed/.gitignore b/messagebus/test/src/tests/speed/.gitignore new file mode 100644 index 00000000000..326da75ebb6 --- /dev/null +++ b/messagebus/test/src/tests/speed/.gitignore @@ -0,0 +1,15 @@ +*.class +.depend +Makefile +cpp-client +cpp-server +out.* +pid.* +routing.cfg +slobrok.cfg +speed_test +cpp-client-speed +cpp-server-speed +messagebus_test_speed_test_app +messagebus_test_cpp-client-speed_app +messagebus_test_cpp-server-speed_app diff --git a/messagebus/test/src/tests/speed/CMakeLists.txt b/messagebus/test/src/tests/speed/CMakeLists.txt new file mode 100644 index 00000000000..8e1018ec07c --- /dev/null +++ b/messagebus/test/src/tests/speed/CMakeLists.txt @@ -0,0 +1,17 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_test_speed_test_app + SOURCES + speed.cpp + DEPENDS +) +vespa_add_test(NAME messagebus_test_speed_test_app COMMAND messagebus_test_speed_test_app BENCHMARK) +vespa_add_executable(messagebus_test_cpp-server-speed_app + SOURCES + cpp-server.cpp + DEPENDS +) +vespa_add_executable(messagebus_test_cpp-client-speed_app + SOURCES + cpp-client.cpp + DEPENDS +) diff --git a/messagebus/test/src/tests/speed/DESC b/messagebus/test/src/tests/speed/DESC new file mode 100644 index 00000000000..10734957438 --- /dev/null +++ b/messagebus/test/src/tests/speed/DESC @@ -0,0 +1,4 @@ +This is a simple test that gives a rough idea of the inherent overhead +in messagebus. It sends simple messages back and forth with the +simplest routing setup possible. This test also tests that messagebus +works across Java and C++. diff --git a/messagebus/test/src/tests/speed/FILES b/messagebus/test/src/tests/speed/FILES new file mode 100644 index 00000000000..09f0a5ec1d3 --- /dev/null +++ b/messagebus/test/src/tests/speed/FILES @@ -0,0 +1,8 @@ +speed.cpp +out.server.cpp +out.server.java +cpp-client.cpp +cpp-server.cpp +JavaClient.java +JavaServer.java +routing-template.cfg diff --git a/messagebus/test/src/tests/speed/JavaClient.java b/messagebus/test/src/tests/speed/JavaClient.java new file mode 100644 index 00000000000..b905ab07e91 --- /dev/null +++ b/messagebus/test/src/tests/speed/JavaClient.java @@ -0,0 +1,137 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.test.*; +import com.yahoo.config.*; +import com.yahoo.messagebus.routing.*; +import com.yahoo.messagebus.network.*; +import com.yahoo.messagebus.network.rpc.*; +import java.util.Arrays; +import java.util.logging.*; + +public class JavaClient implements ReplyHandler { + + private static Logger log = Logger.getLogger(JavaClient.class.getName()); + + private static class Counts { + public int okCnt = 0; + public int failCnt = 0; + Counts() {} + Counts(int okCnt, int failCnt) { + this.okCnt = okCnt; + this.failCnt = failCnt; + } + } + + private SourceSession session; + private Counts counts = new Counts(); + private static long mySeq = 100000; + + public JavaClient(RPCMessageBus mb) { + session = mb.getMessageBus().createSourceSession(this, new SourceSessionParams().setTimeout(30)); + } + + public synchronized Counts sample() { + return new Counts(counts.okCnt, counts.failCnt); + } + + public void send() { + send(++mySeq); + } + + public void send(long seq) { + session.send(new MyMessage(seq), "test"); + } + + public void handleReply(Reply reply) { + if ((reply.getProtocol() == SimpleProtocol.NAME) + && (reply.getType() == SimpleProtocol.REPLY) + && (((SimpleReply)reply).getValue().equals("OK"))) + { + synchronized (this) { + counts.okCnt++; + } + } else { + synchronized (this) { + counts.failCnt++; + } + } + try { + send(); + } catch (IllegalStateException ignore) {} // handle paranoia for shutdown source sessions + } + + public void shutdown() { + session.destroy(); + } + + public static void main(String[] args) { + try { + RPCMessageBus mb = new RPCMessageBus( + new MessageBusParams() + .setRetryPolicy(new RetryTransientErrorsPolicy().setBaseDelay(0.1)) + .addProtocol(new SimpleProtocol()), + new RPCNetworkParams() + .setIdentity(new Identity("server/java")) + .setSlobrokConfigId("file:slobrok.cfg"), + "file:routing.cfg"); + JavaClient client = new JavaClient(mb); + + // let the system 'warm up' + Thread.sleep(5000); + + // inject messages into the feedback loop + for (int i = 0; i < 1024; ++i) { + client.send(i); + } + + // let the system 'warm up' + Thread.sleep(5000); + + long start; + long stop; + Counts before; + Counts after; + + start = System.currentTimeMillis(); + before = client.sample(); + Thread.sleep(10000); // Benchmark time + stop = System.currentTimeMillis(); + after = client.sample(); + stop -= start; + double time = (double)stop; + double msgCnt = (double)(after.okCnt - before.okCnt); + double throughput = (msgCnt / time) * 1000.0; + System.out.printf("JAVA-CLIENT: %g msg/s\n", throughput); + client.shutdown(); + mb.destroy(); + if (after.failCnt > before.failCnt) { + System.err.printf("JAVA-CLIENT: FAILED (%d -> %d)\n", + before.failCnt, after.failCnt); + System.exit(1); + } + } catch (Exception e) { + log.log(Level.SEVERE, "JAVA-CLIENT: Failed", e); + System.exit(1); + } + } + + private static class MyMessage extends SimpleMessage { + + final long seqId; + + MyMessage(long seqId) { + super("message"); + this.seqId = seqId; + } + + @Override + public boolean hasSequenceId() { + return true; + } + + @Override + public long getSequenceId() { + return seqId; + } + } +} diff --git a/messagebus/test/src/tests/speed/JavaServer.java b/messagebus/test/src/tests/speed/JavaServer.java new file mode 100644 index 00000000000..afec6dcdba2 --- /dev/null +++ b/messagebus/test/src/tests/speed/JavaServer.java @@ -0,0 +1,54 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.test.*; +import com.yahoo.config.*; +import com.yahoo.messagebus.routing.*; +import com.yahoo.messagebus.network.*; +import com.yahoo.messagebus.network.rpc.*; +import java.util.Arrays; +import java.util.logging.*; + +public class JavaServer implements MessageHandler { + + private static Logger log = Logger.getLogger(JavaServer.class.getName()); + + private DestinationSession session; + + public JavaServer(RPCMessageBus mb) { + session = mb.getMessageBus().createDestinationSession("session", true, this); + } + + public void handleMessage(Message msg) { + if ((msg.getProtocol() == SimpleProtocol.NAME) + && (msg.getType() == SimpleProtocol.MESSAGE) + && (((SimpleMessage)msg).getValue().equals("message"))) + { + Reply reply = new SimpleReply("OK"); + msg.swapState(reply); + session.reply(reply); + } else { + Reply reply = new SimpleReply("FAIL"); + msg.swapState(reply); + session.reply(reply); + } + } + + public static void main(String[] args) { + try { + RPCMessageBus mb = new RPCMessageBus( + Arrays.asList((Protocol)new SimpleProtocol()), + new RPCNetworkParams() + .setIdentity(new Identity("server/java")) + .setSlobrokConfigId("file:slobrok.cfg"), + "file:routing.cfg"); + JavaServer server = new JavaServer(mb); + System.out.println("java server started"); + while (true) { + Thread.sleep(1000); + } + } catch (Exception e) { + log.log(Level.SEVERE, "JAVA-SERVER: Failed", e); + System.exit(1); + } + } +} diff --git a/messagebus/test/src/tests/speed/cpp-client.cpp b/messagebus/test/src/tests/speed/cpp-client.cpp new file mode 100644 index 00000000000..c0c9d621a20 --- /dev/null +++ b/messagebus/test/src/tests/speed/cpp-client.cpp @@ -0,0 +1,146 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("cpp-client"); +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/routing/retrytransienterrorspolicy.h> +#include <vespa/messagebus/rpcmessagebus.h> +#include <vespa/messagebus/sourcesession.h> +#include <vespa/messagebus/sourcesessionparams.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/vespalib/util/sync.h> + +using namespace mbus; + +class Client : public IReplyHandler +{ +private: + vespalib::Lock _lock; + uint32_t _okCnt; + uint32_t _failCnt; + SourceSession::UP _session; + static uint64_t _seq; +public: + Client(MessageBus &bus, const SourceSessionParams ¶ms); + ~Client(); + void send(); + void send(uint64_t seq); + void sample(uint32_t &okCnt, uint32_t &failCnt); + void handleReply(Reply::UP reply); +}; +uint64_t Client::_seq = 100000; + +Client::Client(MessageBus &bus, const SourceSessionParams ¶ms) + : _lock(), + _okCnt(0), + _failCnt(0), + _session(bus.createSourceSession(*this, params)) +{ +} + +Client::~Client() +{ + _session->close(); +} + +void +Client::send() { + send(++_seq); +} + +void +Client::send(uint64_t seq) { + Message::UP msg(new SimpleMessage("message", true, seq)); + _session->send(std::move(msg), "test"); +} + +void +Client::sample(uint32_t &okCnt, uint32_t &failCnt) { + vespalib::LockGuard guard(_lock); + okCnt = _okCnt; + failCnt = _failCnt; +} + +void +Client::handleReply(Reply::UP reply) { + if ((reply->getProtocol() == SimpleProtocol::NAME) + && (reply->getType() == SimpleProtocol::REPLY) + && (static_cast<SimpleReply&>(*reply).getValue() == "OK")) + { + vespalib::LockGuard guard(_lock); + ++_okCnt; + } else { + fprintf(stderr, "BAD REPLY\n"); + for (uint32_t i = 0; i < reply->getNumErrors(); ++i) { + fprintf(stderr, "ERR[%d]: code=%d, msg=%s\n", i, + reply->getError(i).getCode(), + reply->getError(i).getMessage().c_str()); + } + vespalib::LockGuard guard(_lock); + ++_failCnt; + } + send(); +} + +class App : public FastOS_Application +{ +public: + int Main(); +}; + +int +App::Main() +{ + RetryTransientErrorsPolicy::SP retryPolicy(new RetryTransientErrorsPolicy()); + retryPolicy->setBaseDelay(0.1); + RPCMessageBus mb(MessageBusParams().setRetryPolicy(retryPolicy).addProtocol(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams().setIdentity(Identity("server/cpp")).setSlobrokConfig("file:slobrok.cfg"), + "file:routing.cfg"); + Client client(mb.getMessageBus(), SourceSessionParams().setTimeout(30)); + + // let the system 'warm up' + FastOS_Thread::Sleep(5000); + + // inject messages into the feedback loop + for (uint32_t i = 0; i < 1024; ++i) { + client.send(i); + } + + // let the system 'warm up' + FastOS_Thread::Sleep(5000); + + FastOS_Time start; + FastOS_Time stop; + uint32_t okBefore = 0; + uint32_t okAfter = 0; + uint32_t failBefore = 0; + uint32_t failAfter = 0; + + start.SetNow(); + client.sample(okBefore, failBefore); + FastOS_Thread::Sleep(10000); // Benchmark time + stop.SetNow(); + client.sample(okAfter, failAfter); + stop -= start; + double time = stop.MilliSecs(); + double msgCnt = (double)(okAfter - okBefore); + double throughput = (msgCnt / time) * 1000.0; + fprintf(stdout, "CPP-CLIENT: %g msg/s\n", throughput); + if (failAfter > failBefore) { + fprintf(stderr, "CPP-CLIENT: FAILED (%d -> %d)\n", failBefore, failAfter); + return 1; + } + return 0; +} + +int main(int argc, char **argv) { + fprintf(stderr, "started '%s'\n", argv[0]); + fflush(stderr); + App app; + int r = app.Entry(argc, argv); + fprintf(stderr, "stopping '%s'\n", argv[0]); + fflush(stderr); + return r; +} diff --git a/messagebus/test/src/tests/speed/cpp-server.cpp b/messagebus/test/src/tests/speed/cpp-server.cpp new file mode 100644 index 00000000000..c2cd9bf262a --- /dev/null +++ b/messagebus/test/src/tests/speed/cpp-server.cpp @@ -0,0 +1,77 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("cpp-server"); +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/destinationsession.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/rpcmessagebus.h> +#include <vespa/messagebus/iprotocol.h> +#include <vespa/messagebus/protocolset.h> + +using namespace mbus; + +class Server : public IMessageHandler +{ +private: + DestinationSession::UP _session; +public: + Server(MessageBus &bus); + ~Server(); + void handleMessage(Message::UP msg); +}; + +Server::Server(MessageBus &bus) + : _session(bus.createDestinationSession("session", true, *this)) +{ + fprintf(stderr, "cpp server started\n"); +} + +Server::~Server() +{ + _session.reset(); +} + +void +Server::handleMessage(Message::UP msg) { + if ((msg->getProtocol() == SimpleProtocol::NAME) + && (msg->getType() == SimpleProtocol::MESSAGE) + && (static_cast<SimpleMessage&>(*msg).getValue() == "message")) + { + Reply::UP reply(new SimpleReply("OK")); + msg->swapState(*reply); + _session->reply(std::move(reply)); + } else { + Reply::UP reply(new SimpleReply("FAIL")); + msg->swapState(*reply); + _session->reply(std::move(reply)); + } +} + +class App : public FastOS_Application +{ +public: + int Main(); +}; + +int +App::Main() +{ + RPCMessageBus mb(ProtocolSet().add(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams() + .setIdentity(Identity("server/cpp")) + .setSlobrokConfig("file:slobrok.cfg"), + "file:routing.cfg"); + Server server(mb.getMessageBus()); + while (true) { + FastOS_Thread::Sleep(1000); + } + return 0; +} + +int main(int argc, char **argv) { + App app; + return app.Entry(argc, argv); +} diff --git a/messagebus/test/src/tests/speed/ctl.sh b/messagebus/test/src/tests/speed/ctl.sh new file mode 100755 index 00000000000..ea969749808 --- /dev/null +++ b/messagebus/test/src/tests/speed/ctl.sh @@ -0,0 +1,3 @@ +#!/bin/sh +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +sh ../../binref/progctl.sh progdefs.sh "$@" diff --git a/messagebus/test/src/tests/speed/progdefs.sh b/messagebus/test/src/tests/speed/progdefs.sh new file mode 100644 index 00000000000..4e0390142cf --- /dev/null +++ b/messagebus/test/src/tests/speed/progdefs.sh @@ -0,0 +1,3 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +prog server cpp "" "./messagebus_test_cpp-server-speed_app" +prog server java "" "../../binref/runjava JavaServer" diff --git a/messagebus/test/src/tests/speed/routing-template.cfg b/messagebus/test/src/tests/speed/routing-template.cfg new file mode 100644 index 00000000000..4b938c9cc82 --- /dev/null +++ b/messagebus/test/src/tests/speed/routing-template.cfg @@ -0,0 +1,11 @@ +routingtable[1] +routingtable[0].protocol "Simple" +routingtable[0].hop[1] +routingtable[0].hop[0].name "server" +routingtable[0].hop[0].selector "server/session" +routingtable[0].hop[0].recipient[1] +routingtable[0].hop[0].recipient[0] "server/session" +routingtable[0].route[1] +routingtable[0].route[0].name "test" +routingtable[0].route[0].hop[1] +routingtable[0].route[0].hop[0] "server" diff --git a/messagebus/test/src/tests/speed/speed.cpp b/messagebus/test/src/tests/speed/speed.cpp new file mode 100644 index 00000000000..31ea419ce5c --- /dev/null +++ b/messagebus/test/src/tests/speed/speed.cpp @@ -0,0 +1,51 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("speed_test"); +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/vespalib/util/stringfmt.h> + +using namespace mbus; +using vespalib::make_string; + +TEST_SETUP(Test); + +int +Test::Main() +{ + TEST_INIT("speed_test"); + Slobrok slobrok; + { // Make slobrok config + EXPECT_EQUAL(system("echo slobrok[1] > slobrok.cfg"), 0); + EXPECT_EQUAL(system(make_string("echo 'slobrok[0].connectionspec tcp/localhost:%d' " + ">> slobrok.cfg", slobrok.port()).c_str()), 0); + } + { // CPP SERVER + { // Make routing config + EXPECT_EQUAL(system("cat routing-template.cfg | sed 's#session#cpp/session#' > routing.cfg"), 0); + } + fprintf(stderr, "STARTING CPP-SERVER\n"); + EXPECT_EQUAL(system("sh ctl.sh start server cpp"), 0); + fprintf(stderr, "STARTING CPP-CLIENT\n"); + EXPECT_EQUAL(system("./messagebus_test_cpp-client-speed_app"), 0); + fprintf(stderr, "STARTING JAVA-CLIENT\n"); + EXPECT_EQUAL(system("../../binref/runjava JavaClient"), 0); + fprintf(stderr, "STOPPING\n"); + EXPECT_EQUAL(system("sh ctl.sh stop server cpp"), 0); + } + { // JAVA SERVER + { // Make routing config + EXPECT_EQUAL(system("cat routing-template.cfg | sed 's#session#java/session#' > routing.cfg"), 0); + } + fprintf(stderr, "STARTING JAVA-SERVER\n"); + EXPECT_EQUAL(system("sh ctl.sh start server java"), 0); + fprintf(stderr, "STARTING CPP-CLIENT\n"); + EXPECT_EQUAL(system("./messagebus_test_cpp-client-speed_app"), 0); + fprintf(stderr, "STARTING JAVA-CLIENT\n"); + EXPECT_EQUAL(system("../../binref/runjava JavaClient"), 0); + fprintf(stderr, "STOPPING\n"); + EXPECT_EQUAL(system("sh ctl.sh stop server java"), 0); + } + TEST_DONE(); +} diff --git a/messagebus/test/src/tests/speed/speed_test.sh b/messagebus/test/src/tests/speed/speed_test.sh new file mode 100644 index 00000000000..f9789961fa7 --- /dev/null +++ b/messagebus/test/src/tests/speed/speed_test.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +. ../../binref/env.sh + +$BINREF/compilejava JavaServer.java +$BINREF/compilejava JavaClient.java + +(ulimit -c; ulimit -H -c; ulimit -c unlimited; ./messagebus_test_speed_test_app) diff --git a/messagebus/test/src/tests/trace/.gitignore b/messagebus/test/src/tests/trace/.gitignore new file mode 100644 index 00000000000..1be907ec2f8 --- /dev/null +++ b/messagebus/test/src/tests/trace/.gitignore @@ -0,0 +1,12 @@ +*.class +.depend +Makefile +cpp-server +out.* +pid.* +routing.cfg +slobrok.cfg +trace_test +/cpp-server-trace +messagebus_test_trace_test_app +messagebus_test_cpp-server-trace_app diff --git a/messagebus/test/src/tests/trace/CMakeLists.txt b/messagebus/test/src/tests/trace/CMakeLists.txt new file mode 100644 index 00000000000..5cebc9ae667 --- /dev/null +++ b/messagebus/test/src/tests/trace/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(messagebus_test_trace_test_app + SOURCES + trace.cpp + DEPENDS +) +vespa_add_test(NAME messagebus_test_trace_test_app NO_VALGRIND COMMAND sh trace_test.sh) +vespa_add_executable(messagebus_test_cpp-server-trace_app + SOURCES + cpp-server.cpp + DEPENDS +) diff --git a/messagebus/test/src/tests/trace/DESC b/messagebus/test/src/tests/trace/DESC new file mode 100644 index 00000000000..452e75aefea --- /dev/null +++ b/messagebus/test/src/tests/trace/DESC @@ -0,0 +1 @@ +trace test. Take a look at trace.cpp for details. diff --git a/messagebus/test/src/tests/trace/FILES b/messagebus/test/src/tests/trace/FILES new file mode 100644 index 00000000000..891e4df6273 --- /dev/null +++ b/messagebus/test/src/tests/trace/FILES @@ -0,0 +1,19 @@ +trace.cpp +cpp-server.cpp +JavaServer.java +routing.cfg +out.server.cpp1 +out.server.cpp2 +out.server.cpp3 +out.server.cpp4 +out.server.cpp5 +out.server.cpp6 +out.server.cpp7 +out.server.java1 +out.server.java2 +out.server.java3 +out.server.java4 +out.server.java5 +out.server.java6 +out.server.java7 +progdefs.sh diff --git a/messagebus/test/src/tests/trace/JavaServer.java b/messagebus/test/src/tests/trace/JavaServer.java new file mode 100644 index 00000000000..5dfe15e3d0b --- /dev/null +++ b/messagebus/test/src/tests/trace/JavaServer.java @@ -0,0 +1,97 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.test.*; +import com.yahoo.config.*; +import com.yahoo.messagebus.routing.*; +import com.yahoo.messagebus.network.*; +import com.yahoo.messagebus.network.rpc.*; +import java.util.Arrays; +import java.util.logging.*; + +public class JavaServer implements MessageHandler, ReplyHandler { + + private static Logger log = Logger.getLogger(JavaServer.class.getName()); + + private IntermediateSession session; + private String name; + + public JavaServer(RPCMessageBus mb, String name) { + session = mb.getMessageBus().createIntermediateSession("session", true, this, this); + this.name = name; + } + + public void handleMessage(Message msg) { + msg.getTrace().trace(1, name + " (message)", false); + if (msg.getRoute() == null || !msg.getRoute().hasHops()) { + System.out.println("**** Server '" + name + "' replying."); + Reply reply = new EmptyReply(); + msg.swapState(reply); + handleReply(reply); + } else { + System.out.println("**** Server '" + name + "' forwarding message."); + session.forward(msg); + } + } + + public void handleReply(Reply reply) { + reply.getTrace().trace(1, name + " (reply)", false); + session.forward(reply); + } + + public static void main(String[] args) { + if (args.length != 1) { + System.err.println("usage: JavaServer <service prefix>"); + System.exit(1); + } + String name = args[0]; + SimpleProtocol protocol = new SimpleProtocol(); + protocol.addPolicyFactory("All", new SimpleProtocol.PolicyFactory() { + @Override + public RoutingPolicy create(String param) { + return new AllPolicy(); + } + }); + try { + RPCMessageBus mb = new RPCMessageBus( + Arrays.<Protocol>asList(protocol), + new RPCNetworkParams() + .setIdentity(new Identity(name)) + .setSlobrokConfigId("file:slobrok.cfg"), + "file:routing.cfg"); + JavaServer server = new JavaServer(mb, name); + System.out.printf("java server started name=%s\n", name); + while (true) { + Thread.sleep(1000); + } + } catch (Exception e) { + log.log(Level.SEVERE, "JAVA-SERVER: Failed", e); + System.exit(1); + } + } + + private static class AllPolicy implements RoutingPolicy { + + @Override + public void select(RoutingContext ctx) { + ctx.addChildren(ctx.getMatchedRecipients()); + } + + @Override + public void merge(RoutingContext ctx) { + EmptyReply ret = new EmptyReply(); + for (RoutingNodeIterator it = ctx.getChildIterator(); + it.isValid(); it.next()) { + Reply reply = it.getReplyRef(); + for (int i = 0; i < reply.getNumErrors(); ++i) { + ret.addError(reply.getError(i)); + } + } + ctx.setReply(ret); + } + + @Override + public void destroy() { + + } + } +} diff --git a/messagebus/test/src/tests/trace/cpp-server.cpp b/messagebus/test/src/tests/trace/cpp-server.cpp new file mode 100644 index 00000000000..76e20bc3cfd --- /dev/null +++ b/messagebus/test/src/tests/trace/cpp-server.cpp @@ -0,0 +1,90 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("cpp-server"); +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/destinationsession.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <vespa/messagebus/rpcmessagebus.h> +#include <vespa/messagebus/iprotocol.h> +#include <vespa/messagebus/protocolset.h> +#include <vespa/messagebus/emptyreply.h> + +using namespace mbus; + +class Server : public IMessageHandler, + public IReplyHandler +{ +private: + IntermediateSession::UP _session; + std::string _name; +public: + Server(MessageBus &bus, const std::string &name); + ~Server(); + void handleMessage(Message::UP msg); + void handleReply(Reply::UP reply); +}; + +Server::Server(MessageBus &bus, const std::string &name) + : _session(bus.createIntermediateSession("session", true, *this, *this)), + _name(name) +{ + fprintf(stderr, "cpp server started: %s\n", _name.c_str()); +} + +Server::~Server() +{ + _session.reset(); +} + +void +Server::handleMessage(Message::UP msg) { + msg->getTrace().trace(1, _name + " (message)", false); + if (!msg->getRoute().hasHops()) { + fprintf(stderr, "**** Server '%s' replying.\n", _name.c_str()); + Reply::UP reply(new EmptyReply()); + msg->swapState(*reply); + handleReply(std::move(reply)); + } else { + fprintf(stderr, "**** Server '%s' forwarding message.\n", _name.c_str()); + _session->forward(std::move(msg)); + } +} + +void +Server::handleReply(Reply::UP reply) { + reply->getTrace().trace(1, _name + " (reply)", false); + _session->forward(std::move(reply)); +} + +class App : public FastOS_Application +{ +public: + int Main(); +}; + +int +App::Main() +{ + if (_argc != 2) { + fprintf(stderr, "usage: %s <service-prefix>\n", _argv[0]); + return 1; + } + RPCMessageBus mb(ProtocolSet().add(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams() + .setIdentity(Identity(_argv[1])) + .setSlobrokConfig("file:slobrok.cfg"), + "file:routing.cfg"); + Server server(mb.getMessageBus(), _argv[1]); + while (true) { + FastOS_Thread::Sleep(1000); + } + return 0; +} + +int main(int argc, char **argv) { + App app; + return app.Entry(argc, argv); +} diff --git a/messagebus/test/src/tests/trace/ctl.sh b/messagebus/test/src/tests/trace/ctl.sh new file mode 100755 index 00000000000..ea969749808 --- /dev/null +++ b/messagebus/test/src/tests/trace/ctl.sh @@ -0,0 +1,3 @@ +#!/bin/sh +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +sh ../../binref/progctl.sh progdefs.sh "$@" diff --git a/messagebus/test/src/tests/trace/progdefs.sh b/messagebus/test/src/tests/trace/progdefs.sh new file mode 100644 index 00000000000..fd35b6503e2 --- /dev/null +++ b/messagebus/test/src/tests/trace/progdefs.sh @@ -0,0 +1,15 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +prog server cpp1 "" "./messagebus_test_cpp-server-trace_app server/cpp/1/A" +prog server cpp2 "" "./messagebus_test_cpp-server-trace_app server/cpp/2/A" +prog server cpp3 "" "./messagebus_test_cpp-server-trace_app server/cpp/2/B" +prog server cpp4 "" "./messagebus_test_cpp-server-trace_app server/cpp/3/A" +prog server cpp5 "" "./messagebus_test_cpp-server-trace_app server/cpp/3/B" +prog server cpp6 "" "./messagebus_test_cpp-server-trace_app server/cpp/3/C" +prog server cpp7 "" "./messagebus_test_cpp-server-trace_app server/cpp/3/D" +prog server java1 "" "../../binref/runjava JavaServer server/java/1/A" +prog server java2 "" "../../binref/runjava JavaServer server/java/2/A" +prog server java3 "" "../../binref/runjava JavaServer server/java/2/B" +prog server java4 "" "../../binref/runjava JavaServer server/java/3/A" +prog server java5 "" "../../binref/runjava JavaServer server/java/3/B" +prog server java6 "" "../../binref/runjava JavaServer server/java/3/C" +prog server java7 "" "../../binref/runjava JavaServer server/java/3/D" diff --git a/messagebus/test/src/tests/trace/trace.cpp b/messagebus/test/src/tests/trace/trace.cpp new file mode 100644 index 00000000000..94550460c84 --- /dev/null +++ b/messagebus/test/src/tests/trace/trace.cpp @@ -0,0 +1,113 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include <vespa/fastos/fastos.h> +#include <vespa/log/log.h> +LOG_SETUP("trace_test"); +#include <vespa/vespalib/testkit/testapp.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/vespalib/util/stringfmt.h> +#include <vespa/messagebus/emptyreply.h> +#include <vespa/messagebus/messagebus.h> +#include <vespa/messagebus/sourcesession.h> +#include <vespa/messagebus/rpcmessagebus.h> +#include <vespa/messagebus/intermediatesession.h> +#include <vespa/messagebus/destinationsession.h> +#include <vespa/messagebus/testlib/slobrok.h> +#include <vespa/messagebus/testlib/testserver.h> +#include <vespa/messagebus/routing/routingspec.h> +#include <vespa/messagebus/testlib/receptor.h> +#include <vespa/messagebus/sourcesessionparams.h> +#include <vespa/messagebus/testlib/simplemessage.h> +#include <vespa/messagebus/testlib/simplereply.h> +#include <vespa/messagebus/testlib/simpleprotocol.h> +#include <iostream> + +using namespace mbus; +using vespalib::make_string; + +TEST_SETUP(Test); + +bool +waitSlobrok(RPCMessageBus &mbus, const std::string &pattern) +{ + for (int i = 0; i < 30000; i++) { + slobrok::api::MirrorAPI::SpecList res = mbus.getRPCNetwork().getMirror().lookup(pattern); + if (res.size() > 0) { + return true; + } + FastOS_Thread::Sleep(10); + } + return false; +} + +int +Test::Main() +{ + TEST_INIT("trace_test"); + Slobrok slobrok; + { // Make slobrok config + EXPECT_TRUE(system("echo slobrok[1] > slobrok.cfg") == 0); + EXPECT_TRUE(system(make_string("echo 'slobrok[0].connectionspec tcp/localhost:%d' " + ">> slobrok.cfg", slobrok.port()).c_str()) == 0); + } + EXPECT_TRUE(system("sh ctl.sh start all") == 0); + RPCMessageBus mb(ProtocolSet().add(IProtocol::SP(new SimpleProtocol())), + RPCNetworkParams().setSlobrokConfig("file:slobrok.cfg"), + "file:routing.cfg"); + EXPECT_TRUE(waitSlobrok(mb, "server/cpp/1/A/session")); + EXPECT_TRUE(waitSlobrok(mb, "server/cpp/2/A/session")); + EXPECT_TRUE(waitSlobrok(mb, "server/cpp/2/B/session")); + EXPECT_TRUE(waitSlobrok(mb, "server/cpp/3/A/session")); + EXPECT_TRUE(waitSlobrok(mb, "server/cpp/3/B/session")); + EXPECT_TRUE(waitSlobrok(mb, "server/cpp/3/C/session")); + EXPECT_TRUE(waitSlobrok(mb, "server/cpp/3/D/session")); + EXPECT_TRUE(waitSlobrok(mb, "server/java/1/A/session")); + EXPECT_TRUE(waitSlobrok(mb, "server/java/2/A/session")); + EXPECT_TRUE(waitSlobrok(mb, "server/java/2/B/session")); + EXPECT_TRUE(waitSlobrok(mb, "server/java/3/A/session")); + EXPECT_TRUE(waitSlobrok(mb, "server/java/3/B/session")); + EXPECT_TRUE(waitSlobrok(mb, "server/java/3/C/session")); + EXPECT_TRUE(waitSlobrok(mb, "server/java/3/D/session")); + + TraceNode e3 = TraceNode() + .addChild(TraceNode().addChild("server/cpp/3/A (message)").addChild("server/cpp/3/A (reply)")) + .addChild(TraceNode().addChild("server/cpp/3/B (message)").addChild("server/cpp/3/B (reply)")) + .addChild(TraceNode().addChild("server/cpp/3/C (message)").addChild("server/cpp/3/C (reply)")) + .addChild(TraceNode().addChild("server/cpp/3/D (message)").addChild("server/cpp/3/D (reply)")) + .addChild(TraceNode().addChild("server/java/3/A (message)").addChild("server/java/3/A (reply)")) + .addChild(TraceNode().addChild("server/java/3/B (message)").addChild("server/java/3/B (reply)")) + .addChild(TraceNode().addChild("server/java/3/C (message)").addChild("server/java/3/C (reply)")) + .addChild(TraceNode().addChild("server/java/3/D (message)").addChild("server/java/3/D (reply)")).setStrict(false); + TraceNode e2 = TraceNode() + .addChild(TraceNode().addChild("server/cpp/2/A (message)").addChild(e3).addChild("server/cpp/2/A (reply)")) + .addChild(TraceNode().addChild("server/cpp/2/B (message)").addChild(e3).addChild("server/cpp/2/B (reply)")) + .addChild(TraceNode().addChild("server/java/2/A (message)").addChild(e3).addChild("server/java/2/A (reply)")) + .addChild(TraceNode().addChild("server/java/2/B (message)").addChild(e3).addChild("server/java/2/B (reply)")).setStrict(false); + TraceNode expect = TraceNode() + .addChild(TraceNode().addChild("server/cpp/1/A (message)").addChild(e2).addChild("server/cpp/1/A (reply)")) + .addChild(TraceNode().addChild("server/java/1/A (message)").addChild(e2).addChild("server/java/1/A (reply)")).setStrict(false); + expect.normalize(); + + Receptor src; + Reply::UP reply; + SourceSession::UP ss = mb.getMessageBus().createSourceSession(src, SourceSessionParams()); + for (int i = 0; i < 50; ++i) { + Message::UP msg(new SimpleMessage("test")); + msg->getTrace().setLevel(1); + ss->send(std::move(msg), "test"); + reply = src.getReply(10); + if (reply.get() != NULL) { + reply->getTrace().getRoot().normalize(); + // resending breaks the trace, so retry until it has expected form + if (!reply->hasErrors() && reply->getTrace().getRoot().encode() == expect.encode()) { + break; + } + } + std::cout << "Attempt " << i << " got errors, retrying in 1 second.." << std::endl; + FastOS_Thread::Sleep(1000); + } + + EXPECT_TRUE(!reply->hasErrors()); + EXPECT_EQUAL(reply->getTrace().getRoot().encode(), expect.encode()); + EXPECT_TRUE(system("sh ctl.sh stop all") == 0); + TEST_DONE(); +} diff --git a/messagebus/test/src/tests/trace/trace_test.sh b/messagebus/test/src/tests/trace/trace_test.sh new file mode 100644 index 00000000000..bfb2edbf870 --- /dev/null +++ b/messagebus/test/src/tests/trace/trace_test.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +. ../../binref/env.sh + +$BINREF/compilejava JavaServer.java + +./messagebus_test_trace_test_app diff --git a/messagebus/test/testrun/.gitignore b/messagebus/test/testrun/.gitignore new file mode 100644 index 00000000000..b29b0c6486c --- /dev/null +++ b/messagebus/test/testrun/.gitignore @@ -0,0 +1,9 @@ +test-report.html +test-report.html.* +test.*.*.desc +test.*.*.file.* +test.*.*.files.html +test.*.*.log +tmp.* +xsync.log +/test.*.*.result |