summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJake Farrell <jfarrell@apache.org>2012-03-22 21:49:10 +0000
committerJake Farrell <jfarrell@apache.org>2012-03-22 21:49:10 +0000
commitb95b0ffa720ffdebd829861ed420d10deab6c852 (patch)
tree2ca9cee28c31cc7fdc507039bc7e7018c64c6485
parentf485e2bdb39483e776b34dd70d1ab4c9e3a7f533 (diff)
downloadthrift-b95b0ffa720ffdebd829861ed420d10deab6c852.tar.gz
THRIFT-1500: d programming language support
Client: D Patch: David Nadlinger D program language library and additions git-svn-id: https://svn.apache.org/repos/asf/thrift/trunk@1304085 13f79535-47bb-0310-9956-ffa450edef68
-rw-r--r--aclocal/ax_check_openssl.m4124
-rw-r--r--aclocal/ax_dmd.m4107
-rw-r--r--compiler/cpp/Makefile.am3
-rw-r--r--compiler/cpp/src/generate/t_d_generator.cc772
-rwxr-xr-xconfigure.ac104
-rw-r--r--lib/Makefile.am4
-rw-r--r--lib/d/Makefile.am181
-rw-r--r--lib/d/README58
-rw-r--r--lib/d/src/thrift/async/base.d228
-rw-r--r--lib/d/src/thrift/async/libevent.d461
-rw-r--r--lib/d/src/thrift/async/socket.d357
-rw-r--r--lib/d/src/thrift/async/ssl.d292
-rw-r--r--lib/d/src/thrift/base.d123
-rw-r--r--lib/d/src/thrift/codegen/async_client.d255
-rw-r--r--lib/d/src/thrift/codegen/async_client_pool.d906
-rw-r--r--lib/d/src/thrift/codegen/base.d950
-rw-r--r--lib/d/src/thrift/codegen/client.d484
-rw-r--r--lib/d/src/thrift/codegen/client_pool.d262
-rw-r--r--lib/d/src/thrift/codegen/idlgen.d767
-rw-r--r--lib/d/src/thrift/codegen/processor.d497
-rw-r--r--lib/d/src/thrift/index.d33
-rw-r--r--lib/d/src/thrift/internal/algorithm.d55
-rw-r--r--lib/d/src/thrift/internal/codegen.d438
-rw-r--r--lib/d/src/thrift/internal/ctfe.d96
-rw-r--r--lib/d/src/thrift/internal/endian.d75
-rw-r--r--lib/d/src/thrift/internal/resource_pool.d419
-rw-r--r--lib/d/src/thrift/internal/socket.d96
-rw-r--r--lib/d/src/thrift/internal/ssl.d240
-rw-r--r--lib/d/src/thrift/internal/ssl_bio.d190
-rw-r--r--lib/d/src/thrift/internal/test/protocol.d183
-rw-r--r--lib/d/src/thrift/internal/test/server.d110
-rw-r--r--lib/d/src/thrift/internal/traits.d141
-rw-r--r--lib/d/src/thrift/protocol/base.d436
-rw-r--r--lib/d/src/thrift/protocol/binary.d414
-rw-r--r--lib/d/src/thrift/protocol/compact.d695
-rw-r--r--lib/d/src/thrift/protocol/json.d979
-rw-r--r--lib/d/src/thrift/protocol/processor.d145
-rw-r--r--lib/d/src/thrift/server/base.d147
-rw-r--r--lib/d/src/thrift/server/nonblocking.d1397
-rw-r--r--lib/d/src/thrift/server/simple.d181
-rw-r--r--lib/d/src/thrift/server/taskpool.d302
-rw-r--r--lib/d/src/thrift/server/threaded.d215
-rw-r--r--lib/d/src/thrift/server/transport/base.d133
-rw-r--r--lib/d/src/thrift/server/transport/socket.d380
-rw-r--r--lib/d/src/thrift/server/transport/ssl.d88
-rw-r--r--lib/d/src/thrift/transport/base.d370
-rw-r--r--lib/d/src/thrift/transport/buffered.d215
-rw-r--r--lib/d/src/thrift/transport/file.d1100
-rw-r--r--lib/d/src/thrift/transport/framed.d334
-rw-r--r--lib/d/src/thrift/transport/http.d459
-rw-r--r--lib/d/src/thrift/transport/memory.d233
-rw-r--r--lib/d/src/thrift/transport/piped.d219
-rw-r--r--lib/d/src/thrift/transport/range.d147
-rw-r--r--lib/d/src/thrift/transport/socket.d453
-rw-r--r--lib/d/src/thrift/transport/ssl.d680
-rw-r--r--lib/d/src/thrift/transport/zlib.d497
-rw-r--r--lib/d/src/thrift/util/awaitable.d212
-rw-r--r--lib/d/src/thrift/util/cancellation.d105
-rw-r--r--lib/d/src/thrift/util/future.d549
-rw-r--r--lib/d/src/thrift/util/hashset.d145
-rw-r--r--lib/d/test/Makefile.am123
-rw-r--r--lib/d/test/async_test.d396
-rw-r--r--lib/d/test/async_test_runner.sh6
-rw-r--r--lib/d/test/client_pool_test.d416
-rw-r--r--lib/d/test/openssl.test.cnf14
-rw-r--r--lib/d/test/serialization_benchmark.d70
-rw-r--r--lib/d/test/stress_test_server.d81
-rw-r--r--lib/d/test/test_utils.d96
-rw-r--r--lib/d/test/thrift_test_client.d372
-rw-r--r--lib/d/test/thrift_test_common.d92
-rw-r--r--lib/d/test/thrift_test_runner.sh65
-rw-r--r--lib/d/test/thrift_test_server.d264
-rw-r--r--lib/d/test/transport_test.d803
-rw-r--r--test/StressTest.thrift1
-rw-r--r--tutorial/d/Makefile37
-rw-r--r--tutorial/d/async_client.d86
-rw-r--r--tutorial/d/client.d64
-rw-r--r--tutorial/d/server.d111
-rw-r--r--tutorial/shared.thrift1
-rw-r--r--tutorial/tutorial.thrift1
80 files changed, 23339 insertions, 1 deletions
diff --git a/aclocal/ax_check_openssl.m4 b/aclocal/ax_check_openssl.m4
new file mode 100644
index 000000000..a87c5a6b6
--- /dev/null
+++ b/aclocal/ax_check_openssl.m4
@@ -0,0 +1,124 @@
+# ===========================================================================
+# http://www.gnu.org/software/autoconf-archive/ax_check_openssl.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_CHECK_OPENSSL([action-if-found[, action-if-not-found]])
+#
+# DESCRIPTION
+#
+# Look for OpenSSL in a number of default spots, or in a user-selected
+# spot (via --with-openssl). Sets
+#
+# OPENSSL_INCLUDES to the include directives required
+# OPENSSL_LIBS to the -l directives required
+# OPENSSL_LDFLAGS to the -L or -R flags required
+#
+# and calls ACTION-IF-FOUND or ACTION-IF-NOT-FOUND appropriately
+#
+# This macro sets OPENSSL_INCLUDES such that source files should use the
+# openssl/ directory in include directives:
+#
+# #include <openssl/hmac.h>
+#
+# LICENSE
+#
+# Copyright (c) 2009,2010 Zmanda Inc. <http://www.zmanda.com/>
+# Copyright (c) 2009,2010 Dustin J. Mitchell <dustin@zmanda.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 8
+
+AU_ALIAS([CHECK_SSL], [AX_CHECK_OPENSSL])
+AC_DEFUN([AX_CHECK_OPENSSL], [
+ found=false
+ AC_ARG_WITH([openssl],
+ [AS_HELP_STRING([--with-openssl=DIR],
+ [root of the OpenSSL directory])],
+ [
+ case "$withval" in
+ "" | y | ye | yes | n | no)
+ AC_MSG_ERROR([Invalid --with-openssl value])
+ ;;
+ *) ssldirs="$withval"
+ ;;
+ esac
+ ], [
+ # if pkg-config is installed and openssl has installed a .pc file,
+ # then use that information and don't search ssldirs
+ AC_PATH_PROG([PKG_CONFIG], [pkg-config])
+ if test x"$PKG_CONFIG" != x""; then
+ OPENSSL_LDFLAGS=`$PKG_CONFIG openssl --libs-only-L 2>/dev/null`
+ if test $? = 0; then
+ OPENSSL_LIBS=`$PKG_CONFIG openssl --libs-only-l 2>/dev/null`
+ OPENSSL_INCLUDES=`$PKG_CONFIG openssl --cflags-only-I 2>/dev/null`
+ found=true
+ fi
+ fi
+
+ # no such luck; use some default ssldirs
+ if ! $found; then
+ ssldirs="/usr/local/ssl /usr/lib/ssl /usr/ssl /usr/pkg /usr/local /usr"
+ fi
+ ]
+ )
+
+
+ # note that we #include <openssl/foo.h>, so the OpenSSL headers have to be in
+ # an 'openssl' subdirectory
+
+ if ! $found; then
+ OPENSSL_INCLUDES=
+ for ssldir in $ssldirs; do
+ AC_MSG_CHECKING([for openssl/ssl.h in $ssldir])
+ if test -f "$ssldir/include/openssl/ssl.h"; then
+ OPENSSL_INCLUDES="-I$ssldir/include"
+ OPENSSL_LDFLAGS="-L$ssldir/lib"
+ OPENSSL_LIBS="-lssl -lcrypto"
+ found=true
+ AC_MSG_RESULT([yes])
+ break
+ else
+ AC_MSG_RESULT([no])
+ fi
+ done
+
+ # if the file wasn't found, well, go ahead and try the link anyway -- maybe
+ # it will just work!
+ fi
+
+ # try the preprocessor and linker with our new flags,
+ # being careful not to pollute the global LIBS, LDFLAGS, and CPPFLAGS
+
+ AC_MSG_CHECKING([whether compiling and linking against OpenSSL works])
+ echo "Trying link with OPENSSL_LDFLAGS=$OPENSSL_LDFLAGS;" \
+ "OPENSSL_LIBS=$OPENSSL_LIBS; OPENSSL_INCLUDES=$OPENSSL_INCLUDES" >&AS_MESSAGE_LOG_FD
+
+ save_LIBS="$LIBS"
+ save_LDFLAGS="$LDFLAGS"
+ save_CPPFLAGS="$CPPFLAGS"
+ LDFLAGS="$LDFLAGS $OPENSSL_LDFLAGS"
+ LIBS="$OPENSSL_LIBS $LIBS"
+ CPPFLAGS="$OPENSSL_INCLUDES $CPPFLAGS"
+ AC_LINK_IFELSE(
+ [AC_LANG_PROGRAM([#include <openssl/ssl.h>], [SSL_new(NULL)])],
+ [
+ AC_MSG_RESULT([yes])
+ $1
+ ], [
+ AC_MSG_RESULT([no])
+ $2
+ ])
+ CPPFLAGS="$save_CPPFLAGS"
+ LDFLAGS="$save_LDFLAGS"
+ LIBS="$save_LIBS"
+
+ AC_SUBST([OPENSSL_INCLUDES])
+ AC_SUBST([OPENSSL_LIBS])
+ AC_SUBST([OPENSSL_LDFLAGS])
+])
diff --git a/aclocal/ax_dmd.m4 b/aclocal/ax_dmd.m4
new file mode 100644
index 000000000..13b84b021
--- /dev/null
+++ b/aclocal/ax_dmd.m4
@@ -0,0 +1,107 @@
+dnl @synopsis AX_DMD
+dnl
+dnl Test for the presence of a DMD-compatible D2 compiler, and (optionally)
+dnl specified modules on the import path.
+dnl
+dnl If "DMD" is defined in the environment, that will be the only
+dnl dmd command tested. Otherwise, a hard-coded list will be used.
+dnl
+dnl After AX_DMD runs, the shell variables "success" and "ax_dmd" are set to
+dnl "yes" or "no", and "DMD" is set to the appropriate command. Furthermore,
+dnl "dmd_optlink" will be set to "yes" or "no" depending on whether OPTLINK is
+dnl used as the linker (DMD/Windows), and "dmd_of_dirsep" will be set to the
+dnl directory separator to use when passing -of to DMD (OPTLINK requires a
+dnl backslash).
+dnl
+dnl AX_CHECK_D_MODULE must be run after AX_DMD. It tests for the presence of a
+dnl module in the import path of the chosen compiler, and sets the shell
+dnl variable "success" to "yes" or "no".
+dnl
+dnl @category D
+dnl @version 2011-05-31
+dnl @license AllPermissive
+dnl
+dnl Copyright (C) 2009 David Reiss
+dnl Copyright (C) 2011 David Nadlinger
+dnl Copying and distribution of this file, with or without modification,
+dnl are permitted in any medium without royalty provided the copyright
+dnl notice and this notice are preserved.
+
+
+AC_DEFUN([AX_DMD],
+ [
+ dnl Hard-coded default commands to test.
+ DMD_PROGS="dmd,gdmd,ldmd"
+
+ dnl Allow the user to specify an alternative.
+ if test -n "$DMD" ; then
+ DMD_PROGS="$DMD"
+ fi
+
+ AC_MSG_CHECKING(for DMD)
+
+ # std.algorithm as a quick way to check for D2/Phobos.
+ echo "import std.algorithm; void main() {}" > configtest_ax_dmd.d
+ success=no
+ oIFS="$IFS"
+
+ IFS=","
+ for DMD in $DMD_PROGS ; do
+ IFS="$oIFS"
+
+ echo "Running \"$DMD configtest_ax_dmd.d\"" >&AS_MESSAGE_LOG_FD
+ if $DMD configtest_ax_dmd.d >&AS_MESSAGE_LOG_FD 2>&1 ; then
+ success=yes
+ break
+ fi
+ done
+
+ if test "$success" != "yes" ; then
+ AC_MSG_RESULT(no)
+ DMD=""
+ else
+ AC_MSG_RESULT(yes)
+ fi
+
+ ax_dmd="$success"
+
+ # Test whether OPTLINK is used by trying if DMD accepts -L/? without
+ # erroring out.
+ if test "$success" == "yes" ; then
+ AC_MSG_CHECKING(whether DMD uses OPTLINK)
+ echo "Running \”$DMD -L/? configtest_ax_dmd.d\"" >&AS_MESSAGE_LOG_FD
+ if $DMD -L/? configtest_ax_dmd.d >&AS_MESSAGE_LOG_FD 2>&1 ; then
+ AC_MSG_RESULT(yes)
+ dmd_optlink="yes"
+
+ # This actually produces double slashes in the final configure
+ # output, but at least it works.
+ dmd_of_dirsep="\\\\"
+ else
+ AC_MSG_RESULT(no)
+ dmd_optlink="no"
+ dmd_of_dirsep="/"
+ fi
+ fi
+
+ rm -f configtest_ax_dmd*
+ ])
+
+
+AC_DEFUN([AX_CHECK_D_MODULE],
+ [
+ AC_MSG_CHECKING(for D module [$1])
+
+ echo "import $1; void main() {}" > configtest_ax_dmd.d
+
+ echo "Running \"$DMD configtest_ax_dmd.d\"" >&AS_MESSAGE_LOG_FD
+ if $DMD -c configtest_ax_dmd.d >&AS_MESSAGE_LOG_FD 2>&1 ; then
+ AC_MSG_RESULT(yes)
+ success=yes
+ else
+ AC_MSG_RESULT(no)
+ success=no
+ fi
+
+ rm -f configtest_ax_dmd*
+ ])
diff --git a/compiler/cpp/Makefile.am b/compiler/cpp/Makefile.am
index a5723a3ae..b7d1b5042 100644
--- a/compiler/cpp/Makefile.am
+++ b/compiler/cpp/Makefile.am
@@ -83,7 +83,8 @@ thrift_SOURCES += src/generate/t_c_glib_generator.cc \
src/generate/t_js_generator.cc \
src/generate/t_javame_generator.cc \
src/generate/t_delphi_generator.cc \
- src/generate/t_go_generator.cc
+ src/generate/t_go_generator.cc \
+ src/generate/t_d_generator.cc
thrift_CPPFLAGS = -I$(srcdir)/src
thrift_CXXFLAGS = -Wall
diff --git a/compiler/cpp/src/generate/t_d_generator.cc b/compiler/cpp/src/generate/t_d_generator.cc
new file mode 100644
index 000000000..db5e17fdc
--- /dev/null
+++ b/compiler/cpp/src/generate/t_d_generator.cc
@@ -0,0 +1,772 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * Contains some contributions under the Thrift Software License.
+ * Please see doc/old-thrift-license.txt in the Thrift distribution for
+ * details.
+ */
+
+#include <cassert>
+
+#include <fstream>
+#include <iostream>
+#include <set>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include <sys/stat.h>
+
+#include "platform.h"
+#include "t_oop_generator.h"
+using namespace std;
+
+
+/**
+ * D code generator.
+ *
+ * generate_*() functions are called by the base class to emit code for the
+ * given entity, print_*() functions write a piece of code to the passed
+ * stream, and render_*() return a string containing the D representation of
+ * the passed entity.
+ */
+class t_d_generator : public t_oop_generator {
+ public:
+ t_d_generator(
+ t_program* program,
+ const std::map<string, string>& parsed_options,
+ const string& option_string)
+ : t_oop_generator(program)
+ {
+ (void) parsed_options;
+ (void) option_string;
+ out_dir_base_ = "gen-d";
+ }
+
+ protected:
+ virtual void init_generator() {
+ // Make output directory
+ MKDIR(get_out_dir().c_str());
+
+ string dir = program_->get_namespace("d");
+ string subdir = get_out_dir();
+ string::size_type loc;
+ while ((loc = dir.find(".")) != string::npos) {
+ subdir = subdir + "/" + dir.substr(0, loc);
+ MKDIR(subdir.c_str());
+ dir = dir.substr(loc+1);
+ }
+ if (!dir.empty()) {
+ subdir = subdir + "/" + dir;
+ MKDIR(subdir.c_str());
+ }
+
+ package_dir_ = subdir + "/";
+
+ // Make output file
+ string f_types_name = package_dir_ + program_name_ + "_types.d";
+ f_types_.open(f_types_name.c_str());
+
+ // Print header
+ f_types_ <<
+ autogen_comment() <<
+ "module " << render_package(*program_) << program_name_ << "_types;" << endl <<
+ endl;
+
+ print_default_imports(f_types_);
+
+ // Include type modules from other imported programs.
+ const vector<t_program*>& includes = program_->get_includes();
+ for (size_t i = 0; i < includes.size(); ++i) {
+ f_types_ <<
+ "import " << render_package(*(includes[i])) <<
+ includes[i]->get_name() << "_types;" << endl;
+ }
+ if (!includes.empty()) f_types_ << endl;
+ }
+
+ virtual void close_generator() {
+ // Close output file
+ f_types_.close();
+ }
+
+ virtual void generate_consts(std::vector<t_const*> consts) {
+ if (!consts.empty()) {
+ string f_consts_name = package_dir_+program_name_+"_constants.d";
+ ofstream f_consts;
+ f_consts.open(f_consts_name.c_str());
+
+ f_consts <<
+ autogen_comment() <<
+ "module " << render_package(*program_) << program_name_ << "_constants;" << endl
+ << endl;
+
+ print_default_imports(f_consts);
+
+ f_consts <<
+ "import " << render_package(*get_program()) << program_name_ << "_types;" << endl <<
+ endl;
+
+ vector<t_const*>::iterator c_iter;
+ for (c_iter = consts.begin(); c_iter != consts.end(); ++c_iter) {
+ string name = (*c_iter)->get_name();
+ t_type* type = (*c_iter)->get_type();
+ indent(f_consts) << "immutable(" << render_type_name(type) << ") " <<
+ name << ";" << endl;
+ }
+
+ f_consts <<
+ endl <<
+ "static this() {" << endl;
+ indent_up();
+
+ bool first = true;
+ for (c_iter = consts.begin(); c_iter != consts.end(); ++c_iter) {
+ if (first) {
+ first = false;
+ } else {
+ f_consts << endl;
+ }
+ t_type* type = (*c_iter)->get_type();
+ indent(f_consts) << (*c_iter)->get_name() << " = ";
+ if (!is_immutable_type(type)) {
+ f_consts << "cast(immutable(" << render_type_name(type) << ")) ";
+ }
+ f_consts <<
+ render_const_value(type, (*c_iter)->get_value()) << ";" << endl;
+ }
+ indent_down();
+ indent(f_consts) <<
+ "}" << endl;
+ }
+ }
+
+ virtual void generate_typedef(t_typedef* ttypedef) {
+ f_types_ <<
+ indent() << "alias " << render_type_name(ttypedef->get_type()) << " " <<
+ ttypedef->get_symbolic() << ";" << endl << endl;
+ }
+
+ virtual void generate_enum(t_enum* tenum) {
+ vector<t_enum_value*> constants = tenum->get_constants();
+
+ string enum_name = tenum->get_name();
+ f_types_ <<
+ indent() << "enum " << enum_name << " {" << endl;
+
+ indent_up();
+
+ vector<t_enum_value*>::const_iterator c_iter;
+ bool first = true;
+ for (c_iter = constants.begin(); c_iter != constants.end(); ++c_iter) {
+ if (first) {
+ first = false;
+ } else {
+ f_types_ << "," << endl;
+ }
+ indent(f_types_) << (*c_iter)->get_name();
+ if ((*c_iter)->has_value()) {
+ f_types_ << " = " << (*c_iter)->get_value();
+ }
+ }
+
+ f_types_ << endl;
+ indent_down();
+ indent(f_types_) << "}" << endl;
+
+ f_types_ << endl;
+ }
+
+ virtual void generate_struct(t_struct* tstruct) {
+ print_struct_definition(f_types_, tstruct, false);
+ }
+
+ virtual void generate_xception(t_struct* txception) {
+ print_struct_definition(f_types_, txception, true);
+ }
+
+ virtual void generate_service(t_service* tservice) {
+ string svc_name = tservice->get_name();
+
+ // Service implementation file includes
+ string f_servicename = package_dir_ + svc_name + ".d";
+ std::ofstream f_service;
+ f_service.open(f_servicename.c_str());
+ f_service <<
+ autogen_comment() <<
+ "module " << render_package(*program_) << svc_name << ";" << endl <<
+ endl;
+
+ print_default_imports(f_service);
+
+ f_service << "import " << render_package(*get_program()) << program_name_ <<
+ "_types;" << endl;
+
+ t_service* extends_service = tservice->get_extends();
+ if (extends_service != NULL) {
+ f_service <<
+ "import " << render_package(*(extends_service->get_program())) <<
+ extends_service->get_name() << ";" << endl;
+ }
+
+ f_service << endl;
+
+ string extends = "";
+ if (tservice->get_extends() != NULL) {
+ extends = " : " + render_type_name(tservice->get_extends());
+ }
+
+ f_service <<
+ indent() << "interface " << svc_name << extends << " {" << endl;
+ indent_up();
+
+ // Collect all the exception types service methods can throw so we can
+ // emit the necessary aliases later.
+ set<t_type*> exception_types;
+
+ // Print the method signatures.
+ vector<t_function*> functions = tservice->get_functions();
+ vector<t_function*>::iterator fn_iter;
+ for (fn_iter = functions.begin(); fn_iter != functions.end(); ++fn_iter) {
+ f_service << indent();
+ print_function_signature(f_service, *fn_iter);
+ f_service << ";" << endl;
+
+ const vector<t_field*>& exceptions = (*fn_iter)->get_xceptions()->get_members();
+ vector<t_field*>::const_iterator ex_iter;
+ for (ex_iter = exceptions.begin(); ex_iter != exceptions.end(); ++ex_iter) {
+ exception_types.insert((*ex_iter)->get_type());
+ }
+ }
+
+ // Alias the exception types into the current scope.
+ if (!exception_types.empty()) f_service << endl;
+ set<t_type*>::const_iterator et_iter;
+ for (et_iter = exception_types.begin(); et_iter != exception_types.end(); ++et_iter) {
+ indent(f_service) << "alias " << render_package(*(*et_iter)->get_program()) <<
+ (*et_iter)->get_program()->get_name() << "_types" << "." <<
+ (*et_iter)->get_name() << " " << (*et_iter)->get_name() << ";" << endl;
+ }
+
+ // Write the method metadata.
+ ostringstream meta;
+ indent_up();
+ bool first = true;
+ for (fn_iter = functions.begin(); fn_iter != functions.end(); ++fn_iter) {
+ if ((*fn_iter)->get_arglist()->get_members().empty() &&
+ (*fn_iter)->get_xceptions()->get_members().empty() &&
+ !(*fn_iter)->is_oneway()) {
+ continue;
+ }
+
+ if (first) {
+ first = false;
+ } else {
+ meta << ",";
+ }
+
+ meta << endl <<
+ indent() << "TMethodMeta(`" << (*fn_iter)->get_name() << "`, " << endl;
+ indent_up();
+ indent(meta) << "[";
+
+ bool first = true;
+ const vector<t_field*> &params = (*fn_iter)->get_arglist()->get_members();
+ vector<t_field*>::const_iterator p_iter;
+ for (p_iter = params.begin(); p_iter != params.end(); ++p_iter) {
+ if (first) {
+ first = false;
+ } else {
+ meta << ", ";
+ }
+
+ meta << "TParamMeta(`" << (*p_iter)->get_name() << "`, " << (*p_iter)->get_key();
+
+ t_const_value* cv = (*p_iter)->get_value();
+ if (cv != NULL) {
+ meta << ", q{" << render_const_value((*p_iter)->get_type(), cv) << "}";
+ }
+ meta << ")";
+ }
+
+ meta << "]";
+
+ if (!(*fn_iter)->get_xceptions()->get_members().empty() ||
+ (*fn_iter)->is_oneway()) {
+ meta << "," << endl <<
+ indent() << "[";
+
+ bool first = true;
+ const vector<t_field*>& exceptions =
+ (*fn_iter)->get_xceptions()->get_members();
+ vector<t_field*>::const_iterator ex_iter;
+ for (ex_iter = exceptions.begin(); ex_iter != exceptions.end(); ++ex_iter) {
+ if (first) {
+ first = false;
+ } else {
+ meta << ", ";
+ }
+
+ meta << "TExceptionMeta(`" << (*ex_iter)->get_name() <<
+ "`, " << (*ex_iter)->get_key() << ", `" <<
+ (*ex_iter)->get_type()->get_name() << "`)";
+ }
+
+ meta << "]";
+ }
+
+ if ((*fn_iter)->is_oneway()) {
+ meta << "," << endl <<
+ indent() << "TMethodType.ONEWAY";
+ }
+
+ indent_down();
+ meta << endl <<
+ indent() << ")";
+ }
+ indent_down();
+
+ string meta_str(meta.str());
+ if (!meta_str.empty()) {
+ f_service << endl <<
+ indent() << "enum methodMeta = [" << meta_str << endl <<
+ indent() << "];" << endl;
+ }
+
+ indent_down();
+ indent(f_service) << "}" << endl;
+
+
+ // Server skeleton generation.
+ string f_skeletonname = package_dir_ + svc_name + "_server.skeleton.d";
+ std::ofstream f_skeleton;
+ f_skeleton.open(f_skeletonname.c_str());
+ print_server_skeleton(f_skeleton, tservice);
+ f_skeleton.close();
+ }
+
+ private:
+ /**
+ * Writes a server skeleton for the passed service to out.
+ */
+ void print_server_skeleton(ostream &out, t_service* tservice) {
+ string svc_name = tservice->get_name();
+
+ out <<
+ "/*" << endl <<
+ " * This auto-generated skeleton file illustrates how to build a server. If you" << endl <<
+ " * intend to customize it, you should edit a copy with another file name to " << endl <<
+ " * avoid overwriting it when running the generator again." << endl <<
+ " */" << endl <<
+ "module " << render_package(*tservice->get_program()) << svc_name << "_server;" << endl <<
+ endl <<
+ "import std.stdio;" << endl <<
+ "import thrift.codegen.processor;" << endl <<
+ "import thrift.protocol.binary;" << endl <<
+ "import thrift.server.simple;" << endl <<
+ "import thrift.server.transport.socket;" << endl <<
+ "import thrift.transport.buffered;" << endl <<
+ "import thrift.util.hashset;" << endl <<
+ endl <<
+ "import " << render_package(*tservice->get_program()) << svc_name << ";" << endl <<
+ "import " << render_package(*get_program()) << program_name_ << "_types;" << endl <<
+ endl <<
+ endl <<
+ "class " << svc_name << "Handler : " << svc_name << " {" << endl;
+
+ indent_up();
+ out <<
+ indent() << "this() {" << endl <<
+ indent() << " // Your initialization goes here." << endl <<
+ indent() << "}" << endl <<
+ endl;
+
+ vector<t_function*> functions = tservice->get_functions();
+ vector<t_function*>::iterator f_iter;
+ for (f_iter = functions.begin(); f_iter != functions.end(); ++f_iter) {
+ out << indent();
+ print_function_signature(out, *f_iter);
+ out << " {" << endl;
+
+ indent_up();
+
+ out <<
+ indent() << "// Your implementation goes here." << endl <<
+ indent() << "writeln(\"" << (*f_iter)->get_name() << " called\");" << endl;
+
+ t_base_type* rt = (t_base_type*)(*f_iter)->get_returntype();
+ if (rt->get_base() != t_base_type::TYPE_VOID) {
+ indent(out) << "return typeof(return).init;" << endl;
+ }
+
+ indent_down();
+
+ out <<
+ indent() << "}" << endl <<
+ endl;
+ }
+
+ indent_down();
+ out <<
+ "}" << endl <<
+ endl;
+
+ out <<
+ indent() << "void main() {" << endl;
+ indent_up();
+ out <<
+ indent() << "auto protocolFactory = new TBinaryProtocolFactory!();" << endl <<
+ indent() << "auto processor = new TServiceProcessor!" << svc_name << "(new " << svc_name << "Handler);" << endl <<
+ indent() << "auto serverTransport = new TServerSocket(9090);" << endl <<
+ indent() << "auto transportFactory = new TBufferedTransportFactory;" << endl <<
+
+ indent() << "auto server = new TSimpleServer(" << endl <<
+ indent() << " processor, serverTransport, transportFactory, protocolFactory);" << endl <<
+ indent() << "server.serve();" << endl;
+ indent_down();
+ out <<
+ "}" << endl;
+ }
+
+ /**
+ * Writes the definition of a struct or an exception type to out.
+ */
+ void print_struct_definition(ostream& out, t_struct* tstruct, bool is_exception) {
+ const vector<t_field*>& members = tstruct->get_members();
+
+ if (is_exception) {
+ indent(out) << "class " << tstruct->get_name() << " : TException {" << endl;
+ } else {
+ indent(out) << "struct " << tstruct->get_name() << " {" << endl;
+ }
+ indent_up();
+
+ // Declare all fields.
+ vector<t_field*>::const_iterator m_iter;
+ for (m_iter = members.begin(); m_iter != members.end(); ++m_iter) {
+ indent(out) << render_type_name((*m_iter)->get_type()) << " " <<
+ (*m_iter)->get_name() << ";" << endl;
+ }
+
+ if (!members.empty()) indent(out) << endl;
+ indent(out) << "mixin TStructHelpers!(";
+
+ if (!members.empty()) {
+ // If there are any fields, construct the TFieldMeta array to pass to
+ // TStructHelpers. We can't just pass an empty array if not because []
+ // doesn't pass the TFieldMeta[] constraint.
+ out << "[";
+ indent_up();
+
+ bool first = true;
+ vector<t_field*>::const_iterator m_iter;
+ for (m_iter = members.begin(); m_iter != members.end(); ++m_iter) {
+ if (first) {
+ first = false;
+ } else {
+ out << ",";
+ }
+ out << endl;
+
+ indent(out) << "TFieldMeta(`" << (*m_iter)->get_name() << "`, " <<
+ (*m_iter)->get_key();
+
+ t_const_value* cv = (*m_iter)->get_value();
+ t_field::e_req req = (*m_iter)->get_req();
+ out << ", " << render_req(req);
+ if (cv != NULL) {
+ out << ", q{" << render_const_value((*m_iter)->get_type(), cv) << "}";
+ }
+ out << ")";
+ }
+
+ indent_down();
+ out << endl << indent() << "]";
+ }
+
+ out << ");" << endl;
+
+ indent_down();
+ indent(out) <<
+ "}" << endl <<
+ endl;
+ }
+
+ /**
+ * Prints the D function signature (including return type) for the given
+ * method.
+ */
+ void print_function_signature(ostream& out, t_function* fn) {
+ out << render_type_name(fn->get_returntype()) <<
+ " " << fn->get_name() << "(";
+
+ const vector<t_field*>& fields = fn->get_arglist()->get_members();
+ vector<t_field*>::const_iterator f_iter;
+ bool first = true;
+ for (f_iter = fields.begin(); f_iter != fields.end(); ++f_iter) {
+ if (first) {
+ first = false;
+ } else {
+ out << ", ";
+ }
+ out << render_type_name((*f_iter)->get_type(), true) << " " <<
+ (*f_iter)->get_name();
+ }
+
+ out << ")";
+ }
+
+ /**
+ * Returns the D representation of value. The result is guaranteed to be a
+ * single expression; for complex types, immediately called delegate
+ * literals are used to achieve this.
+ */
+ string render_const_value(t_type* type, t_const_value* value) {
+ // Resolve any typedefs.
+ type = get_true_type(type);
+
+ ostringstream out;
+ if (type->is_base_type()) {
+ t_base_type::t_base tbase = ((t_base_type*)type)->get_base();
+ switch (tbase) {
+ case t_base_type::TYPE_STRING:
+ out << '"' << get_escaped_string(value) << '"';
+ break;
+ case t_base_type::TYPE_BOOL:
+ out << ((value->get_integer() > 0) ? "true" : "false");
+ break;
+ case t_base_type::TYPE_BYTE:
+ case t_base_type::TYPE_I16:
+ out << "cast(" << render_type_name(type) << ")" << value->get_integer();
+ break;
+ case t_base_type::TYPE_I32:
+ out << value->get_integer();
+ break;
+ case t_base_type::TYPE_I64:
+ out << value->get_integer() << "L";
+ break;
+ case t_base_type::TYPE_DOUBLE:
+ if (value->get_type() == t_const_value::CV_INTEGER) {
+ out << value->get_integer();
+ } else {
+ out << value->get_double();
+ }
+ break;
+ default:
+ throw "Compiler error: No const of base type " +
+ t_base_type::t_base_name(tbase);
+ }
+ } else if (type->is_enum()) {
+ out << "cast(" << render_type_name(type) << ")" << value->get_integer();
+ } else {
+ out << "{" << endl;
+ indent_up();
+
+ indent(out) << render_type_name(type) << " v;" << endl;
+ if (type->is_struct() || type->is_xception()) {
+ indent(out) << "v = " << (type->is_xception() ? "new " : "") <<
+ render_type_name(type) << "();" << endl;
+
+ const vector<t_field*>& fields = ((t_struct*)type)->get_members();
+ vector<t_field*>::const_iterator f_iter;
+ const map<t_const_value*, t_const_value*>& val = value->get_map();
+ map<t_const_value*, t_const_value*>::const_iterator v_iter;
+ for (v_iter = val.begin(); v_iter != val.end(); ++v_iter) {
+ t_type* field_type = NULL;
+ for (f_iter = fields.begin(); f_iter != fields.end(); ++f_iter) {
+ if ((*f_iter)->get_name() == v_iter->first->get_string()) {
+ field_type = (*f_iter)->get_type();
+ }
+ }
+ if (field_type == NULL) {
+ throw "Type error: " + type->get_name() + " has no field " +
+ v_iter->first->get_string();
+ }
+ string val = render_const_value(field_type, v_iter->second);
+ indent(out) << "v.set!`" << v_iter->first->get_string() <<
+ "`(" << val << ");" << endl;
+ }
+ } else if (type->is_map()) {
+ t_type* ktype = ((t_map*)type)->get_key_type();
+ t_type* vtype = ((t_map*)type)->get_val_type();
+ const map<t_const_value*, t_const_value*>& val = value->get_map();
+ map<t_const_value*, t_const_value*>::const_iterator v_iter;
+ for (v_iter = val.begin(); v_iter != val.end(); ++v_iter) {
+ string key = render_const_value(ktype, v_iter->first);
+ string val = render_const_value(vtype, v_iter->second);
+ indent(out) << "v[";
+ if (!is_immutable_type(ktype)) {
+ out << "cast(immutable(" << render_type_name(ktype) << "))";
+ }
+ out << key << "] = " << val << ";" << endl;
+ }
+ } else if (type->is_list()) {
+ t_type* etype = ((t_list*)type)->get_elem_type();
+ const vector<t_const_value*>& val = value->get_list();
+ vector<t_const_value*>::const_iterator v_iter;
+ for (v_iter = val.begin(); v_iter != val.end(); ++v_iter) {
+ string val = render_const_value(etype, *v_iter);
+ indent(out) << "v ~= " << val << ";" << endl;
+ }
+ } else if (type->is_set()) {
+ t_type* etype = ((t_set*)type)->get_elem_type();
+ const vector<t_const_value*>& val = value->get_list();
+ vector<t_const_value*>::const_iterator v_iter;
+ for (v_iter = val.begin(); v_iter != val.end(); ++v_iter) {
+ string val = render_const_value(etype, *v_iter);
+ indent(out) << "v ~= " << val << ";" << endl;
+ }
+ } else {
+ throw "Compiler error: Invalid type in render_const_value: " +
+ type->get_name();
+ }
+ indent(out) << "return v;" << endl;
+
+ indent_down();
+ indent(out) << "}()";
+ }
+
+ return out.str();
+ }
+
+ /**
+ * Returns the D package to which modules for program are written (with a
+ * trailing dot, if not empty).
+ */
+ string render_package(const t_program& program) const {
+ string package = program.get_namespace("d");
+ if (package.size() == 0) return "";
+ return package + ".";
+ }
+
+ /**
+ * Returns the name of the D repesentation of ttype.
+ *
+ * If isArg is true, a const reference to the type will be returned for
+ * structs.
+ */
+ string render_type_name(const t_type* ttype, bool isArg = false) const {
+ if (ttype->is_base_type()) {
+ t_base_type::t_base tbase = ((t_base_type*)ttype)->get_base();
+ switch (tbase) {
+ case t_base_type::TYPE_VOID:
+ return "void";
+ case t_base_type::TYPE_STRING:
+ return "string";
+ case t_base_type::TYPE_BOOL:
+ return "bool";
+ case t_base_type::TYPE_BYTE:
+ return "byte";
+ case t_base_type::TYPE_I16:
+ return "short";
+ case t_base_type::TYPE_I32:
+ return "int";
+ case t_base_type::TYPE_I64:
+ return "long";
+ case t_base_type::TYPE_DOUBLE:
+ return "double";
+ default:
+ throw "Compiler error: No D type name for base type " +
+ t_base_type::t_base_name(tbase);
+ }
+ }
+
+ if (ttype->is_container()) {
+ t_container* tcontainer = (t_container*) ttype;
+ if (tcontainer->has_cpp_name()) {
+ return tcontainer->get_cpp_name();
+ } else if (ttype->is_map()) {
+ t_map* tmap = (t_map*) ttype;
+ t_type* ktype = tmap->get_key_type();
+
+ string name = render_type_name(tmap->get_val_type()) + "[";
+ if (!is_immutable_type(ktype)) {
+ name += "immutable(";
+ }
+ name += render_type_name(ktype);
+ if (!is_immutable_type(ktype)) {
+ name += ")";
+ }
+ name += "]";
+ return name;
+ } else if (ttype->is_set()) {
+ t_set* tset = (t_set*) ttype;
+ return "HashSet!(" + render_type_name(tset->get_elem_type()) + ")";
+ } else if (ttype->is_list()) {
+ t_list* tlist = (t_list*) ttype;
+ return render_type_name(tlist->get_elem_type()) + "[]";
+ }
+ }
+
+ if (ttype->is_struct() && isArg) {
+ return "ref const(" + ttype->get_name() + ")";
+ } else {
+ return ttype->get_name();
+ }
+ }
+
+ /**
+ * Returns the D TReq enum member corresponding to req.
+ */
+ string render_req(t_field::e_req req) const {
+ switch (req) {
+ case t_field::T_OPT_IN_REQ_OUT:
+ return "TReq.OPT_IN_REQ_OUT";
+ case t_field::T_OPTIONAL:
+ return "TReq.OPTIONAL";
+ case t_field::T_REQUIRED:
+ return "TReq.REQUIRED";
+ default:
+ throw "Compiler error: Invalid requirement level: " + req;
+ }
+ }
+
+ /**
+ * Writes the default list of imports (which are written to every generated
+ * module) to f.
+ */
+ void print_default_imports(ostream& out) {
+ indent(out) <<
+ "import thrift.base;" << endl <<
+ "import thrift.codegen.base;" << endl <<
+ "import thrift.util.hashset;" << endl <<
+ endl;
+ }
+
+ /**
+ * Returns whether type is »intrinsically immutable«, in the sense that
+ * a value of that type is implicitly castable to immutable(type), and it is
+ * allowed for AA keys without an immutable() qualifier.
+ */
+ bool is_immutable_type(t_type* type) const {
+ t_type* ttype = get_true_type(type);
+ return ttype->is_base_type() || ttype->is_enum();
+ }
+
+ /*
+ * File streams, stored here to avoid passing them as parameters to every
+ * function.
+ */
+ ofstream f_types_;
+ ofstream f_header_;
+
+ string package_dir_;
+};
+
+THRIFT_REGISTER_GENERATOR(d, "D", "")
+
diff --git a/configure.ac b/configure.ac
index 9960731da..2290889d5 100755
--- a/configure.ac
+++ b/configure.ac
@@ -89,6 +89,13 @@ AC_ARG_VAR([GOBIN], [Binary directory for Go.
Default = "/usr/local/bin"])
AS_IF([test "x$GOBIN" = x], [GOBIN="/usr/local/bin"])
+AC_ARG_VAR([D_IMPORT_PREFIX], [Prefix for installing D modules.
+ [INCLUDEDIR/d2]])
+AS_IF([test "x$D_IMPORT_PREFIX" = x], [D_IMPORT_PREFIX="${includedir}/d2"])
+
+AC_ARG_VAR([DMD_LIBEVENT_FLAGS], [DMD flags for linking libevent (auto-detected if not set).])
+AC_ARG_VAR([DMD_OPENSSL_FLAGS], [DMD flags for linking OpenSSL (auto-detected if not set).])
+
AC_PROG_CC
AC_PROG_CPP
AC_PROG_CXX
@@ -287,6 +294,94 @@ if test "$with_go" = "yes"; then
fi
AM_CONDITIONAL(WITH_GO, [test "$have_go" = "yes"])
+
+AX_THRIFT_LIB(d, [D], yes)
+if test "$with_d" = "yes"; then
+ AX_DMD
+ AC_SUBST(DMD)
+ if test "x$DMD" != "x"; then
+ have_d="yes"
+ fi
+fi
+
+# Determine actual name of the generated D library for use in the command line
+# when compiling tests. This is needed because the -l<lib> syntax doesn't work
+# with OPTLINK (Windows).
+lib_prefix=lib
+lib_suffix=a
+case "$host_os" in
+ cygwin* | mingw* | pw32* | cegcc*)
+ lib_prefix=""
+ lib_suffix=lib
+ ;;
+esac
+D_LIB_NAME="${lib_prefix}thriftd.${lib_suffix}"
+AC_SUBST(D_LIB_NAME)
+D_EVENT_LIB_NAME="${lib_prefix}thriftd-event.${lib_suffix}"
+AC_SUBST(D_EVENT_LIB_NAME)
+D_SSL_LIB_NAME="${lib_prefix}thriftd-ssl.${lib_suffix}"
+AC_SUBST(D_SSL_LIB_NAME)
+
+if test "$have_d" = "yes"; then
+ AX_CHECK_D_MODULE(deimos.event2.event)
+ have_deimos_event2=$success
+
+ with_d_event_tests="no"
+ if test "$have_deimos_event2" = "yes"; then
+ if test "x$DMD_LIBEVENT_FLAGS" = "x"; then
+ if test "$dmd_optlink" = "yes"; then
+ AC_MSG_WARN([D libevent interface found, but cannot auto-detect \
+linker flags for OPTLINK. Please set DMD_LIBEVENT_FLAGS manually.])
+ else
+ AX_LIB_EVENT([2.0])
+ if test "$success" = "yes"; then
+ DMD_LIBEVENT_FLAGS=$(echo "$LIBEVENT_LDFLAGS $LIBEVENT_LIBS" | \
+ sed -e 's/^ *//g;s/ *$//g;s/^\(.\)/-L\1/g;s/ */ -L/g')
+ with_d_event_tests="yes"
+ else
+ AC_MSG_WARN([D libevent interface present, but libevent library not found.])
+ fi
+ fi
+ else
+ with_d_event_tests="yes"
+ fi
+ fi
+
+ AX_CHECK_D_MODULE(deimos.openssl.ssl)
+ have_deimos_openssl=$success
+
+ with_d_ssl_tests="no"
+ if test "$have_deimos_openssl" = "yes"; then
+ if test "x$DMD_OPENSSL_FLAGS" = "x"; then
+ if test "$dmd_optlink" = "yes"; then
+ AC_MSG_WARN([D OpenSSL interface found, but cannot auto-detect \
+linker flags for OPTLINK. Please set DMD_OPENSSL_FLAGS manually.])
+ else
+ AX_CHECK_OPENSSL([with_d_ssl_tests="yes"])
+ if test "$with_d_ssl_tests" = "yes"; then
+ DMD_OPENSSL_FLAGS=$(echo "$OPENSSL_LDFLAGS $OPENSSL_LIBS" | \
+ sed -e 's/^ *//g;s/ *$//g;s/^\(.\)/-L\1/g;s/ */ -L/g')
+ else
+ AC_MSG_WARN([D OpenSSL interface present, but OpenSSL library not found.])
+ fi
+ fi
+ else
+ with_d_ssl_tests="yes"
+ fi
+ fi
+fi
+
+AM_CONDITIONAL(WITH_D, [test "$have_d" = "yes"])
+AM_CONDITIONAL(DMD_OPTLINK, [test "$dmd_optlink" = "yes"])
+AC_SUBST(DMD_OF_DIRSEP, "$dmd_of_dirsep")
+AM_CONDITIONAL(HAVE_DEIMOS_EVENT2, [test "$have_deimos_event2" = "yes"])
+AM_CONDITIONAL(WITH_D_EVENT_TESTS, [test "$with_d_event_tests" = "yes"])
+AC_SUBST(DMD_LIBEVENT_FLAGS)
+AM_CONDITIONAL(HAVE_DEIMOS_OPENSSL, [test "$have_deimos_openssl" = "yes"])
+AM_CONDITIONAL(WITH_D_SSL_TESTS, [test "$with_d_ssl_tests" = "yes"])
+AC_SUBST(DMD_OPENSSL_FLAGS)
+
+
have_tests=yes
if test "$with_tests" = "no"; then
have_tests="no"
@@ -490,6 +585,8 @@ AC_CONFIG_FILES([
lib/c_glib/thrift_c_glib.pc
lib/c_glib/test/Makefile
lib/csharp/Makefile
+ lib/d/Makefile
+ lib/d/test/Makefile
lib/erl/Makefile
lib/hs/Makefile
lib/java/Makefile
@@ -530,6 +627,7 @@ echo "Building Perl Library ........ : $have_perl"
echo "Building PHP Library ......... : $have_php"
echo "Building Erlang Library ...... : $have_erlang"
echo "Building Go Library .......... : $have_go"
+echo "Building D Library ........... : $have_d"
if test "$have_cpp" = "yes" ; then
echo
echo "C++ Library:"
@@ -589,6 +687,12 @@ if test "$have_go" = "yes" ; then
echo " Using GO Compiler.......... : $GO_C"
echo " Using GO Linker............ : $GO_L"
fi
+if test "$have_d" = "yes" ; then
+ echo
+ echo "Using D Compiler ............. : $DMD"
+ echo "Building D libevent tests .... : $with_d_event_tests"
+ echo "Building D SSL tests ......... : $with_d_ssl_tests"
+fi
echo
echo "If something is missing that you think should be present,"
echo "please skim the output of configure to find the missing"
diff --git a/lib/Makefile.am b/lib/Makefile.am
index cd62d1921..f95428f7e 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -62,6 +62,10 @@ if WITH_PHP
SUBDIRS += php
endif
+if WITH_D
+SUBDIRS += d
+endif
+
# All of the libs that don't use Automake need to go in here
# so they will end up in our release tarballs.
EXTRA_DIST = \
diff --git a/lib/d/Makefile.am b/lib/d/Makefile.am
new file mode 100644
index 000000000..d76a07eff
--- /dev/null
+++ b/lib/d/Makefile.am
@@ -0,0 +1,181 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+SUBDIRS = . test
+
+#
+# Enumeration of all the public and private modules.
+#
+# We unconditionally install all of them, even if libevent or OpenSSL are
+# not available, but build the respective libraries only if the Deimos headers
+# could be found.
+#
+d_thriftmodules = $(addprefix thrift/, base)
+d_thriftdir = $(D_IMPORT_PREFIX)/thrift
+d_thrift_DATA = $(addprefix src/, $(addsuffix .d, $(d_thriftmodules)))
+
+d_asyncmodules = $(addprefix thrift/async/, base libevent socket ssl)
+d_asyncdir = $(d_thriftdir)/async
+d_async_DATA = $(addprefix src/, $(addsuffix .d, $(d_asyncmodules)))
+
+d_codegenmodules = $(addprefix thrift/codegen/, async_client \
+ async_client_pool base client client_pool idlgen processor)
+d_codegendir = $(d_thriftdir)/codegen
+d_codegen_DATA = $(addprefix src/, $(addsuffix .d, $(d_codegenmodules)))
+
+d_protocolmodules = $(addprefix thrift/protocol/, base binary compact json \
+ processor)
+d_protocoldir = $(d_thriftdir)/protocol
+d_protocol_DATA = $(addprefix src/, $(addsuffix .d, $(d_protocolmodules)))
+
+d_servermodules = $(addprefix thrift/server/, base simple nonblocking \
+ taskpool threaded)
+d_serverdir = $(d_thriftdir)/server
+d_server_DATA = $(addprefix src/, $(addsuffix .d, $(d_servermodules)))
+
+d_servertransportmodules = $(addprefix thrift/server/transport/, base socket ssl)
+d_servertransportdir = $(d_thriftdir)/server/transport
+d_servertransport_DATA = $(addprefix src/, $(addsuffix .d, \
+ $(d_servertransportmodules)))
+
+d_transportmodules = $(addprefix thrift/transport/, base buffered file \
+ framed http memory piped range socket ssl zlib)
+d_transportdir = $(d_thriftdir)/transport
+d_transport_DATA = $(addprefix src/, $(addsuffix .d, $(d_transportmodules)))
+
+d_utilmodules = $(addprefix thrift/util/, awaitable cancellation future \
+ hashset)
+d_utildir = $(d_thriftdir)/util
+d_util_DATA = $(addprefix src/, $(addsuffix .d, $(d_utilmodules)))
+
+d_internalmodules = $(addprefix thrift/internal/, algorithm codegen ctfe \
+ endian resource_pool socket ssl ssl_bio traits)
+d_internaldir = $(d_thriftdir)/internal
+d_internal_DATA = $(addprefix src/, $(addsuffix .d, $(d_internalmodules)))
+
+d_testmodules = $(addprefix thrift/internal/test/, protocol server)
+d_testdir = $(d_internaldir)/test
+d_test_DATA = $(addprefix src/, $(addsuffix .d, $(d_testmodules)))
+
+d_publicmodules = $(d_thriftmodules) $(d_asyncmodules) \
+ $(d_codegenmodules) $(d_protocolmodules) $(d_servermodules) \
+ $(d_servertransportmodules) $(d_transportmodules) $(d_utilmodules)
+d_publicsources = $(addprefix src/, $(addsuffix .d, $(d_publicmodules)))
+
+d_modules = $(d_publicmodules) $(d_internalmodules) $(d_testmodules)
+
+# List modules with external dependencies and remove them from the main list
+d_libevent_dependent_modules = thrift/async/libevent thrift/server/nonblocking
+d_openssl_dependent_modules = thrift/async/ssl thrift/internal/ssl \
+ thrift/internal/ssl_bio thrift/transport/ssl thrift/server/transport/ssl
+d_main_modules = $(filter-out $(d_libevent_dependent_modules) \
+ $(d_openssl_dependent_modules),$(d_modules))
+
+
+d_lib_flags = -w -wi -Isrc -lib
+all_targets =
+
+#
+# libevent-dependent modules.
+#
+if HAVE_DEIMOS_EVENT2
+$(D_EVENT_LIB_NAME): $(addprefix src/, $(addsuffix .d, $(d_libevent_dependent_modules)))
+ $(DMD) -of$(D_EVENT_LIB_NAME) $(d_lib_flags) $^
+all_targets += $(D_EVENT_LIB_NAME)
+endif
+
+#
+# OpenSSL-dependent modules.
+#
+if HAVE_DEIMOS_OPENSSL
+$(D_SSL_LIB_NAME): $(addprefix src/, $(addsuffix .d, $(d_openssl_dependent_modules)))
+ $(DMD) -of$(D_SSL_LIB_NAME) $(d_lib_flags) $^
+all_targets += $(D_SSL_LIB_NAME)
+endif
+
+#
+# Main library target.
+#
+$(D_LIB_NAME): $(addprefix src/, $(addsuffix .d, $(d_main_modules)))
+ $(DMD) -of$(D_LIB_NAME) $(d_lib_flags) $^
+all_targets += $(D_LIB_NAME)
+
+
+#
+# Documentation target (requires Dil).
+#
+docs: $(d_publicsources) src/thrift/index.d
+ dil ddoc docs -hl --kandil $^
+
+
+#
+# Hook custom library targets into the automake all/install targets.
+#
+all-local: $(all_targets)
+
+install-exec-local:
+ $(INSTALL_PROGRAM) $(all_targets) $(DESTDIR)$(libdir)
+
+
+clean-local:
+ $(RM) -rf docs $(D_LIB_NAME) $(D_EVENT_LIB_NAME) $(D_SSL_LIB_NAME) unittest
+
+
+#
+# Unit tests (built both in debug and release mode).
+#
+d_test_flags = -unittest -w -wi -I$(top_srcdir)/lib/d/src
+
+# There just must be some way to reassign a variable without warnings in
+# Automake...
+d_test_modules__ = $(d_modules)
+
+if WITH_D_EVENT_TESTS
+d_test_flags += $(DMD_LIBEVENT_FLAGS)
+d_test_modules_ = $(d_test_modules__)
+else
+d_test_modules_ = $(filter-out $(d_libevent_dependent_modules), $(d_test_modules__))
+endif
+
+if WITH_D_SSL_TESTS
+d_test_flags += $(DMD_OPENSSL_FLAGS)
+d_test_modules = $(d_test_modules_)
+else
+d_test_modules = $(filter-out $(d_openssl_dependent_modules), $(d_test_modules_))
+endif
+
+unittest/emptymain.d: unittest/.directory
+ @echo 'void main(){}' >$@
+
+unittest/.directory:
+ mkdir -p unittest || exists unittest
+ touch $@
+
+unittest/debug/%: src/%.d $(all_targets) unittest/emptymain.d
+ $(DMD) -gc -of$(subst /,$(DMD_OF_DIRSEP),$@) $(d_test_flags) $^
+
+unittest/release/%: src/%.d $(all_targets) unittest/emptymain.d
+ $(DMD) -O -release -of$(subst /,$(DMD_OF_DIRSEP),$@) $(d_test_flags) $^
+
+TESTS = $(addprefix unittest/debug/, $(d_test_modules)) \
+ $(addprefix unittest/release/, $(d_test_modules))
+
+
+EXTRA_DIST = \
+ README
diff --git a/lib/d/README b/lib/d/README
new file mode 100644
index 000000000..5d37e4fe7
--- /dev/null
+++ b/lib/d/README
@@ -0,0 +1,58 @@
+Thrift D Software Library
+=========================
+
+License
+-------
+
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+
+Testing
+-------
+
+D support in Thrift is covered by two sets of tests: first,
+the unit test blocks contained in the D source files, and
+second, the more extensive testing applications in the test/
+subdirectory, which also make use of the Thrift compiler.
+Both are built when running "make check", but only the
+unit tests are immediately run, however – the separate test
+cases typically run longer or require manual intervention.
+It might also be prudent to run the independent tests,
+which typically consist of a server and a client part,
+against the other language implementations.
+
+To build the unit tests on Windows, the easiest way might
+be to manually create a file containing an empty main() and
+invoke the compiler by running the following in the src/
+directory (PowerShell syntax):
+
+dmd -ofunittest -unittest -w $(dir -r -filter '*.d' -name)
+
+If you want to run the test clients/servers in OpenSSL
+mode, you have to provide »server-private-key.pem« and
+»server-certificate.pem« files in the directory the server
+executable resides in, and a »trusted-ca-certificate.pem«
+file for the client. The easiest way is to generate a new
+self signed certificate using the provided config file
+(openssl.test.cnf):
+
+openssl req -new -x509 -nodes -config openssl.test.cnf \
+ -out server-certificate.pem
+cat server-certificate.pem > trusted-ca-certificate.pem
+
+This steps are also performed automatically by the
+Autotools build system if the files are not present.
diff --git a/lib/d/src/thrift/async/base.d b/lib/d/src/thrift/async/base.d
new file mode 100644
index 000000000..8debc3be0
--- /dev/null
+++ b/lib/d/src/thrift/async/base.d
@@ -0,0 +1,228 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Defines the interface used for client-side handling of asynchronous
+ * I/O operations, based on coroutines.
+ *
+ * The main piece of the »client side« (e.g. for TAsyncClient users) of the
+ * API is TFuture, which represents an asynchronously executed operation,
+ * which can have a return value, throw exceptions, and which can be waited
+ * upon.
+ *
+ * On the »implementation side«, the idea is that by using a TAsyncTransport
+ * instead of a normal TTransport and executing the work through a
+ * TAsyncManager, the same code as for synchronous I/O can be used for
+ * asynchronous operation as well, for example:
+ *
+ * ---
+ * auto socket = new TAsyncSocket(someTAsyncSocketManager(), host, port);
+ * // …
+ * socket.asyncManager.execute(socket, {
+ * SomeThriftStruct s;
+ *
+ * // Waiting for socket I/O will not block an entire thread but cause
+ * // the async manager to execute another task in the meantime, because
+ * // we are using TAsyncSocket instead of TSocket.
+ * s.read(socket);
+ *
+ * // Do something with s, e.g. set a TPromise result to it.
+ * writeln(s);
+ * });
+ * ---
+ */
+module thrift.async.base;
+
+import core.time : Duration, dur;
+import std.socket/+ : Socket+/; // DMD @@BUG314@@
+import thrift.base;
+import thrift.transport.base;
+import thrift.util.cancellation;
+
+/**
+ * Manages one or more asynchronous transport resources (e.g. sockets in the
+ * case of TAsyncSocketManager) and allows work items to be submitted for them.
+ *
+ * Implementations will typically run one or more background threads for
+ * executing the work, which is one of the reasons for a TAsyncManager to be
+ * used. Each work item is run in its own fiber and is expected to yield() away
+ * while waiting for time-consuming operations.
+ *
+ * The second important purpose of TAsyncManager is to serialize access to
+ * the transport resources – without taking care of that, e.g. issuing multiple
+ * RPC calls over the same connection in rapid succession would likely lead to
+ * more than one request being written at the same time, causing only garbage
+ * to arrive at the remote end.
+ *
+ * All methods are thread-safe.
+ */
+interface TAsyncManager {
+ /**
+ * Submits a work item to be executed asynchronously.
+ *
+ * Access to asnyc transports is serialized – if two work items associated
+ * with the same transport are submitted, the second delegate will not be
+ * invoked until the first has returned, even it the latter context-switches
+ * away (because it is waiting for I/O) and the async manager is idle
+ * otherwise.
+ *
+ * Optionally, a TCancellation instance can be specified. If present,
+ * triggering it will be considered a request to cancel the work item, if it
+ * is still waiting for the associated transport to become available.
+ * Delegates which are already being processed (i.e. waiting for I/O) are not
+ * affected because this would bring the connection into an undefined state
+ * (as probably half-written request or a half-read response would be left
+ * behind).
+ *
+ * Params:
+ * transport = The TAsyncTransport the work delegate will operate on. Must
+ * be associated with this TAsyncManager instance.
+ * work = The operations to execute on the given transport. Must never
+ * throw, errors should be handled in another way. nothrow semantics are
+ * difficult to enforce in combination with fibres though, so currently
+ * exceptions are just swallowed by TAsyncManager implementations.
+ * cancellation = If set, can be used to request cancellatinon of this work
+ * item if it is still waiting to be executed.
+ *
+ * Note: The work item will likely be executed in a different thread, so make
+ * sure the code it relies on is thread-safe. An exception are the async
+ * transports themselves, to which access is serialized as noted above.
+ */
+ void execute(TAsyncTransport transport, void delegate() work,
+ TCancellation cancellation = null
+ ) in {
+ assert(transport.asyncManager is this,
+ "The given transport must be associated with this TAsyncManager.");
+ }
+
+ /**
+ * Submits a delegate to be executed after a certain amount of time has
+ * passed.
+ *
+ * The actual amount of time elapsed can be higher if the async manager
+ * instance is busy and thus should not be relied on. The
+ *
+ * Params:
+ * duration = The amount of time to wait before starting to execute the
+ * work delegate.
+ * work = The code to execute after the specified amount of time has passed.
+ *
+ * Example:
+ * ---
+ * // A very basic example – usually, the actuall work item would enqueue
+ * // some async transport operation.
+ * auto asyncMangager = someAsyncManager();
+ *
+ * TFuture!int calculate() {
+ * // Create a promise and asynchronously set its value after three
+ * // seconds have passed.
+ * auto promise = new TPromise!int;
+ * asyncManager.delay(dur!"seconds"(3), {
+ * promise.succeed(42);
+ * });
+ *
+ * // Immediately return it to the caller.
+ * return promise;
+ * }
+ *
+ * // This will wait until the result is available and then print it.
+ * writeln(calculate().waitGet());
+ * ---
+ */
+ void delay(Duration duration, void delegate() work);
+
+ /**
+ * Shuts down all background threads or other facilities that might have
+ * been started in order to execute work items. This function is typically
+ * called during program shutdown.
+ *
+ * If there are still tasks to be executed when the timeout expires, any
+ * currently executed work items will never receive any notifications
+ * for async transports managed by this instance, queued work items will
+ * be silently dropped, and implementations are allowed to leak resources.
+ *
+ * Params:
+ * waitFinishTimeout = If positive, waits for all work items to be
+ * finished for the specified amount of time, if negative, waits for
+ * completion without ever timing out, if zero, immediately shuts down
+ * the background facilities.
+ */
+ bool stop(Duration waitFinishTimeout = dur!"hnsecs"(-1));
+}
+
+/**
+ * A TTransport which uses a TAsyncManager to schedule non-blocking operations.
+ *
+ * The actual type of device is not specified; typically, implementations will
+ * depend on an interface derived from TAsyncManager to be notified of changes
+ * in the transport state.
+ *
+ * The peeking, reading, writing and flushing methods must always be called
+ * from within the associated async manager.
+ */
+interface TAsyncTransport : TTransport {
+ /**
+ * The TAsyncManager associated with this transport.
+ */
+ TAsyncManager asyncManager() @property;
+}
+
+/**
+ * A TAsyncManager providing notificiations for socket events.
+ */
+interface TAsyncSocketManager : TAsyncManager {
+ /**
+ * Adds a listener that is triggered once when an event of the specified type
+ * occurs, and removed afterwards.
+ *
+ * Params:
+ * socket = The socket to listen for events at.
+ * eventType = The type of the event to listen for.
+ * timeout = The period of time after which the listener will be called
+ * with TAsyncEventReason.TIMED_OUT if no event happened.
+ * listener = The delegate to call when an event happened.
+ */
+ void addOneshotListener(Socket socket, TAsyncEventType eventType,
+ Duration timeout, TSocketEventListener listener);
+
+ /// Ditto
+ void addOneshotListener(Socket socket, TAsyncEventType eventType,
+ TSocketEventListener listener);
+}
+
+/**
+ * Types of events that can happen for an asynchronous transport.
+ */
+enum TAsyncEventType {
+ READ, /// New data became available to read.
+ WRITE /// The transport became ready to be written to.
+}
+
+/**
+ * The type of the delegates used to register socket event handlers.
+ */
+alias void delegate(TAsyncEventReason callReason) TSocketEventListener;
+
+/**
+ * The reason a listener was called.
+ */
+enum TAsyncEventReason : byte {
+ NORMAL, /// The event listened for was triggered normally.
+ TIMED_OUT /// A timeout for the event was set, and it expired.
+}
diff --git a/lib/d/src/thrift/async/libevent.d b/lib/d/src/thrift/async/libevent.d
new file mode 100644
index 000000000..3358bef34
--- /dev/null
+++ b/lib/d/src/thrift/async/libevent.d
@@ -0,0 +1,461 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.async.libevent;
+
+import core.atomic;
+import core.time : Duration, dur;
+import core.exception : onOutOfMemoryError;
+import core.memory : GC;
+import core.thread : Fiber, Thread;
+import core.sync.condition;
+import core.sync.mutex;
+import core.stdc.stdlib : free, malloc;
+import deimos.event2.event;
+import std.array : empty, front, popFront;
+import std.conv : text, to;
+import std.exception : enforce;
+import std.socket : Socket, socketPair;
+import thrift.base;
+import thrift.async.base;
+import thrift.internal.socket;
+import thrift.internal.traits;
+import thrift.util.cancellation;
+
+// To avoid DMD @@BUG6395@@.
+import thrift.internal.algorithm;
+
+/**
+ * A TAsyncManager implementation based on libevent.
+ *
+ * The libevent loop for handling non-blocking sockets is run in a background
+ * thread, which is lazily spawned. The thread is not daemonized to avoid
+ * crashes on program shutdown, it is only stopped when the manager instance
+ * is destroyed. So, to ensure a clean program teardown, either make sure this
+ * instance gets destroyed (e.g. by using scope), or manually call stop() at
+ * the end.
+ */
+class TLibeventAsyncManager : TAsyncSocketManager {
+ this() {
+ eventBase_ = event_base_new();
+
+ // Set up the socket pair for transferring control messages to the event
+ // loop.
+ auto pair = socketPair();
+ controlSendSocket_ = pair[0];
+ controlReceiveSocket_ = pair[1];
+ controlReceiveSocket_.blocking = false;
+
+ // Register an event for receiving control messages.
+ controlReceiveEvent_ = event_new(eventBase_, controlReceiveSocket_.handle,
+ EV_READ | EV_PERSIST | EV_ET, assumeNothrow(&controlMsgReceiveCallback),
+ cast(void*)this);
+ event_add(controlReceiveEvent_, null);
+
+ queuedCountMutex_ = new Mutex;
+ zeroQueuedCondition_ = new Condition(queuedCountMutex_);
+ }
+
+ ~this() {
+ // stop() should be safe to call, because either we don't have a worker
+ // thread running and it is a no-op anyway, or it is guaranteed to be
+ // still running (blocked in event_base_loop), and thus guaranteed not to
+ // be garbage collected yet.
+ stop(dur!"hnsecs"(0));
+
+ event_free(controlReceiveEvent_);
+ event_base_free(eventBase_);
+ eventBase_ = null;
+ }
+
+ override void execute(TAsyncTransport transport, Work work,
+ TCancellation cancellation = null
+ ) {
+ if (cancellation && cancellation.triggered) return;
+
+ // Keep track that there is a new work item to be processed.
+ incrementQueuedCount();
+
+ ensureWorkerThreadRunning();
+
+ // We should be able to send the control message as a whole – we currently
+ // assume to be able to receive it at once as well. If this proves to be
+ // unstable (e.g. send could possibly return early if the receiving buffer
+ // is full and the blocking call gets interrupted by a signal), it could
+ // be changed to a more sophisticated scheme.
+
+ // Make sure the delegate context doesn't get GCd while the work item is
+ // on the wire.
+ GC.addRoot(work.ptr);
+
+ // Send work message.
+ sendControlMsg(ControlMsg(MsgType.WORK, work, transport));
+
+ if (cancellation) {
+ cancellation.triggering.addCallback({
+ sendControlMsg(ControlMsg(MsgType.CANCEL, work, transport));
+ });
+ }
+ }
+
+ override void delay(Duration duration, void delegate() work) {
+ incrementQueuedCount();
+
+ ensureWorkerThreadRunning();
+
+ const tv = toTimeval(duration);
+
+ // DMD @@BUG@@: Cannot deduce T to void delegate() here.
+ registerOneshotEvent!(void delegate())(
+ -1, 0, assumeNothrow(&delayCallback), &tv,
+ {
+ work();
+ decrementQueuedCount();
+ }
+ );
+ }
+
+ override bool stop(Duration waitFinishTimeout = dur!"hnsecs"(-1)) {
+ bool cleanExit = true;
+
+ synchronized (this) {
+ if (workerThread_) {
+ synchronized (queuedCountMutex_) {
+ if (waitFinishTimeout > dur!"hnsecs"(0)) {
+ if (queuedCount_ > 0) {
+ zeroQueuedCondition_.wait(waitFinishTimeout);
+ }
+ } else if (waitFinishTimeout < dur!"hnsecs"(0)) {
+ while (queuedCount_ > 0) zeroQueuedCondition_.wait();
+ } else {
+ // waitFinishTimeout is zero, immediately exit in all cases.
+ }
+ cleanExit = (queuedCount_ == 0);
+ }
+
+ event_base_loopbreak(eventBase_);
+ sendControlMsg(ControlMsg(MsgType.SHUTDOWN));
+ workerThread_.join();
+ workQueues_ = null;
+ // We have nuked all currently enqueued items, so set the count to
+ // zero. This is safe to do without locking, since the worker thread
+ // is down.
+ queuedCount_ = 0;
+ atomicStore(*(cast(shared)&workerThread_), cast(shared(Thread))null);
+ }
+ }
+
+ return cleanExit;
+ }
+
+ override void addOneshotListener(Socket socket, TAsyncEventType eventType,
+ TSocketEventListener listener
+ ) {
+ addOneshotListenerImpl(socket, eventType, null, listener);
+ }
+
+ override void addOneshotListener(Socket socket, TAsyncEventType eventType,
+ Duration timeout, TSocketEventListener listener
+ ) {
+ if (timeout <= dur!"hnsecs"(0)) {
+ addOneshotListenerImpl(socket, eventType, null, listener);
+ } else {
+ // This is not really documented well, but libevent does not require to
+ // keep the timeval around after the event was added.
+ auto tv = toTimeval(timeout);
+ addOneshotListenerImpl(socket, eventType, &tv, listener);
+ }
+ }
+
+private:
+ alias void delegate() Work;
+
+ void addOneshotListenerImpl(Socket socket, TAsyncEventType eventType,
+ const(timeval)* timeout, TSocketEventListener listener
+ ) {
+ registerOneshotEvent(socket.handle, libeventEventType(eventType),
+ assumeNothrow(&socketCallback), timeout, listener);
+ }
+
+ void registerOneshotEvent(T)(evutil_socket_t fd, short type,
+ event_callback_fn callback, const(timeval)* timeout, T payload
+ ) {
+ // Create a copy of the payload on the C heap.
+ auto payloadMem = malloc(payload.sizeof);
+ if (!payloadMem) onOutOfMemoryError();
+ (cast(T*)payloadMem)[0 .. 1] = payload;
+ GC.addRange(payloadMem, payload.sizeof);
+
+ auto result = event_base_once(eventBase_, fd, type, callback,
+ payloadMem, timeout);
+
+ // Assuming that we didn't get our arguments wrong above, the only other
+ // situation in which event_base_once can fail is when it can't allocate
+ // memory.
+ if (result != 0) onOutOfMemoryError();
+ }
+
+ enum MsgType : ubyte {
+ SHUTDOWN,
+ WORK,
+ CANCEL
+ }
+
+ struct ControlMsg {
+ MsgType type;
+ Work work;
+ TAsyncTransport transport;
+ }
+
+ /**
+ * Starts the worker thread if it is not already running.
+ */
+ void ensureWorkerThreadRunning() {
+ // Technically, only half barriers would be required here, but adding the
+ // argument seems to trigger a DMD template argument deduction @@BUG@@.
+ if (!atomicLoad(*(cast(shared)&workerThread_))) {
+ synchronized (this) {
+ if (!workerThread_) {
+ auto thread = new Thread({ event_base_loop(eventBase_, 0); });
+ thread.start();
+ atomicStore(*(cast(shared)&workerThread_), cast(shared)thread);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sends a control message to the worker thread.
+ */
+ void sendControlMsg(const(ControlMsg) msg) {
+ auto result = controlSendSocket_.send((&msg)[0 .. 1]);
+ enum size = msg.sizeof;
+ enforce(result == size, new TException(text(
+ "Sending control message of type ", msg.type, " failed (", result,
+ " bytes instead of ", size, " transmitted).")));
+ }
+
+ /**
+ * Receives messages from the control message socket and acts on them. Called
+ * from the worker thread.
+ */
+ void receiveControlMsg() {
+ // Read as many new work items off the socket as possible (at least one
+ // should be available, as we got notified by libevent).
+ ControlMsg msg;
+ ptrdiff_t bytesRead;
+ while (true) {
+ bytesRead = controlReceiveSocket_.receive(cast(ubyte[])((&msg)[0 .. 1]));
+
+ if (bytesRead < 0) {
+ auto errno = getSocketErrno();
+ if (errno != WOULD_BLOCK_ERRNO) {
+ logError("Reading control message, some work item will possibly " ~
+ "never be executed: %s", socketErrnoString(errno));
+ }
+ }
+ if (bytesRead != msg.sizeof) break;
+
+ // Everything went fine, we received a new control message.
+ final switch (msg.type) {
+ case MsgType.SHUTDOWN:
+ // The message was just intended to wake us up for shutdown.
+ break;
+
+ case MsgType.CANCEL:
+ // When processing a cancellation, we must not touch the first item,
+ // since it is already being processed.
+ auto queue = workQueues_[msg.transport];
+ if (queue.length > 0) {
+ workQueues_[msg.transport] = [queue[0]] ~
+ removeEqual(queue[1 .. $], msg.work);
+ }
+ break;
+
+ case MsgType.WORK:
+ // Now that the work item is back in the D world, we don't need the
+ // extra GC root for the context pointer anymore (see execute()).
+ GC.removeRoot(msg.work.ptr);
+
+ // Add the work item to the queue and execute it.
+ auto queue = msg.transport in workQueues_;
+ if (queue is null || (*queue).empty) {
+ // If the queue is empty, add the new work item to the queue as well,
+ // but immediately start executing it.
+ workQueues_[msg.transport] = [msg.work];
+ executeWork(msg.transport, msg.work);
+ } else {
+ (*queue) ~= msg.work;
+ }
+ break;
+ }
+ }
+
+ // If the last read was successful, but didn't read enough bytes, we got
+ // a problem.
+ if (bytesRead > 0) {
+ logError("Unexpected partial control message read (%s byte(s) " ~
+ "instead of %s), some work item will possibly never be executed.",
+ bytesRead, msg.sizeof);
+ }
+ }
+
+ /**
+ * Executes the given work item and all others enqueued for the same
+ * transport in a new fiber. Called from the worker thread.
+ */
+ void executeWork(TAsyncTransport transport, Work work) {
+ (new Fiber({
+ auto item = work;
+ while (true) {
+ try {
+ // Execute the actual work. It will possibly add listeners to the
+ // event loop and yield away if it has to wait for blocking
+ // operations. It is quite possible that another fiber will modify
+ // the work queue for the current transport.
+ item();
+ } catch (Exception e) {
+ // This should never happen, just to be sure the worker thread
+ // doesn't stop working in mysterious ways because of an unhandled
+ // exception.
+ logError("Exception thrown by work item: %s", e);
+ }
+
+ // Remove the item from the work queue.
+ // Note: Due to the value semantics of array slices, we have to
+ // re-lookup this on every iteration. This could be solved, but I'd
+ // rather replace this directly with a queue type once one becomes
+ // available in Phobos.
+ auto queue = workQueues_[transport];
+ assert(queue.front == item);
+ queue.popFront();
+ workQueues_[transport] = queue;
+
+ // Now that the work item is done, no longer count it as queued.
+ decrementQueuedCount();
+
+ if (queue.empty) break;
+
+ // If the queue is not empty, execute the next waiting item.
+ item = queue.front;
+ }
+ })).call();
+ }
+
+ /**
+ * Increments the amount of queued items.
+ */
+ void incrementQueuedCount() {
+ synchronized (queuedCountMutex_) {
+ ++queuedCount_;
+ }
+ }
+
+ /**
+ * Decrements the amount of queued items.
+ */
+ void decrementQueuedCount() {
+ synchronized (queuedCountMutex_) {
+ assert(queuedCount_ > 0);
+ --queuedCount_;
+ if (queuedCount_ == 0) {
+ zeroQueuedCondition_.notifyAll();
+ }
+ }
+ }
+
+ static extern(C) void controlMsgReceiveCallback(evutil_socket_t, short,
+ void *managerThis
+ ) {
+ (cast(TLibeventAsyncManager)managerThis).receiveControlMsg();
+ }
+
+ static extern(C) void socketCallback(evutil_socket_t, short flags,
+ void *arg
+ ) {
+ auto reason = (flags & EV_TIMEOUT) ? TAsyncEventReason.TIMED_OUT :
+ TAsyncEventReason.NORMAL;
+ (*(cast(TSocketEventListener*)arg))(reason);
+ GC.removeRange(arg);
+ clear(arg);
+ free(arg);
+ }
+
+ static extern(C) void delayCallback(evutil_socket_t, short flags,
+ void *arg
+ ) {
+ assert(flags & EV_TIMEOUT);
+ (*(cast(void delegate()*)arg))();
+ GC.removeRange(arg);
+ clear(arg);
+ free(arg);
+ }
+
+ Thread workerThread_;
+
+ event_base* eventBase_;
+
+ /// The socket used for receiving new work items in the event loop. Paired
+ /// with controlSendSocket_. Invalid (i.e. TAsyncWorkItem.init) items are
+ /// ignored and can be used to wake up the worker thread.
+ Socket controlReceiveSocket_;
+ event* controlReceiveEvent_;
+
+ /// The socket used to send new work items to the event loop. It is
+ /// expected that work items can always be read at once from it, i.e. that
+ /// there will never be short reads.
+ Socket controlSendSocket_;
+
+ /// Queued up work delegates for async transports. This also includes
+ /// currently active ones, they are removed from the queue on completion,
+ /// which is relied on by the control message receive fiber (the main one)
+ /// to decide whether to immediately start executing items or not.
+ // TODO: This should really be of some queue type, not an array slice, but
+ // std.container doesn't have anything.
+ Work[][TAsyncTransport] workQueues_;
+
+ /// The total number of work items not yet finished (queued and currently
+ /// excecuted) and delays not yet executed.
+ uint queuedCount_;
+
+ /// Protects queuedCount_.
+ Mutex queuedCountMutex_;
+
+ /// Triggered when queuedCount_ reaches zero, protected by queuedCountMutex_.
+ Condition zeroQueuedCondition_;
+}
+
+private {
+ timeval toTimeval(const(Duration) dur) {
+ timeval tv = {tv_sec: cast(int)dur.total!"seconds"(),
+ tv_usec: dur.fracSec.usecs};
+ return tv;
+ }
+
+ /**
+ * Returns the libevent flags combination to represent a given TAsyncEventType.
+ */
+ short libeventEventType(TAsyncEventType type) {
+ final switch (type) {
+ case TAsyncEventType.READ:
+ return EV_READ | EV_ET;
+ case TAsyncEventType.WRITE:
+ return EV_WRITE | EV_ET;
+ }
+ }
+}
diff --git a/lib/d/src/thrift/async/socket.d b/lib/d/src/thrift/async/socket.d
new file mode 100644
index 000000000..6de13d916
--- /dev/null
+++ b/lib/d/src/thrift/async/socket.d
@@ -0,0 +1,357 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.async.socket;
+
+import core.thread : Fiber;
+import core.time : dur, Duration;
+import std.array : empty;
+import std.conv : to;
+import std.exception : enforce;
+import std.socket;
+import thrift.base;
+import thrift.async.base;
+import thrift.transport.base;
+import thrift.transport.socket : TSocketBase;
+import thrift.internal.endian;
+import thrift.internal.socket;
+
+version (Windows) {
+ import std.c.windows.winsock : connect;
+} else version (Posix) {
+ import core.sys.posix.sys.socket : connect;
+} else static assert(0, "Don't know connect on this platform.");
+
+/**
+ * Non-blocking socket implementation of the TTransport interface.
+ *
+ * Whenever a socket operation would block, TAsyncSocket registers a callback
+ * with the specified TAsyncSocketManager and yields.
+ *
+ * As for thrift.transport.socket, due to the limitations of std.socket,
+ * currently only TCP/IP sockets are supported (i.e. Unix domain sockets are
+ * not).
+ */
+class TAsyncSocket : TSocketBase, TAsyncTransport {
+ /**
+ * Constructor that takes an already created, connected (!) socket.
+ *
+ * Params:
+ * asyncManager = The TAsyncSocketManager to use for non-blocking I/O.
+ * socket = Already created, connected socket object. Will be switched to
+ * non-blocking mode if it isn't already.
+ */
+ this(TAsyncSocketManager asyncManager, Socket socket) {
+ asyncManager_ = asyncManager;
+ socket.blocking = false;
+ super(socket);
+ }
+
+ /**
+ * Creates a new unconnected socket that will connect to the given host
+ * on the given port.
+ *
+ * Params:
+ * asyncManager = The TAsyncSocketManager to use for non-blocking I/O.
+ * host = Remote host.
+ * port = Remote port.
+ */
+ this(TAsyncSocketManager asyncManager, string host, ushort port) {
+ asyncManager_ = asyncManager;
+ super(host, port);
+ }
+
+ override TAsyncManager asyncManager() @property {
+ return asyncManager_;
+ }
+
+ /**
+ * Asynchronously connects the socket.
+ *
+ * Completes without blocking and defers further operations on the socket
+ * until the connection is established. If connecting fails, this is
+ * currently not indicated in any way other than every call to read/write
+ * failing.
+ */
+ override void open() {
+ if (isOpen) return;
+
+ enforce(!host_.empty, new TTransportException(
+ "Cannot open null host.", TTransportException.Type.NOT_OPEN));
+ enforce(port_ != 0, new TTransportException(
+ "Cannot open with null port.", TTransportException.Type.NOT_OPEN));
+
+
+ // Cannot use std.socket.Socket.connect here because it hides away
+ // EINPROGRESS/WSAWOULDBLOCK.
+ Address addr;
+ try {
+ // Currently, we just go with the first address returned, could be made
+ // more intelligent though – IPv6?
+ addr = getAddress(host_, port_)[0];
+ } catch (Exception e) {
+ throw new TTransportException(`Unable to resolve host "` ~ host_ ~ `".`,
+ TTransportException.Type.NOT_OPEN, __FILE__, __LINE__, e);
+ }
+
+ socket_ = new TcpSocket(addr.addressFamily);
+ socket_.blocking = false;
+ setSocketOpts();
+
+ auto errorCode = connect(socket_.handle, addr.name(), addr.nameLen());
+ if (errorCode == 0) {
+ // If the connection could be established immediately, just return. I
+ // don't know if this ever happens.
+ return;
+ }
+
+ auto errno = getSocketErrno();
+ if (errno != CONNECT_INPROGRESS_ERRNO) {
+ throw new TTransportException(`Could not establish connection to "` ~
+ host_ ~ `": ` ~ socketErrnoString(errno),
+ TTransportException.Type.NOT_OPEN);
+ }
+
+ // This is the expected case: connect() signalled that the connection
+ // is being established in the background. Queue up a work item with the
+ // async manager which just defers any other operations on this
+ // TAsyncSocket instance until the socket is ready.
+ asyncManager_.execute(this,
+ {
+ auto fiber = Fiber.getThis();
+ TAsyncEventReason reason = void;
+ asyncManager_.addOneshotListener(socket_, TAsyncEventType.WRITE,
+ connectTimeout,
+ scopedDelegate((TAsyncEventReason r){ reason = r; fiber.call(); })
+ );
+ Fiber.yield();
+
+ if (reason == TAsyncEventReason.TIMED_OUT) {
+ // Close the connection, so that subsequent work items fail immediately.
+ closeImmediately();
+ return;
+ }
+
+ int errorCode = void;
+ socket_.getOption(SocketOptionLevel.SOCKET, cast(SocketOption)SO_ERROR,
+ errorCode);
+
+ if (errorCode) {
+ logInfo("Could not connect TAsyncSocket: %s",
+ socketErrnoString(errorCode));
+
+ // Close the connection, so that subsequent work items fail immediately.
+ closeImmediately();
+ return;
+ }
+
+ }
+ );
+ }
+
+ /**
+ * Closes the socket.
+ *
+ * Will block until all currently active operations are finished before the
+ * socket is closed.
+ */
+ override void close() {
+ if (!isOpen) return;
+
+ import core.sync.condition;
+ import core.sync.mutex;
+
+ auto doneMutex = new Mutex;
+ auto doneCond = new Condition(doneMutex);
+ synchronized (doneMutex) {
+ asyncManager_.execute(this,
+ scopedDelegate(
+ {
+ closeImmediately();
+ synchronized (doneMutex) doneCond.notifyAll();
+ }
+ )
+ );
+ doneCond.wait();
+ }
+ }
+
+ override bool peek() {
+ if (!isOpen) return false;
+
+ ubyte buf;
+ auto r = socket_.receive((&buf)[0..1], SocketFlags.PEEK);
+ if (r == Socket.ERROR) {
+ auto lastErrno = getSocketErrno();
+ static if (connresetOnPeerShutdown) {
+ if (lastErrno == ECONNRESET) {
+ closeImmediately();
+ return false;
+ }
+ }
+ throw new TTransportException("Peeking into socket failed: " ~
+ socketErrnoString(lastErrno), TTransportException.Type.UNKNOWN);
+ }
+ return (r > 0);
+ }
+
+ override size_t read(ubyte[] buf) {
+ enforce(isOpen, new TTransportException(
+ "Cannot read if socket is not open.", TTransportException.Type.NOT_OPEN));
+
+ typeof(getSocketErrno()) lastErrno;
+
+ auto r = yieldOnBlock(socket_.receive(cast(void[])buf),
+ TAsyncEventType.READ);
+
+ // If recv went fine, immediately return.
+ if (r >= 0) return r;
+
+ // Something went wrong, find out how to handle it.
+ lastErrno = getSocketErrno();
+
+ static if (connresetOnPeerShutdown) {
+ // See top comment.
+ if (lastErrno == ECONNRESET) {
+ return 0;
+ }
+ }
+
+ throw new TTransportException("Receiving from socket failed: " ~
+ socketErrnoString(lastErrno), TTransportException.Type.UNKNOWN);
+ }
+
+ override void write(in ubyte[] buf) {
+ size_t sent;
+ while (sent < buf.length) {
+ sent += writeSome(buf[sent .. $]);
+ }
+ assert(sent == buf.length);
+ }
+
+ override size_t writeSome(in ubyte[] buf) {
+ enforce(isOpen, new TTransportException(
+ "Cannot write if socket is not open.", TTransportException.Type.NOT_OPEN));
+
+ auto r = yieldOnBlock(socket_.send(buf), TAsyncEventType.WRITE);
+
+ // Everything went well, just return the number of bytes written.
+ if (r > 0) return r;
+
+ // Handle error conditions.
+ if (r < 0) {
+ auto lastErrno = getSocketErrno();
+
+ auto type = TTransportException.Type.UNKNOWN;
+ if (isSocketCloseErrno(lastErrno)) {
+ type = TTransportException.Type.NOT_OPEN;
+ closeImmediately();
+ }
+
+ throw new TTransportException("Sending to socket failed: " ~
+ socketErrnoString(lastErrno), type);
+ }
+
+ // send() should never return 0.
+ throw new TTransportException("Sending to socket failed (0 bytes written).",
+ TTransportException.Type.UNKNOWN);
+ }
+
+ /// The amount of time in which a conncetion must be established before the
+ /// open() call times out.
+ Duration connectTimeout = dur!"seconds"(5);
+
+private:
+ void closeImmediately() {
+ socket_.close();
+ socket_ = null;
+ }
+
+ T yieldOnBlock(T)(lazy T call, TAsyncEventType eventType) {
+ while (true) {
+ auto result = call();
+ if (result != Socket.ERROR || getSocketErrno() != WOULD_BLOCK_ERRNO) return result;
+
+ // We got an EAGAIN result, register a callback to return here once some
+ // event happens and yield.
+
+ Duration timeout = void;
+ final switch (eventType) {
+ case TAsyncEventType.READ:
+ timeout = recvTimeout_;
+ break;
+ case TAsyncEventType.WRITE:
+ timeout = sendTimeout_;
+ break;
+ }
+
+ auto fiber = Fiber.getThis();
+ assert(fiber, "Current fiber null – not running in TAsyncManager?");
+ TAsyncEventReason eventReason = void;
+ asyncManager_.addOneshotListener(socket_, eventType, timeout,
+ scopedDelegate((TAsyncEventReason reason) {
+ eventReason = reason;
+ fiber.call();
+ })
+ );
+
+ // Yields execution back to the async manager, will return back here once
+ // the above listener is called.
+ Fiber.yield();
+
+ if (eventReason == TAsyncEventReason.TIMED_OUT) {
+ // If we are cancelling the request due to a timed out operation, the
+ // connection is in an undefined state, because the server could decide
+ // to send the requested data later, or we could have already been half-
+ // way into writing a request. Thus, we close the connection to make any
+ // possibly queued up work items fail immediately. Besides, the server
+ // is not very likely to immediately recover after a socket-level
+ // timeout has expired anyway.
+ closeImmediately();
+
+ throw new TTransportException("Timed out while waiting for socket " ~
+ "to get ready to " ~ to!string(eventType) ~ ".",
+ TTransportException.Type.TIMED_OUT);
+ }
+ }
+ }
+
+ /// The TAsyncSocketManager to use for non-blocking I/O.
+ TAsyncSocketManager asyncManager_;
+}
+
+private {
+ // std.socket doesn't include SO_ERROR for reasons unknown.
+ version (linux) {
+ enum SO_ERROR = 4;
+ } else version (OSX) {
+ enum SO_ERROR = 0x1007;
+ } else version (FreeBSD) {
+ enum SO_ERROR = 0x1007;
+ } else version (Win32) {
+ import std.c.windows.winsock : SO_ERROR;
+ } else static assert(false, "Don't know SO_ERROR on this platform.");
+
+ // This hack forces a delegate literal to be scoped, even if it is passed to
+ // a function accepting normal delegates as well. DMD likes to allocate the
+ // context on the heap anyway, but it seems to work for LDC.
+ import std.traits : isDelegate;
+ auto scopedDelegate(D)(scope D d) if (isDelegate!D) {
+ return d;
+ }
+}
diff --git a/lib/d/src/thrift/async/ssl.d b/lib/d/src/thrift/async/ssl.d
new file mode 100644
index 000000000..fe6242613
--- /dev/null
+++ b/lib/d/src/thrift/async/ssl.d
@@ -0,0 +1,292 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.async.ssl;
+
+import core.thread : Fiber;
+import core.time : Duration;
+import std.array : empty;
+import std.conv : to;
+import std.exception : enforce;
+import std.socket;
+import deimos.openssl.err;
+import deimos.openssl.ssl;
+import thrift.base;
+import thrift.async.base;
+import thrift.async.socket;
+import thrift.internal.ssl;
+import thrift.internal.ssl_bio;
+import thrift.transport.base;
+import thrift.transport.ssl;
+
+/**
+ * Provides SSL/TLS encryption for async sockets.
+ *
+ * This implementation should be considered experimental, as it context-switches
+ * between fibers from within OpenSSL calls, and the safety of this has not yet
+ * been verified.
+ *
+ * For obvious reasons (the SSL connection is stateful), more than one instance
+ * should never be used on a given socket at the same time.
+ */
+// Note: This could easily be extended to other transports in the future as well.
+// There are only two parts of the implementation which don't work with a generic
+// TTransport: 1) the certificate verification, for which peer name/address are
+// needed from the socket, and 2) the connection shutdown, where the associated
+// async manager is needed because close() is not usually called from within a
+// work item.
+final class TAsyncSSLSocket : TBaseTransport {
+ /**
+ * Constructor.
+ *
+ * Params:
+ * context = The SSL socket context to use. A reference to it is stored so
+ * that it does not get cleaned up while the socket is used.
+ * transport = The underlying async network transport to use for
+ * communication.
+ */
+ this(TAsyncSocket underlyingSocket, TSSLContext context) {
+ socket_ = underlyingSocket;
+ context_ = context;
+ serverSide_ = context.serverSide;
+ accessManager_ = context.accessManager;
+ }
+
+ override bool isOpen() @property {
+ if (ssl_ is null || !socket_.isOpen) return false;
+
+ auto shutdown = SSL_get_shutdown(ssl_);
+ bool shutdownReceived = (shutdown & SSL_RECEIVED_SHUTDOWN) != 0;
+ bool shutdownSent = (shutdown & SSL_SENT_SHUTDOWN) != 0;
+ return !(shutdownReceived && shutdownSent);
+ }
+
+ override bool peek() {
+ if (!isOpen) return false;
+ checkHandshake();
+
+ byte bt = void;
+ auto rc = SSL_peek(ssl_, &bt, bt.sizeof);
+ sslEnforce(rc >= 0, "SSL_peek");
+
+ if (rc == 0) {
+ ERR_clear_error();
+ }
+ return (rc > 0);
+ }
+
+ override void open() {
+ enforce(!serverSide_, "Cannot open a server-side SSL socket.");
+ if (isOpen) return;
+
+ if (ssl_) {
+ // If the underlying socket was automatically closed because of an error
+ // (i.e. close() was called from inside a socket method), we can land
+ // here with the SSL object still allocated; delete it here.
+ cleanupSSL();
+ }
+
+ socket_.open();
+ }
+
+ override void close() {
+ if (!isOpen) return;
+
+ if (ssl_ !is null) {
+ // SSL needs to send/receive data over the socket as part of the shutdown
+ // protocol, so we must execute the calls in the context of the associated
+ // async manager. On the other hand, TTransport clients expect the socket
+ // to be closed when close() returns, so we have to block until the
+ // shutdown work item has been executed.
+ import core.sync.condition;
+ import core.sync.mutex;
+
+ int rc = void;
+ auto doneMutex = new Mutex;
+ auto doneCond = new Condition(doneMutex);
+ synchronized (doneMutex) {
+ socket_.asyncManager.execute(socket_, {
+ rc = SSL_shutdown(ssl_);
+ if (rc == 0) {
+ rc = SSL_shutdown(ssl_);
+ }
+ synchronized (doneMutex) doneCond.notifyAll();
+ });
+ doneCond.wait();
+ }
+
+ if (rc < 0) {
+ // Do not throw an exception here as leaving the transport "open" will
+ // probably produce only more errors, and the chance we can do
+ // something about the error e.g. by retrying is very low.
+ logError("Error while shutting down SSL: %s", getSSLException());
+ }
+
+ cleanupSSL();
+ }
+
+ socket_.close();
+ }
+
+ override size_t read(ubyte[] buf) {
+ checkHandshake();
+ auto rc = SSL_read(ssl_, buf.ptr, cast(int)buf.length);
+ sslEnforce(rc >= 0, "SSL_read");
+ return rc;
+ }
+
+ override void write(in ubyte[] buf) {
+ checkHandshake();
+
+ // Loop in case SSL_MODE_ENABLE_PARTIAL_WRITE is set in SSL_CTX.
+ size_t written = 0;
+ while (written < buf.length) {
+ auto bytes = SSL_write(ssl_, buf.ptr + written,
+ cast(int)(buf.length - written));
+ sslEnforce(bytes > 0, "SSL_write");
+ written += bytes;
+ }
+ }
+
+ override void flush() {
+ checkHandshake();
+
+ auto bio = SSL_get_wbio(ssl_);
+ enforce(bio !is null, new TSSLException("SSL_get_wbio returned null"));
+
+ auto rc = BIO_flush(bio);
+ sslEnforce(rc == 1, "BIO_flush");
+ }
+
+ /**
+ * Whether to use client or server side SSL handshake protocol.
+ */
+ bool serverSide() @property const {
+ return serverSide_;
+ }
+
+ /// Ditto
+ void serverSide(bool value) @property {
+ serverSide_ = value;
+ }
+
+ /**
+ * The access manager to use.
+ */
+ void accessManager(TAccessManager value) @property {
+ accessManager_ = value;
+ }
+
+private:
+ /**
+ * If the condition is false, cleans up the SSL connection and throws the
+ * exception for the last SSL error.
+ */
+ void sslEnforce(bool condition, string location) {
+ if (!condition) {
+ // We need to fetch the error first, as the error stack will be cleaned
+ // when shutting down SSL.
+ auto e = getSSLException(location);
+ cleanupSSL();
+ throw e;
+ }
+ }
+
+ /**
+ * Frees the SSL connection object and clears the SSL error state.
+ */
+ void cleanupSSL() {
+ SSL_free(ssl_);
+ ssl_ = null;
+ ERR_remove_state(0);
+ }
+
+ /**
+ * Makes sure the SSL connection is up and running, and initializes it if not.
+ */
+ void checkHandshake() {
+ enforce(socket_.isOpen, new TTransportException(
+ TTransportException.Type.NOT_OPEN));
+
+ if (ssl_ !is null) return;
+ ssl_ = context_.createSSL();
+
+ auto bio = createTTransportBIO(socket_, false);
+ SSL_set_bio(ssl_, bio, bio);
+
+ int rc = void;
+ if (serverSide_) {
+ rc = SSL_accept(ssl_);
+ } else {
+ rc = SSL_connect(ssl_);
+ }
+ enforce(rc > 0, getSSLException());
+
+ auto addr = socket_.getPeerAddress();
+ authorize(ssl_, accessManager_, addr,
+ (serverSide_ ? addr.toHostNameString() : socket_.host));
+ }
+
+ TAsyncSocket socket_;
+ bool serverSide_;
+ SSL* ssl_;
+ TSSLContext context_;
+ TAccessManager accessManager_;
+}
+
+/**
+ * Wraps passed TAsyncSocket instances into TAsyncSSLSockets.
+ *
+ * Typically used with TAsyncClient. As an unfortunate consequence of the
+ * async client design, the passed transports cannot be statically verified to
+ * be of type TAsyncSocket. Instead, the type is verified at runtime – if a
+ * transport of an unexpected type is passed to getTransport(), it fails,
+ * throwing a TTransportException.
+ *
+ * Example:
+ * ---
+ * auto context = nwe TSSLContext();
+ * ... // Configure SSL context.
+ * auto factory = new TAsyncSSLSocketFactory(context);
+ *
+ * auto socket = new TAsyncSocket(someAsyncManager, host, port);
+ * socket.open();
+ *
+ * auto client = new TAsyncClient!Service(transport, factory,
+ * new TBinaryProtocolFactory!());
+ * ---
+ */
+class TAsyncSSLSocketFactory : TTransportFactory {
+ ///
+ this(TSSLContext context) {
+ context_ = context;
+ }
+
+ override TAsyncSSLSocket getTransport(TTransport transport) {
+ auto socket = cast(TAsyncSocket)transport;
+ enforce(socket, new TTransportException(
+ "TAsyncSSLSocketFactory requires a TAsyncSocket to work on, not a " ~
+ to!string(typeid(transport)) ~ ".",
+ TTransportException.Type.INTERNAL_ERROR
+ ));
+ return new TAsyncSSLSocket(socket, context_);
+ }
+
+private:
+ TSSLContext context_;
+}
diff --git a/lib/d/src/thrift/base.d b/lib/d/src/thrift/base.d
new file mode 100644
index 000000000..266201b73
--- /dev/null
+++ b/lib/d/src/thrift/base.d
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.base;
+
+/**
+ * Common base class for all Thrift exceptions.
+ */
+class TException : Exception {
+ ///
+ this(string msg = "", string file = __FILE__, size_t line = __LINE__,
+ Throwable next = null)
+ {
+ super(msg, file, line, next);
+ }
+}
+
+/**
+ * An operation failed because one or more sub-tasks failed.
+ */
+class TCompoundOperationException : TException {
+ ///
+ this(string msg, Exception[] exceptions, string file = __FILE__,
+ size_t line = __LINE__, Throwable next = null)
+ {
+ super(msg, file, line, next);
+ this.exceptions = exceptions;
+ }
+
+ /// The exceptions thrown by the children of the operation. If applicable,
+ /// the list is ordered in the same way the exceptions occured.
+ Exception[] exceptions;
+}
+
+/// The Thrift version string, used for informative purposes.
+// Note: This is currently hardcoded, but will likely be filled in by the build
+// system in future versions.
+enum VERSION = "0.9.0 dev";
+
+/**
+ * Functions used for logging inside Thrift.
+ *
+ * By default, the formatted messages are written to stdout/stderr, but this
+ * behavior can be overwritten by providing custom g_{Info, Error}LogSink
+ * handlers.
+ *
+ * Examples:
+ * ---
+ * logInfo("An informative message.");
+ * logError("Some error occured: %s", e);
+ * ---
+ */
+alias logFormatted!g_infoLogSink logInfo;
+alias logFormatted!g_errorLogSink logError; /// Ditto
+
+/**
+ * Error and info log message sinks.
+ *
+ * These delegates are called with the log message passed as const(char)[]
+ * argument, and can be overwritten to hook the Thrift libraries up with a
+ * custom logging system. By default, they forward all output to stdout/stderr.
+ */
+__gshared void delegate(const(char)[]) g_infoLogSink;
+__gshared void delegate(const(char)[]) g_errorLogSink; /// Ditto
+
+shared static this() {
+ import std.stdio;
+
+ g_infoLogSink = (const(char)[] text) {
+ stdout.writeln(text);
+ };
+
+ g_errorLogSink = (const(char)[] text) {
+ stderr.writeln(text);
+ };
+}
+
+// This should be private, if it could still be used through the aliases then.
+template logFormatted(alias target) {
+ void logFormatted(string file = __FILE__, int line = __LINE__,
+ T...)(string format, T args) if (
+ __traits(compiles, { target(""); })
+ ) {
+ import std.format, std.stdio;
+ if (target !is null) {
+ scope(exit) g_formatBuffer.clear();
+
+ // Phobos @@BUG@@: If the empty string put() is removed, Appender.data
+ // stays empty.
+ g_formatBuffer.put("");
+
+ formattedWrite(g_formatBuffer, "%s:%s: ", file, line);
+
+ static if (T.length == 0) {
+ g_formatBuffer.put(format);
+ } else {
+ formattedWrite(g_formatBuffer, format, args);
+ }
+ target(g_formatBuffer.data);
+ }
+ }
+}
+
+private {
+ // Use a global, but thread-local buffer for constructing log messages.
+ import std.array : Appender;
+ Appender!(char[]) g_formatBuffer;
+}
diff --git a/lib/d/src/thrift/codegen/async_client.d b/lib/d/src/thrift/codegen/async_client.d
new file mode 100644
index 000000000..e916dea15
--- /dev/null
+++ b/lib/d/src/thrift/codegen/async_client.d
@@ -0,0 +1,255 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.codegen.async_client;
+
+import std.conv : text, to;
+import std.traits : ParameterStorageClass, ParameterStorageClassTuple,
+ ParameterTypeTuple, ReturnType;
+import thrift.base;
+import thrift.async.base;
+import thrift.codegen.base;
+import thrift.codegen.client;
+import thrift.internal.codegen;
+import thrift.internal.ctfe;
+import thrift.protocol.base;
+import thrift.transport.base;
+import thrift.util.cancellation;
+import thrift.util.future;
+
+/**
+ * Asynchronous Thrift service client which returns the results as TFutures an
+ * uses a TAsyncManager to perform the actual work.
+ *
+ * TAsyncClientBase serves as a supertype for all TAsyncClients for the same
+ * service, which might be instantiated with different concrete protocol types
+ * (there is no covariance for template type parameters), and extends
+ * TFutureInterface!Interface. If Interface is derived from another service
+ * BaseInterface, it also extends TAsyncClientBase!BaseInterface.
+ *
+ * TAsyncClient implements TAsyncClientBase and offers two constructors with
+ * the following signatures:
+ * ---
+ * this(TAsyncTransport trans, TTransportFactory tf, TProtocolFactory pf);
+ * this(TAsyncTransport trans, TTransportFactory itf, TTransportFactory otf,
+ * TProtocolFactory ipf, TProtocolFactory opf);
+ * ---
+ *
+ * Again, if Interface represents a derived Thrift service,
+ * TAsyncClient!Interface is also derived from TAsyncClient!BaseInterface.
+ *
+ * TAsyncClient can exclusively be used with TAsyncTransports, as it needs to
+ * access the associated TAsyncManager. To set up any wrapper transports
+ * (e.g. buffered, framed) on top of it and to instanciate the protocols to use,
+ * TTransportFactory and TProtocolFactory instances are passed to the
+ * constructors – the three argument constructor is a shortcut if the same
+ * transport and protocol are to be used for both input and output, which is
+ * the most common case.
+ *
+ * If the same transport factory is passed for both input and output transports,
+ * only a single wrapper transport will be created and used for both directions.
+ * This allows easy implementation of protocols like SSL.
+ *
+ * Just as TClient does, TAsyncClient also takes two optional template
+ * arguments which can be used for specifying the actual TProtocol
+ * implementation used for optimization purposes, as virtual calls can
+ * completely be eliminated then. If the actual types of the protocols
+ * instantiated by the factories used does not match the ones statically
+ * specified in the template parameters, a TException is thrown during
+ * construction.
+ *
+ * Example:
+ * ---
+ * // A simple Thrift service.
+ * interface Foo { int foo(); }
+ *
+ * // Create a TAsyncSocketManager – thrift.async.libevent is used for this
+ * // example.
+ * auto manager = new TLibeventAsyncManager;
+ *
+ * // Set up an async transport to use.
+ * auto socket = new TAsyncSocket(manager, host, port);
+ *
+ * // Create a client instance.
+ * auto client = new TAsyncClient!Foo(
+ * socket,
+ * new TBufferedTransportFactory, // Wrap the socket in a TBufferedTransport.
+ * new TBinaryProtocolFactory!() // Use the Binary protocol.
+ * );
+ *
+ * // Call foo and use the returned future.
+ * auto result = client.foo();
+ * pragma(msg, typeof(result)); // TFuture!int
+ * int resultValue = result.waitGet(); // Waits until the result is available.
+ * ---
+ */
+interface TAsyncClientBase(Interface) if (isBaseService!Interface) :
+ TFutureInterface!Interface
+{
+ /**
+ * The underlying TAsyncTransport used by this client instance.
+ */
+ TAsyncTransport transport() @property;
+}
+
+/// Ditto
+interface TAsyncClientBase(Interface) if (isDerivedService!Interface) :
+ TAsyncClientBase!(BaseService!Interface), TFutureInterface!Interface
+{}
+
+/// Ditto
+template TAsyncClient(Interface, InputProtocol = TProtocol, OutputProtocol = void) if (
+ isService!Interface && isTProtocol!InputProtocol &&
+ (isTProtocol!OutputProtocol || is(OutputProtocol == void))
+) {
+ mixin({
+ static if (isDerivedService!Interface) {
+ string code = "class TAsyncClient : " ~
+ "TAsyncClient!(BaseService!Interface, InputProtocol, OutputProtocol), " ~
+ "TAsyncClientBase!Interface {\n";
+ code ~= q{
+ this(TAsyncTransport trans, TTransportFactory tf, TProtocolFactory pf) {
+ this(trans, tf, tf, pf, pf);
+ }
+
+ this(TAsyncTransport trans, TTransportFactory itf,
+ TTransportFactory otf, TProtocolFactory ipf, TProtocolFactory opf
+ ) {
+ super(trans, itf, otf, ipf, opf);
+ client_ = new typeof(client_)(iprot_, oprot_);
+ }
+
+ private TClient!(Interface, IProt, OProt) client_;
+ };
+ } else {
+ string code = "class TAsyncClient : TAsyncClientBase!Interface {";
+ code ~= q{
+ alias InputProtocol IProt;
+ static if (isTProtocol!OutputProtocol) {
+ alias OutputProtocol OProt;
+ } else {
+ static assert(is(OutputProtocol == void));
+ alias InputProtocol OProt;
+ }
+
+ this(TAsyncTransport trans, TTransportFactory tf, TProtocolFactory pf) {
+ this(trans, tf, tf, pf, pf);
+ }
+
+ this(TAsyncTransport trans, TTransportFactory itf,
+ TTransportFactory otf, TProtocolFactory ipf, TProtocolFactory opf
+ ) {
+ import std.exception;
+ transport_ = trans;
+
+ auto ip = itf.getTransport(trans);
+ TTransport op = void;
+ if (itf == otf) {
+ op = ip;
+ } else {
+ op = otf.getTransport(trans);
+ }
+
+ auto iprot = ipf.getProtocol(ip);
+ iprot_ = cast(IProt)iprot;
+ enforce(iprot_, new TException(text("Input protocol not of the " ~
+ "specified concrete type (", IProt.stringof, ").")));
+
+ auto oprot = opf.getProtocol(op);
+ oprot_ = cast(OProt)oprot;
+ enforce(oprot_, new TException(text("Output protocol not of the " ~
+ "specified concrete type (", OProt.stringof, ").")));
+
+ client_ = new typeof(client_)(iprot_, oprot_);
+ }
+
+ override TAsyncTransport transport() @property {
+ return transport_;
+ }
+
+ protected TAsyncTransport transport_;
+ protected IProt iprot_;
+ protected OProt oprot_;
+ private TClient!(Interface, IProt, OProt) client_;
+ };
+ }
+
+ foreach (methodName;
+ FilterMethodNames!(Interface, __traits(derivedMembers, Interface))
+ ) {
+ string[] paramList;
+ string[] paramNames;
+ foreach (i, _; ParameterTypeTuple!(mixin("Interface." ~ methodName))) {
+ immutable paramName = "param" ~ to!string(i + 1);
+ immutable storage = ParameterStorageClassTuple!(
+ mixin("Interface." ~ methodName))[i];
+
+ paramList ~= ((storage & ParameterStorageClass.ref_) ? "ref " : "") ~
+ "ParameterTypeTuple!(Interface." ~ methodName ~ ")[" ~
+ to!string(i) ~ "] " ~ paramName;
+ paramNames ~= paramName;
+ }
+ paramList ~= "TCancellation cancellation = null";
+
+ immutable returnTypeCode = "ReturnType!(Interface." ~ methodName ~ ")";
+ code ~= "TFuture!(" ~ returnTypeCode ~ ") " ~ methodName ~ "(" ~
+ ctfeJoin(paramList) ~ ") {\n";
+
+ // Create the future instance that will repesent the result.
+ code ~= "auto promise = new TPromise!(" ~ returnTypeCode ~ ");\n";
+
+ // Prepare delegate which executes the TClient method call.
+ code ~= "auto work = {\n";
+ code ~= "try {\n";
+ code ~= "static if (is(ReturnType!(Interface." ~ methodName ~
+ ") == void)) {\n";
+ code ~= "client_." ~ methodName ~ "(" ~ ctfeJoin(paramNames) ~ ");\n";
+ code ~= "promise.succeed();\n";
+ code ~= "} else {\n";
+ code ~= "auto result = client_." ~ methodName ~ "(" ~
+ ctfeJoin(paramNames) ~ ");\n";
+ code ~= "promise.succeed(result);\n";
+ code ~= "}\n";
+ code ~= "} catch (Exception e) {\n";
+ code ~= "promise.fail(e);\n";
+ code ~= "}\n";
+ code ~= "};\n";
+
+ // If the request is cancelled, set the result promise to cancelled
+ // as well. This could be moved into an additional TAsyncWorkItem
+ // delegate parameter.
+ code ~= q{
+ if (cancellation) {
+ cancellation.triggering.addCallback({
+ promise.cancel();
+ });
+ }
+ };
+
+ // Enqueue the work item and immediately return the promise (resp. its
+ // future interface).
+ code ~= "transport_.asyncManager.execute(transport_, work, cancellation);\n";
+ code ~= "return promise;\n";
+ code ~= "}\n";
+
+ }
+
+ code ~= "}\n";
+ return code;
+ }());
+}
diff --git a/lib/d/src/thrift/codegen/async_client_pool.d b/lib/d/src/thrift/codegen/async_client_pool.d
new file mode 100644
index 000000000..26cb975a3
--- /dev/null
+++ b/lib/d/src/thrift/codegen/async_client_pool.d
@@ -0,0 +1,906 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Utilities for asynchronously querying multiple servers, building on
+ * TAsyncClient.
+ *
+ * Terminology note: The names of the artifacts defined in this module are
+ * derived from »client pool«, because they operate on a pool of
+ * TAsyncClients. However, from a architectural point of view, they often
+ * represent a pool of hosts a Thrift client application communicates with
+ * using RPC calls.
+ */
+module thrift.codegen.async_client_pool;
+
+import core.sync.mutex;
+import core.time : Duration, dur;
+import std.algorithm : map;
+import std.array : array, empty;
+import std.exception : enforce;
+import std.traits : ParameterTypeTuple, ReturnType;
+import thrift.base;
+import thrift.codegen.base;
+import thrift.codegen.async_client;
+import thrift.internal.algorithm;
+import thrift.internal.codegen;
+import thrift.util.awaitable;
+import thrift.util.cancellation;
+import thrift.util.future;
+import thrift.internal.resource_pool;
+
+/**
+ * Represents a generic client pool which implements TFutureInterface!Interface
+ * using multiple TAsyncClients.
+ */
+interface TAsyncClientPoolBase(Interface) if (isService!Interface) :
+ TFutureInterface!Interface
+{
+ /// Shorthand for the client type this pool operates on.
+ alias TAsyncClientBase!Interface Client;
+
+ /**
+ * Adds a client to the pool.
+ */
+ void addClient(Client client);
+
+ /**
+ * Removes a client from the pool.
+ *
+ * Returns: Whether the client was found in the pool.
+ */
+ bool removeClient(Client client);
+
+ /**
+ * Called to determine whether an exception comes from a client from the
+ * pool not working properly, or if it an exception thrown at the
+ * application level.
+ *
+ * If the delegate returns true, the server/connection is considered to be
+ * at fault, if it returns false, the exception is just passed on to the
+ * caller.
+ *
+ * By default, returns true for instances of TTransportException and
+ * TApplicationException, false otherwise.
+ */
+ bool delegate(Exception) rpcFaultFilter() const @property;
+ void rpcFaultFilter(bool delegate(Exception)) @property; /// Ditto
+
+ /**
+ * Whether to open the underlying transports of a client before trying to
+ * execute a method if they are not open. This is usually desirable
+ * because it allows e.g. to automatically reconnect to a remote server
+ * if the network connection is dropped.
+ *
+ * Defaults to true.
+ */
+ bool reopenTransports() const @property;
+ void reopenTransports(bool) @property; /// Ditto
+}
+
+immutable bool delegate(Exception) defaultRpcFaultFilter;
+static this() {
+ defaultRpcFaultFilter = (Exception e) {
+ import thrift.protocol.base;
+ import thrift.transport.base;
+ return (
+ (cast(TTransportException)e !is null) ||
+ (cast(TApplicationException)e !is null)
+ );
+ };
+}
+
+/**
+ * A TAsyncClientPoolBase implementation which queries multiple servers in a
+ * row until a request succeeds, the result of which is then returned.
+ *
+ * The definition of »success« can be customized using the rpcFaultFilter()
+ * delegate property. If it is non-null and calling it for an exception set by
+ * a failed method invocation returns true, the error is considered to be
+ * caused by the RPC layer rather than the application layer, and the next
+ * server in the pool is tried. If there are no more clients to try, the
+ * operation is marked as failed with a TCompoundOperationException.
+ *
+ * If a TAsyncClient in the pool fails with an RPC exception for a number of
+ * consecutive tries, it is temporarily disabled (not tried any longer) for
+ * a certain duration. Both the limit and the timeout can be configured. If all
+ * clients fail (and keepTrying is false), the operation fails with a
+ * TCompoundOperationException which contains the collected RPC exceptions.
+ */
+final class TAsyncClientPool(Interface) if (isService!Interface) :
+ TAsyncClientPoolBase!Interface
+{
+ ///
+ this(Client[] clients) {
+ pool_ = new TResourcePool!Client(clients);
+ rpcFaultFilter_ = defaultRpcFaultFilter;
+ reopenTransports_ = true;
+ }
+
+ /+override+/ void addClient(Client client) {
+ pool_.add(client);
+ }
+
+ /+override+/ bool removeClient(Client client) {
+ return pool_.remove(client);
+ }
+
+ /**
+ * Whether to keep trying to find a working client if all have failed in a
+ * row.
+ *
+ * Defaults to false.
+ */
+ bool keepTrying() const @property {
+ return pool_.cycle;
+ }
+
+ /// Ditto
+ void keepTrying(bool value) @property {
+ pool_.cycle = value;
+ }
+
+ /**
+ * Whether to use a random permutation of the client pool on every call to
+ * execute(). This can be used e.g. as a simple form of load balancing.
+ *
+ * Defaults to true.
+ */
+ bool permuteClients() const @property {
+ return pool_.permute;
+ }
+
+ /// Ditto
+ void permuteClients(bool value) @property {
+ pool_.permute = value;
+ }
+
+ /**
+ * The number of consecutive faults after which a client is disabled until
+ * faultDisableDuration has passed. 0 to never disable clients.
+ *
+ * Defaults to 0.
+ */
+ ushort faultDisableCount() const @property {
+ return pool_.faultDisableCount;
+ }
+
+ /// Ditto
+ void faultDisableCount(ushort value) @property {
+ pool_.faultDisableCount = value;
+ }
+
+ /**
+ * The duration for which a client is no longer considered after it has
+ * failed too often.
+ *
+ * Defaults to one second.
+ */
+ Duration faultDisableDuration() const @property {
+ return pool_.faultDisableDuration;
+ }
+
+ /// Ditto
+ void faultDisableDuration(Duration value) @property {
+ pool_.faultDisableDuration = value;
+ }
+
+ /+override+/ bool delegate(Exception) rpcFaultFilter() const @property {
+ return rpcFaultFilter_;
+ }
+
+ /+override+/ void rpcFaultFilter(bool delegate(Exception) value) @property {
+ rpcFaultFilter_ = value;
+ }
+
+ /+override+/ bool reopenTransports() const @property {
+ return reopenTransports_;
+ }
+
+ /+override+/ void reopenTransports(bool value) @property {
+ reopenTransports_ = value;
+ }
+
+ mixin(fallbackPoolForwardCode!Interface());
+
+protected:
+ // The actual worker implementation to which RPC method calls are forwarded.
+ auto executeOnPool(string method, Args...)(Args args,
+ TCancellation cancellation
+ ) {
+ auto clients = pool_[];
+ if (clients.empty) {
+ throw new TException("No clients available to try.");
+ }
+
+ auto promise = new TPromise!(ReturnType!(MemberType!(Interface, method)));
+ Exception[] rpcExceptions;
+
+ void tryNext() {
+ while (clients.empty) {
+ Client next;
+ Duration waitTime;
+ if (clients.willBecomeNonempty(next, waitTime)) {
+ if (waitTime > dur!"hnsecs"(0)) {
+ if (waitTime < dur!"usecs"(10)) {
+ import core.thread;
+ Thread.sleep(waitTime);
+ } else {
+ next.transport.asyncManager.delay(waitTime, { tryNext(); });
+ return;
+ }
+ }
+ } else {
+ promise.fail(new TCompoundOperationException("All clients failed.",
+ rpcExceptions));
+ return;
+ }
+ }
+
+ auto client = clients.front;
+ clients.popFront;
+
+ if (reopenTransports) {
+ if (!client.transport.isOpen) {
+ try {
+ client.transport.open();
+ } catch (Exception e) {
+ pool_.recordFault(client);
+ tryNext();
+ return;
+ }
+ }
+ }
+
+ auto future = mixin("client." ~ method)(args, cancellation);
+ future.completion.addCallback({
+ if (future.status == TFutureStatus.CANCELLED) {
+ promise.cancel();
+ return;
+ }
+
+ auto e = future.getException();
+ if (e) {
+ if (rpcFaultFilter_ && rpcFaultFilter_(e)) {
+ pool_.recordFault(client);
+ rpcExceptions ~= e;
+ tryNext();
+ return;
+ }
+ }
+ pool_.recordSuccess(client);
+ promise.complete(future);
+ });
+ }
+
+ tryNext();
+ return promise;
+ }
+
+private:
+ TResourcePool!Client pool_;
+ bool delegate(Exception) rpcFaultFilter_;
+ bool reopenTransports_;
+}
+
+/**
+ * TAsyncClientPool construction helper to avoid having to explicitly
+ * specify the interface type, i.e. to allow the constructor being called
+ * using IFTI (see $(DMDBUG 6082, D Bugzilla enhancement request 6082)).
+ */
+TAsyncClientPool!Interface tAsyncClientPool(Interface)(
+ TAsyncClientBase!Interface[] clients
+) if (isService!Interface) {
+ return new typeof(return)(clients);
+}
+
+private {
+ // Cannot use an anonymous delegate literal for this because they aren't
+ // allowed in class scope.
+ string fallbackPoolForwardCode(Interface)() {
+ string code = "";
+
+ foreach (methodName; AllMemberMethodNames!Interface) {
+ enum qn = "Interface." ~ methodName;
+ code ~= "TFuture!(ReturnType!(" ~ qn ~ ")) " ~ methodName ~
+ "(ParameterTypeTuple!(" ~ qn ~ ") args, TCancellation cancellation = null) {\n";
+ code ~= "return executeOnPool!(`" ~ methodName ~ "`)(args, cancellation);\n";
+ code ~= "}\n";
+ }
+
+ return code;
+ }
+}
+
+/**
+ * A TAsyncClientPoolBase implementation which queries multiple servers at
+ * the same time and returns the first success response.
+ *
+ * The definition of »success« can be customized using the rpcFaultFilter()
+ * delegate property. If it is non-null and calling it for an exception set by
+ * a failed method invocation returns true, the error is considered to be
+ * caused by the RPC layer rather than the application layer, and the next
+ * server in the pool is tried. If all clients fail, the operation is marked
+ * as failed with a TCompoundOperationException.
+ */
+final class TAsyncFastestClientPool(Interface) if (isService!Interface) :
+ TAsyncClientPoolBase!Interface
+{
+ ///
+ this(Client[] clients) {
+ clients_ = clients;
+ rpcFaultFilter_ = defaultRpcFaultFilter;
+ reopenTransports_ = true;
+ }
+
+ /+override+/ void addClient(Client client) {
+ clients_ ~= client;
+ }
+
+ /+override+/ bool removeClient(Client client) {
+ auto oldLength = clients_.length;
+ clients_ = removeEqual(clients_, client);
+ return clients_.length < oldLength;
+ }
+
+
+ /+override+/ bool delegate(Exception) rpcFaultFilter() const @property {
+ return rpcFaultFilter_;
+ }
+
+ /+override+/ void rpcFaultFilter(bool delegate(Exception) value) @property {
+ rpcFaultFilter_ = value;
+ }
+
+ /+override+/bool reopenTransports() const @property {
+ return reopenTransports_;
+ }
+
+ /+override+/ void reopenTransports(bool value) @property {
+ reopenTransports_ = value;
+ }
+
+ mixin(fastestPoolForwardCode!Interface());
+
+private:
+ Client[] clients_;
+ bool delegate(Exception) rpcFaultFilter_;
+ bool reopenTransports_;
+}
+
+/**
+ * TAsyncFastestClientPool construction helper to avoid having to explicitly
+ * specify the interface type, i.e. to allow the constructor being called
+ * using IFTI (see $(DMDBUG 6082, D Bugzilla enhancement request 6082)).
+ */
+TAsyncFastestClientPool!Interface tAsyncFastestClientPool(Interface)(
+ TAsyncClientBase!Interface[] clients
+) if (isService!Interface) {
+ return new typeof(return)(clients);
+}
+
+private {
+ // Cannot use an anonymous delegate literal for this because they aren't
+ // allowed in class scope.
+ string fastestPoolForwardCode(Interface)() {
+ string code = "";
+
+ foreach (methodName; AllMemberMethodNames!Interface) {
+ enum qn = "Interface." ~ methodName;
+ code ~= "TFuture!(ReturnType!(" ~ qn ~ ")) " ~ methodName ~
+ "(ParameterTypeTuple!(" ~ qn ~ ") args, " ~
+ "TCancellation cancellation = null) {\n";
+ code ~= "enum methodName = `" ~ methodName ~ "`;\n";
+ code ~= q{
+ alias ReturnType!(MemberType!(Interface, methodName)) ResultType;
+
+ auto childCancellation = new TCancellationOrigin;
+
+ TFuture!ResultType[] futures;
+ futures.reserve(clients_.length);
+
+ foreach (c; clients_) {
+ if (reopenTransports) {
+ if (!c.transport.isOpen) {
+ try {
+ c.transport.open();
+ } catch (Exception e) {
+ continue;
+ }
+ }
+ }
+ futures ~= mixin("c." ~ methodName)(args, childCancellation);
+ }
+
+ return new FastestPoolJob!(ResultType)(
+ futures, rpcFaultFilter, cancellation, childCancellation);
+ };
+ code ~= "}\n";
+ }
+
+ return code;
+ }
+
+ final class FastestPoolJob(Result) : TFuture!Result {
+ this(TFuture!Result[] poolFutures, bool delegate(Exception) rpcFaultFilter,
+ TCancellation cancellation, TCancellationOrigin childCancellation
+ ) {
+ resultPromise_ = new TPromise!Result;
+ poolFutures_ = poolFutures;
+ rpcFaultFilter_ = rpcFaultFilter;
+ childCancellation_ = childCancellation;
+
+ foreach (future; poolFutures) {
+ future.completion.addCallback({
+ auto f = future;
+ return { completionCallback(f); };
+ }());
+ if (future.status != TFutureStatus.RUNNING) {
+ // If the current future is already completed, we are done, don't
+ // bother adding callbacks for the others (they would just return
+ // immediately after acquiring the lock).
+ return;
+ }
+ }
+
+ if (cancellation) {
+ cancellation.triggering.addCallback({
+ resultPromise_.cancel();
+ childCancellation.trigger();
+ });
+ }
+ }
+
+ TFutureStatus status() const @property {
+ return resultPromise_.status;
+ }
+
+ TAwaitable completion() @property {
+ return resultPromise_.completion;
+ }
+
+ Result get() {
+ return resultPromise_.get();
+ }
+
+ Exception getException() {
+ return resultPromise_.getException();
+ }
+
+ private:
+ void completionCallback(TFuture!Result future) {
+ synchronized {
+ if (future.status == TFutureStatus.CANCELLED) {
+ assert(resultPromise_.status != TFutureStatus.RUNNING);
+ return;
+ }
+
+ if (resultPromise_.status != TFutureStatus.RUNNING) {
+ // The operation has already been completed. This can happen if
+ // another client completed first, but this callback was already
+ // waiting for the lock when it called cancel().
+ return;
+ }
+
+ if (future.status == TFutureStatus.FAILED) {
+ auto e = future.getException();
+ if (rpcFaultFilter_ && rpcFaultFilter_(e)) {
+ rpcExceptions_ ~= e;
+
+ if (rpcExceptions_.length == poolFutures_.length) {
+ resultPromise_.fail(new TCompoundOperationException(
+ "All child operations failed, unable to retrieve a result.",
+ rpcExceptions_
+ ));
+ }
+
+ return;
+ }
+ }
+
+ // Store the result to the target promise.
+ resultPromise_.complete(future);
+
+ // Cancel the other futures, we would just discard their results.
+ // Note: We do this after we have stored the results to our promise,
+ // see the assert at the top of the function.
+ childCancellation_.trigger();
+ }
+ }
+
+ TPromise!Result resultPromise_;
+ TFuture!Result[] poolFutures_;
+ Exception[] rpcExceptions_;
+ bool delegate(Exception) rpcFaultFilter_;
+ TCancellationOrigin childCancellation_;
+ }
+}
+
+/**
+ * Allows easily aggregating results from a number of TAsyncClients.
+ *
+ * Contrary to TAsync{Fallback, Fastest}ClientPool, this class does not
+ * simply implement TFutureInterface!Interface. It manages a pool of clients,
+ * but allows the user to specify a custom accumulator function to use or to
+ * iterate over the results using a TFutureAggregatorRange.
+ *
+ * For each service method, TAsyncAggregator offers a method
+ * accepting the same arguments, and an optional TCancellation instance, just
+ * like with TFutureInterface. The return type, however, is a proxy object
+ * that offers the following methods:
+ * ---
+ * /++
+ * + Returns a thrift.util.future.TFutureAggregatorRange for the results of
+ * + the client pool method invocations.
+ * +
+ * + The [] (slicing) operator can also be used to obtain the range.
+ * +
+ * + Params:
+ * + timeout = A timeout to pass to the TFutureAggregatorRange constructor,
+ * + defaults to zero (no timeout).
+ * +/
+ * TFutureAggregatorRange!ReturnType range(Duration timeout = dur!"hnsecs"(0));
+ * auto opSlice() { return range(); } /// Ditto
+ *
+ * /++
+ * + Returns a future that gathers the results from the clients in the pool
+ * + and invokes a user-supplied accumulator function on them, returning its
+ * + return value to the client.
+ * +
+ * + In addition to the TFuture!AccumulatedType interface (where
+ * + AccumulatedType is the return type of the accumulator function), the
+ * + returned object also offers two additional methods, finish() and
+ * + finishGet(): By default, the accumulator functions is called after all
+ * + the results from the pool clients have become available. Calling finish()
+ * + causes the accumulator future to stop waiting for other results and
+ * + immediately invoking the accumulator function on the results currently
+ * + available. If all results are already available, finish() is a no-op.
+ * + finishGet() is a convenience shortcut for combining it with
+ * + a call to get() immediately afterwards, like waitGet() is for wait().
+ * +
+ * + The acc alias can point to any callable accepting either an array of
+ * + return values or an array of return values and an array of exceptions;
+ * + see isAccumulator!() for details. The default accumulator concatenates
+ * + return values that can be concatenated with each others (e.g. arrays),
+ * + and simply returns an array of values otherwise, failing with a
+ * + TCompoundOperationException no values were returned.
+ * +
+ * + The accumulator function is not executed in any of the async manager
+ * + worker threads associated with the async clients, but instead it is
+ * + invoked when the actual result is requested for the first time after the
+ * + operation has been completed. This also includes checking the status
+ * + of the operation once it is no longer running, since the accumulator
+ * + has to be run to determine whether the operation succeeded or failed.
+ * +/
+ * auto accumulate(alias acc = defaultAccumulator)() if (isAccumulator!acc);
+ * ---
+ *
+ * Example:
+ * ---
+ * // Some Thrift service.
+ * interface Foo {
+ * int foo(string name);
+ * byte[] bar();
+ * }
+ *
+ * // Create the aggregator pool – client0, client1, client2 are some
+ * // TAsyncClient!Foo instances, but in theory could also be other
+ * // TFutureInterface!Foo implementations (e.g. some async client pool).
+ * auto pool = new TAsyncAggregator!Foo([client0, client1, client2]);
+ *
+ * foreach (val; pool.foo("baz").range(dur!"seconds"(1))) {
+ * // Process all the results that are available before a second has passed,
+ * // in the order they arrive.
+ * writeln(val);
+ * }
+ *
+ * auto sumRoots = pool.foo("baz").accumulate!((int[] vals, Exceptions[] exs){
+ * if (vals.empty) {
+ * throw new TCompoundOperationException("All clients failed", exs);
+ * }
+ *
+ * // Just to illustrate that the type of the values can change, convert the
+ * // numbers to double and sum up their roots.
+ * double result = 0;
+ * foreach (v; vals) result += sqrt(cast(double)v);
+ * return result;
+ * })();
+ *
+ * // Wait up to three seconds for the result, and then accumulate what has
+ * // arrived so far.
+ * sumRoots.completion.wait(dur!"seconds"(3));
+ * writeln(sumRoots.finishGet());
+ *
+ * // For scalars, the default accumulator returns an array of the values.
+ * pragma(msg, typeof(pool.foo("").accumulate().get()); // int[].
+ *
+ * // For lists, etc., it concatenates the results together.
+ * pragma(msg, typeof(pool.bar().accumulate().get())); // byte[].
+ * ---
+ *
+ * Note: For the accumulate!() interface, you might currently hit a »cannot use
+ * local '…' as parameter to non-global template accumulate«-error, see
+ * $(DMDBUG 5710, DMD issue 5710). If your accumulator function does not need
+ * to access the surrounding scope, you might want to use a function literal
+ * instead of a delegate to avoid the issue.
+ */
+class TAsyncAggregator(Interface) if (isBaseService!Interface) {
+ /// Shorthand for the client type this instance operates on.
+ alias TAsyncClientBase!Interface Client;
+
+ ///
+ this(Client[] clients) {
+ clients_ = clients;
+ }
+
+ /// Whether to open the underlying transports of a client before trying to
+ /// execute a method if they are not open. This is usually desirable
+ /// because it allows e.g. to automatically reconnect to a remote server
+ /// if the network connection is dropped.
+ ///
+ /// Defaults to true.
+ bool reopenTransports = true;
+
+ mixin AggregatorOpDispatch!();
+
+private:
+ Client[] clients_;
+}
+
+/// Ditto
+class TAsyncAggregator(Interface) if (isDerivedService!Interface) :
+ TAsyncAggregator!(BaseService!Interface)
+{
+ /// Shorthand for the client type this instance operates on.
+ alias TAsyncClientBase!Interface Client;
+
+ ///
+ this(Client[] clients) {
+ super(cast(TAsyncClientBase!(BaseService!Interface)[])clients);
+ }
+
+ mixin AggregatorOpDispatch!();
+}
+
+/**
+ * Whether fun is a valid accumulator function for values of type ValueType.
+ *
+ * For this to be true, fun must be a callable matching one of the following
+ * argument lists:
+ * ---
+ * fun(ValueType[] values);
+ * fun(ValueType[] values, Exception[] exceptions);
+ * ---
+ *
+ * The second version is passed the collected array exceptions from all the
+ * clients in the pool.
+ *
+ * The return value of the accumulator function is passed to the client (via
+ * the result future). If it throws an exception, the operation is marked as
+ * failed with the given exception instead.
+ */
+template isAccumulator(ValueType, alias fun) {
+ enum isAccumulator = is(typeof(fun(cast(ValueType[])[]))) ||
+ is(typeof(fun(cast(ValueType[])[], cast(Exception[])[])));
+}
+
+/**
+ * TAsyncAggregator construction helper to avoid having to explicitly
+ * specify the interface type, i.e. to allow the constructor being called
+ * using IFTI (see $(DMDBUG 6082, D Bugzilla enhancement request 6082)).
+ */
+TAsyncAggregator!Interface tAsyncAggregator(Interface)(
+ TAsyncClientBase!Interface[] clients
+) if (isService!Interface) {
+ return new typeof(return)(clients);
+}
+
+private {
+ mixin template AggregatorOpDispatch() {
+ auto opDispatch(string name, Args...)(Args args) if (
+ is(typeof(mixin("Interface.init." ~ name)(args)))
+ ) {
+ alias ReturnType!(MemberType!(Interface, name)) ResultType;
+
+ auto childCancellation = new TCancellationOrigin;
+
+ TFuture!ResultType[] futures;
+ futures.reserve(clients_.length);
+
+ foreach (c; cast(Client[])clients_) {
+ if (reopenTransports) {
+ if (!c.transport.isOpen) {
+ try {
+ c.transport.open();
+ } catch (Exception e) {
+ continue;
+ }
+ }
+ }
+ futures ~= mixin("c." ~ name)(args, childCancellation);
+ }
+
+ return AggregationResult!ResultType(futures, childCancellation);
+ }
+ }
+
+ struct AggregationResult(T) {
+ auto opSlice() {
+ return range();
+ }
+
+ auto range(Duration timeout = dur!"hnsecs"(0)) {
+ return tFutureAggregatorRange(futures_, childCancellation_, timeout);
+ }
+
+ auto accumulate(alias acc = defaultAccumulator)() if (isAccumulator!(T, acc)) {
+ return new AccumulatorJob!(T, acc)(futures_, childCancellation_);
+ }
+
+ private:
+ TFuture!T[] futures_;
+ TCancellationOrigin childCancellation_;
+ }
+
+ auto defaultAccumulator(T)(T[] values, Exception[] exceptions) {
+ if (values.empty) {
+ throw new TCompoundOperationException("All clients failed",
+ exceptions);
+ }
+
+ static if (is(typeof(T.init ~ T.init))) {
+ import std.algorithm;
+ return reduce!"a ~ b"(values);
+ } else {
+ return values;
+ }
+ }
+
+ final class AccumulatorJob(T, alias accumulator) if (
+ isAccumulator!(T, accumulator)
+ ) : TFuture!(AccumulatorResult!(T, accumulator)) {
+ this(TFuture!T[] futures, TCancellationOrigin childCancellation) {
+ futures_ = futures;
+ childCancellation_ = childCancellation;
+ resultMutex_ = new Mutex;
+ completionEvent_ = new TOneshotEvent;
+
+ foreach (future; futures) {
+ future.completion.addCallback({
+ auto f = future;
+ return {
+ synchronized (resultMutex_) {
+ if (f.status == TFutureStatus.CANCELLED) {
+ if (!finished_) {
+ status_ = TFutureStatus.CANCELLED;
+ finished_ = true;
+ }
+ return;
+ }
+
+ if (f.status == TFutureStatus.FAILED) {
+ exceptions_ ~= f.getException();
+ } else {
+ results_ ~= f.get();
+ }
+
+ if (results_.length + exceptions_.length == futures_.length) {
+ finished_ = true;
+ completionEvent_.trigger();
+ }
+ }
+ };
+ }());
+ }
+ }
+
+ TFutureStatus status() @property {
+ synchronized (resultMutex_) {
+ if (!finished_) return TFutureStatus.RUNNING;
+ if (status_ != TFutureStatus.RUNNING) return status_;
+
+ try {
+ result_ = invokeAccumulator!accumulator(results_, exceptions_);
+ status_ = TFutureStatus.SUCCEEDED;
+ } catch (Exception e) {
+ exception_ = e;
+ status_ = TFutureStatus.FAILED;
+ }
+
+ return status_;
+ }
+ }
+
+ TAwaitable completion() @property {
+ return completionEvent_;
+ }
+
+ AccumulatorResult!(T, accumulator) get() {
+ auto s = status;
+
+ enforce(s != TFutureStatus.RUNNING,
+ new TFutureException("Operation not yet completed."));
+
+ if (s == TFutureStatus.CANCELLED) throw new TCancelledException;
+ if (s == TFutureStatus.FAILED) throw exception_;
+ return result_;
+ }
+
+ Exception getException() {
+ auto s = status;
+ enforce(s != TFutureStatus.RUNNING,
+ new TFutureException("Operation not yet completed."));
+
+ if (s == TFutureStatus.CANCELLED) throw new TCancelledException;
+
+ if (s == TFutureStatus.SUCCEEDED) {
+ return null;
+ }
+ return exception_;
+ }
+
+ void finish() {
+ synchronized (resultMutex_) {
+ if (!finished_) {
+ finished_ = true;
+ childCancellation_.trigger();
+ completionEvent_.trigger();
+ }
+ }
+ }
+
+ auto finishGet() {
+ finish();
+ return get();
+ }
+
+ private:
+ TFuture!T[] futures_;
+ TCancellationOrigin childCancellation_;
+
+ bool finished_;
+ T[] results_;
+ Exception[] exceptions_;
+
+ TFutureStatus status_;
+ Mutex resultMutex_;
+ union {
+ AccumulatorResult!(T, accumulator) result_;
+ Exception exception_;
+ }
+ TOneshotEvent completionEvent_;
+ }
+
+ auto invokeAccumulator(alias accumulator, T)(
+ T[] values, Exception[] exceptions
+ ) if (
+ isAccumulator!(T, accumulator)
+ ) {
+ static if (is(typeof(accumulator(values, exceptions)))) {
+ return accumulator(values, exceptions);
+ } else {
+ return accumulator(values);
+ }
+ }
+
+ template AccumulatorResult(T, alias acc) {
+ alias typeof(invokeAccumulator!acc(cast(T[])[], cast(Exception[])[]))
+ AccumulatorResult;
+ }
+}
diff --git a/lib/d/src/thrift/codegen/base.d b/lib/d/src/thrift/codegen/base.d
new file mode 100644
index 000000000..e7e3ead39
--- /dev/null
+++ b/lib/d/src/thrift/codegen/base.d
@@ -0,0 +1,950 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Code generation metadata and templates used for implementing struct
+ * serialization.
+ *
+ * Many templates can be customized using field meta data, which is read from
+ * a manifest constant member of the given type called fieldMeta (if present),
+ * and is concatenated with the elements from the optional fieldMetaData
+ * template alias parameter.
+ *
+ * Some code generation templates take account of the optional TVerboseCodegen
+ * version declaration, which causes warning messages to be emitted if no
+ * metadata for a field/method has been found and the default behavior is
+ * used instead. If this version is not defined, the templates just silently
+ * behave like the Thrift compiler does in this situation, i.e. automatically
+ * assign negative ids (starting at -1) for fields and assume TReq.AUTO as
+ * requirement level.
+ */
+// Implementation note: All the templates in here taking a field metadata
+// parameter should ideally have a constraint that restricts the alias to
+// TFieldMeta[]-typed values, but the is() expressions seems to always fail.
+module thrift.codegen.base;
+
+import std.algorithm : find;
+import std.array : empty, front;
+import std.conv : to;
+import std.exception : enforce;
+import std.traits : BaseTypeTuple, isPointer, isSomeFunction, pointerTarget,
+ ReturnType;
+import thrift.base;
+import thrift.internal.codegen;
+import thrift.protocol.base;
+
+/*
+ * Thrift struct/service meta data, which is used to store information from
+ * the interface definition files not representable in plain D, i.e. field
+ * requirement levels, Thrift field IDs, etc.
+ */
+
+/**
+ * Struct field requirement levels.
+ */
+enum TReq {
+ /// Detect the requiredness from the field type: if it is nullable, treat
+ /// the field as optional, if it is non-nullable, treat the field as
+ /// required. This is the default used for handling structs not generated
+ /// from an IDL file, and never emitted by the Thrift compiler. TReq.AUTO
+ /// shouldn't be specified explicitly.
+ // Implementation note: thrift.codegen templates use
+ // thrift.internal.codegen.memberReq to resolve AUTO to REQUIRED/OPTIONAL
+ // instead of handling it directly.
+ AUTO,
+
+ /// The field is treated as optional when deserializing/receiving the struct
+ /// and as required when serializing/sending. This is the Thrift default if
+ /// neither "required" nor "optional" are specified in the IDL file.
+ OPT_IN_REQ_OUT,
+
+ /// The field is optional.
+ OPTIONAL,
+
+ /// The field is required.
+ REQUIRED,
+
+ /// Ignore the struct field when serializing/deserializing.
+ IGNORE
+}
+
+/**
+ * The way how methods are called.
+ */
+enum TMethodType {
+ /// Called in the normal two-way scheme consisting of a request and a
+ /// response.
+ REGULAR,
+
+ /// A fire-and-forget one-way method, where no response is sent and the
+ /// client immediately returns.
+ ONEWAY
+}
+
+/**
+ * Compile-time metadata for a struct field.
+ */
+struct TFieldMeta {
+ /// The name of the field. Used for matching a TFieldMeta with the actual
+ /// D struct member during code generation.
+ string name;
+
+ /// The (Thrift) id of the field.
+ short id;
+
+ /// Whether the field is requried.
+ TReq req;
+
+ /// A code string containing a D expression for the default value, if there
+ /// is one.
+ string defaultValue;
+}
+
+/**
+ * Compile-time metadata for a service method.
+ */
+struct TMethodMeta {
+ /// The name of the method. Used for matching a TMethodMeta with the actual
+ /// method during code generation.
+ string name;
+
+ /// Meta information for the parameteres.
+ TParamMeta[] params;
+
+ /// Specifies which exceptions can be thrown by the method. All other
+ /// exceptions are converted to a TApplicationException instead.
+ TExceptionMeta[] exceptions;
+
+ /// The fundamental type of the method.
+ TMethodType type;
+}
+
+/**
+ * Compile-time metadata for a service method parameter.
+ */
+struct TParamMeta {
+ /// The name of the parameter. Contrary to TFieldMeta, it only serves
+ /// decorative purposes here.
+ string name;
+
+ /// The Thrift id of the parameter in the param struct.
+ short id;
+
+ /// A code string containing a D expression for the default value for the
+ /// parameter, if any.
+ string defaultValue;
+}
+
+/**
+ * Compile-time metadata for a service method exception annotation.
+ */
+struct TExceptionMeta {
+ /// The name of the exception »return value«. Contrary to TFieldMeta, it
+ /// only serves decorative purposes here, as it is only used in code not
+ /// visible to processor implementations/service clients.
+ string name;
+
+ /// The Thrift id of the exception field in the return value struct.
+ short id;
+
+ /// The name of the exception type.
+ string type;
+}
+
+/**
+ * A pair of two TPorotocols. To be used in places where a list of protocols
+ * is expected, for specifying different protocols for input and output.
+ */
+struct TProtocolPair(InputProtocol, OutputProtocol) if (
+ isTProtocol!InputProtocol && isTProtocol!OutputProtocol
+) {}
+
+/**
+ * true if T is a TProtocolPair.
+ */
+template isTProtocolPair(T) {
+ static if (is(T _ == TProtocolPair!(I, O), I, O)) {
+ enum isTProtocolPair = true;
+ } else {
+ enum isTProtocolPair = false;
+ }
+}
+
+unittest {
+ static assert(isTProtocolPair!(TProtocolPair!(TProtocol, TProtocol)));
+ static assert(!isTProtocolPair!TProtocol);
+}
+
+/**
+ * true if T is a TProtocol or a TProtocolPair.
+ */
+template isTProtocolOrPair(T) {
+ enum isTProtocolOrPair = isTProtocol!T || isTProtocolPair!T;
+}
+
+unittest {
+ static assert(isTProtocolOrPair!TProtocol);
+ static assert(isTProtocolOrPair!(TProtocolPair!(TProtocol, TProtocol)));
+ static assert(!isTProtocolOrPair!void);
+}
+
+/**
+ * true if T represents a Thrift service.
+ */
+template isService(T) {
+ enum isService = isBaseService!T || isDerivedService!T;
+}
+
+/**
+ * true if T represents a Thrift service not derived from another service.
+ */
+template isBaseService(T) {
+ static if(is(T _ == interface) &&
+ (!is(T TBases == super) || TBases.length == 0)
+ ) {
+ enum isBaseService = true;
+ } else {
+ enum isBaseService = false;
+ }
+}
+
+/**
+ * true if T represents a Thrift service derived from another service.
+ */
+template isDerivedService(T) {
+ static if(is(T _ == interface) &&
+ is(T TBases == super) && TBases.length == 1
+ ) {
+ enum isDerivedService = isService!(TBases[0]);
+ } else {
+ enum isDerivedService = false;
+ }
+}
+
+/**
+ * For derived services, gets the base service interface.
+ */
+template BaseService(T) if (isDerivedService!T) {
+ alias BaseTypeTuple!T[0] BaseService;
+}
+
+
+/*
+ * Code generation templates.
+ */
+
+/**
+ * Mixin template defining additional helper methods for using a struct with
+ * Thrift, and a member called isSetFlags if the struct contains any fields
+ * for which an »is set« flag is needed.
+ *
+ * It can only be used inside structs or Exception classes.
+ *
+ * For example, consider the following struct definition:
+ * ---
+ * struct Foo {
+ * string a;
+ * int b;
+ * int c;
+ *
+ * mixin TStructHelpers!([
+ * TFieldMeta("a", 1), // Implicitly optional (nullable).
+ * TFieldMeta("b", 2), // Implicitly required (non-nullable).
+ * TFieldMeta("c", 3, TReq.REQUIRED, "4")
+ * ]);
+ * }
+ * ---
+ *
+ * TStructHelper adds the following methods to the struct:
+ * ---
+ * /++
+ * + Sets member fieldName to the given value and marks it as set.
+ * +
+ * + Examples:
+ * + ---
+ * + auto f = Foo();
+ * + f.set!"b"(12345);
+ * + assert(f.isSet!"b");
+ * + ---
+ * +/
+ * void set(string fieldName)(MemberType!(This, fieldName) value);
+ *
+ * /++
+ * + Resets member fieldName to the init property of its type and marks it as
+ * + not set.
+ * +
+ * + Examples:
+ * + ---
+ * + // Set f.b to some value.
+ * + auto f = Foo();
+ * + f.set!"b"(12345);
+ * +
+ * + f.unset!b();
+ * +
+ * + // f.b is now unset again.
+ * + assert(!f.isSet!"b");
+ * + ---
+ * +/
+ * void unset(string fieldName)();
+ *
+ * /++
+ * + Returns whether member fieldName is set.
+ * +
+ * + Examples:
+ * + ---
+ * + auto f = Foo();
+ * + assert(!f.isSet!"b");
+ * + f.set!"b"(12345);
+ * + assert(f.isSet!"b");
+ * + ---
+ * +/
+ * bool isSet(string fieldName)() const @property;
+ *
+ * /++
+ * + Returns a string representation of the struct.
+ * +
+ * + Examples:
+ * + ---
+ * + auto f = Foo();
+ * + f.a = "a string";
+ * + assert(f.toString() == `Foo("a string", 0 (unset), 4)`);
+ * + ---
+ * +/
+ * string toString() const;
+ *
+ * /++
+ * + Deserializes the struct, setting its members to the values read from the
+ * + protocol. Forwards to readStruct(this, proto);
+ * +/
+ * void read(Protocol)(Protocol proto) if (isTProtocol!Protocol);
+ *
+ * /++
+ * + Serializes the struct to the target protocol. Forwards to
+ * + writeStruct(this, proto);
+ * +/
+ * void write(Protocol)(Protocol proto) const if (isTProtocol!Protocol);
+ * ---
+ *
+ * Additionally, an opEquals() implementation is provided which simply
+ * compares all fields, but disregards the is set struct, if any (the exact
+ * signature obviously differs between structs and exception classes). The
+ * metadata is stored in a manifest constant called fieldMeta.
+ *
+ * Note: To set the default values for fields where one has been specified in
+ * the field metadata, a parameterless static opCall is generated, because D
+ * does not allow parameterless (default) constructors for structs. Thus, be
+ * always to use to initialize structs:
+ * ---
+ * Foo foo; // Wrong!
+ * auto foo = Foo(); // Correct.
+ * ---
+ */
+mixin template TStructHelpers(alias fieldMetaData = cast(TFieldMeta[])null) if (
+ is(typeof(fieldMetaData) : TFieldMeta[])
+) {
+ import std.algorithm : canFind;
+ import thrift.codegen.base;
+ import thrift.internal.codegen : isNullable, MemberType, mergeFieldMeta,
+ FieldNames;
+ import thrift.protocol.base : TProtocol, isTProtocol;
+
+ alias typeof(this) This;
+ static assert(is(This == struct) || is(This : Exception),
+ "TStructHelpers can only be used inside a struct or an Exception class.");
+
+ static if (is(TIsSetFlags!(This, fieldMetaData))) {
+ // If we need to keep isSet flags around, create an instance of the
+ // container struct.
+ TIsSetFlags!(This, fieldMetaData) isSetFlags;
+ enum fieldMeta = fieldMetaData ~ [TFieldMeta("isSetFlags", 0, TReq.IGNORE)];
+ } else {
+ enum fieldMeta = fieldMetaData;
+ }
+
+ void set(string fieldName)(MemberType!(This, fieldName) value) if (
+ is(MemberType!(This, fieldName))
+ ) {
+ __traits(getMember, this, fieldName) = value;
+ static if (is(typeof(mixin("this.isSetFlags." ~ fieldName)) : bool)) {
+ __traits(getMember, this.isSetFlags, fieldName) = true;
+ }
+ }
+
+ void unset(string fieldName)() if (is(MemberType!(This, fieldName))) {
+ static if (is(typeof(mixin("this.isSetFlags." ~ fieldName)) : bool)) {
+ __traits(getMember, this.isSetFlags, fieldName) = false;
+ }
+ __traits(getMember, this, fieldName) = MemberType!(This, fieldName).init;
+ }
+
+ bool isSet(string fieldName)() const @property if (
+ is(MemberType!(This, fieldName))
+ ) {
+ static if (isNullable!(MemberType!(This, fieldName))) {
+ return __traits(getMember, this, fieldName) !is null;
+ } else static if (is(typeof(mixin("this.isSetFlags." ~ fieldName)) : bool)) {
+ return __traits(getMember, this.isSetFlags, fieldName);
+ } else {
+ // This is a required field, which is always set.
+ return true;
+ }
+ }
+
+ static if (is(This _ == class)) {
+ override string toString() const {
+ return thriftToStringImpl();
+ }
+
+ override bool opEquals(Object other) const {
+ auto rhs = cast(This)other;
+ if (rhs) {
+ return thriftOpEqualsImpl(rhs);
+ }
+
+ return (cast()super).opEquals(other);
+ }
+ } else {
+ string toString() const {
+ return thriftToStringImpl();
+ }
+
+ bool opEquals(ref const This other) const {
+ return thriftOpEqualsImpl(other);
+ }
+ }
+
+ private string thriftToStringImpl() const {
+ import std.conv : to;
+ string result = This.stringof ~ "(";
+ mixin({
+ string code = "";
+ bool first = true;
+ foreach (name; FieldNames!(This, fieldMeta)) {
+ if (first) {
+ first = false;
+ } else {
+ code ~= "result ~= `, `;\n";
+ }
+ code ~= "result ~= `" ~ name ~ ": ` ~ to!string(cast()this." ~ name ~ ");\n";
+ code ~= "if (!isSet!q{" ~ name ~ "}) {\n";
+ code ~= "result ~= ` (unset)`;\n";
+ code ~= "}\n";
+ }
+ return code;
+ }());
+ result ~= ")";
+ return result;
+ }
+
+ private bool thriftOpEqualsImpl(const ref This rhs) const {
+ foreach (name; FieldNames!This) {
+ if (mixin("this." ~ name) != mixin("rhs." ~ name)) return false;
+ }
+ return true;
+ }
+
+ static if (canFind!`!a.defaultValue.empty`(mergeFieldMeta!(This, fieldMetaData))) {
+ static if (is(This _ == class)) {
+ this() {
+ mixin(thriftFieldInitCode!(mergeFieldMeta!(This, fieldMetaData))("this"));
+ }
+ } else {
+ // DMD @@BUG@@: Have to use auto here to avoid »no size yet for forward
+ // reference« errors.
+ static auto opCall() {
+ auto result = This.init;
+ mixin(thriftFieldInitCode!(mergeFieldMeta!(This, fieldMetaData))("result"));
+ return result;
+ }
+ }
+ }
+
+ void read(Protocol)(Protocol proto) if (isTProtocol!Protocol) {
+ // Need to explicitly specify fieldMetaData here, since it isn't already
+ // picked up in some situations (e.g. the TArgs struct for methods with
+ // multiple parameters in async_test_servers) otherwise. Due to a DMD
+ // @@BUG@@, we need to explicitly specify the other template parameters
+ // as well.
+ readStruct!(This, Protocol, fieldMetaData, false)(this, proto);
+ }
+
+ void write(Protocol)(Protocol proto) const if (isTProtocol!Protocol) {
+ writeStruct!(This, Protocol, fieldMetaData, false)(this, proto);
+ }
+}
+
+// DMD @@BUG@@: Having this inside TStructHelpers leads to weird lookup errors
+// (e.g. for std.arry.empty).
+string thriftFieldInitCode(alias fieldMeta)(string thisName) {
+ string code = "";
+ foreach (field; fieldMeta) {
+ if (field.defaultValue.empty) continue;
+ code ~= thisName ~ "." ~ field.name ~ " = " ~ field.defaultValue ~ ";\n";
+ }
+ return code;
+}
+
+version (unittest) {
+ // Cannot make this nested in the unittest block due to a »no size yet for
+ // forward reference« error.
+ struct Foo {
+ string a;
+ int b;
+ int c;
+
+ mixin TStructHelpers!([
+ TFieldMeta("a", 1),
+ TFieldMeta("b", 2, TReq.OPT_IN_REQ_OUT),
+ TFieldMeta("c", 3, TReq.REQUIRED, "4")
+ ]);
+ }
+}
+unittest {
+ auto f = Foo();
+
+ f.set!"b"(12345);
+ assert(f.isSet!"b");
+ f.unset!"b"();
+ assert(!f.isSet!"b");
+ f.set!"b"(12345);
+ assert(f.isSet!"b");
+ f.unset!"b"();
+
+ f.a = "a string";
+ assert(f.toString() == `Foo(a: a string, b: 0 (unset), c: 4)`);
+}
+
+/**
+ * Generates an eponymous struct with boolean flags for the non-required
+ * non-nullable fields of T, if any, or nothing otherwise (i.e. the template
+ * body is empty).
+ *
+ * Nullable fields are just set to null to signal »not set«.
+ *
+ * In most cases, you do not want to use this directly, but via TStructHelpers
+ * instead.
+ */
+// DMD @@BUG@@: Using getFieldMeta!T in here horribly breaks things to the point
+// where getFieldMeta is *instantiated twice*, with different bodies. This is
+// connected to the position of »enum fieldMeta« in TStructHelpers.
+template TIsSetFlags(T, alias fieldMetaData) {
+ mixin({
+ string boolDefinitions;
+ foreach (name; __traits(derivedMembers, T)) {
+ static if (!is(MemberType!(T, name)) || is(MemberType!(T, name) == void)) {
+ // We hit something strange like the TStructHelpers template itself,
+ // just ignore.
+ } else static if (isNullable!(MemberType!(T, name))) {
+ // If the field is nullable, we don't need an isSet flag as we can map
+ // unset to null.
+ } else static if (memberReq!(T, name, fieldMetaData) != TReq.REQUIRED) {
+ boolDefinitions ~= "bool " ~ name ~ ";\n";
+ }
+ }
+ if (!boolDefinitions.empty) {
+ return "struct TIsSetFlags {\n" ~ boolDefinitions ~ "}";
+ } else {
+ return "";
+ }
+ }());
+}
+
+/**
+ * Deserializes a Thrift struct from a protocol.
+ *
+ * Using the Protocol template parameter, the concrete TProtocol to use can be
+ * be specified. If the pointerStruct parameter is set to true, the struct
+ * fields are expected to be pointers to the actual data. This is used
+ * internally (combined with TPResultStruct) and usually should not be used in
+ * user code.
+ *
+ * This is a free function to make it possible to read exisiting structs from
+ * the wire without altering their definitions.
+ */
+void readStruct(T, Protocol, alias fieldMetaData = cast(TFieldMeta[])null,
+ bool pointerStruct = false)(ref T s, Protocol p) if (isTProtocol!Protocol)
+{
+ mixin({
+ string code;
+
+ // Check that all fields for which there is meta info are actually in the
+ // passed struct type.
+ foreach (field; mergeFieldMeta!(T, fieldMetaData)) {
+ code ~= "static assert(is(MemberType!(T, `" ~ field.name ~ "`)));\n";
+ }
+
+ // Returns the code string for reading a value of type F off the wire and
+ // assigning it to v. The level parameter is used to make sure that there
+ // are no conflicting variable names on recursive calls.
+ string readValueCode(ValueType)(string v, size_t level = 0) {
+ // Some non-ambigous names to use (shadowing is not allowed in D).
+ immutable i = "i" ~ to!string(level);
+ immutable elem = "elem" ~ to!string(level);
+ immutable key = "key" ~ to!string(level);
+ immutable list = "list" ~ to!string(level);
+ immutable map = "map" ~ to!string(level);
+ immutable set = "set" ~ to!string(level);
+ immutable value = "value" ~ to!string(level);
+
+ alias FullyUnqual!ValueType F;
+
+ static if (is(F == bool)) {
+ return v ~ " = p.readBool();";
+ } else static if (is(F == byte)) {
+ return v ~ " = p.readByte();";
+ } else static if (is(F == double)) {
+ return v ~ " = p.readDouble();";
+ } else static if (is(F == short)) {
+ return v ~ " = p.readI16();";
+ } else static if (is(F == int)) {
+ return v ~ " = p.readI32();";
+ } else static if (is(F == long)) {
+ return v ~ " = p.readI64();";
+ } else static if (is(F : string)) {
+ return v ~ " = p.readString();";
+ } else static if (is(F == enum)) {
+ return v ~ " = cast(typeof(" ~ v ~ "))p.readI32();";
+ } else static if (is(F _ : E[], E)) {
+ return "{\n" ~
+ "auto " ~ list ~ " = p.readListBegin();\n" ~
+ // TODO: Check element type here?
+ v ~ " = new typeof(" ~ v ~ "[0])[" ~ list ~ ".size];\n" ~
+ "foreach (" ~ i ~ "; 0 .. " ~ list ~ ".size) {\n" ~
+ readValueCode!E(v ~ "[" ~ i ~ "]", level + 1) ~ "\n" ~
+ "}\n" ~
+ "p.readListEnd();\n" ~
+ "}";
+ } else static if (is(F _ : V[K], K, V)) {
+ return "{\n" ~
+ "auto " ~ map ~ " = p.readMapBegin();" ~
+ v ~ " = null;\n" ~
+ // TODO: Check key/value types here?
+ "foreach (" ~ i ~ "; 0 .. " ~ map ~ ".size) {\n" ~
+ "FullyUnqual!(typeof(" ~ v ~ ".keys[0])) " ~ key ~ ";\n" ~
+ readValueCode!K(key, level + 1) ~ "\n" ~
+ "typeof(" ~ v ~ ".values[0]) " ~ value ~ ";\n" ~
+ readValueCode!V(value, level + 1) ~ "\n" ~
+ v ~ "[cast(typeof(" ~ v ~ ".keys[0]))" ~ key ~ "] = " ~ value ~ ";\n" ~
+ "}\n" ~
+ "p.readMapEnd();" ~
+ "}";
+ } else static if (is(F _ : HashSet!(E), E)) {
+ return "{\n" ~
+ "auto " ~ set ~ " = p.readSetBegin();" ~
+ // TODO: Check element type here?
+ v ~ " = new typeof(" ~ v ~ ")();\n" ~
+ "foreach (" ~ i ~ "; 0 .. " ~ set ~ ".size) {\n" ~
+ "typeof(" ~ v ~ "[][0]) " ~ elem ~ ";\n" ~
+ readValueCode!E(elem, level + 1) ~ "\n" ~
+ v ~ " ~= " ~ elem ~ ";\n" ~
+ "}\n" ~
+ "p.readSetEnd();" ~
+ "}";
+ } else static if (is(F == struct) || is(F : TException)) {
+ static if (is(F == struct)) {
+ auto result = v ~ " = typeof(" ~ v ~ ")();\n";
+ } else {
+ auto result = v ~ " = new typeof(" ~ v ~ ")();\n";
+ }
+
+ static if (__traits(compiles, F.init.read(TProtocol.init))) {
+ result ~= v ~ ".read(p);";
+ } else {
+ result ~= "readStruct(" ~ v ~ ", p);";
+ }
+ return result;
+ } else {
+ static assert(false, "Cannot represent type in Thrift: " ~ F.stringof);
+ }
+ }
+
+ string readFieldCode(FieldType)(string name, short id, TReq req) {
+ static if (pointerStruct && isPointer!FieldType) {
+ immutable v = "(*s." ~ name ~ ")";
+ alias pointerTarget!FieldType F;
+ } else {
+ immutable v = "s." ~ name;
+ alias FieldType F;
+ }
+
+ string code = "case " ~ to!string(id) ~ ":\n";
+ code ~= "if (f.type == " ~ dToTTypeString!F ~ ") {\n";
+ code ~= readValueCode!F(v) ~ "\n";
+ if (req == TReq.REQUIRED) {
+ // For required fields, set the corresponding local isSet variable.
+ code ~= "isSet_" ~ name ~ " = true;\n";
+ } else if (!isNullable!F){
+ code ~= "s.isSetFlags." ~ name ~ " = true;\n";
+ }
+ code ~= "} else skip(p, f.type);\n";
+ code ~= "break;\n";
+ return code;
+ }
+
+ // Code for the local boolean flags used to make sure required fields have
+ // been found.
+ string isSetFlagCode = "";
+
+ // Code for checking whether the flags for the required fields are true.
+ string isSetCheckCode = "";
+
+ /// Code for the case statements storing the fields to the result struct.
+ string readMembersCode = "";
+
+ // The last automatically assigned id – fields with no meta information
+ // are assigned (in lexical order) descending negative ids, starting with
+ // -1, just like the Thrift compiler does.
+ short lastId;
+
+ foreach (name; FieldNames!T) {
+ enum req = memberReq!(T, name, fieldMetaData);
+ if (req == TReq.REQUIRED) {
+ // For required fields, generate local bool flags to keep track
+ // whether the field has been encountered.
+ immutable n = "isSet_" ~ name;
+ isSetFlagCode ~= "bool " ~ n ~ ";\n";
+ isSetCheckCode ~= "enforce(" ~ n ~ ", new TProtocolException(" ~
+ "`Required field '" ~ name ~ "' not found in serialized data`, " ~
+ "TProtocolException.Type.INVALID_DATA));\n";
+ }
+
+ enum meta = find!`a.name == b`(mergeFieldMeta!(T, fieldMetaData), name);
+ static if (meta.empty) {
+ --lastId;
+ version (TVerboseCodegen) {
+ code ~= "pragma(msg, `[thrift.codegen.base.readStruct] Warning: No " ~
+ "meta information for field '" ~ name ~ "' in struct '" ~
+ T.stringof ~ "'. Assigned id: " ~ to!string(lastId) ~ ".`);\n";
+ }
+ readMembersCode ~= readFieldCode!(MemberType!(T, name))(
+ name, lastId, req);
+ } else static if (req != TReq.IGNORE) {
+ readMembersCode ~= readFieldCode!(MemberType!(T, name))(
+ name, meta.front.id, req);
+ }
+ }
+
+ code ~= isSetFlagCode;
+ code ~= "p.readStructBegin();\n";
+ code ~= "while (true) {\n";
+ code ~= "auto f = p.readFieldBegin();\n";
+ code ~= "if (f.type == TType.STOP) break;\n";
+ code ~= "switch(f.id) {\n";
+ code ~= readMembersCode;
+ code ~= "default: skip(p, f.type);\n";
+ code ~= "}\n";
+ code ~= "p.readFieldEnd();\n";
+ code ~= "}\n";
+ code ~= "p.readStructEnd();\n";
+ code ~= isSetCheckCode;
+
+ return code;
+ }());
+}
+
+/**
+ * Serializes a struct to the target protocol.
+ *
+ * Using the Protocol template parameter, the concrete TProtocol to use can be
+ * be specified. If the pointerStruct parameter is set to true, the struct
+ * fields are expected to be pointers to the actual data. This is used
+ * internally (combined with TPargsStruct) and usually should not be used in
+ * user code.
+ *
+ * This is a free function to make it possible to read exisiting structs from
+ * the wire without altering their definitions.
+ */
+void writeStruct(T, Protocol, alias fieldMetaData = cast(TFieldMeta[])null,
+ bool pointerStruct = false) (const T s, Protocol p) if (isTProtocol!Protocol)
+{
+ mixin({
+ // Check that all fields for which there is meta info are actually in the
+ // passed struct type.
+ string code = "";
+ foreach (field; mergeFieldMeta!(T, fieldMetaData)) {
+ code ~= "static assert(is(MemberType!(T, `" ~ field.name ~ "`)));\n";
+ }
+
+ // Check that required nullable members are non-null.
+ // WORKAROUND: To stop LDC from emitting the manifest constant »meta« below
+ // into the writeStruct function body this is inside the string mixin
+ // block – the code wouldn't depend on it (this is an LDC bug, and because
+ // of it a new array would be allocate on each method invocation at runtime).
+ foreach (name; StaticFilter!(
+ Compose!(isNullable, PApply!(MemberType, T)),
+ FieldNames!T
+ )) {
+ static if (memberReq!(T, name, fieldMetaData) == TReq.REQUIRED) {
+ code ~= `enforce(__traits(getMember, s, name) !is null,
+ new TException("Required field '` ~ name ~ `' is null."));\n`;
+ }
+ }
+
+ return code;
+ }());
+
+ p.writeStructBegin(TStruct(T.stringof));
+ mixin({
+ string writeValueCode(ValueType)(string v, size_t level = 0) {
+ // Some non-ambigous names to use (shadowing is not allowed in D).
+ immutable elem = "elem" ~ to!string(level);
+ immutable key = "key" ~ to!string(level);
+ immutable value = "value" ~ to!string(level);
+
+ alias FullyUnqual!ValueType F;
+ static if (is(F == bool)) {
+ return "p.writeBool(" ~ v ~ ");";
+ } else static if (is(F == byte)) {
+ return "p.writeByte(" ~ v ~ ");";
+ } else static if (is(F == double)) {
+ return "p.writeDouble(" ~ v ~ ");";
+ } else static if (is(F == short)) {
+ return "p.writeI16(" ~ v ~ ");";
+ } else static if (is(F == int)) {
+ return "p.writeI32(" ~ v ~ ");";
+ } else static if (is(F == long)) {
+ return "p.writeI64(" ~ v ~ ");";
+ } else static if (is(F : string)) {
+ return "p.writeString(" ~ v ~ ");";
+ } else static if (is(F == enum)) {
+ return "p.writeI32(cast(int)" ~ v ~ ");";
+ } else static if (is(F _ : E[], E)) {
+ return "p.writeListBegin(TList(" ~ dToTTypeString!E ~ ", " ~ v ~
+ ".length));\n" ~
+ "foreach (" ~ elem ~ "; " ~ v ~ ") {\n" ~
+ writeValueCode!E(elem, level + 1) ~ "\n" ~
+ "}\n" ~
+ "p.writeListEnd();";
+ } else static if (is(F _ : V[K], K, V)) {
+ return "p.writeMapBegin(TMap(" ~ dToTTypeString!K ~ ", " ~
+ dToTTypeString!V ~ ", " ~ v ~ ".length));\n" ~
+ "foreach (" ~ key ~ ", " ~ value ~ "; " ~ v ~ ") {\n" ~
+ writeValueCode!K(key, level + 1) ~ "\n" ~
+ writeValueCode!V(value, level + 1) ~ "\n" ~
+ "}\n" ~
+ "p.writeMapEnd();";
+ } else static if (is(F _ : HashSet!E, E)) {
+ return "p.writeSetBegin(TSet(" ~ dToTTypeString!E ~ ", " ~ v ~
+ ".length));\n" ~
+ "foreach (" ~ elem ~ "; " ~ v ~ "[]) {\n" ~
+ writeValueCode!E(elem, level + 1) ~ "\n" ~
+ "}\n" ~
+ "p.writeSetEnd();";
+ } else static if (is(F == struct) || is(F : TException)) {
+ static if (__traits(compiles, F.init.write(TProtocol.init))) {
+ return v ~ ".write(p);";
+ } else {
+ return "writeStruct(" ~ v ~ ", p);";
+ }
+ } else {
+ static assert(false, "Cannot represent type in Thrift: " ~ F.stringof);
+ }
+ }
+
+ string writeFieldCode(FieldType)(string name, short id, TReq req) {
+ string code;
+ if (!pointerStruct && req == TReq.OPTIONAL) {
+ code ~= "if (s.isSet!`" ~ name ~ "`) {\n";
+ }
+
+ static if (pointerStruct && isPointer!FieldType) {
+ immutable v = "(*s." ~ name ~ ")";
+ alias pointerTarget!FieldType F;
+ } else {
+ immutable v = "s." ~ name;
+ alias FieldType F;
+ }
+
+ code ~= "p.writeFieldBegin(TField(`" ~ name ~ "`, " ~ dToTTypeString!F ~
+ ", " ~ to!string(id) ~ "));\n";
+ code ~= writeValueCode!F(v) ~ "\n";
+ code ~= "p.writeFieldEnd();\n";
+
+ if (!pointerStruct && req == TReq.OPTIONAL) {
+ code ~= "}\n";
+ }
+ return code;
+ }
+
+ // The last automatically assigned id – fields with no meta information
+ // are assigned (in lexical order) descending negative ids, starting with
+ // -1, just like the Thrift compiler does.
+ short lastId;
+
+ string code = "";
+ foreach (name; FieldNames!T) {
+ alias MemberType!(T, name) F;
+ enum req = memberReq!(T, name, fieldMetaData);
+ enum meta = find!`a.name == b`(mergeFieldMeta!(T, fieldMetaData), name);
+ if (meta.empty) {
+ --lastId;
+ version (TVerboseCodegen) {
+ code ~= "pragma(msg, `[thrift.codegen.base.writeStruct] Warning: No " ~
+ "meta information for field '" ~ name ~ "' in struct '" ~
+ T.stringof ~ "'. Assigned id: " ~ to!string(lastId) ~ ".`);\n";
+ }
+ code ~= writeFieldCode!F(name, lastId, req);
+ } else if (req != TReq.IGNORE) {
+ code ~= writeFieldCode!F(name, meta.front.id, req);
+ }
+ }
+
+ return code;
+ }());
+ p.writeFieldStop();
+ p.writeStructEnd();
+}
+
+private {
+ /*
+ * Returns a D code string containing the matching TType value for a passed
+ * D type, e.g. dToTTypeString!byte == "TType.BYTE".
+ */
+ template dToTTypeString(T) {
+ static if (is(FullyUnqual!T == bool)) {
+ enum dToTTypeString = "TType.BOOL";
+ } else static if (is(FullyUnqual!T == byte)) {
+ enum dToTTypeString = "TType.BYTE";
+ } else static if (is(FullyUnqual!T == double)) {
+ enum dToTTypeString = "TType.DOUBLE";
+ } else static if (is(FullyUnqual!T == short)) {
+ enum dToTTypeString = "TType.I16";
+ } else static if (is(FullyUnqual!T == int)) {
+ enum dToTTypeString = "TType.I32";
+ } else static if (is(FullyUnqual!T == long)) {
+ enum dToTTypeString = "TType.I64";
+ } else static if (is(FullyUnqual!T : string)) {
+ enum dToTTypeString = "TType.STRING";
+ } else static if (is(FullyUnqual!T == enum)) {
+ enum dToTTypeString = "TType.I32";
+ } else static if (is(FullyUnqual!T _ : U[], U)) {
+ enum dToTTypeString = "TType.LIST";
+ } else static if (is(FullyUnqual!T _ : V[K], K, V)) {
+ enum dToTTypeString = "TType.MAP";
+ } else static if (is(FullyUnqual!T _ : HashSet!E, E)) {
+ enum dToTTypeString = "TType.SET";
+ } else static if (is(FullyUnqual!T == struct)) {
+ enum dToTTypeString = "TType.STRUCT";
+ } else static if (is(FullyUnqual!T : TException)) {
+ enum dToTTypeString = "TType.STRUCT";
+ } else {
+ static assert(false, "Cannot represent type in Thrift: " ~ T.stringof);
+ }
+ }
+}
diff --git a/lib/d/src/thrift/codegen/client.d b/lib/d/src/thrift/codegen/client.d
new file mode 100644
index 000000000..9755ea8a3
--- /dev/null
+++ b/lib/d/src/thrift/codegen/client.d
@@ -0,0 +1,484 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.codegen.client;
+
+import std.algorithm : find;
+import std.array : empty, front;
+import std.conv : to;
+import std.traits : isSomeFunction, ParameterStorageClass,
+ ParameterStorageClassTuple, ParameterTypeTuple, ReturnType;
+import thrift.codegen.base;
+import thrift.internal.codegen;
+import thrift.internal.ctfe;
+import thrift.protocol.base;
+
+/**
+ * Thrift service client, which implements an interface by synchronously
+ * calling a server over a TProtocol.
+ *
+ * TClientBase simply extends Interface with generic input/output protocol
+ * properties to serve as a supertype for all TClients for the same service,
+ * which might be instantiated with different concrete protocol types (there
+ * is no covariance for template type parameters). If Interface is derived
+ * from another interface BaseInterface, it also extends
+ * TClientBase!BaseInterface.
+ *
+ * TClient is the class that actually implements TClientBase. Just as
+ * TClientBase, it is also derived from TClient!BaseInterface for inheriting
+ * services.
+ *
+ * TClient takes two optional template arguments which can be used for
+ * specifying the actual TProtocol implementation used for optimization
+ * purposes, as virtual calls can completely be eliminated then. If
+ * OutputProtocol is not specified, it is assumed to be the same as
+ * InputProtocol. The protocol properties defined by TClientBase are exposed
+ * with their concrete type (return type covariance).
+ *
+ * In addition to implementing TClientBase!Interface, TClient offers the
+ * following constructors:
+ * ---
+ * this(InputProtocol iprot, OutputProtocol oprot);
+ * // Only if is(InputProtocol == OutputProtocol), to use the same protocol
+ * // for both input and output:
+ * this(InputProtocol prot);
+ * ---
+ *
+ * The sequence id of the method calls starts at zero and is automatically
+ * incremented.
+ */
+interface TClientBase(Interface) if (isBaseService!Interface) : Interface {
+ /**
+ * The input protocol used by the client.
+ */
+ TProtocol inputProtocol() @property;
+
+ /**
+ * The output protocol used by the client.
+ */
+ TProtocol outputProtocol() @property;
+}
+
+/// Ditto
+interface TClientBase(Interface) if (isDerivedService!Interface) :
+ TClientBase!(BaseService!Interface), Interface {}
+
+/// Ditto
+template TClient(Interface, InputProtocol = TProtocol, OutputProtocol = void) if (
+ isService!Interface && isTProtocol!InputProtocol &&
+ (isTProtocol!OutputProtocol || is(OutputProtocol == void))
+) {
+ mixin({
+ static if (isDerivedService!Interface) {
+ string code = "class TClient : TClient!(BaseService!Interface, " ~
+ "InputProtocol, OutputProtocol), TClientBase!Interface {\n";
+ code ~= q{
+ this(IProt iprot, OProt oprot) {
+ super(iprot, oprot);
+ }
+
+ static if (is(IProt == OProt)) {
+ this(IProt prot) {
+ super(prot);
+ }
+ }
+
+ // DMD @@BUG@@: If these are not present in this class (would be)
+ // inherited anyway, »not implemented« errors are raised.
+ override IProt inputProtocol() @property {
+ return super.inputProtocol;
+ }
+ override OProt outputProtocol() @property {
+ return super.outputProtocol;
+ }
+ };
+ } else {
+ string code = "class TClient : TClientBase!Interface {";
+ code ~= q{
+ alias InputProtocol IProt;
+ static if (isTProtocol!OutputProtocol) {
+ alias OutputProtocol OProt;
+ } else {
+ static assert(is(OutputProtocol == void));
+ alias InputProtocol OProt;
+ }
+
+ this(IProt iprot, OProt oprot) {
+ iprot_ = iprot;
+ oprot_ = oprot;
+ }
+
+ static if (is(IProt == OProt)) {
+ this(IProt prot) {
+ this(prot, prot);
+ }
+ }
+
+ IProt inputProtocol() @property {
+ return iprot_;
+ }
+
+ OProt outputProtocol() @property {
+ return oprot_;
+ }
+
+ protected IProt iprot_;
+ protected OProt oprot_;
+ protected int seqid_;
+ };
+ }
+
+ foreach (methodName; __traits(derivedMembers, Interface)) {
+ static if (isSomeFunction!(mixin("Interface." ~ methodName))) {
+ bool methodMetaFound;
+ TMethodMeta methodMeta;
+ static if (is(typeof(Interface.methodMeta) : TMethodMeta[])) {
+ enum meta = find!`a.name == b`(Interface.methodMeta, methodName);
+ if (!meta.empty) {
+ methodMetaFound = true;
+ methodMeta = meta.front;
+ }
+ }
+
+ // Generate the code for sending.
+ string[] paramList;
+ string paramAssignCode;
+ foreach (i, _; ParameterTypeTuple!(mixin("Interface." ~ methodName))) {
+ // Use the param name speficied in the meta information if any –
+ // just cosmetics in this case.
+ string paramName;
+ if (methodMetaFound && i < methodMeta.params.length) {
+ paramName = methodMeta.params[i].name;
+ } else {
+ paramName = "param" ~ to!string(i + 1);
+ }
+
+ immutable storage = ParameterStorageClassTuple!(
+ mixin("Interface." ~ methodName))[i];
+ paramList ~= ((storage & ParameterStorageClass.ref_) ? "ref " : "") ~
+ "ParameterTypeTuple!(Interface." ~ methodName ~ ")[" ~
+ to!string(i) ~ "] " ~ paramName;
+ paramAssignCode ~= "args." ~ paramName ~ " = &" ~ paramName ~ ";\n";
+ }
+ code ~= "ReturnType!(Interface." ~ methodName ~ ") " ~ methodName ~
+ "(" ~ ctfeJoin(paramList) ~ ") {\n";
+
+ code ~= "immutable methodName = `" ~ methodName ~ "`;\n";
+
+ immutable paramStructType =
+ "TPargsStruct!(Interface, `" ~ methodName ~ "`)";
+ code ~= paramStructType ~ " args = " ~ paramStructType ~ "();\n";
+ code ~= paramAssignCode;
+ code ~= "oprot_.writeMessageBegin(TMessage(`" ~ methodName ~
+ "`, TMessageType.CALL, ++seqid_));\n";
+ code ~= "args.write(oprot_);\n";
+ code ~= "oprot_.writeMessageEnd();\n";
+ code ~= "oprot_.transport.flush();\n";
+
+ // If this is not a oneway method, generate the recieving code.
+ if (!methodMetaFound || methodMeta.type != TMethodType.ONEWAY) {
+ code ~= "TPresultStruct!(Interface, `" ~ methodName ~ "`) result;\n";
+
+ if (!is(ReturnType!(mixin("Interface." ~ methodName)) == void)) {
+ code ~= "ReturnType!(Interface." ~ methodName ~ ") _return;\n";
+ code ~= "result.success = &_return;\n";
+ }
+
+ // TODO: The C++ implementation checks for matching method name here,
+ // should we do as well?
+ code ~= q{
+ auto msg = iprot_.readMessageBegin();
+ scope (exit) {
+ iprot_.readMessageEnd();
+ iprot_.transport.readEnd();
+ }
+
+ if (msg.type == TMessageType.EXCEPTION) {
+ auto x = new TApplicationException(null);
+ x.read(iprot_);
+ iprot_.transport.readEnd();
+ throw x;
+ }
+ if (msg.type != TMessageType.REPLY) {
+ skip(iprot_, TType.STRUCT);
+ iprot_.transport.readEnd();
+ }
+ if (msg.seqid != seqid_) {
+ throw new TApplicationException(
+ methodName ~ " failed: Out of sequence response.",
+ TApplicationException.Type.BAD_SEQUENCE_ID
+ );
+ }
+ result.read(iprot_);
+ };
+
+ if (methodMetaFound) {
+ foreach (e; methodMeta.exceptions) {
+ code ~= "if (result.isSet!`" ~ e.name ~ "`) throw result." ~
+ e.name ~ ";\n";
+ }
+ }
+
+ if (!is(ReturnType!(mixin("Interface." ~ methodName)) == void)) {
+ code ~= q{
+ if (result.isSet!`success`) return _return;
+ throw new TApplicationException(
+ methodName ~ " failed: Unknown result.",
+ TApplicationException.Type.MISSING_RESULT
+ );
+ };
+ }
+ }
+ code ~= "}\n";
+ }
+ }
+
+ code ~= "}\n";
+ return code;
+ }());
+}
+
+/**
+ * TClient construction helper to avoid having to explicitly specify
+ * the protocol types, i.e. to allow the constructor being called using IFTI
+ * (see $(DMDBUG 6082, D Bugzilla enhancement requet 6082)).
+ */
+TClient!(Interface, Prot) tClient(Interface, Prot)(Prot prot) if (
+ isService!Interface && isTProtocol!Prot
+) {
+ return new TClient!(Interface, Prot)(prot);
+}
+
+/// Ditto
+TClient!(Interface, IProt, Oprot) tClient(Interface, IProt, OProt)
+ (IProt iprot, OProt oprot) if (
+ isService!Interface && isTProtocol!IProt && isTProtocol!OProt
+) {
+ return new TClient!(Interface, IProt, OProt)(iprot, oprot);
+}
+
+/**
+ * Represents the arguments of a Thrift method call, as pointers to the (const)
+ * parameter type to avoid copying.
+ *
+ * There should usually be no reason to use this struct directly without the
+ * help of TClient, but it is documented publicly to help debugging in case
+ * of CTFE errors.
+ *
+ * Consider this example:
+ * ---
+ * interface Foo {
+ * int bar(string a, bool b);
+ *
+ * enum methodMeta = [
+ * TMethodMeta("bar", [TParamMeta("a", 1), TParamMeta("b", 2)])
+ * ];
+ * }
+ *
+ * alias TPargsStruct!(Foo, "bar") FooBarPargs;
+ * ---
+ *
+ * The definition of FooBarPargs is equivalent to (ignoring the necessary
+ * metadata to assign the field IDs):
+ * ---
+ * struct FooBarPargs {
+ * const(string)* a;
+ * const(bool)* b;
+ *
+ * void write(Protocol)(Protocol proto) const if (isTProtocol!Protocol);
+ * }
+ * ---
+ */
+template TPargsStruct(Interface, string methodName) {
+ static assert(is(typeof(mixin("Interface." ~ methodName))),
+ "Could not find method '" ~ methodName ~ "' in '" ~ Interface.stringof ~ "'.");
+ mixin({
+ bool methodMetaFound;
+ TMethodMeta methodMeta;
+ static if (is(typeof(Interface.methodMeta) : TMethodMeta[])) {
+ auto meta = find!`a.name == b`(Interface.methodMeta, methodName);
+ if (!meta.empty) {
+ methodMetaFound = true;
+ methodMeta = meta.front;
+ }
+ }
+
+ string memberCode;
+ string[] fieldMetaCodes;
+ foreach (i, _; ParameterTypeTuple!(mixin("Interface." ~ methodName))) {
+ // If we have no meta information, just use param1, param2, etc. as
+ // field names, it shouldn't really matter anyway. 1-based »indexing«
+ // is used to match the common scheme in the Thrift world.
+ string memberId;
+ string memberName;
+ if (methodMetaFound && i < methodMeta.params.length) {
+ memberId = to!string(methodMeta.params[i].id);
+ memberName = methodMeta.params[i].name;
+ } else {
+ memberId = to!string(i + 1);
+ memberName = "param" ~ to!string(i + 1);
+ }
+
+ // Workaround for DMD @@BUG@@ 6056: make an intermediary alias for the
+ // parameter type, and declare the member using const(memberNameType)*.
+ memberCode ~= "alias ParameterTypeTuple!(Interface." ~ methodName ~
+ ")[" ~ to!string(i) ~ "] " ~ memberName ~ "Type;\n";
+ memberCode ~= "const(" ~ memberName ~ "Type)* " ~ memberName ~ ";\n";
+
+ fieldMetaCodes ~= "TFieldMeta(`" ~ memberName ~ "`, " ~ memberId ~
+ ", TReq.OPT_IN_REQ_OUT)";
+ }
+
+ string code = "struct TPargsStruct {\n";
+ code ~= memberCode;
+ version (TVerboseCodegen) {
+ if (!methodMetaFound &&
+ ParameterTypeTuple!(mixin("Interface." ~ methodName)).length > 0)
+ {
+ code ~= "pragma(msg, `[thrift.codegen.base.TPargsStruct] Warning: No " ~
+ "meta information for method '" ~ methodName ~ "' in service '" ~
+ Interface.stringof ~ "' found.`);\n";
+ }
+ }
+ code ~= "void write(P)(P proto) const if (isTProtocol!P) {\n";
+ code ~= "writeStruct!(typeof(this), P, [" ~ ctfeJoin(fieldMetaCodes) ~
+ "], true)(this, proto);\n";
+ code ~= "}\n";
+ code ~= "}\n";
+ return code;
+ }());
+}
+
+/**
+ * Represents the result of a Thrift method call, using a pointer to the return
+ * value to avoid copying.
+ *
+ * There should usually be no reason to use this struct directly without the
+ * help of TClient, but it is documented publicly to help debugging in case
+ * of CTFE errors.
+ *
+ * Consider this example:
+ * ---
+ * interface Foo {
+ * int bar(string a);
+ *
+ * alias .FooException FooException;
+ *
+ * enum methodMeta = [
+ * TMethodMeta("bar",
+ * [TParamMeta("a", 1)],
+ * [TExceptionMeta("fooe", 1, "FooException")]
+ * )
+ * ];
+ * }
+ * alias TPresultStruct!(Foo, "bar") FooBarPresult;
+ * ---
+ *
+ * The definition of FooBarPresult is equivalent to (ignoring the necessary
+ * metadata to assign the field IDs):
+ * ---
+ * struct FooBarPresult {
+ * int* success;
+ * Foo.FooException fooe;
+ *
+ * struct IsSetFlags {
+ * bool success;
+ * }
+ * IsSetFlags isSetFlags;
+ *
+ * bool isSet(string fieldName)() const @property;
+ * void read(Protocol)(Protocol proto) if (isTProtocol!Protocol);
+ * }
+ * ---
+ */
+template TPresultStruct(Interface, string methodName) {
+ static assert(is(typeof(mixin("Interface." ~ methodName))),
+ "Could not find method '" ~ methodName ~ "' in '" ~ Interface.stringof ~ "'.");
+
+ mixin({
+ string code = "struct TPresultStruct {\n";
+
+ string[] fieldMetaCodes;
+
+ alias ReturnType!(mixin("Interface." ~ methodName)) ResultType;
+ static if (!is(ResultType == void)) {
+ code ~= q{
+ ReturnType!(mixin("Interface." ~ methodName))* success;
+ };
+ fieldMetaCodes ~= "TFieldMeta(`success`, 0, TReq.OPTIONAL)";
+
+ static if (!isNullable!ResultType) {
+ code ~= q{
+ struct IsSetFlags {
+ bool success;
+ }
+ IsSetFlags isSetFlags;
+ };
+ fieldMetaCodes ~= "TFieldMeta(`isSetFlags`, 0, TReq.IGNORE)";
+ }
+ }
+
+ bool methodMetaFound;
+ static if (is(typeof(Interface.methodMeta) : TMethodMeta[])) {
+ auto meta = find!`a.name == b`(Interface.methodMeta, methodName);
+ if (!meta.empty) {
+ foreach (e; meta.front.exceptions) {
+ code ~= "Interface." ~ e.type ~ " " ~ e.name ~ ";\n";
+ fieldMetaCodes ~= "TFieldMeta(`" ~ e.name ~ "`, " ~ to!string(e.id) ~
+ ", TReq.OPTIONAL)";
+ }
+ methodMetaFound = true;
+ }
+ }
+
+ version (TVerboseCodegen) {
+ if (!methodMetaFound &&
+ ParameterTypeTuple!(mixin("Interface." ~ methodName)).length > 0)
+ {
+ code ~= "pragma(msg, `[thrift.codegen.base.TPresultStruct] Warning: No " ~
+ "meta information for method '" ~ methodName ~ "' in service '" ~
+ Interface.stringof ~ "' found.`);\n";
+ }
+ }
+
+ code ~= q{
+ bool isSet(string fieldName)() const @property if (
+ is(MemberType!(typeof(this), fieldName))
+ ) {
+ static if (fieldName == "success") {
+ static if (isNullable!(typeof(*success))) {
+ return *success !is null;
+ } else {
+ return isSetFlags.success;
+ }
+ } else {
+ // We are dealing with an exception member, which, being a nullable
+ // type (exceptions are always classes), has no isSet flag.
+ return __traits(getMember, this, fieldName) !is null;
+ }
+ }
+ };
+
+ code ~= "void read(P)(P proto) if (isTProtocol!P) {\n";
+ code ~= "readStruct!(typeof(this), P, [" ~ ctfeJoin(fieldMetaCodes) ~
+ "], true)(this, proto);\n";
+ code ~= "}\n";
+ code ~= "}\n";
+ return code;
+ }());
+}
diff --git a/lib/d/src/thrift/codegen/client_pool.d b/lib/d/src/thrift/codegen/client_pool.d
new file mode 100644
index 000000000..ebf5b60fe
--- /dev/null
+++ b/lib/d/src/thrift/codegen/client_pool.d
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.codegen.client_pool;
+
+import core.time : dur, Duration, TickDuration;
+import std.traits : ParameterTypeTuple, ReturnType;
+import thrift.base;
+import thrift.codegen.base;
+import thrift.codegen.client;
+import thrift.internal.codegen;
+import thrift.internal.resource_pool;
+
+/**
+ * Manages a pool of TClients for the given interface, forwarding RPC calls to
+ * members of the pool.
+ *
+ * If a request fails, another client from the pool is tried, and optionally,
+ * a client is disabled for a configurable amount of time if it fails too
+ * often. If all clients fail (and keepTrying is false), a
+ * TCompoundOperationException is thrown, containing all the collected RPC
+ * exceptions.
+ */
+class TClientPool(Interface) if (isService!Interface) : Interface {
+ /// Shorthand for TClientBase!Interface, the client type this instance
+ /// operates on.
+ alias TClientBase!Interface Client;
+
+ /**
+ * Creates a new instance and adds the given clients to the pool.
+ */
+ this(Client[] clients) {
+ pool_ = new TResourcePool!Client(clients);
+
+ rpcFaultFilter = (Exception e) {
+ import thrift.protocol.base;
+ import thrift.transport.base;
+ return (
+ (cast(TTransportException)e !is null) ||
+ (cast(TApplicationException)e !is null)
+ );
+ };
+ }
+
+ /**
+ * Executes an operation on the first currently active client.
+ *
+ * If the operation fails (throws an exception for which rpcFaultFilter is
+ * true), the failure is recorded and the next client in the pool is tried.
+ *
+ * Throws: Any non-rpc exception that occurs, a TCompoundOperationException
+ * if all clients failed with an rpc exception (if keepTrying is false).
+ *
+ * Example:
+ * ---
+ * interface Foo { string bar(); }
+ * auto poolClient = tClientPool([tClient!Foo(someProtocol)]);
+ * auto result = poolClient.execute((c){ return c.bar(); });
+ * ---
+ */
+ ResultType execute(ResultType)(scope ResultType delegate(Client) work) {
+ return executeOnPool!Client(work);
+ }
+
+ /**
+ * Adds a client to the pool.
+ */
+ void addClient(Client client) {
+ pool_.add(client);
+ }
+
+ /**
+ * Removes a client from the pool.
+ *
+ * Returns: Whether the client was found in the pool.
+ */
+ bool removeClient(Client client) {
+ return pool_.remove(client);
+ }
+
+ mixin(poolForwardCode!Interface());
+
+ /// Whether to open the underlying transports of a client before trying to
+ /// execute a method if they are not open. This is usually desirable
+ /// because it allows e.g. to automatically reconnect to a remote server
+ /// if the network connection is dropped.
+ ///
+ /// Defaults to true.
+ bool reopenTransports = true;
+
+ /// Called to determine whether an exception comes from a client from the
+ /// pool not working properly, or if it an exception thrown at the
+ /// application level.
+ ///
+ /// If the delegate returns true, the server/connection is considered to be
+ /// at fault, if it returns false, the exception is just passed on to the
+ /// caller.
+ ///
+ /// By default, returns true for instances of TTransportException and
+ /// TApplicationException, false otherwise.
+ bool delegate(Exception) rpcFaultFilter;
+
+ /**
+ * Whether to keep trying to find a working client if all have failed in a
+ * row.
+ *
+ * Defaults to false.
+ */
+ bool keepTrying() const @property {
+ return pool_.cycle;
+ }
+
+ /// Ditto
+ void keepTrying(bool value) @property {
+ pool_.cycle = value;
+ }
+
+ /**
+ * Whether to use a random permutation of the client pool on every call to
+ * execute(). This can be used e.g. as a simple form of load balancing.
+ *
+ * Defaults to true.
+ */
+ bool permuteClients() const @property {
+ return pool_.permute;
+ }
+
+ /// Ditto
+ void permuteClients(bool value) @property {
+ pool_.permute = value;
+ }
+
+ /**
+ * The number of consecutive faults after which a client is disabled until
+ * faultDisableDuration has passed. 0 to never disable clients.
+ *
+ * Defaults to 0.
+ */
+ ushort faultDisableCount() @property {
+ return pool_.faultDisableCount;
+ }
+
+ /// Ditto
+ void faultDisableCount(ushort value) @property {
+ pool_.faultDisableCount = value;
+ }
+
+ /**
+ * The duration for which a client is no longer considered after it has
+ * failed too often.
+ *
+ * Defaults to one second.
+ */
+ Duration faultDisableDuration() @property {
+ return pool_.faultDisableDuration;
+ }
+
+ /// Ditto
+ void faultDisableDuration(Duration value) @property {
+ pool_.faultDisableDuration = value;
+ }
+
+protected:
+ ResultType executeOnPool(ResultType)(scope ResultType delegate(Client) work) {
+ auto clients = pool_[];
+ if (clients.empty) {
+ throw new TException("No clients available to try.");
+ }
+
+ while (true) {
+ Exception[] rpcExceptions;
+ while (!clients.empty) {
+ auto c = clients.front;
+ clients.popFront;
+ try {
+ scope (success) {
+ pool_.recordSuccess(c);
+ }
+
+ if (reopenTransports) {
+ c.inputProtocol.transport.open();
+ c.outputProtocol.transport.open();
+ }
+
+ return work(c);
+ } catch (Exception e) {
+ if (rpcFaultFilter && rpcFaultFilter(e)) {
+ pool_.recordFault(c);
+ rpcExceptions ~= e;
+ } else {
+ // We are dealing with a normal exception thrown by the
+ // server-side method, just pass it on. As far as we are
+ // concerned, the method call succeded.
+ pool_.recordSuccess(c);
+ throw e;
+ }
+ }
+ }
+
+ // If we get here, no client succeeded during the current iteration.
+ Duration waitTime;
+ Client dummy;
+ if (clients.willBecomeNonempty(dummy, waitTime)) {
+ if (waitTime > dur!"hnsecs"(0)) {
+ import core.thread;
+ Thread.sleep(waitTime);
+ }
+ } else {
+ throw new TCompoundOperationException("All clients failed.",
+ rpcExceptions);
+ }
+ }
+ }
+
+private:
+ TResourcePool!Client pool_;
+}
+
+private {
+ // Cannot use an anonymous delegate literal for this because they aren't
+ // allowed in class scope.
+ string poolForwardCode(Interface)() {
+ string code = "";
+
+ foreach (methodName; AllMemberMethodNames!Interface) {
+ enum qn = "Interface." ~ methodName;
+ code ~= "ReturnType!(" ~ qn ~ ") " ~ methodName ~
+ "(ParameterTypeTuple!(" ~ qn ~ ") args) {\n";
+ code ~= "return executeOnPool((Client c){ return c." ~
+ methodName ~ "(args); });\n";
+ code ~= "}\n";
+ }
+
+ return code;
+ }
+}
+
+/**
+ * TClientPool construction helper to avoid having to explicitly specify
+ * the interface type, i.e. to allow the constructor being called using IFTI
+ * (see $(DMDBUG 6082, D Bugzilla enhancement requet 6082)).
+ */
+TClientPool!Interface tClientPool(Interface)(
+ TClientBase!Interface[] clients
+) if (isService!Interface) {
+ return new typeof(return)(clients);
+}
diff --git a/lib/d/src/thrift/codegen/idlgen.d b/lib/d/src/thrift/codegen/idlgen.d
new file mode 100644
index 000000000..03e9b903e
--- /dev/null
+++ b/lib/d/src/thrift/codegen/idlgen.d
@@ -0,0 +1,767 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Contains <b>experimental</b> functionality for generating Thrift IDL files
+ * (.thrift) from existing D data structures, i.e. the reverse of what the
+ * Thrift compiler does.
+ */
+module thrift.codegen.idlgen;
+
+import std.algorithm : find;
+import std.array : empty, front;
+import std.conv : to;
+import std.traits : EnumMembers, isSomeFunction, OriginalType,
+ ParameterTypeTuple, ReturnType;
+import std.typetuple : allSatisfy, staticIndexOf, staticMap, NoDuplicates,
+ TypeTuple;
+import thrift.base;
+import thrift.codegen.base;
+import thrift.internal.codegen;
+import thrift.internal.ctfe;
+import thrift.util.hashset;
+
+/**
+ * True if the passed type is a Thrift entity (struct, exception, enum,
+ * service).
+ */
+alias Any!(isStruct, isException, isEnum, isService) isThriftEntity;
+
+/**
+ * Returns an IDL string describing the passed »root« entities and all types
+ * they depend on.
+ */
+template idlString(Roots...) if (allSatisfy!(isThriftEntity, Roots)) {
+ enum idlString = idlStringImpl!Roots.result;
+}
+
+private {
+ template idlStringImpl(Roots...) if (allSatisfy!(isThriftEntity, Roots)) {
+ alias ForAllWithList!(
+ ConfinedTuple!(StaticFilter!(isService, Roots)),
+ AddBaseServices
+ ) Services;
+
+ alias TypeTuple!(
+ StaticFilter!(isEnum, Roots),
+ ForAllWithList!(
+ ConfinedTuple!(
+ StaticFilter!(Any!(isException, isStruct), Roots),
+ staticMap!(CompositeTypeDeps, staticMap!(ServiceTypeDeps, Services))
+ ),
+ AddStructWithDeps
+ )
+ ) Types;
+
+ enum result = ctfeJoin(
+ [
+ staticMap!(
+ enumIdlString,
+ StaticFilter!(isEnum, Types)
+ ),
+ staticMap!(
+ structIdlString,
+ StaticFilter!(Any!(isStruct, isException), Types)
+ ),
+ staticMap!(
+ serviceIdlString,
+ Services
+ )
+ ],
+ "\n"
+ );
+ }
+
+ template ServiceTypeDeps(T) if (isService!T) {
+ alias staticMap!(
+ PApply!(MethodTypeDeps, T),
+ FilterMethodNames!(T, __traits(derivedMembers, T))
+ ) ServiceTypeDeps;
+ }
+
+ template MethodTypeDeps(T, string name) if (
+ isService!T && isSomeFunction!(MemberType!(T, name))
+ ) {
+ alias TypeTuple!(
+ ReturnType!(MemberType!(T, name)),
+ ParameterTypeTuple!(MemberType!(T, name)),
+ ExceptionTypes!(T, name)
+ ) MethodTypeDeps;
+ }
+
+ template ExceptionTypes(T, string name) if (
+ isService!T && isSomeFunction!(MemberType!(T, name))
+ ) {
+ mixin({
+ enum meta = find!`a.name == b`(getMethodMeta!T, name);
+ if (meta.empty) return "alias TypeTuple!() ExceptionTypes;";
+
+ string result = "alias TypeTuple!(";
+ foreach (i, e; meta.front.exceptions) {
+ if (i > 0) result ~= ", ";
+ result ~= "mixin(`T." ~ e.type ~ "`)";
+ }
+ result ~= ") ExceptionTypes;";
+ return result;
+ }());
+ }
+
+ template AddBaseServices(T, List...) {
+ static if (staticIndexOf!(T, List) == -1) {
+ alias NoDuplicates!(BaseServices!T, List) AddBaseServices;
+ } else {
+ alias List AddStructWithDeps;
+ }
+ }
+
+ unittest {
+ interface A {}
+ interface B : A {}
+ interface C : B {}
+ interface D : A {}
+
+ static assert(is(AddBaseServices!(C) == TypeTuple!(A, B, C)));
+ static assert(is(ForAllWithList!(ConfinedTuple!(C, D), AddBaseServices) ==
+ TypeTuple!(A, D, B, C)));
+ }
+
+ template BaseServices(T, Rest...) if (isService!T) {
+ static if (isDerivedService!T) {
+ alias BaseServices!(BaseService!T, T, Rest) BaseServices;
+ } else {
+ alias TypeTuple!(T, Rest) BaseServices;
+ }
+ }
+
+ template AddStructWithDeps(T, List...) {
+ static if (staticIndexOf!(T, List) == -1) {
+ // T is not already in the List, so add T and the types it depends on in
+ // the front. Because with the Thrift compiler types can only depend on
+ // other types that have already been defined, we collect all the
+ // dependencies, prepend them to the list, and then prune the duplicates
+ // (keeping the first occurences). If this requirement should ever be
+ // dropped from Thrift, this could be easily adapted to handle circular
+ // dependencies by passing TypeTuple!(T, List) to ForAllWithList instead
+ // of appending List afterwards, and removing the now unneccesary
+ // NoDuplicates.
+ alias NoDuplicates!(
+ ForAllWithList!(
+ ConfinedTuple!(
+ staticMap!(
+ CompositeTypeDeps,
+ staticMap!(
+ PApply!(MemberType, T),
+ FieldNames!T
+ )
+ )
+ ),
+ .AddStructWithDeps,
+ T
+ ),
+ List
+ ) AddStructWithDeps;
+ } else {
+ alias List AddStructWithDeps;
+ }
+ }
+
+ version (unittest) {
+ struct A {}
+ struct B {
+ A a;
+ int b;
+ A c;
+ string d;
+ }
+ struct C {
+ B b;
+ A a;
+ }
+
+ static assert(is(AddStructWithDeps!C == TypeTuple!(A, B, C)));
+
+ struct D {
+ C c;
+ mixin TStructHelpers!([TFieldMeta("c", 0, TReq.IGNORE)]);
+ }
+ static assert(is(AddStructWithDeps!D == TypeTuple!(D)));
+ }
+
+ version (unittest) {
+ // Circles in the type dependency graph are not allowed in Thrift, but make
+ // sure we fail in a sane way instead of crashing the compiler.
+
+ struct Rec1 {
+ Rec2[] other;
+ }
+
+ struct Rec2 {
+ Rec1[] other;
+ }
+
+ static assert(!__traits(compiles, AddStructWithDeps!Rec1));
+ }
+
+ /*
+ * Returns the non-primitive types T directly depends on.
+ *
+ * For example, CompositeTypeDeps!int would yield an empty type tuple,
+ * CompositeTypeDeps!SomeStruct would give SomeStruct, and
+ * CompositeTypeDeps!(A[B]) both CompositeTypeDeps!A and CompositeTypeDeps!B.
+ */
+ template CompositeTypeDeps(T) {
+ static if (is(FullyUnqual!T == bool) || is(FullyUnqual!T == byte) ||
+ is(FullyUnqual!T == short) || is(FullyUnqual!T == int) ||
+ is(FullyUnqual!T == long) || is(FullyUnqual!T : string) ||
+ is(FullyUnqual!T == double) || is(FullyUnqual!T == void)
+ ) {
+ alias TypeTuple!() CompositeTypeDeps;
+ } else static if (is(FullyUnqual!T _ : U[], U)) {
+ alias CompositeTypeDeps!U CompositeTypeDeps;
+ } else static if (is(FullyUnqual!T _ : HashSet!E, E)) {
+ alias CompositeTypeDeps!E CompositeTypeDeps;
+ } else static if (is(FullyUnqual!T _ : V[K], K, V)) {
+ alias TypeTuple!(CompositeTypeDeps!K, CompositeTypeDeps!V) CompositeTypeDeps;
+ } else static if (is(FullyUnqual!T == enum) || is(FullyUnqual!T == struct) ||
+ is(FullyUnqual!T : TException)
+ ) {
+ alias TypeTuple!(FullyUnqual!T) CompositeTypeDeps;
+ } else {
+ static assert(false, "Cannot represent type in Thrift: " ~ T.stringof);
+ }
+ }
+}
+
+/**
+ * Returns an IDL string describing the passed service. IDL code for any type
+ * dependcies is not included.
+ */
+template serviceIdlString(T) if (isService!T) {
+ enum serviceIdlString = {
+ string result = "service " ~ T.stringof;
+ static if (isDerivedService!T) {
+ result ~= " extends " ~ BaseService!T.stringof;
+ }
+ result ~= " {\n";
+
+ foreach (methodName; FilterMethodNames!(T, __traits(derivedMembers, T))) {
+ result ~= " ";
+
+ enum meta = find!`a.name == b`(T.methodMeta, methodName);
+
+ static if (!meta.empty && meta.front.type == TMethodType.ONEWAY) {
+ result ~= "oneway ";
+ }
+
+ alias ReturnType!(MemberType!(T, methodName)) RT;
+ static if (is(RT == void)) {
+ // We special-case this here instead of adding void to dToIdlType to
+ // avoid accepting things like void[].
+ result ~= "void ";
+ } else {
+ result ~= dToIdlType!RT ~ " ";
+ }
+ result ~= methodName ~ "(";
+
+ short lastId;
+ foreach (i, ParamType; ParameterTypeTuple!(MemberType!(T, methodName))) {
+ static if (!meta.empty && i < meta.front.params.length) {
+ enum havePM = true;
+ } else {
+ enum havePM = false;
+ }
+
+ short id;
+ static if (havePM) {
+ id = meta.front.params[i].id;
+ } else {
+ id = --lastId;
+ }
+
+ string paramName;
+ static if (havePM) {
+ paramName = meta.front.params[i].name;
+ } else {
+ paramName = "param" ~ to!string(i + 1);
+ }
+
+ result ~= to!string(id) ~ ": " ~ dToIdlType!ParamType ~ " " ~ paramName;
+
+ static if (havePM && !meta.front.params[i].defaultValue.empty) {
+ result ~= " = " ~ dToIdlConst(mixin(meta.front.params[i].defaultValue));
+ } else {
+ // Unfortunately, getting the default value for parameters from a
+ // function alias isn't possible – we can't transfer the default
+ // value to the IDL e.g. for interface Foo { void foo(int a = 5); }
+ // without the user explicitly declaring it in metadata.
+ }
+ result ~= ", ";
+ }
+ result ~= ")";
+
+ static if (!meta.empty && !meta.front.exceptions.empty) {
+ result ~= " throws (";
+ foreach (e; meta.front.exceptions) {
+ result ~= to!string(e.id) ~ ": " ~ e.type ~ " " ~ e.name ~ ", ";
+ }
+ result ~= ")";
+ }
+
+ result ~= ",\n";
+ }
+
+ result ~= "}\n";
+ return result;
+ }();
+}
+
+/**
+ * Returns an IDL string describing the passed enum. IDL code for any type
+ * dependcies is not included.
+ */
+template enumIdlString(T) if (isEnum!T) {
+ enum enumIdlString = {
+ static assert(is(OriginalType!T : long),
+ "Can only have integer enums in Thrift (not " ~ OriginalType!T.stringof ~
+ ", for " ~ T.stringof ~ ").");
+
+ string result = "enum " ~ T.stringof ~ " {\n";
+
+ foreach (name; __traits(derivedMembers, T)) {
+ result ~= " " ~ name ~ " = " ~ dToIdlConst(GetMember!(T, name)) ~ ",\n";
+ }
+
+ result ~= "}\n";
+ return result;
+ }();
+}
+
+/**
+ * Returns an IDL string describing the passed struct. IDL code for any type
+ * dependcies is not included.
+ */
+template structIdlString(T) if (isStruct!T || isException!T) {
+ enum structIdlString = {
+ mixin({
+ string code = "";
+ foreach (field; getFieldMeta!T) {
+ code ~= "static assert(is(MemberType!(T, `" ~ field.name ~ "`)));\n";
+ }
+ return code;
+ }());
+
+ string result;
+ static if (isException!T) {
+ result = "exception ";
+ } else {
+ result = "struct ";
+ }
+ result ~= T.stringof ~ " {\n";
+
+ // The last automatically assigned id – fields with no meta information
+ // are assigned (in lexical order) descending negative ids, starting with
+ // -1, just like the Thrift compiler does.
+ short lastId;
+
+ foreach (name; FieldNames!T) {
+ enum meta = find!`a.name == b`(getFieldMeta!T, name);
+
+ static if (meta.empty || meta.front.req != TReq.IGNORE) {
+ short id;
+ static if (meta.empty) {
+ id = --lastId;
+ } else {
+ id = meta.front.id;
+ }
+
+ result ~= " " ~ to!string(id) ~ ":";
+ static if (!meta.empty) {
+ result ~= dToIdlReq(meta.front.req);
+ }
+ result ~= " " ~ dToIdlType!(MemberType!(T, name)) ~ " " ~ name;
+
+ static if (!meta.empty && !meta.front.defaultValue.empty) {
+ result ~= " = " ~ dToIdlConst(mixin(meta.front.defaultValue));
+ } else static if (__traits(compiles, fieldInitA!(T, name))) {
+ static if (is(typeof(fieldInitA!(T, name))) &&
+ !is(typeof(fieldInitA!(T, name)) == void)
+ ) {
+ result ~= " = " ~ dToIdlConst(fieldInitA!(T, name));
+ }
+ } else static if (is(typeof(fieldInitB!(T, name))) &&
+ !is(typeof(fieldInitB!(T, name)) == void)
+ ) {
+ result ~= " = " ~ dToIdlConst(fieldInitB!(T, name));
+ }
+ result ~= ",\n";
+ }
+ }
+
+ result ~= "}\n";
+ return result;
+ }();
+}
+
+private {
+ // This very convoluted way of doing things was chosen because putting the
+ // static if directly into structIdlString caused »not evaluatable at compile
+ // time« errors to slip through even though typeof() was used, resp. the
+ // condition to be true even though the value couldn't actually be read at
+ // compile time due to a @@BUG@@ in DMD 2.055.
+ // The extra »compiled« field in fieldInitA is needed because we must not try
+ // to use != if !is compiled as well (but was false), e.g. for floating point
+ // types.
+ template fieldInitA(T, string name) {
+ static if (mixin("T.init." ~ name) !is MemberType!(T, name).init) {
+ enum fieldInitA = mixin("T.init." ~ name);
+ }
+ }
+
+ template fieldInitB(T, string name) {
+ static if (mixin("T.init." ~ name) != MemberType!(T, name).init) {
+ enum fieldInitB = mixin("T.init." ~ name);
+ }
+ }
+
+ template dToIdlType(T) {
+ static if (is(FullyUnqual!T == bool)) {
+ enum dToIdlType = "bool";
+ } else static if (is(FullyUnqual!T == byte)) {
+ enum dToIdlType = "byte";
+ } else static if (is(FullyUnqual!T == double)) {
+ enum dToIdlType = "double";
+ } else static if (is(FullyUnqual!T == short)) {
+ enum dToIdlType = "i16";
+ } else static if (is(FullyUnqual!T == int)) {
+ enum dToIdlType = "i32";
+ } else static if (is(FullyUnqual!T == long)) {
+ enum dToIdlType = "i64";
+ } else static if (is(FullyUnqual!T : string)) {
+ enum dToIdlType = "string";
+ } else static if (is(FullyUnqual!T _ : U[], U)) {
+ enum dToIdlType = "list<" ~ dToIdlType!U ~ ">";
+ } else static if (is(FullyUnqual!T _ : V[K], K, V)) {
+ enum dToIdlType = "map<" ~ dToIdlType!K ~ ", " ~ dToIdlType!V ~ ">";
+ } else static if (is(FullyUnqual!T _ : HashSet!E, E)) {
+ enum dToIdlType = "set<" ~ dToIdlType!E ~ ">";
+ } else static if (is(FullyUnqual!T == struct) || is(FullyUnqual!T == enum) ||
+ is(FullyUnqual!T : TException)
+ ) {
+ enum dToIdlType = FullyUnqual!(T).stringof;
+ } else {
+ static assert(false, "Cannot represent type in Thrift: " ~ T.stringof);
+ }
+ }
+
+ string dToIdlReq(TReq req) {
+ switch (req) {
+ case TReq.REQUIRED: return " required";
+ case TReq.OPTIONAL: return " optional";
+ default: return "";
+ }
+ }
+
+ string dToIdlConst(T)(T value) {
+ static if (is(FullyUnqual!T == bool)) {
+ return value ? "1" : "0";
+ } else static if (is(FullyUnqual!T == byte) ||
+ is(FullyUnqual!T == short) || is(FullyUnqual!T == int) ||
+ is(FullyUnqual!T == long)
+ ) {
+ return to!string(value);
+ } else static if (is(FullyUnqual!T : string)) {
+ return `"` ~ to!string(value) ~ `"`;
+ } else static if (is(FullyUnqual!T == double)) {
+ return ctfeToString(value);
+ } else static if (is(FullyUnqual!T _ : U[], U) ||
+ is(FullyUnqual!T _ : HashSet!E, E)
+ ) {
+ string result = "[";
+ foreach (e; value) {
+ result ~= dToIdlConst(e) ~ ", ";
+ }
+ result ~= "]";
+ return result;
+ } else static if (is(FullyUnqual!T _ : V[K], K, V)) {
+ string result = "{";
+ foreach (key, val; value) {
+ result ~= dToIdlConst(key) ~ ": " ~ dToIdlConst(val) ~ ", ";
+ }
+ result ~= "}";
+ return result;
+ } else static if (is(FullyUnqual!T == enum)) {
+ import std.conv;
+ import std.traits;
+ return to!string(cast(OriginalType!T)value);
+ } else static if (is(FullyUnqual!T == struct) ||
+ is(FullyUnqual!T : TException)
+ ) {
+ string result = "{";
+ foreach (name; __traits(derivedMembers, T)) {
+ static if (memberReq!(T, name) != TReq.IGNORE) {
+ result ~= name ~ ": " ~ dToIdlConst(mixin("value." ~ name)) ~ ", ";
+ }
+ }
+ result ~= "}";
+ return result;
+ } else {
+ static assert(false, "Cannot represent type in Thrift: " ~ T.stringof);
+ }
+ }
+}
+
+version (unittest) {
+ enum Foo {
+ a = 1,
+ b = 10,
+ c = 5
+ }
+
+ static assert(enumIdlString!Foo ==
+`enum Foo {
+ a = 1,
+ b = 10,
+ c = 5,
+}
+`);
+}
+
+
+version (unittest) {
+ struct WithoutMeta {
+ string a;
+ int b;
+ }
+
+ struct WithDefaults {
+ string a = "asdf";
+ double b = 3.1415;
+ WithoutMeta c;
+
+ mixin TStructHelpers!([
+ TFieldMeta("c", 1, TReq.init, `WithoutMeta("foo", 3)`)
+ ]);
+ }
+
+ // These are from DebugProtoTest.thrift.
+ struct OneOfEach {
+ bool im_true;
+ bool im_false;
+ byte a_bite;
+ short integer16;
+ int integer32;
+ long integer64;
+ double double_precision;
+ string some_characters;
+ string zomg_unicode;
+ bool what_who;
+ string base64;
+ byte[] byte_list;
+ short[] i16_list;
+ long[] i64_list;
+
+ mixin TStructHelpers!([
+ TFieldMeta(`im_true`, 1),
+ TFieldMeta(`im_false`, 2),
+ TFieldMeta(`a_bite`, 3, TReq.OPT_IN_REQ_OUT, q{cast(byte)127}),
+ TFieldMeta(`integer16`, 4, TReq.OPT_IN_REQ_OUT, q{cast(short)32767}),
+ TFieldMeta(`integer32`, 5),
+ TFieldMeta(`integer64`, 6, TReq.OPT_IN_REQ_OUT, q{10000000000L}),
+ TFieldMeta(`double_precision`, 7),
+ TFieldMeta(`some_characters`, 8),
+ TFieldMeta(`zomg_unicode`, 9),
+ TFieldMeta(`what_who`, 10),
+ TFieldMeta(`base64`, 11),
+ TFieldMeta(`byte_list`, 12, TReq.OPT_IN_REQ_OUT, q{{
+ byte[] v;
+ v ~= cast(byte)1;
+ v ~= cast(byte)2;
+ v ~= cast(byte)3;
+ return v;
+ }()}),
+ TFieldMeta(`i16_list`, 13, TReq.OPT_IN_REQ_OUT, q{{
+ short[] v;
+ v ~= cast(short)1;
+ v ~= cast(short)2;
+ v ~= cast(short)3;
+ return v;
+ }()}),
+ TFieldMeta(`i64_list`, 14, TReq.OPT_IN_REQ_OUT, q{{
+ long[] v;
+ v ~= 1L;
+ v ~= 2L;
+ v ~= 3L;
+ return v;
+ }()})
+ ]);
+ }
+
+ struct Bonk {
+ int type;
+ string message;
+
+ mixin TStructHelpers!([
+ TFieldMeta(`type`, 1),
+ TFieldMeta(`message`, 2)
+ ]);
+ }
+
+ struct HolyMoley {
+ OneOfEach[] big;
+ HashSet!(string[]) contain;
+ Bonk[][string] bonks;
+
+ mixin TStructHelpers!([
+ TFieldMeta(`big`, 1),
+ TFieldMeta(`contain`, 2),
+ TFieldMeta(`bonks`, 3)
+ ]);
+ }
+
+ static assert(structIdlString!WithoutMeta ==
+`struct WithoutMeta {
+ -1: string a,
+ -2: i32 b,
+}
+`);
+
+ static assert(structIdlString!WithDefaults ==
+`struct WithDefaults {
+ -1: string a = "asdf",
+ -2: double b = 3.1415,
+ 1: WithoutMeta c = {a: "foo", b: 3, },
+}
+`);
+
+ static assert(structIdlString!OneOfEach ==
+`struct OneOfEach {
+ 1: bool im_true,
+ 2: bool im_false,
+ 3: byte a_bite = 127,
+ 4: i16 integer16 = 32767,
+ 5: i32 integer32,
+ 6: i64 integer64 = 10000000000,
+ 7: double double_precision,
+ 8: string some_characters,
+ 9: string zomg_unicode,
+ 10: bool what_who,
+ 11: string base64,
+ 12: list<byte> byte_list = [1, 2, 3, ],
+ 13: list<i16> i16_list = [1, 2, 3, ],
+ 14: list<i64> i64_list = [1, 2, 3, ],
+}
+`);
+
+ static assert(structIdlString!Bonk ==
+`struct Bonk {
+ 1: i32 type,
+ 2: string message,
+}
+`);
+
+ static assert(structIdlString!HolyMoley ==
+`struct HolyMoley {
+ 1: list<OneOfEach> big,
+ 2: set<list<string>> contain,
+ 3: map<string, list<Bonk>> bonks,
+}
+`);
+}
+
+version (unittest) {
+ class ExceptionWithAMap : TException {
+ string blah;
+ string[string] map_field;
+
+ mixin TStructHelpers!([
+ TFieldMeta(`blah`, 1),
+ TFieldMeta(`map_field`, 2)
+ ]);
+ }
+
+ interface Srv {
+ void voidMethod();
+ int primitiveMethod();
+ OneOfEach structMethod();
+ void methodWithDefaultArgs(int something);
+ void onewayMethod();
+ void exceptionMethod();
+
+ alias .ExceptionWithAMap ExceptionWithAMap;
+
+ enum methodMeta = [
+ TMethodMeta(`methodWithDefaultArgs`,
+ [TParamMeta(`something`, 1, q{2})]
+ ),
+ TMethodMeta(`onewayMethod`,
+ [],
+ [],
+ TMethodType.ONEWAY
+ ),
+ TMethodMeta(`exceptionMethod`,
+ [],
+ [
+ TExceptionMeta("a", 1, "ExceptionWithAMap"),
+ TExceptionMeta("b", 2, "ExceptionWithAMap")
+ ]
+ )
+ ];
+ }
+
+ interface ChildSrv : Srv {
+ int childMethod(int arg);
+ }
+
+ static assert(idlString!ChildSrv ==
+`exception ExceptionWithAMap {
+ 1: string blah,
+ 2: map<string, string> map_field,
+}
+
+struct OneOfEach {
+ 1: bool im_true,
+ 2: bool im_false,
+ 3: byte a_bite = 127,
+ 4: i16 integer16 = 32767,
+ 5: i32 integer32,
+ 6: i64 integer64 = 10000000000,
+ 7: double double_precision,
+ 8: string some_characters,
+ 9: string zomg_unicode,
+ 10: bool what_who,
+ 11: string base64,
+ 12: list<byte> byte_list = [1, 2, 3, ],
+ 13: list<i16> i16_list = [1, 2, 3, ],
+ 14: list<i64> i64_list = [1, 2, 3, ],
+}
+
+service Srv {
+ void voidMethod(),
+ i32 primitiveMethod(),
+ OneOfEach structMethod(),
+ void methodWithDefaultArgs(1: i32 something = 2, ),
+ oneway void onewayMethod(),
+ void exceptionMethod() throws (1: ExceptionWithAMap a, 2: ExceptionWithAMap b, ),
+}
+
+service ChildSrv extends Srv {
+ i32 childMethod(-1: i32 param1, ),
+}
+`);
+}
diff --git a/lib/d/src/thrift/codegen/processor.d b/lib/d/src/thrift/codegen/processor.d
new file mode 100644
index 000000000..e6b77fad5
--- /dev/null
+++ b/lib/d/src/thrift/codegen/processor.d
@@ -0,0 +1,497 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.codegen.processor;
+
+import std.algorithm : find;
+import std.array : empty, front;
+import std.conv : to;
+import std.traits : ParameterTypeTuple, ReturnType, Unqual;
+import std.typetuple : allSatisfy, TypeTuple;
+import std.variant : Variant;
+import thrift.base;
+import thrift.codegen.base;
+import thrift.internal.codegen;
+import thrift.internal.ctfe;
+import thrift.protocol.base;
+import thrift.protocol.processor;
+
+/**
+ * Service processor for Interface, which implements TProcessor by
+ * synchronously forwarding requests for the service methods to a handler
+ * implementing Interface.
+ *
+ * The generated class implements TProcessor and additionally allows a
+ * TProcessorEventHandler to be specified via the public eventHandler property.
+ * The constructor takes a single argument of type Interface, which is the
+ * handler to forward the requests to:
+ * ---
+ * this(Interface iface);
+ * TProcessorEventHandler eventHandler;
+ * ---
+ *
+ * If Interface is derived from another service BaseInterface, this class is
+ * also derived from TServiceProcessor!BaseInterface.
+ *
+ * The optional Protocols template tuple parameter can be used to specify
+ * one or more TProtocol implementations to specifically generate code for. If
+ * the actual types of the protocols passed to process() at runtime match one
+ * of the items from the list, the optimized code paths are taken, otherwise,
+ * a generic TProtocol version is used as fallback. For cases where the input
+ * and output protocols differ, TProtocolPair!(InputProtocol, OutputProtocol)
+ * can be used in the Protocols list:
+ * ---
+ * interface FooService { void foo(); }
+ * class FooImpl { override void foo {} }
+ *
+ * // Provides fast path if TBinaryProtocol!TBufferedTransport is used for
+ * // both input and output:
+ * alias TServiceProcessor!(FooService, TBinaryProtocol!TBufferedTransport)
+ * BinaryProcessor;
+ *
+ * auto proc = new BinaryProcessor(new FooImpl());
+ *
+ * // Low overhead.
+ * proc.process(tBinaryProtocol(tBufferTransport(someSocket)));
+ *
+ * // Not in the specialization list – higher overhead.
+ * proc.process(tBinaryProtocol(tFramedTransport(someSocket)));
+ *
+ * // Same as above, but optimized for the Compact protocol backed by a
+ * // TPipedTransport for input and a TBufferedTransport for output.
+ * alias TServiceProcessor!(FooService, TProtocolPair!(
+ * TCompactProtocol!TPipedTransport, TCompactProtocol!TBufferedTransport)
+ * ) MixedProcessor;
+ * ---
+ */
+template TServiceProcessor(Interface, Protocols...) if (
+ isService!Interface && allSatisfy!(isTProtocolOrPair, Protocols)
+) {
+ mixin({
+ static if (is(Interface BaseInterfaces == super) && BaseInterfaces.length > 0) {
+ static assert(BaseInterfaces.length == 1,
+ "Services cannot be derived from more than one parent.");
+
+ string code = "class TServiceProcessor : " ~
+ "TServiceProcessor!(BaseService!Interface) {\n";
+ code ~= "private Interface iface_;\n";
+
+ string constructorCode = "this(Interface iface) {\n";
+ constructorCode ~= "super(iface);\n";
+ constructorCode ~= "iface_ = iface;\n";
+ } else {
+ string code = "class TServiceProcessor : TProcessor {";
+ code ~= q{
+ override bool process(TProtocol iprot, TProtocol oprot,
+ Variant context = Variant()
+ ) {
+ auto msg = iprot.readMessageBegin();
+
+ void writeException(TApplicationException e) {
+ oprot.writeMessageBegin(TMessage(msg.name, TMessageType.EXCEPTION,
+ msg.seqid));
+ e.write(oprot);
+ oprot.writeMessageEnd();
+ oprot.transport.writeEnd();
+ oprot.transport.flush();
+ }
+
+ if (msg.type != TMessageType.CALL && msg.type != TMessageType.ONEWAY) {
+ skip(iprot, TType.STRUCT);
+ iprot.readMessageEnd();
+ iprot.transport.readEnd();
+
+ writeException(new TApplicationException(
+ TApplicationException.Type.INVALID_MESSAGE_TYPE));
+ return false;
+ }
+
+ auto dg = msg.name in processMap_;
+ if (!dg) {
+ skip(iprot, TType.STRUCT);
+ iprot.readMessageEnd();
+ iprot.transport.readEnd();
+
+ writeException(new TApplicationException("Invalid method name: '" ~
+ msg.name ~ "'.", TApplicationException.Type.INVALID_MESSAGE_TYPE));
+
+ return false;
+ }
+
+ (*dg)(msg.seqid, iprot, oprot, context);
+ return true;
+ }
+
+ TProcessorEventHandler eventHandler;
+
+ alias void delegate(int, TProtocol, TProtocol, Variant) ProcessFunc;
+ protected ProcessFunc[string] processMap_;
+ private Interface iface_;
+ };
+
+ string constructorCode = "this(Interface iface) {\n";
+ constructorCode ~= "iface_ = iface;\n";
+ }
+
+ // Generate the handling code for each method, consisting of the dispatch
+ // function, registering it in the constructor, and the actual templated
+ // handler function.
+ foreach (methodName;
+ FilterMethodNames!(Interface, __traits(derivedMembers, Interface))
+ ) {
+ // Register the processing function in the constructor.
+ immutable procFuncName = "process_" ~ methodName;
+ immutable dispatchFuncName = procFuncName ~ "_protocolDispatch";
+ constructorCode ~= "processMap_[`" ~ methodName ~ "`] = &" ~
+ dispatchFuncName ~ ";\n";
+
+ bool methodMetaFound;
+ TMethodMeta methodMeta;
+ static if (is(typeof(Interface.methodMeta) : TMethodMeta[])) {
+ enum meta = find!`a.name == b`(Interface.methodMeta, methodName);
+ if (!meta.empty) {
+ methodMetaFound = true;
+ methodMeta = meta.front;
+ }
+ }
+
+ // The dispatch function to call the specialized handler functions. We
+ // test the protocols if they can be converted to one of the passed
+ // protocol types, and if not, fall back to the generic TProtocol
+ // version of the processing function.
+ code ~= "void " ~ dispatchFuncName ~
+ "(int seqid, TProtocol iprot, TProtocol oprot, Variant context) {\n";
+ code ~= "foreach (Protocol; TypeTuple!(Protocols, TProtocol)) {\n";
+ code ~= q{
+ static if (is(Protocol _ : TProtocolPair!(I, O), I, O)) {
+ alias I IProt;
+ alias O OProt;
+ } else {
+ alias Protocol IProt;
+ alias Protocol OProt;
+ }
+ auto castedIProt = cast(IProt)iprot;
+ auto castedOProt = cast(OProt)oprot;
+ };
+ code ~= "if (castedIProt && castedOProt) {\n";
+ code ~= procFuncName ~
+ "!(IProt, OProt)(seqid, castedIProt, castedOProt, context);\n";
+ code ~= "return;\n";
+ code ~= "}\n";
+ code ~= "}\n";
+ code ~= "throw new TException(`Internal error: Null iprot/oprot " ~
+ "passed to processor protocol dispatch function.`);\n";
+ code ~= "}\n";
+
+ // The actual handler function, templated on the input and output
+ // protocol types.
+ code ~= "void " ~ procFuncName ~ "(IProt, OProt)(int seqid, IProt " ~
+ "iprot, OProt oprot, Variant connectionContext) " ~
+ "if (isTProtocol!IProt && isTProtocol!OProt) {\n";
+ code ~= "TArgsStruct!(Interface, `" ~ methodName ~ "`) args;\n";
+
+ // Store the (qualified) method name in a manifest constant to avoid
+ // having to litter the code below with lots of string manipulation.
+ code ~= "enum methodName = `" ~ methodName ~ "`;\n";
+
+ code ~= q{
+ enum qName = Interface.stringof ~ "." ~ methodName;
+
+ Variant callContext;
+ if (eventHandler) {
+ callContext = eventHandler.createContext(qName, connectionContext);
+ }
+
+ scope (exit) {
+ if (eventHandler) {
+ eventHandler.deleteContext(callContext, qName);
+ }
+ }
+
+ if (eventHandler) eventHandler.preRead(callContext, qName);
+
+ args.read(iprot);
+ iprot.readMessageEnd();
+ iprot.transport.readEnd();
+
+ if (eventHandler) eventHandler.postRead(callContext, qName);
+ };
+
+ code ~= "TResultStruct!(Interface, `" ~ methodName ~ "`) result;\n";
+ code ~= "try {\n";
+
+ // Generate the parameter list to pass to the called iface function.
+ string[] paramList;
+ foreach (i, _; ParameterTypeTuple!(mixin("Interface." ~ methodName))) {
+ string paramName;
+ if (methodMetaFound && i < methodMeta.params.length) {
+ paramName = methodMeta.params[i].name;
+ } else {
+ paramName = "param" ~ to!string(i + 1);
+ }
+ paramList ~= "args." ~ paramName;
+ }
+
+ immutable call = "iface_." ~ methodName ~ "(" ~ ctfeJoin(paramList) ~ ")";
+ if (is(ReturnType!(mixin("Interface." ~ methodName)) == void)) {
+ code ~= call ~ ";\n";
+ } else {
+ code ~= "result.set!`success`(" ~ call ~ ");\n";
+ }
+
+ // If this is not a oneway method, generate the recieving code.
+ if (!methodMetaFound || methodMeta.type != TMethodType.ONEWAY) {
+ if (methodMetaFound) {
+ foreach (e; methodMeta.exceptions) {
+ code ~= "} catch (Interface." ~ e.type ~ " " ~ e.name ~ ") {\n";
+ code ~= "result.set!`" ~ e.name ~ "`(" ~ e.name ~ ");\n";
+ }
+ }
+ code ~= "}\n";
+
+ code ~= q{
+ catch (Exception e) {
+ if (eventHandler) {
+ eventHandler.handlerError(callContext, qName, e);
+ }
+
+ auto x = new TApplicationException(to!string(e));
+ oprot.writeMessageBegin(
+ TMessage(methodName, TMessageType.EXCEPTION, seqid));
+ x.write(oprot);
+ oprot.writeMessageEnd();
+ oprot.transport.writeEnd();
+ oprot.transport.flush();
+ return;
+ }
+
+ if (eventHandler) eventHandler.preWrite(callContext, qName);
+
+ oprot.writeMessageBegin(TMessage(methodName,
+ TMessageType.REPLY, seqid));
+ result.write(oprot);
+ oprot.writeMessageEnd();
+ oprot.transport.writeEnd();
+ oprot.transport.flush();
+
+ if (eventHandler) eventHandler.postWrite(callContext, qName);
+ };
+ } else {
+ // For oneway methods, we obviously cannot notify the client of any
+ // exceptions, just call the event handler if one is set.
+ code ~= "}\n";
+ code ~= q{
+ catch (Exception e) {
+ if (eventHandler) {
+ eventHandler.handlerError(callContext, qName, e);
+ }
+ return;
+ }
+
+ if (eventHandler) eventHandler.onewayComplete(callContext, qName);
+ };
+ }
+ code ~= "}\n";
+
+ }
+
+ code ~= constructorCode ~ "}\n";
+ code ~= "}\n";
+
+ return code;
+ }());
+}
+
+/**
+ * A struct representing the arguments of a Thrift method call.
+ *
+ * There should usually be no reason to use this directly without the help of
+ * TServiceProcessor, but it is documented publicly to help debugging in case
+ * of CTFE errors.
+ *
+ * Consider this example:
+ * ---
+ * interface Foo {
+ * int bar(string a, bool b);
+ *
+ * enum methodMeta = [
+ * TMethodMeta("bar", [TParamMeta("a", 1), TParamMeta("b", 2)])
+ * ];
+ * }
+ *
+ * alias TArgsStruct!(Foo, "bar") FooBarArgs;
+ * ---
+ *
+ * The definition of FooBarArgs is equivalent to:
+ * ---
+ * struct FooBarArgs {
+ * string a;
+ * bool b;
+ *
+ * mixin TStructHelpers!([TFieldMeta("a", 1, TReq.OPT_IN_REQ_OUT),
+ * TFieldMeta("b", 2, TReq.OPT_IN_REQ_OUT)]);
+ * }
+ * ---
+ *
+ * If the TVerboseCodegen version is defined, a warning message is issued at
+ * compilation if no TMethodMeta for Interface.methodName is found.
+ */
+template TArgsStruct(Interface, string methodName) {
+ static assert(is(typeof(mixin("Interface." ~ methodName))),
+ "Could not find method '" ~ methodName ~ "' in '" ~ Interface.stringof ~ "'.");
+ mixin({
+ bool methodMetaFound;
+ TMethodMeta methodMeta;
+ static if (is(typeof(Interface.methodMeta) : TMethodMeta[])) {
+ auto meta = find!`a.name == b`(Interface.methodMeta, methodName);
+ if (!meta.empty) {
+ methodMetaFound = true;
+ methodMeta = meta.front;
+ }
+ }
+
+ string memberCode;
+ string[] fieldMetaCodes;
+ foreach (i, _; ParameterTypeTuple!(mixin("Interface." ~ methodName))) {
+ // If we have no meta information, just use param1, param2, etc. as
+ // field names, it shouldn't really matter anyway. 1-based »indexing«
+ // is used to match the common scheme in the Thrift world.
+ string memberId;
+ string memberName;
+ if (methodMetaFound && i < methodMeta.params.length) {
+ memberId = to!string(methodMeta.params[i].id);
+ memberName = methodMeta.params[i].name;
+ } else {
+ memberId = to!string(i + 1);
+ memberName = "param" ~ to!string(i + 1);
+ }
+
+ // Unqual!() is needed to generate mutable fields for ref const()
+ // struct parameters.
+ memberCode ~= "Unqual!(ParameterTypeTuple!(Interface." ~ methodName ~
+ ")[" ~ to!string(i) ~ "])" ~ memberName ~ ";\n";
+
+ fieldMetaCodes ~= "TFieldMeta(`" ~ memberName ~ "`, " ~ memberId ~
+ ", TReq.OPT_IN_REQ_OUT)";
+ }
+
+ string code = "struct TArgsStruct {\n";
+ code ~= memberCode;
+ version (TVerboseCodegen) {
+ if (!methodMetaFound &&
+ ParameterTypeTuple!(mixin("Interface." ~ methodName)).length > 0)
+ {
+ code ~= "pragma(msg, `[thrift.codegen.processor.TArgsStruct] Warning: No " ~
+ "meta information for method '" ~ methodName ~ "' in service '" ~
+ Interface.stringof ~ "' found.`);\n";
+ }
+ }
+ immutable fieldMetaCode =
+ fieldMetaCodes.empty ? "" : "[" ~ ctfeJoin(fieldMetaCodes) ~ "]";
+ code ~= "mixin TStructHelpers!(" ~ fieldMetaCode ~ ");\n";
+ code ~= "}\n";
+ return code;
+ }());
+}
+
+/**
+ * A struct representing the result of a Thrift method call.
+ *
+ * It contains a field called "success" for the return value of the function
+ * (with id 0), and additional fields for the exceptions declared for the
+ * method, if any.
+ *
+ * There should usually be no reason to use this directly without the help of
+ * TServiceProcessor, but it is documented publicly to help debugging in case
+ * of CTFE errors.
+ *
+ * Consider the following example:
+ * ---
+ * interface Foo {
+ * int bar(string a);
+ *
+ * alias .FooException FooException;
+ *
+ * enum methodMeta = [
+ * TMethodMeta("bar",
+ * [TParamMeta("a", 1)],
+ * [TExceptionMeta("fooe", 1, "FooException")]
+ * )
+ * ];
+ * }
+ * alias TResultStruct!(Foo, "bar") FooBarResult;
+ * ---
+ *
+ * The definition of FooBarResult is equivalent to:
+ * ---
+ * struct FooBarResult {
+ * int success;
+ * FooException fooe;
+ *
+ * mixin(TStructHelpers!([TFieldMeta("success", 0, TReq.OPTIONAL),
+ * TFieldMeta("fooe", 1, TReq.OPTIONAL)]));
+ * }
+ * ---
+ *
+ * If the TVerboseCodegen version is defined, a warning message is issued at
+ * compilation if no TMethodMeta for Interface.methodName is found.
+ */
+template TResultStruct(Interface, string methodName) {
+ static assert(is(typeof(mixin("Interface." ~ methodName))),
+ "Could not find method '" ~ methodName ~ "' in '" ~ Interface.stringof ~ "'.");
+
+ mixin({
+ string code = "struct TResultStruct {\n";
+
+ string[] fieldMetaCodes;
+
+ static if (!is(ReturnType!(mixin("Interface." ~ methodName)) == void)) {
+ code ~= "ReturnType!(Interface." ~ methodName ~ ") success;\n";
+ fieldMetaCodes ~= "TFieldMeta(`success`, 0, TReq.OPTIONAL)";
+ }
+
+ bool methodMetaFound;
+ static if (is(typeof(Interface.methodMeta) : TMethodMeta[])) {
+ auto meta = find!`a.name == b`(Interface.methodMeta, methodName);
+ if (!meta.empty) {
+ foreach (e; meta.front.exceptions) {
+ code ~= "Interface." ~ e.type ~ " " ~ e.name ~ ";\n";
+ fieldMetaCodes ~= "TFieldMeta(`" ~ e.name ~ "`, " ~ to!string(e.id) ~
+ ", TReq.OPTIONAL)";
+ }
+ methodMetaFound = true;
+ }
+ }
+
+ version (TVerboseCodegen) {
+ if (!methodMetaFound &&
+ ParameterTypeTuple!(mixin("Interface." ~ methodName)).length > 0)
+ {
+ code ~= "pragma(msg, `[thrift.codegen.processor.TResultStruct] Warning: No " ~
+ "meta information for method '" ~ methodName ~ "' in service '" ~
+ Interface.stringof ~ "' found.`);\n";
+ }
+ }
+
+ immutable fieldMetaCode =
+ fieldMetaCodes.empty ? "" : "[" ~ ctfeJoin(fieldMetaCodes) ~ "]";
+ code ~= "mixin TStructHelpers!(" ~ fieldMetaCode ~ ");\n";
+ code ~= "}\n";
+ return code;
+ }());
+}
diff --git a/lib/d/src/thrift/index.d b/lib/d/src/thrift/index.d
new file mode 100644
index 000000000..12914b625
--- /dev/null
+++ b/lib/d/src/thrift/index.d
@@ -0,0 +1,33 @@
+Ddoc
+
+<h2>Package overview</h2>
+
+<dl>
+ <dt>$(D_CODE thrift.async)</dt>
+ <dd>Support infrastructure for handling client-side asynchronous operations using non-blocking I/O and coroutines.</dd>
+
+ <dt>$(D_CODE thrift.codegen)</dt>
+ <dd>
+ <p>Templates used for generating Thrift clients/processors from regular D struct and interface definitions.</p>
+ <p><strong>Note:</strong> Several artifacts in these modules have options for specifying the exact protocol types used. In this case, the amount of virtual calls can be greatly reduced and as a result, the code also can be optimized better. If performance is not a concern or the actual protocol type is not known at compile time, these parameters can just be left at their defaults.
+ </p>
+ </dd>
+
+ <dt>$(D_CODE thrift.internal)</dt>
+ <dd>Internal helper modules used by the Thrift library. This package is not part of the public API, and no stability guarantees are given whatsoever.</dd>
+
+ <dt>$(D_CODE thrift.protocol)</dt>
+ <dd>The Thrift protocol implemtations which specify how to pass messages over a TTransport.</dd>
+
+ <dt>$(D_CODE thrift.server)</dt>
+ <dd>Generic Thrift server implementations handling clients over a TTransport interface and forwarding requests to a TProcessor (which is in turn usually provided by thrift.codegen).</dd>
+
+ <dt>$(D_CODE thrift.transport)</dt>
+ <dd>The TTransport data source/sink interface used in the Thrift library and its imiplementations.</dd>
+
+ <dt>$(D_CODE thrift.util)</dt>
+ <dd>General-purpose utility modules not specific to Thrift, part of the public API.</dd>
+</dl>
+
+Macros:
+ TITLE = Thrift D Software Library
diff --git a/lib/d/src/thrift/internal/algorithm.d b/lib/d/src/thrift/internal/algorithm.d
new file mode 100644
index 000000000..0938ac269
--- /dev/null
+++ b/lib/d/src/thrift/internal/algorithm.d
@@ -0,0 +1,55 @@
+/**
+ * Contains a modified version of std.algorithm.remove that doesn't take an
+ * alias parameter to avoid DMD @@BUG6395@@.
+ */
+module thrift.internal.algorithm;
+
+import std.algorithm : move;
+import std.exception;
+import std.functional;
+import std.range;
+import std.traits;
+
+enum SwapStrategy
+{
+ unstable,
+ semistable,
+ stable,
+}
+
+Range removeEqual(SwapStrategy s = SwapStrategy.stable, Range, E)(Range range, E e)
+if (isBidirectionalRange!Range)
+{
+ auto result = range;
+ static if (s != SwapStrategy.stable)
+ {
+ for (;!range.empty;)
+ {
+ if (range.front !is e)
+ {
+ range.popFront;
+ continue;
+ }
+ move(range.back, range.front);
+ range.popBack;
+ result.popBack;
+ }
+ }
+ else
+ {
+ auto tgt = range;
+ for (; !range.empty; range.popFront)
+ {
+ if (range.front is e)
+ {
+ // yank this guy
+ result.popBack;
+ continue;
+ }
+ // keep this guy
+ move(range.front, tgt.front);
+ tgt.popFront;
+ }
+ }
+ return result;
+}
diff --git a/lib/d/src/thrift/internal/codegen.d b/lib/d/src/thrift/internal/codegen.d
new file mode 100644
index 000000000..d6ce0a90e
--- /dev/null
+++ b/lib/d/src/thrift/internal/codegen.d
@@ -0,0 +1,438 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module thrift.internal.codegen;
+
+import std.traits : InterfacesTuple, isSomeFunction, isSomeString;
+import std.typetuple : staticIndexOf, staticMap, NoDuplicates, TypeTuple;
+import thrift.codegen.base;
+
+/**
+ * Removes all type qualifiers from T.
+ *
+ * In contrast to std.traits.Unqual, FullyUnqual also removes qualifiers from
+ * array elements (e.g. immutable(byte[]) -> byte[], not immutable(byte)[]),
+ * excluding strings (string isn't reduced to char[]).
+ */
+template FullyUnqual(T) {
+ static if (is(T _ == const(U), U)) {
+ alias FullyUnqual!U FullyUnqual;
+ } else static if (is(T _ == immutable(U), U)) {
+ alias FullyUnqual!U FullyUnqual;
+ } else static if (is(T _ == shared(U), U)) {
+ alias FullyUnqual!U FullyUnqual;
+ } else static if (is(T _ == U[], U) && !isSomeString!T) {
+ alias FullyUnqual!(U)[] FullyUnqual;
+ } else static if (is(T _ == V[K], K, V)) {
+ alias FullyUnqual!(V)[FullyUnqual!K] FullyUnqual;
+ } else {
+ alias T FullyUnqual;
+ }
+}
+
+/**
+ * true if null can be assigned to the passed type, false if not.
+ */
+template isNullable(T) {
+ enum isNullable = __traits(compiles, { T t = null; });
+}
+
+template isStruct(T) {
+ enum isStruct = is(T == struct);
+}
+
+template isException(T) {
+ enum isException = is(T : Exception);
+}
+
+template isEnum(T) {
+ enum isEnum = is(T == enum);
+}
+
+/**
+ * Aliases itself to T.name.
+ */
+template GetMember(T, string name) {
+ mixin("alias T." ~ name ~ " GetMember;");
+}
+
+/**
+ * Aliases itself to typeof(symbol).
+ */
+template TypeOf(alias symbol) {
+ alias typeof(symbol) TypeOf;
+}
+
+/**
+ * Aliases itself to the type of the T member called name.
+ */
+alias Compose!(TypeOf, GetMember) MemberType;
+
+/**
+ * Returns the field metadata array for T if any, or an empty array otherwise.
+ */
+template getFieldMeta(T) if (isStruct!T || isException!T) {
+ static if (is(typeof(T.fieldMeta) == TFieldMeta[])) {
+ enum getFieldMeta = T.fieldMeta;
+ } else {
+ enum TFieldMeta[] getFieldMeta = [];
+ }
+}
+
+/**
+ * Merges the field metadata array for D with the passed array.
+ */
+template mergeFieldMeta(T, alias fieldMetaData = cast(TFieldMeta[])null) {
+ // Note: We don't use getFieldMeta here to avoid bug if it is instantiated
+ // from TIsSetFlags, see comment there.
+ static if (is(typeof(T.fieldMeta) == TFieldMeta[])) {
+ enum mergeFieldMeta = T.fieldMeta ~ fieldMetaData;
+ } else {
+ enum TFieldMeta[] mergeFieldMeta = fieldMetaData;
+ }
+}
+
+/**
+ * Returns the field requirement level for T.name.
+ */
+template memberReq(T, string name, alias fieldMetaData = cast(TFieldMeta[])null) {
+ enum memberReq = memberReqImpl!(T, name, fieldMetaData).result;
+}
+
+private {
+ import std.algorithm : find;
+ // DMD @@BUG@@: Missing import leads to failing build without error
+ // message in unittest/debug/thrift/codegen/async_client.
+ import std.array : empty, front;
+
+ template memberReqImpl(T, string name, alias fieldMetaData) {
+ enum meta = find!`a.name == b`(mergeFieldMeta!(T, fieldMetaData), name);
+ static if (meta.empty || meta.front.req == TReq.AUTO) {
+ static if (isNullable!(MemberType!(T, name))) {
+ enum result = TReq.OPTIONAL;
+ } else {
+ enum result = TReq.REQUIRED;
+ }
+ } else {
+ enum result = meta.front.req;
+ }
+ }
+}
+
+
+template notIgnored(T, string name, alias fieldMetaData = cast(TFieldMeta[])null) {
+ enum notIgnored = memberReq!(T, name, fieldMetaData) != TReq.IGNORE;
+}
+
+/**
+ * Returns the method metadata array for T if any, or an empty array otherwise.
+ */
+template getMethodMeta(T) if (isService!T) {
+ static if (is(typeof(T.methodMeta) == TMethodMeta[])) {
+ enum getMethodMeta = T.methodMeta;
+ } else {
+ enum TMethodMeta[] getMethodMeta = [];
+ }
+}
+
+
+/**
+ * true if T.name is a member variable. Exceptions include methods, static
+ * members, artifacts like package aliases, …
+ */
+template isValueMember(T, string name) {
+ static if (!is(MemberType!(T, name))) {
+ enum isValueMember = false;
+ } else static if (
+ is(MemberType!(T, name) == void) ||
+ isSomeFunction!(MemberType!(T, name)) ||
+ __traits(compiles, { return mixin("T." ~ name); }())
+ ) {
+ enum isValueMember = false;
+ } else {
+ enum isValueMember = true;
+ }
+}
+
+/**
+ * Returns a tuple containing the names of the fields of T, not including
+ * inherited fields. If a member is marked as TReq.IGNORE, it is not included
+ * as well.
+ */
+template FieldNames(T, alias fieldMetaData = cast(TFieldMeta[])null) {
+ alias StaticFilter!(
+ All!(
+ PApply!(isValueMember, T),
+ PApply!(notIgnored, T, PApplySkip, fieldMetaData)
+ ),
+ __traits(derivedMembers, T)
+ ) FieldNames;
+}
+
+template derivedMembers(T) {
+ alias TypeTuple!(__traits(derivedMembers, T)) derivedMembers;
+}
+
+template AllMemberMethodNames(T) if (isService!T) {
+ alias NoDuplicates!(
+ FilterMethodNames!(
+ T,
+ staticMap!(
+ derivedMembers,
+ TypeTuple!(T, InterfacesTuple!T)
+ )
+ )
+ ) AllMemberMethodNames;
+}
+
+private template FilterMethodNames(T, MemberNames...) {
+ alias StaticFilter!(
+ CompilesAndTrue!(
+ Compose!(isSomeFunction, TypeOf, PApply!(GetMember, T))
+ ),
+ MemberNames
+ ) FilterMethodNames;
+}
+
+/**
+ * Returns a type tuple containing only the elements of T for which the
+ * eponymous template predicate pred is true.
+ *
+ * Example:
+ * ---
+ * alias StaticFilter!(isIntegral, int, string, long, float[]) Filtered;
+ * static assert(is(Filtered == TypeTuple!(int, long)));
+ * ---
+ */
+template StaticFilter(alias pred, T...) {
+ static if (T.length == 0) {
+ alias TypeTuple!() StaticFilter;
+ } else static if (pred!(T[0])) {
+ alias TypeTuple!(T[0], StaticFilter!(pred, T[1 .. $])) StaticFilter;
+ } else {
+ alias StaticFilter!(pred, T[1 .. $]) StaticFilter;
+ }
+}
+
+/**
+ * Binds the first n arguments of a template to a particular value (where n is
+ * the number of arguments passed to PApply).
+ *
+ * The passed arguments are always applied starting from the left. However,
+ * the special PApplySkip marker template can be used to indicate that an
+ * argument should be skipped, so that e.g. the first and third argument
+ * to a template can be fixed, but the second and remaining arguments would
+ * still be left undefined.
+ *
+ * Skipping a number of parameters, but not providing enough arguments to
+ * assign all of them during instantiation of the resulting template is an
+ * error.
+ *
+ * Example:
+ * ---
+ * struct Foo(T, U, V) {}
+ * alias PApply!(Foo, int, long) PartialFoo;
+ * static assert(is(PartialFoo!float == Foo!(int, long, float)));
+ *
+ * alias PApply!(Test, int, PApplySkip, float) SkippedTest;
+ * static assert(is(SkippedTest!long == Test!(int, long, float)));
+ * ---
+ */
+template PApply(alias Target, T...) {
+ template PApply(U...) {
+ alias Target!(PApplyMergeArgs!(ConfinedTuple!T, U).Result) PApply;
+ }
+}
+
+/// Ditto.
+template PApplySkip() {}
+
+private template PApplyMergeArgs(alias Preset, Args...) {
+ static if (Preset.length == 0) {
+ alias Args Result;
+ } else {
+ enum nextSkip = staticIndexOf!(PApplySkip, Preset.Tuple);
+ static if (nextSkip == -1) {
+ alias TypeTuple!(Preset.Tuple, Args) Result;
+ } else static if (Args.length == 0) {
+ // Have to use a static if clause instead of putting the condition
+ // directly into the assert to avoid DMD trying to access Args[0]
+ // nevertheless below.
+ static assert(false,
+ "PArgsSkip encountered, but no argument left to bind.");
+ } else {
+ alias TypeTuple!(
+ Preset.Tuple[0 .. nextSkip],
+ Args[0],
+ PApplyMergeArgs!(
+ ConfinedTuple!(Preset.Tuple[nextSkip + 1 .. $]),
+ Args[1 .. $]
+ ).Result
+ ) Result;
+ }
+ }
+}
+
+unittest {
+ struct Test(T, U, V) {}
+ alias PApply!(Test, int, long) PartialTest;
+ static assert(is(PartialTest!float == Test!(int, long, float)));
+
+ alias PApply!(Test, int, PApplySkip, float) SkippedTest;
+ static assert(is(SkippedTest!long == Test!(int, long, float)));
+
+ alias PApply!(Test, int, PApplySkip, PApplySkip) TwoSkipped;
+ static assert(!__traits(compiles, TwoSkipped!long));
+}
+
+
+/**
+ * Composes a number of templates. The result is a template equivalent to
+ * all the passed templates evaluated from right to left, akin to the
+ * mathematical function composition notation: Instantiating Compose!(A, B, C)
+ * is the same as instantiating A!(B!(C!(…))).
+ *
+ * This is especially useful for creating a template to use with staticMap/
+ * StaticFilter, as demonstrated below.
+ *
+ * Example:
+ * ---
+ * template AllMethodNames(T) {
+ * alias StaticFilter!(
+ * CompilesAndTrue!(
+ * Compose!(isSomeFunction, TypeOf, PApply!(GetMember, T))
+ * ),
+ * __traits(allMembers, T)
+ * ) AllMethodNames;
+ * }
+ *
+ * pragma(msg, AllMethodNames!Object);
+ * ---
+ */
+template Compose(T...) {
+ static if (T.length == 0) {
+ template Compose(U...) {
+ alias U Compose;
+ }
+ } else {
+ template Compose(U...) {
+ alias Instantiate!(T[0], Instantiate!(.Compose!(T[1 .. $]), U)) Compose;
+ }
+ }
+}
+
+/**
+ * Instantiates the given template with the given list of parameters.
+ *
+ * Used to work around syntactic limiations of D with regard to instantiating
+ * a template from a type tuple (e.g. T[0]!(...) is not valid) or a template
+ * returning another template (e.g. Foo!(Bar)!(Baz) is not allowed).
+ */
+template Instantiate(alias Template, Params...) {
+ alias Template!Params Instantiate;
+}
+
+/**
+ * Combines several template predicates using logical AND, i.e. instantiating
+ * All!(a, b, c) with parameters P for some templates a, b, c is equivalent to
+ * a!P && b!P && c!P.
+ *
+ * The templates are evaluated from left to right, aborting evaluation in a
+ * shurt-cut manner if a false result is encountered, in which case the latter
+ * instantiations do not need to compile.
+ */
+template All(T...) {
+ static if (T.length == 0) {
+ template All(U...) {
+ enum All = true;
+ }
+ } else {
+ template All(U...) {
+ static if (Instantiate!(T[0], U)) {
+ alias Instantiate!(.All!(T[1 .. $]), U) All;
+ } else {
+ enum All = false;
+ }
+ }
+ }
+}
+
+/**
+ * Combines several template predicates using logical OR, i.e. instantiating
+ * Any!(a, b, c) with parameters P for some templates a, b, c is equivalent to
+ * a!P || b!P || c!P.
+ *
+ * The templates are evaluated from left to right, aborting evaluation in a
+ * shurt-cut manner if a true result is encountered, in which case the latter
+ * instantiations do not need to compile.
+ */
+template Any(T...) {
+ static if (T.length == 0) {
+ template Any(U...) {
+ enum Any = false;
+ }
+ } else {
+ template Any(U...) {
+ static if (Instantiate!(T[0], U)) {
+ enum Any = true;
+ } else {
+ alias Instantiate!(.Any!(T[1 .. $]), U) Any;
+ }
+ }
+ }
+}
+
+template ConfinedTuple(T...) {
+ alias T Tuple;
+ enum length = T.length;
+}
+
+/*
+ * foreach (Item; Items) {
+ * List = Operator!(Item, List);
+ * }
+ * where Items is a ConfinedTuple and List is a type tuple.
+ */
+template ForAllWithList(alias Items, alias Operator, List...) if (
+ is(typeof(Items.length) : size_t)
+){
+ static if (Items.length == 0) {
+ alias List ForAllWithList;
+ } else {
+ alias .ForAllWithList!(
+ ConfinedTuple!(Items.Tuple[1 .. $]),
+ Operator,
+ Operator!(Items.Tuple[0], List)
+ ) ForAllWithList;
+ }
+}
+
+/**
+ * Wraps the passed template predicate so it returns true if it compiles and
+ * evaluates to true, false it it doesn't compile or evaluates to false.
+ */
+template CompilesAndTrue(alias T) {
+ template CompilesAndTrue(U...) {
+ static if (is(typeof(T!U) : bool)) {
+ enum bool CompilesAndTrue = T!U;
+ } else {
+ enum bool CompilesAndTrue = false;
+ }
+ }
+}
diff --git a/lib/d/src/thrift/internal/ctfe.d b/lib/d/src/thrift/internal/ctfe.d
new file mode 100644
index 000000000..3b10a787b
--- /dev/null
+++ b/lib/d/src/thrift/internal/ctfe.d
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module thrift.internal.ctfe;
+
+import std.conv : to;
+import std.traits;
+
+/*
+ * Simple eager join() for strings, std.algorithm.join isn't CTFEable yet.
+ */
+string ctfeJoin(string[] strings, string separator = ", ") {
+ string result;
+ if (strings.length > 0) {
+ result ~= strings[0];
+ foreach (s; strings[1..$]) {
+ result ~= separator ~ s;
+ }
+ }
+ return result;
+}
+
+/*
+ * A very primitive to!string() implementation for floating point numbers that
+ * is evaluatable at compile time.
+ *
+ * There is a wealth of problems associated with the algorithm used (e.g. 5.0
+ * prints as 4.999…, incorrect rounding, etc.), but a better alternative should
+ * be included with the D standard library instead of implementing it here.
+ */
+string ctfeToString(T)(T val) if (isFloatingPoint!T) {
+ if (val is T.nan) return "nan";
+ if (val is T.infinity) return "inf";
+ if (val is -T.infinity) return "-inf";
+ if (val is 0.0) return "0";
+ if (val is -0.0) return "-0";
+
+ auto b = val;
+
+ string result;
+ if (b < 0) {
+ result ~= '-';
+ b *= -1;
+ }
+
+ short magnitude;
+ while (b >= 10) {
+ ++magnitude;
+ b /= 10;
+ }
+ while (b < 1) {
+ --magnitude;
+ b *= 10;
+ }
+
+ foreach (i; 0 .. T.dig) {
+ if (i == 1) result ~= '.';
+
+ auto first = cast(ubyte)b;
+ result ~= to!string(first);
+
+ b -= first;
+ import std.math;
+ if (b < pow(10.0, i - T.dig)) break;
+ b *= 10;
+ }
+
+ if (magnitude != 0) result ~= "e" ~ to!string(magnitude);
+ return result;
+}
+
+unittest {
+ static assert(ctfeToString(double.infinity) == "inf");
+ static assert(ctfeToString(-double.infinity) == "-inf");
+ static assert(ctfeToString(double.nan) == "nan");
+ static assert(ctfeToString(0.0) == "0");
+ static assert(ctfeToString(-0.0) == "-0");
+ static assert(ctfeToString(3.1415) == "3.1415");
+ static assert(ctfeToString(2e-200) == "2e-200");
+}
diff --git a/lib/d/src/thrift/internal/endian.d b/lib/d/src/thrift/internal/endian.d
new file mode 100644
index 000000000..31b9814ef
--- /dev/null
+++ b/lib/d/src/thrift/internal/endian.d
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+ * Simple helpers for handling typical byte order-related issues.
+ */
+module thrift.internal.endian;
+
+import core.bitop : bswap;
+import std.traits : isIntegral;
+
+union IntBuf(T) {
+ ubyte[T.sizeof] bytes;
+ T value;
+}
+
+T byteSwap(T)(T t) pure nothrow @trusted if (isIntegral!T) {
+ static if (T.sizeof == 2) {
+ return cast(T)((t & 0xff) << 8) | cast(T)((t & 0xff00) >> 8);
+ } else static if (T.sizeof == 4) {
+ return cast(T)bswap(cast(uint)t);
+ } else static if (T.sizeof == 8) {
+ return cast(T)byteSwap(cast(uint)(t & 0xffffffff)) << 32 |
+ cast(T)bswap(cast(uint)(t >> 32));
+ } else static assert(false, "Type of size " ~ to!string(T.sizeof) ~ " not supported.");
+}
+
+T doNothing(T)(T val) { return val; }
+
+version (BigEndian) {
+ alias doNothing hostToNet;
+ alias doNothing netToHost;
+ alias byteSwap hostToLe;
+ alias byteSwap leToHost;
+} else {
+ alias byteSwap hostToNet;
+ alias byteSwap netToHost;
+ alias doNothing hostToLe;
+ alias doNothing leToHost;
+}
+
+unittest {
+ import std.exception;
+
+ IntBuf!short s;
+ s.bytes = [1, 2];
+ s.value = byteSwap(s.value);
+ enforce(s.bytes == [2, 1]);
+
+ IntBuf!int i;
+ i.bytes = [1, 2, 3, 4];
+ i.value = byteSwap(i.value);
+ enforce(i.bytes == [4, 3, 2, 1]);
+
+ IntBuf!long l;
+ l.bytes = [1, 2, 3, 4, 5, 6, 7, 8];
+ l.value = byteSwap(l.value);
+ enforce(l.bytes == [8, 7, 6, 5, 4, 3, 2, 1]);
+}
diff --git a/lib/d/src/thrift/internal/resource_pool.d b/lib/d/src/thrift/internal/resource_pool.d
new file mode 100644
index 000000000..8bd6ac56e
--- /dev/null
+++ b/lib/d/src/thrift/internal/resource_pool.d
@@ -0,0 +1,419 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.internal.resource_pool;
+
+import core.time : Duration, dur, TickDuration;
+import std.algorithm : minPos, reduce, remove;
+import std.array : array, empty;
+import std.exception : enforce;
+import std.conv : to;
+import std.random : randomCover, rndGen;
+import std.range : zip;
+import thrift.internal.algorithm : removeEqual;
+
+/**
+ * A pool of resources, which can be iterated over, and where resources that
+ * have failed too often can be temporarily disabled.
+ *
+ * This class is oblivious to the actual resource type managed.
+ */
+final class TResourcePool(Resource) {
+ /**
+ * Constructs a new instance.
+ *
+ * Params:
+ * resources = The initial members of the pool.
+ */
+ this(Resource[] resources) {
+ resources_ = resources;
+ }
+
+ /**
+ * Adds a resource to the pool.
+ */
+ void add(Resource resource) {
+ resources_ ~= resource;
+ }
+
+ /**
+ * Removes a resource from the pool.
+ *
+ * Returns: Whether the resource could be found in the pool.
+ */
+ bool remove(Resource resource) {
+ auto oldLength = resources_.length;
+ resources_ = removeEqual(resources_, resource);
+ return resources_.length < oldLength;
+ }
+
+ /**
+ * Returns an »enriched« input range to iterate over the pool members.
+ */
+ struct Range {
+ /**
+ * Whether the range is empty.
+ *
+ * This is the case if all members of the pool have been popped (or skipped
+ * because they were disabled) and TResourcePool.cycle is false, or there
+ * is no element to return in cycle mode because all have been temporarily
+ * disabled.
+ */
+ bool empty() @property {
+ // If no resources are in the pool, the range will never become non-empty.
+ if (resources_.empty) return true;
+
+ // If we already got the next resource in the cache, it doesn't matter
+ // whether there are more.
+ if (cached_) return false;
+
+ size_t examineCount;
+ if (parent_.cycle) {
+ // We want to check all the resources, but not iterate more than once
+ // to avoid spinning in a loop if nothing is available.
+ examineCount = resources_.length;
+ } else {
+ // When not in cycle mode, we just iterate the list exactly once. If all
+ // items have been consumed, the interval below is empty.
+ examineCount = resources_.length - nextIndex_;
+ }
+
+ foreach (i; 0 .. examineCount) {
+ auto r = resources_[(nextIndex_ + i) % resources_.length];
+ auto fi = r in parent_.faultInfos_;
+
+ if (fi && fi.resetTime != fi.resetTime.init) {
+ // The argument to < needs to be an lvalue…
+ auto currentTick = TickDuration.currSystemTick;
+ if (fi.resetTime < currentTick) {
+ // The timeout expired, remove the resource from the list and go
+ // ahead trying it.
+ parent_.faultInfos_.remove(r);
+ } else {
+ // The timeout didn't expire yet, try the next resource.
+ continue;
+ }
+ }
+
+ cache_ = r;
+ cached_ = true;
+ nextIndex_ = nextIndex_ + i + 1;
+ return false;
+ }
+
+ // If we get here, all resources are currently inactive or the non-cycle
+ // pool has been exhausted, so there is nothing we can do.
+ nextIndex_ = nextIndex_ + examineCount;
+ return true;
+ }
+
+ /**
+ * Returns the first resource in the range.
+ */
+ Resource front() @property {
+ enforce(!empty);
+ return cache_;
+ }
+
+ /**
+ * Removes the first resource from the range.
+ *
+ * Usually, this is combined with a call to TResourcePool.recordSuccess()
+ * or recordFault().
+ */
+ void popFront() {
+ enforce(!empty);
+ cached_ = false;
+ }
+
+ /**
+ * Returns whether the range will become non-empty at some point in the
+ * future, and provides additional information when this will happen and
+ * what will be the next resource.
+ *
+ * Makes only sense to call on empty ranges.
+ *
+ * Params:
+ * next = The next resource that will become available.
+ * waitTime = The duration until that resource will become available.
+ */
+ bool willBecomeNonempty(out Resource next, out Duration waitTime) {
+ // If no resources are in the pool, the range will never become non-empty.
+ if (resources_.empty) return true;
+
+ // If cycle mode is not enabled, a range never becomes non-empty after
+ // being empty once, because all the elements have already been
+ // used/skipped in order to become empty.
+ if (!parent_.cycle) return false;
+
+ auto fi = parent_.faultInfos_;
+ auto nextPair = minPos!"a[1].resetTime < b[1].resetTime"(
+ zip(fi.keys, fi.values)
+ ).front;
+
+ next = nextPair[0];
+ waitTime = to!Duration(nextPair[1].resetTime - TickDuration.currSystemTick);
+
+ return true;
+ }
+
+ private:
+ this(TResourcePool parent, Resource[] resources) {
+ parent_ = parent;
+ resources_ = resources;
+ }
+
+ TResourcePool parent_;
+
+ /// All available resources. We keep a copy of it as to not get confused
+ /// when resources are added to/removed from the parent pool.
+ Resource[] resources_;
+
+ /// After we have determined the next element in empty(), we store it here.
+ Resource cache_;
+
+ /// Whether there is currently something in the cache.
+ bool cached_;
+
+ /// The index to start searching from at the next call to empty().
+ size_t nextIndex_;
+ }
+
+ /// Ditto
+ Range opSlice() {
+ auto res = resources_;
+ if (permute) {
+ res = array(randomCover(res, rndGen));
+ }
+ return Range(this, res);
+ }
+
+ /**
+ * Records a success for an operation on the given resource, cancelling a
+ * fault streak, if any.
+ */
+ void recordSuccess(Resource resource) {
+ if (resource in faultInfos_) {
+ faultInfos_.remove(resource);
+ }
+ }
+
+ /**
+ * Records a fault for the given resource.
+ *
+ * If a resource fails consecutively for more than faultDisableCount times,
+ * it is temporarily disabled (no longer considered) until
+ * faultDisableDuration has passed.
+ */
+ void recordFault(Resource resource) {
+ auto fi = resource in faultInfos_;
+
+ if (!fi) {
+ faultInfos_[resource] = FaultInfo();
+ fi = resource in faultInfos_;
+ }
+
+ ++fi.count;
+ if (fi.count >= faultDisableCount) {
+ // If the resource has hit the fault count limit, disable it for
+ // specified duration.
+ fi.resetTime = TickDuration.currSystemTick +
+ TickDuration.from!"hnsecs"(faultDisableDuration.total!"hnsecs");
+ }
+ }
+
+ /**
+ * Whether to randomly permute the order of the resources in the pool when
+ * taking a range using opSlice().
+ *
+ * This can be used e.g. as a simple form of load balancing.
+ */
+ bool permute = true;
+
+ /**
+ * Whether to keep iterating over the pool members after all have been
+ * returned/have failed once.
+ */
+ bool cycle = false;
+
+ /**
+ * The number of consecutive faults after which a resource is disabled until
+ * faultDisableDuration has passed. Zero to never disable resources.
+ *
+ * Defaults to zero.
+ */
+ ushort faultDisableCount = 0;
+
+ /**
+ * The duration for which a resource is no longer considered after it has
+ * failed too often.
+ *
+ * Defaults to one second.
+ */
+ Duration faultDisableDuration = dur!"seconds"(1);
+
+private:
+ Resource[] resources_;
+ FaultInfo[Resource] faultInfos_;
+}
+
+private {
+ struct FaultInfo {
+ ushort count;
+ TickDuration resetTime;
+ }
+}
+
+import std.datetime;
+import thrift.base;
+
+unittest {
+ import core.thread;
+
+ auto a = new Object;
+ auto b = new Object;
+ auto c = new Object;
+ auto objs = [a, b, c];
+ auto pool = new TResourcePool!Object(objs);
+ pool.permute = false;
+ pool.faultDisableDuration = dur!"msecs"(5);
+ Object dummyRes = void;
+ Duration dummyDur = void;
+
+ {
+ auto r = pool[];
+
+ foreach (i, o; objs) {
+ enforce(!r.empty);
+ enforce(r.front == o);
+ r.popFront();
+ }
+
+ enforce(r.empty);
+ enforce(!r.willBecomeNonempty(dummyRes, dummyDur));
+ }
+
+ {
+ pool.faultDisableCount = 2;
+
+ enforce(pool[].front == a);
+ pool.recordFault(a);
+ enforce(pool[].front == a);
+ pool.recordSuccess(a);
+ enforce(pool[].front == a);
+ pool.recordFault(a);
+ enforce(pool[].front == a);
+ pool.recordFault(a);
+
+ auto r = pool[];
+ enforce(r.front == b);
+ r.popFront();
+ enforce(r.front == c);
+ r.popFront();
+ enforce(r.empty);
+ enforce(!r.willBecomeNonempty(dummyRes, dummyDur));
+
+ Thread.sleep(dur!"msecs"(5));
+ // Not in cycle mode, has to be still empty after the timeouts expired.
+ enforce(r.empty);
+ enforce(!r.willBecomeNonempty(dummyRes, dummyDur));
+
+ foreach (o; objs) pool.recordSuccess(o);
+ }
+
+ {
+ pool.faultDisableCount = 1;
+
+ pool.recordFault(a);
+ Thread.sleep(dur!"usecs"(1));
+ pool.recordFault(b);
+ Thread.sleep(dur!"usecs"(1));
+ pool.recordFault(c);
+
+ auto r = pool[];
+ enforce(r.empty);
+ enforce(!r.willBecomeNonempty(dummyRes, dummyDur));
+
+ foreach (o; objs) pool.recordSuccess(o);
+ }
+
+ pool.cycle = true;
+
+ {
+ auto r = pool[];
+
+ foreach (o; objs ~ objs) {
+ enforce(!r.empty);
+ enforce(r.front == o);
+ r.popFront();
+ }
+ }
+
+ {
+ pool.faultDisableCount = 2;
+
+ enforce(pool[].front == a);
+ pool.recordFault(a);
+ enforce(pool[].front == a);
+ pool.recordSuccess(a);
+ enforce(pool[].front == a);
+ pool.recordFault(a);
+ enforce(pool[].front == a);
+ pool.recordFault(a);
+
+ auto r = pool[];
+ enforce(r.front == b);
+ r.popFront();
+ enforce(r.front == c);
+ r.popFront();
+ enforce(r.front == b);
+
+ Thread.sleep(dur!"msecs"(5));
+
+ r.popFront();
+ enforce(r.front == c);
+
+ r.popFront();
+ enforce(r.front == a);
+
+ enforce(pool[].front == a);
+
+ foreach (o; objs) pool.recordSuccess(o);
+ }
+
+ {
+ pool.faultDisableCount = 1;
+
+ pool.recordFault(a);
+ Thread.sleep(dur!"usecs"(1));
+ pool.recordFault(b);
+ Thread.sleep(dur!"usecs"(1));
+ pool.recordFault(c);
+
+ auto r = pool[];
+ enforce(r.empty);
+
+ Object nextRes;
+ Duration nextWait;
+ enforce(r.willBecomeNonempty(nextRes, nextWait));
+ enforce(nextRes == a);
+ enforce(nextWait > dur!"hnsecs"(0));
+
+ foreach (o; objs) pool.recordSuccess(o);
+ }
+}
diff --git a/lib/d/src/thrift/internal/socket.d b/lib/d/src/thrift/internal/socket.d
new file mode 100644
index 000000000..43e1ca84f
--- /dev/null
+++ b/lib/d/src/thrift/internal/socket.d
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Abstractions over OS-dependent socket functionality.
+ */
+module thrift.internal.socket;
+
+import std.conv : to;
+
+// FreeBSD and OS X return -1 and set ECONNRESET if socket was closed by
+// the other side, we need to check for that before throwing an exception.
+version (FreeBSD) {
+ enum connresetOnPeerShutdown = true;
+} else version (OSX) {
+ enum connresetOnPeerShutdown = true;
+} else {
+ enum connresetOnPeerShutdown = false;
+}
+
+version (Win32) {
+ import std.c.windows.winsock : WSAGetLastError, WSAEINTR, WSAEWOULDBLOCK;
+ import std.windows.syserror : sysErrorString;
+
+ // These are unfortunately not defined in std.c.windows.winsock, see
+ // http://msdn.microsoft.com/en-us/library/ms740668.aspx.
+ enum WSAECONNRESET = 10054;
+ enum WSAENOTCONN = 10057;
+ enum WSAETIMEDOUT = 10060;
+} else {
+ import core.stdc.errno : getErrno, EAGAIN, ECONNRESET, EINPROGRESS, EINTR,
+ ENOTCONN, EPIPE;
+ import core.stdc.string : strerror;
+}
+
+/*
+ * CONNECT_INPROGRESS_ERRNO: set by connect() for non-blocking sockets if the
+ * connection could not be immediately established.
+ * INTERRUPTED_ERRNO: set when blocking system calls are interrupted by
+ * signals or similar.
+ * TIMEOUT_ERRNO: set when a socket timeout has been exceeded.
+ * WOULD_BLOCK_ERRNO: set when send/recv would block on non-blocking sockets.
+ *
+ * isSocetCloseErrno(errno): returns true if errno indicates that the socket
+ * is logically in closed state now.
+ */
+version (Win32) {
+ alias WSAGetLastError getSocketErrno;
+ enum CONNECT_INPROGRESS_ERRNO = WSAEWOULDBLOCK;
+ enum INTERRUPTED_ERRNO = WSAEINTR;
+ enum TIMEOUT_ERRNO = WSAETIMEDOUT;
+ enum WOULD_BLOCK_ERRNO = WSAEWOULDBLOCK;
+
+ bool isSocketCloseErrno(typeof(getSocketErrno()) errno) {
+ return (errno == WSAECONNRESET || errno == WSAENOTCONN);
+ }
+} else {
+ alias getErrno getSocketErrno;
+ enum CONNECT_INPROGRESS_ERRNO = EINPROGRESS;
+ enum INTERRUPTED_ERRNO = EINTR;
+ enum WOULD_BLOCK_ERRNO = EAGAIN;
+
+ // TODO: The C++ TSocket implementation mentions that EAGAIN can also be
+ // set (undocumentedly) in out of resource conditions; it would be a good
+ // idea to contact the original authors of the C++ code for details and adapt
+ // the code accordingly.
+ enum TIMEOUT_ERRNO = EAGAIN;
+
+ bool isSocketCloseErrno(typeof(getSocketErrno()) errno) {
+ return (errno == EPIPE || errno == ECONNRESET || errno == ENOTCONN);
+ }
+}
+
+string socketErrnoString(uint errno) {
+ version (Win32) {
+ return sysErrorString(errno);
+ } else {
+ return to!string(strerror(errno));
+ }
+}
diff --git a/lib/d/src/thrift/internal/ssl.d b/lib/d/src/thrift/internal/ssl.d
new file mode 100644
index 000000000..47a7cde15
--- /dev/null
+++ b/lib/d/src/thrift/internal/ssl.d
@@ -0,0 +1,240 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.internal.ssl;
+
+import core.memory : GC;
+import core.stdc.config;
+import core.stdc.errno : getErrno;
+import core.stdc.string : strerror;
+import deimos.openssl.err;
+import deimos.openssl.ssl;
+import deimos.openssl.x509v3;
+import std.array : empty, appender;
+import std.conv : to;
+import std.socket : Address;
+import thrift.transport.ssl;
+
+/**
+ * Checks if the peer is authorized after the SSL handshake has been
+ * completed on the given conncetion and throws an TSSLException if not.
+ *
+ * Params:
+ * ssl = The SSL connection to check.
+ * accessManager = The access manager to check the peer againts.
+ * peerAddress = The (IP) address of the peer.
+ * hostName = The host name of the peer.
+ */
+void authorize(SSL* ssl, TAccessManager accessManager,
+ Address peerAddress, lazy string hostName
+) {
+ alias TAccessManager.Decision Decision;
+
+ auto rc = SSL_get_verify_result(ssl);
+ if (rc != X509_V_OK) {
+ throw new TSSLException("SSL_get_verify_result(): " ~
+ to!string(X509_verify_cert_error_string(rc)));
+ }
+
+ auto cert = SSL_get_peer_certificate(ssl);
+ if (cert is null) {
+ // Certificate is not present.
+ if (SSL_get_verify_mode(ssl) & SSL_VERIFY_FAIL_IF_NO_PEER_CERT) {
+ throw new TSSLException(
+ "Authorize: Required certificate not present.");
+ }
+
+ // If we don't have an access manager set, we don't intend to authorize
+ // the client, so everything's fine.
+ if (accessManager) {
+ throw new TSSLException(
+ "Authorize: Certificate required for authorization.");
+ }
+ return;
+ }
+
+ if (accessManager is null) {
+ // No access manager set, can return immediately as the cert is valid
+ // and all peers are authorized.
+ X509_free(cert);
+ return;
+ }
+
+ // both certificate and access manager are present
+ auto decision = accessManager.verify(peerAddress);
+
+ if (decision != Decision.SKIP) {
+ X509_free(cert);
+ if (decision != Decision.ALLOW) {
+ throw new TSSLException("Authorize: Access denied based on remote IP.");
+ }
+ return;
+ }
+
+ // Check subjectAltName(s), if present.
+ auto alternatives = cast(STACK_OF!(GENERAL_NAME)*)
+ X509_get_ext_d2i(cert, NID_subject_alt_name, null, null);
+ if (alternatives != null) {
+ auto count = sk_GENERAL_NAME_num(alternatives);
+ for (int i = 0; decision == Decision.SKIP && i < count; i++) {
+ auto name = sk_GENERAL_NAME_value(alternatives, i);
+ if (name is null) {
+ continue;
+ }
+ auto data = ASN1_STRING_data(name.d.ia5);
+ auto length = ASN1_STRING_length(name.d.ia5);
+ switch (name.type) {
+ case GENERAL_NAME.GEN_DNS:
+ decision = accessManager.verify(hostName, cast(char[])data[0 .. length]);
+ break;
+ case GENERAL_NAME.GEN_IPADD:
+ decision = accessManager.verify(peerAddress, data[0 .. length]);
+ break;
+ default:
+ // Do nothing.
+ }
+ }
+
+ // DMD @@BUG@@: Empty template arguments parens should not be needed.
+ sk_GENERAL_NAME_pop_free!()(alternatives, &GENERAL_NAME_free);
+ }
+
+ // If we are alredy done, return.
+ if (decision != Decision.SKIP) {
+ X509_free(cert);
+ if (decision != Decision.ALLOW) {
+ throw new TSSLException("Authorize: Access denied.");
+ }
+ return;
+ }
+
+ // Check commonName.
+ auto name = X509_get_subject_name(cert);
+ if (name !is null) {
+ X509_NAME_ENTRY* entry;
+ char* utf8;
+ int last = -1;
+ while (decision == Decision.SKIP) {
+ last = X509_NAME_get_index_by_NID(name, NID_commonName, last);
+ if (last == -1)
+ break;
+ entry = X509_NAME_get_entry(name, last);
+ if (entry is null)
+ continue;
+ auto common = X509_NAME_ENTRY_get_data(entry);
+ auto size = ASN1_STRING_to_UTF8(&utf8, common);
+ decision = accessManager.verify(hostName, utf8[0 .. size]);
+ CRYPTO_free(utf8);
+ }
+ }
+ X509_free(cert);
+ if (decision != Decision.ALLOW) {
+ throw new TSSLException("Authorize: Could not authorize peer.");
+ }
+}
+
+/*
+ * OpenSSL error information used for storing D exceptions on the OpenSSL
+ * error stack.
+ */
+enum ERR_LIB_D_EXCEPTION = ERR_LIB_USER;
+enum ERR_F_D_EXCEPTION = 0; // function id - what to use here?
+enum ERR_R_D_EXCEPTION = 1234; // 99 and above are reserved for applications
+enum ERR_FILE_D_EXCEPTION = "d_exception";
+enum ERR_LINE_D_EXCEPTION = 0;
+enum ERR_FLAGS_D_EXCEPTION = 0;
+
+/**
+ * Returns an exception for the last.
+ *
+ * Params:
+ * location = An optional "location" to add to the error message (typically
+ * the last SSL API call).
+ */
+Exception getSSLException(string location = null, string clientFile = __FILE__,
+ size_t clientLine = __LINE__
+) {
+ // We can return either an exception saved from D BIO code, or a "true"
+ // OpenSSL error. Because there can possibly be more than one error on the
+ // error stack, we have to fetch all of them, and pick the last, i.e. newest
+ // one. We concatenate multiple successive OpenSSL error messages into a
+ // single one, but always just return the last D expcetion.
+ string message; // Probably better use an Appender here.
+ bool hadMessage;
+ Exception exception;
+
+ void initMessage() {
+ message.clear();
+ hadMessage = false;
+ if (!location.empty) {
+ message ~= location;
+ message ~= ": ";
+ }
+ }
+ initMessage();
+
+ auto errn = getErrno();
+
+ const(char)* file = void;
+ int line = void;
+ const(char)* data = void;
+ int flags = void;
+ c_ulong code = void;
+ while ((code = ERR_get_error_line_data(&file, &line, &data, &flags)) != 0) {
+ if (ERR_GET_REASON(code) == ERR_R_D_EXCEPTION) {
+ initMessage();
+ GC.removeRoot(cast(void*)data);
+ exception = cast(Exception)data;
+ } else {
+ exception = null;
+
+ if (hadMessage) {
+ message ~= ", ";
+ }
+
+ auto reason = ERR_reason_error_string(code);
+ if (reason) {
+ message ~= "SSL error: " ~ to!string(reason);
+ } else {
+ message ~= "SSL error #" ~ to!string(code);
+ }
+
+ hadMessage = true;
+ }
+ }
+
+ // If the last item from the stack was a D exception, throw it.
+ if (exception) return exception;
+
+ // We are dealing with an OpenSSL error that doesn't root in a D exception.
+ if (!hadMessage) {
+ // If we didn't get an actual error from the stack yet, try errno.
+ string errnString;
+ if (errn != 0) {
+ errnString = to!string(strerror(errn));
+ }
+ if (errnString.empty) {
+ message ~= "Unknown error";
+ } else {
+ message ~= errnString;
+ }
+ }
+
+ message ~= ".";
+ return new TSSLException(message, clientFile, clientLine);
+}
diff --git a/lib/d/src/thrift/internal/ssl_bio.d b/lib/d/src/thrift/internal/ssl_bio.d
new file mode 100644
index 000000000..796be91b0
--- /dev/null
+++ b/lib/d/src/thrift/internal/ssl_bio.d
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Provides a SSL BIO implementation wrapping a Thrift transport.
+ *
+ * This way, SSL I/O can be relayed over Thrift transport without introducing
+ * an additional layer of buffering, especially for the non-blocking
+ * transports.
+ *
+ * For the Thrift transport incarnations of the SSL entities, "tt" is used as
+ * prefix for clarity.
+ */
+module thrift.internal.ssl_bio;
+
+import core.stdc.config;
+import core.stdc.string : strlen;
+import core.memory : GC;
+import deimos.openssl.bio;
+import deimos.openssl.err;
+import thrift.base;
+import thrift.internal.ssl;
+import thrift.transport.base;
+
+/**
+ * Creates an SSL BIO object wrapping the given transport.
+ *
+ * Exceptions thrown by the transport are pushed onto the OpenSSL error stack,
+ * using the location/reason values from thrift.internal.ssl.ERR_*_D_EXCEPTION.
+ *
+ * The transport is assumed to be ready for reading and writing when the BIO
+ * functions are called, it is not opened by the implementation.
+ *
+ * Params:
+ * transport = The transport to wrap.
+ * closeTransport = Whether the close the transport when the SSL BIO is
+ * closed.
+ */
+BIO* createTTransportBIO(TTransport transport, bool closeTransport) {
+ auto result = BIO_new(cast(BIO_METHOD*)&ttBioMethod);
+ if (!result) return null;
+
+ GC.addRoot(cast(void*)transport);
+ BIO_set_fd(result, closeTransport, cast(c_long)cast(void*)transport);
+
+ return result;
+}
+
+private {
+ // Helper to get the Thrift transport assigned with the given BIO.
+ TTransport trans(BIO* b) nothrow {
+ auto result = cast(TTransport)b.ptr;
+ assert(result);
+ return result;
+ }
+
+ void setError(Exception e) nothrow {
+ ERR_put_error(ERR_LIB_D_EXCEPTION, ERR_F_D_EXCEPTION, ERR_R_D_EXCEPTION,
+ ERR_FILE_D_EXCEPTION, ERR_LINE_D_EXCEPTION);
+ try { GC.addRoot(cast(void*)e); } catch {}
+ ERR_set_error_data(cast(char*)e, ERR_FLAGS_D_EXCEPTION);
+ }
+
+ extern(C) int ttWrite(BIO* b, const(char)* data, int length) nothrow {
+ assert(b);
+ if (!data || length <= 0) return 0;
+ try {
+ trans(b).write((cast(ubyte*)data)[0 .. length]);
+ return length;
+ } catch (Exception e) {
+ setError(e);
+ return -1;
+ }
+ }
+
+ extern(C) int ttRead(BIO* b, char* data, int length) nothrow {
+ assert(b);
+ if (!data || length <= 0) return 0;
+ try {
+ return cast(int)trans(b).read((cast(ubyte*)data)[0 .. length]);
+ } catch (Exception e) {
+ setError(e);
+ return -1;
+ }
+ }
+
+ extern(C) int ttPuts(BIO* b, const(char)* str) nothrow {
+ return ttWrite(b, str, cast(int)strlen(str));
+ }
+
+ extern(C) c_long ttCtrl(BIO* b, int cmd, c_long num, void* ptr) nothrow {
+ assert(b);
+
+ switch (cmd) {
+ case BIO_C_SET_FD:
+ // Note that close flag and "fd" are actually reversed here because we
+ // need 64 bit width for the pointer – should probably drop BIO_set_fd
+ // altogether.
+ ttDestroy(b);
+ b.ptr = cast(void*)num;
+ b.shutdown = cast(int)ptr;
+ b.init_ = 1;
+ return 1;
+ case BIO_C_GET_FD:
+ if (!b.init_) return -1;
+ *(cast(void**)ptr) = b.ptr;
+ return cast(c_long)b.ptr;
+ case BIO_CTRL_GET_CLOSE:
+ return b.shutdown;
+ case BIO_CTRL_SET_CLOSE:
+ b.shutdown = cast(int)num;
+ return 1;
+ case BIO_CTRL_FLUSH:
+ try {
+ trans(b).flush();
+ return 1;
+ } catch (Exception e) {
+ setError(e);
+ return -1;
+ }
+ case BIO_CTRL_DUP:
+ // Seems like we have nothing to do on duplication, but couldn't find
+ // any documentation if this actually ever happens during normal SSL
+ // usage.
+ return 1;
+ default:
+ return 0;
+ }
+ }
+
+ extern(C) int ttCreate(BIO* b) nothrow {
+ assert(b);
+ b.init_ = 0;
+ b.num = 0; // User-defined number field, unused here.
+ b.ptr = null;
+ b.flags = 0;
+ return 1;
+ }
+
+ extern(C) int ttDestroy(BIO* b) nothrow {
+ if (!b) return 0;
+
+ int rc = 1;
+ if (b.shutdown) {
+ if (b.init_) {
+ try {
+ trans(b).close();
+ GC.removeRoot(cast(void*)trans(b));
+ b.ptr = null;
+ } catch (Exception e) {
+ setError(e);
+ rc = -1;
+ }
+ }
+ b.init_ = 0;
+ b.flags = 0;
+ }
+
+ return rc;
+ }
+
+ immutable BIO_METHOD ttBioMethod = {
+ BIO_TYPE_SOURCE_SINK,
+ "TTransport",
+ &ttWrite,
+ &ttRead,
+ &ttPuts,
+ null, // gets
+ &ttCtrl,
+ &ttCreate,
+ &ttDestroy,
+ null // callback_ctrl
+ };
+}
diff --git a/lib/d/src/thrift/internal/test/protocol.d b/lib/d/src/thrift/internal/test/protocol.d
new file mode 100644
index 000000000..2d25154de
--- /dev/null
+++ b/lib/d/src/thrift/internal/test/protocol.d
@@ -0,0 +1,183 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.internal.test.protocol;
+
+import std.exception;
+import thrift.transport.memory;
+import thrift.protocol.base;
+
+version (unittest):
+
+void testContainerSizeLimit(Protocol)() if (isTProtocol!Protocol) {
+ auto buffer = new TMemoryBuffer;
+ auto prot = new Protocol(buffer);
+
+ // Make sure reading fails if a container larger than the size limit is read.
+ prot.containerSizeLimit = 3;
+
+ {
+ prot.writeListBegin(TList(TType.I32, 4));
+ prot.writeI32(0); // Make sure size can be read e.g. for JSON protocol.
+ prot.reset();
+
+ auto e = cast(TProtocolException)collectException(prot.readListBegin());
+ enforce(e && e.type == TProtocolException.Type.SIZE_LIMIT);
+ prot.reset();
+ buffer.reset();
+ }
+
+ {
+ prot.writeMapBegin(TMap(TType.I32, TType.I32, 4));
+ prot.writeI32(0); // Make sure size can be read e.g. for JSON protocol.
+ prot.reset();
+
+ auto e = cast(TProtocolException)collectException(prot.readMapBegin());
+ enforce(e && e.type == TProtocolException.Type.SIZE_LIMIT);
+ prot.reset();
+ buffer.reset();
+ }
+
+ {
+ prot.writeSetBegin(TSet(TType.I32, 4));
+ prot.writeI32(0); // Make sure size can be read e.g. for JSON protocol.
+ prot.reset();
+
+ auto e = cast(TProtocolException)collectException(prot.readSetBegin());
+ enforce(e && e.type == TProtocolException.Type.SIZE_LIMIT);
+ prot.reset();
+ buffer.reset();
+ }
+
+ // Make sure reading works if the containers are smaller than the limit or
+ // no limit is set.
+ foreach (limit; [3, 0, -1]) {
+ prot.containerSizeLimit = limit;
+
+ {
+ prot.writeListBegin(TList(TType.I32, 2));
+ prot.writeI32(0);
+ prot.writeI32(1);
+ prot.writeListEnd();
+ prot.reset();
+
+ auto list = prot.readListBegin();
+ enforce(list.elemType == TType.I32);
+ enforce(list.size == 2);
+ enforce(prot.readI32() == 0);
+ enforce(prot.readI32() == 1);
+ prot.readListEnd();
+
+ prot.reset();
+ buffer.reset();
+ }
+
+ {
+ prot.writeMapBegin(TMap(TType.I32, TType.I32, 2));
+ prot.writeI32(0);
+ prot.writeI32(1);
+ prot.writeI32(2);
+ prot.writeI32(3);
+ prot.writeMapEnd();
+ prot.reset();
+
+ auto map = prot.readMapBegin();
+ enforce(map.keyType == TType.I32);
+ enforce(map.valueType == TType.I32);
+ enforce(map.size == 2);
+ enforce(prot.readI32() == 0);
+ enforce(prot.readI32() == 1);
+ enforce(prot.readI32() == 2);
+ enforce(prot.readI32() == 3);
+ prot.readMapEnd();
+
+ prot.reset();
+ buffer.reset();
+ }
+
+ {
+ prot.writeSetBegin(TSet(TType.I32, 2));
+ prot.writeI32(0);
+ prot.writeI32(1);
+ prot.writeSetEnd();
+ prot.reset();
+
+ auto set = prot.readSetBegin();
+ enforce(set.elemType == TType.I32);
+ enforce(set.size == 2);
+ enforce(prot.readI32() == 0);
+ enforce(prot.readI32() == 1);
+ prot.readSetEnd();
+
+ prot.reset();
+ buffer.reset();
+ }
+ }
+}
+
+void testStringSizeLimit(Protocol)() if (isTProtocol!Protocol) {
+ auto buffer = new TMemoryBuffer;
+ auto prot = new Protocol(buffer);
+
+ // Make sure reading fails if a string larger than the size limit is read.
+ prot.stringSizeLimit = 3;
+
+ {
+ prot.writeString("asdf");
+ prot.reset();
+
+ auto e = cast(TProtocolException)collectException(prot.readString());
+ enforce(e && e.type == TProtocolException.Type.SIZE_LIMIT);
+ prot.reset();
+ buffer.reset();
+ }
+
+ {
+ prot.writeBinary([1, 2, 3, 4]);
+ prot.reset();
+
+ auto e = cast(TProtocolException)collectException(prot.readBinary());
+ enforce(e && e.type == TProtocolException.Type.SIZE_LIMIT);
+ prot.reset();
+ buffer.reset();
+ }
+
+ // Make sure reading works if the containers are smaller than the limit or
+ // no limit is set.
+ foreach (limit; [3, 0, -1]) {
+ prot.containerSizeLimit = limit;
+
+ {
+ prot.writeString("as");
+ prot.reset();
+
+ enforce(prot.readString() == "as");
+ prot.reset();
+ buffer.reset();
+ }
+
+ {
+ prot.writeBinary([1, 2]);
+ prot.reset();
+
+ enforce(prot.readBinary() == [1, 2]);
+ prot.reset();
+ buffer.reset();
+ }
+ }
+}
diff --git a/lib/d/src/thrift/internal/test/server.d b/lib/d/src/thrift/internal/test/server.d
new file mode 100644
index 000000000..b5393f022
--- /dev/null
+++ b/lib/d/src/thrift/internal/test/server.d
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.internal.test.server;
+
+import core.sync.condition;
+import core.sync.mutex;
+import core.thread : Thread;
+import std.datetime;
+import std.exception : enforce;
+import std.typecons : WhiteHole;
+import std.variant : Variant;
+import thrift.protocol.base;
+import thrift.protocol.binary;
+import thrift.protocol.processor;
+import thrift.server.base;
+import thrift.server.transport.socket;
+import thrift.transport.base;
+import thrift.util.cancellation;
+
+version(unittest):
+
+/**
+ * Tests if serving is stopped correctly if the cancellation passed to serve()
+ * is triggered.
+ *
+ * Because the tests are run many times in a loop, this is indirectly also a
+ * test whether socket, etc. handles are cleaned up correctly, because the
+ * application will likely run out of handles otherwise.
+ */
+void testServeCancel(Server)(void delegate(Server) serverSetup = null) if (
+ is(Server : TServer)
+) {
+ auto proc = new WhiteHole!TProcessor;
+ auto tf = new TTransportFactory;
+ auto pf = new TBinaryProtocolFactory!();
+
+ // Need a special case for TNonblockingServer which doesn't use
+ // TServerTransport.
+ static if (__traits(compiles, new Server(proc, 0, tf, pf))) {
+ auto server = new Server(proc, 0, tf, pf);
+ } else {
+ auto server = new Server(proc, new TServerSocket(0), tf, pf);
+ }
+
+ // On Windows, we use TCP sockets to replace socketpair(). Since they stay
+ // in TIME_WAIT for some time even if they are properly closed, we have to use
+ // a lower number of iterations to avoid running out of ports/buffer space.
+ version (Windows) {
+ enum ITERATIONS = 100;
+ } else {
+ enum ITERATIONS = 10000;
+ }
+
+ if (serverSetup) serverSetup(server);
+
+ auto servingMutex = new Mutex;
+ auto servingCondition = new Condition(servingMutex);
+ auto doneMutex = new Mutex;
+ auto doneCondition = new Condition(doneMutex);
+
+ class CancellingHandler : TServerEventHandler {
+ void preServe() {
+ synchronized (servingMutex) {
+ servingCondition.notifyAll();
+ }
+ }
+ Variant createContext(TProtocol input, TProtocol output) { return Variant.init; }
+ void deleteContext(Variant serverContext, TProtocol input, TProtocol output) {}
+ void preProcess(Variant serverContext, TTransport transport) {}
+ }
+ server.eventHandler = new CancellingHandler;
+
+ foreach (i; 0 .. ITERATIONS) {
+ synchronized (servingMutex) {
+ auto cancel = new TCancellationOrigin;
+ synchronized (doneMutex) {
+ auto serverThread = new Thread({
+ server.serve(cancel);
+ synchronized (doneMutex) {
+ doneCondition.notifyAll();
+ }
+ });
+ serverThread.isDaemon = true;
+ serverThread.start();
+
+ servingCondition.wait();
+
+ cancel.trigger();
+ enforce(doneCondition.wait(dur!"msecs"(100)));
+ serverThread.join();
+ }
+ }
+ }
+}
diff --git a/lib/d/src/thrift/internal/traits.d b/lib/d/src/thrift/internal/traits.d
new file mode 100644
index 000000000..11e98c575
--- /dev/null
+++ b/lib/d/src/thrift/internal/traits.d
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.internal.traits;
+
+import std.traits;
+
+/**
+ * Adds or removes attributes from the given function (pointer) or delegate
+ * type.
+ *
+ * Besides the base type, two std.traits.FunctionAttribute bitfields are
+ * accepted, representing the attributes to add to the function signature
+ * resp. to remove from it. If an attribute appears in both fields, an error
+ * is raised.
+ *
+ * Params:
+ * T = The base type.
+ * setAttrs = The attributes to add to the type, if not already present.
+ * clearAttrs = The attributes to remove from the type, if present.
+ */
+template ChangeFuncAttrs(
+ T,
+ FunctionAttribute setAttrs = FunctionAttribute.none,
+ FunctionAttribute clearAttrs = FunctionAttribute.none
+) if (isFunctionPointer!T || isDelegate!T) {
+ static assert(!(setAttrs & clearAttrs),
+ "Cannot set and clear attributes at the same time.");
+ mixin({
+ enum newAttrs = (functionAttributes!T | setAttrs) & ~clearAttrs;
+ static assert(!(newAttrs & FunctionAttribute.trusted) ||
+ !(newAttrs & FunctionAttribute.safe),
+ "Cannot have a function/delegate that is both trusted and safe.");
+
+ string result = "alias ";
+
+ static if (functionLinkage!T != "D") {
+ result ~= "extern(" ~ functionLinkage!T ~ ") ";
+ }
+
+ static if (newAttrs & FunctionAttribute.ref_) {
+ result ~= "ref ";
+ }
+
+ result ~= "ReturnType!T";
+
+ static if (isDelegate!T) {
+ result ~= " delegate";
+ } else {
+ result ~= " function";
+ }
+
+ result ~= "(ParameterTypeTuple!T)";
+
+ static if (newAttrs & FunctionAttribute.pure_) {
+ result ~= " pure";
+ }
+ static if (newAttrs & FunctionAttribute.nothrow_) {
+ result ~= " nothrow";
+ }
+ static if (newAttrs & FunctionAttribute.property) {
+ result ~= " @property";
+ }
+ static if (newAttrs & FunctionAttribute.trusted) {
+ result ~= " @trusted";
+ }
+ static if (newAttrs & FunctionAttribute.safe) {
+ result ~= " @safe";
+ }
+
+ result ~= " ChangeFuncAttrs;";
+ return result;
+ }());
+}
+
+/// Ditto
+template ChangeFuncAttrs(
+ T,
+ FunctionAttribute setAttrs = FunctionAttribute.none,
+ FunctionAttribute clearAttrs = FunctionAttribute.none
+) if (is(T == function)) {
+ // To avoid a lot of syntactic headaches, we just use the above version to
+ // operate on the corresponding function pointer type and then remove the
+ // pointer again.
+ alias FunctionTypeOf!(ChangeFuncAttrs!(T*, setAttrs, clearAttrs))
+ ChangeFuncAttrs;
+}
+
+version (unittest) {
+ import std.algorithm;
+ import std.metastrings;
+ import std.typetuple;
+}
+unittest {
+ alias FunctionAttribute FA;
+ foreach (T0; TypeTuple!(
+ int function(int),
+ int delegate(int),
+ FunctionTypeOf!(int function(int))
+ )) {
+ alias ChangeFuncAttrs!(T0, FA.safe) T1;
+ static assert(functionAttributes!T1 == FA.safe);
+
+ alias ChangeFuncAttrs!(T1, FA.nothrow_ | FA.ref_, FA.safe) T2;
+ static assert(functionAttributes!T2 == (FA.nothrow_ | FA.ref_));
+
+ enum allAttrs = reduce!"a | b"([EnumMembers!FA]) & ~FA.safe;
+
+ alias ChangeFuncAttrs!(T2, allAttrs) T3;
+ static assert(functionAttributes!T3 == allAttrs);
+
+ alias ChangeFuncAttrs!(T3, FA.none, allAttrs) T4;
+ static assert(is(T4 == T0));
+ }
+}
+
+/**
+ * Adds »nothrow« to the type of the passed function pointer/delegate, if it
+ * is not already present.
+ *
+ * Technically, assumeNothrow just performs a cast, but using it has the
+ * advantage of being explicitly about the operation that is performed.
+ */
+auto assumeNothrow(T)(T t) if (isFunctionPointer!T || isDelegate!T) {
+ return cast(ChangeFuncAttrs!(T, FunctionAttribute.nothrow_))t;
+}
diff --git a/lib/d/src/thrift/protocol/base.d b/lib/d/src/thrift/protocol/base.d
new file mode 100644
index 000000000..809c847b6
--- /dev/null
+++ b/lib/d/src/thrift/protocol/base.d
@@ -0,0 +1,436 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Defines the basic interface for a Thrift protocol and associated exception
+ * types.
+ *
+ * Most parts of the protocol API are typically not used in client code, as
+ * the actual serialization code is generated by thrift.codegen.* – the only
+ * interesting thing usually is that there are protocols which can be created
+ * from transports and passed around.
+ */
+module thrift.protocol.base;
+
+import thrift.base;
+import thrift.transport.base;
+
+/**
+ * The field types Thrift protocols support.
+ */
+enum TType : byte {
+ STOP = 0, /// Used to mark the end of a sequence of fields.
+ VOID = 1, ///
+ BOOL = 2, ///
+ BYTE = 3, ///
+ DOUBLE = 4, ///
+ I16 = 6, ///
+ I32 = 8, ///
+ I64 = 10, ///
+ STRING = 11, ///
+ STRUCT = 12, ///
+ MAP = 13, ///
+ SET = 14, ///
+ LIST = 15 ///
+}
+
+/**
+ * Types of Thrift RPC messages.
+ */
+enum TMessageType : byte {
+ CALL = 1, /// Call of a normal, two-way RPC method.
+ REPLY = 2, /// Reply to a normal method call.
+ EXCEPTION = 3, /// Reply to a method call if target raised a TApplicationException.
+ ONEWAY = 4 /// Call of a one-way RPC method which is not followed by a reply.
+}
+
+/**
+ * Descriptions of Thrift entities.
+ */
+struct TField {
+ string name;
+ TType type;
+ short id;
+}
+
+/// ditto
+struct TList {
+ TType elemType;
+ size_t size;
+}
+
+/// ditto
+struct TMap {
+ TType keyType;
+ TType valueType;
+ size_t size;
+}
+
+/// ditto
+struct TMessage {
+ string name;
+ TMessageType type;
+ int seqid;
+}
+
+/// ditto
+struct TSet {
+ TType elemType;
+ size_t size;
+}
+
+/// ditto
+struct TStruct {
+ string name;
+}
+
+/**
+ * Interface for a Thrift protocol implementation. Essentially, it defines
+ * a way of reading and writing all the base types, plus a mechanism for
+ * writing out structs with indexed fields.
+ *
+ * TProtocol objects should not be shared across multiple encoding contexts,
+ * as they may need to maintain internal state in some protocols (e.g. JSON).
+ * Note that is is acceptable for the TProtocol module to do its own internal
+ * buffered reads/writes to the underlying TTransport where appropriate (i.e.
+ * when parsing an input XML stream, reading could be batched rather than
+ * looking ahead character by character for a close tag).
+ */
+interface TProtocol {
+ /// The underlying transport used by the protocol.
+ TTransport transport() @property;
+
+ /*
+ * Writing methods.
+ */
+
+ void writeBool(bool b); ///
+ void writeByte(byte b); ///
+ void writeI16(short i16); ///
+ void writeI32(int i32); ///
+ void writeI64(long i64); ///
+ void writeDouble(double dub); ///
+ void writeString(string str); ///
+ void writeBinary(ubyte[] buf); ///
+
+ void writeMessageBegin(TMessage message); ///
+ void writeMessageEnd(); ///
+ void writeStructBegin(TStruct tstruct); ///
+ void writeStructEnd(); ///
+ void writeFieldBegin(TField field); ///
+ void writeFieldEnd(); ///
+ void writeFieldStop(); ///
+ void writeListBegin(TList list); ///
+ void writeListEnd(); ///
+ void writeMapBegin(TMap map); ///
+ void writeMapEnd(); ///
+ void writeSetBegin(TSet set); ///
+ void writeSetEnd(); ///
+
+ /*
+ * Reading methods.
+ */
+
+ bool readBool(); ///
+ byte readByte(); ///
+ short readI16(); ///
+ int readI32(); ///
+ long readI64(); ///
+ double readDouble(); ///
+ string readString(); ///
+ ubyte[] readBinary(); ///
+
+ TMessage readMessageBegin(); ///
+ void readMessageEnd(); ///
+ TStruct readStructBegin(); ///
+ void readStructEnd(); ///
+ TField readFieldBegin(); ///
+ void readFieldEnd(); ///
+ TList readListBegin(); ///
+ void readListEnd(); ///
+ TMap readMapBegin(); ///
+ void readMapEnd(); ///
+ TSet readSetBegin(); ///
+ void readSetEnd(); ///
+
+ /**
+ * Reset any internal state back to a blank slate, if the protocol is
+ * stateful.
+ */
+ void reset();
+}
+
+/**
+ * true if T is a TProtocol.
+ */
+template isTProtocol(T) {
+ enum isTProtocol = is(T : TProtocol);
+}
+
+unittest {
+ static assert(isTProtocol!TProtocol);
+ static assert(!isTProtocol!void);
+}
+
+/**
+ * Creates a protocol operating on a given transport.
+ */
+interface TProtocolFactory {
+ ///
+ TProtocol getProtocol(TTransport trans);
+}
+
+/**
+ * A protocol-level exception.
+ */
+class TProtocolException : TException {
+ /// The possible exception types.
+ enum Type {
+ UNKNOWN, ///
+ INVALID_DATA, ///
+ NEGATIVE_SIZE, ///
+ SIZE_LIMIT, ///
+ BAD_VERSION, ///
+ NOT_IMPLEMENTED ///
+ }
+
+ ///
+ this(Type type, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
+ static string msgForType(Type type) {
+ switch (type) {
+ case Type.UNKNOWN: return "Unknown protocol exception";
+ case Type.INVALID_DATA: return "Invalid data";
+ case Type.NEGATIVE_SIZE: return "Negative size";
+ case Type.SIZE_LIMIT: return "Exceeded size limit";
+ case Type.BAD_VERSION: return "Invalid version";
+ case Type.NOT_IMPLEMENTED: return "Not implemented";
+ default: return "(Invalid exception type)";
+ }
+ }
+ this(msgForType(type), type, file, line, next);
+ }
+
+ ///
+ this(string msg, string file = __FILE__, size_t line = __LINE__,
+ Throwable next = null)
+ {
+ this(msg, Type.UNKNOWN, file, line, next);
+ }
+
+ ///
+ this(string msg, Type type, string file = __FILE__, size_t line = __LINE__,
+ Throwable next = null)
+ {
+ super(msg, file, line, next);
+ type_ = type;
+ }
+
+ ///
+ Type type() const @property {
+ return type_;
+ }
+
+protected:
+ Type type_;
+}
+
+/**
+ * Skips a field of the given type on the protocol.
+ *
+ * The main purpose of skip() is to allow treating struct and cotainer types,
+ * (where multiple primitive types have to be skipped) the same as scalar types
+ * in generated code.
+ */
+void skip(Protocol)(Protocol prot, TType type) if (is(Protocol : TProtocol)) {
+ final switch (type) {
+ case TType.BOOL:
+ prot.readBool();
+ break;
+
+ case TType.BYTE:
+ prot.readByte();
+ break;
+
+ case TType.I16:
+ prot.readI16();
+ break;
+
+ case TType.I32:
+ prot.readI32();
+ break;
+
+ case TType.I64:
+ prot.readI64();
+ break;
+
+ case TType.DOUBLE:
+ prot.readDouble();
+ break;
+
+ case TType.STRING:
+ prot.readBinary();
+ break;
+
+ case TType.STRUCT:
+ prot.readStructBegin();
+ while (true) {
+ auto f = prot.readFieldBegin();
+ if (f.type == TType.STOP) break;
+ skip(prot, f.type);
+ prot.readFieldEnd();
+ }
+ prot.readStructEnd();
+ break;
+
+ case TType.LIST:
+ auto l = prot.readListBegin();
+ foreach (i; 0 .. l.size) {
+ skip(prot, l.elemType);
+ }
+ prot.readListEnd();
+ break;
+
+ case TType.MAP:
+ auto m = prot.readMapBegin();
+ foreach (i; 0 .. m.size) {
+ skip(prot, m.keyType);
+ skip(prot, m.valueType);
+ }
+ prot.readMapEnd();
+ break;
+
+ case TType.SET:
+ auto s = prot.readSetBegin();
+ foreach (i; 0 .. s.size) {
+ skip(prot, s.elemType);
+ }
+ prot.readSetEnd();
+ break;
+ }
+}
+
+/**
+ * Application-level exception.
+ *
+ * It is thrown if an RPC call went wrong on the application layer, e.g. if
+ * the receiver does not know the method name requested or a method invoked by
+ * the service processor throws an exception not part of the Thrift API.
+ */
+class TApplicationException : TException {
+ /// The possible exception types.
+ enum Type {
+ UNKNOWN = 0, ///
+ UNKNOWN_METHOD = 1, ///
+ INVALID_MESSAGE_TYPE = 2, ///
+ WRONG_METHOD_NAME = 3, ///
+ BAD_SEQUENCE_ID = 4, ///
+ MISSING_RESULT = 5, ///
+ INTERNAL_ERROR = 6, ///
+ PROTOCOL_ERROR = 7 ///
+ }
+
+ ///
+ this(Type type, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
+ static string msgForType(Type type) {
+ switch (type) {
+ case Type.UNKNOWN: return "Unknown application exception";
+ case Type.UNKNOWN_METHOD: return "Unknown method";
+ case Type.INVALID_MESSAGE_TYPE: return "Invalid message type";
+ case Type.WRONG_METHOD_NAME: return "Wrong method name";
+ case Type.BAD_SEQUENCE_ID: return "Bad sequence identifier";
+ case Type.MISSING_RESULT: return "Missing result";
+ default: return "(Invalid exception type)";
+ }
+ }
+ this(msgForType(type), type, file, line, next);
+ }
+
+ ///
+ this(string msg, string file = __FILE__, size_t line = __LINE__,
+ Throwable next = null)
+ {
+ this(msg, Type.UNKNOWN, file, line, next);
+ }
+
+ ///
+ this(string msg, Type type, string file = __FILE__, size_t line = __LINE__,
+ Throwable next = null)
+ {
+ super(msg, file, line, next);
+ type_ = type;
+ }
+
+ ///
+ Type type() @property const {
+ return type_;
+ }
+
+ // TODO: Replace hand-written read()/write() with thrift.codegen templates.
+
+ ///
+ void read(TProtocol iprot) {
+ iprot.readStructBegin();
+ while (true) {
+ auto f = iprot.readFieldBegin();
+ if (f.type == TType.STOP) break;
+
+ switch (f.id) {
+ case 1:
+ if (f.type == TType.STRING) {
+ msg = iprot.readString();
+ } else {
+ skip(iprot, f.type);
+ }
+ break;
+ case 2:
+ if (f.type == TType.I32) {
+ type_ = cast(Type)iprot.readI32();
+ } else {
+ skip(iprot, f.type);
+ }
+ break;
+ default:
+ skip(iprot, f.type);
+ break;
+ }
+ }
+ iprot.readStructEnd();
+ }
+
+ ///
+ void write(TProtocol oprot) const {
+ oprot.writeStructBegin(TStruct("TApplicationException"));
+
+ if (msg != null) {
+ oprot.writeFieldBegin(TField("message", TType.STRING, 1));
+ oprot.writeString(msg);
+ oprot.writeFieldEnd();
+ }
+
+ oprot.writeFieldBegin(TField("type", TType.I32, 2));
+ oprot.writeI32(type_);
+ oprot.writeFieldEnd();
+
+ oprot.writeFieldStop();
+ oprot.writeStructEnd();
+ }
+
+private:
+ Type type_;
+}
diff --git a/lib/d/src/thrift/protocol/binary.d b/lib/d/src/thrift/protocol/binary.d
new file mode 100644
index 000000000..13d8fe88e
--- /dev/null
+++ b/lib/d/src/thrift/protocol/binary.d
@@ -0,0 +1,414 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.protocol.binary;
+
+import std.array : uninitializedArray;
+import std.typetuple : allSatisfy, TypeTuple;
+import thrift.protocol.base;
+import thrift.transport.base;
+import thrift.internal.endian;
+
+/**
+ * TProtocol implementation of the Binary Thrift protocol.
+ */
+final class TBinaryProtocol(Transport = TTransport) if (
+ isTTransport!Transport
+) : TProtocol {
+
+ /**
+ * Constructs a new instance.
+ *
+ * Params:
+ * trans = The transport to use.
+ * containerSizeLimit = If positive, the container size is limited to the
+ * given number of items.
+ * stringSizeLimit = If positive, the string length is limited to the
+ * given number of bytes.
+ * strictRead = If false, old peers which do not include the protocol
+ * version are tolerated.
+ * strictWrite = Whether to include the protocol version in the header.
+ */
+ this(Transport trans, int containerSizeLimit = 0, int stringSizeLimit = 0,
+ bool strictRead = false, bool strictWrite = true
+ ) {
+ trans_ = trans;
+ this.containerSizeLimit = containerSizeLimit;
+ this.stringSizeLimit = stringSizeLimit;
+ this.strictRead = strictRead;
+ this.strictWrite = strictWrite;
+ }
+
+ Transport transport() @property {
+ return trans_;
+ }
+
+ void reset() {}
+
+ /**
+ * If false, old peers which do not include the protocol version in the
+ * message header are tolerated.
+ *
+ * Defaults to false.
+ */
+ bool strictRead;
+
+ /**
+ * Whether to include the protocol version in the message header (older
+ * versions didn't).
+ *
+ * Defaults to true.
+ */
+ bool strictWrite;
+
+ /**
+ * If positive, limits the number of items of deserialized containers to the
+ * given amount.
+ *
+ * This is useful to avoid allocating excessive amounts of memory when broken
+ * data is received. If the limit is exceeded, a SIZE_LIMIT-type
+ * TProtocolException is thrown.
+ *
+ * Defaults to zero (no limit).
+ */
+ int containerSizeLimit;
+
+ /**
+ * If positive, limits the length of deserialized strings/binary data to the
+ * given number of bytes.
+ *
+ * This is useful to avoid allocating excessive amounts of memory when broken
+ * data is received. If the limit is exceeded, a SIZE_LIMIT-type
+ * TProtocolException is thrown.
+ *
+ * Defaults to zero (no limit).
+ */
+ int stringSizeLimit;
+
+ /*
+ * Writing methods.
+ */
+
+ void writeBool(bool b) {
+ writeByte(b ? 1 : 0);
+ }
+
+ void writeByte(byte b) {
+ trans_.write((cast(ubyte*)&b)[0 .. 1]);
+ }
+
+ void writeI16(short i16) {
+ short net = hostToNet(i16);
+ trans_.write((cast(ubyte*)&net)[0 .. 2]);
+ }
+
+ void writeI32(int i32) {
+ int net = hostToNet(i32);
+ trans_.write((cast(ubyte*)&net)[0 .. 4]);
+ }
+
+ void writeI64(long i64) {
+ long net = hostToNet(i64);
+ trans_.write((cast(ubyte*)&net)[0 .. 8]);
+ }
+
+ void writeDouble(double dub) {
+ static assert(double.sizeof == ulong.sizeof);
+ auto bits = hostToNet(*cast(ulong*)(&dub));
+ trans_.write((cast(ubyte*)&bits)[0 .. 8]);
+ }
+
+ void writeString(string str) {
+ writeBinary(cast(ubyte[])str);
+ }
+
+ void writeBinary(ubyte[] buf) {
+ assert(buf.length <= int.max);
+ writeI32(cast(int)buf.length);
+ trans_.write(buf);
+ }
+
+ void writeMessageBegin(TMessage message) {
+ if (strictWrite) {
+ int versn = VERSION_1 | message.type;
+ writeI32(versn);
+ writeString(message.name);
+ writeI32(message.seqid);
+ } else {
+ writeString(message.name);
+ writeByte(message.type);
+ writeI32(message.seqid);
+ }
+ }
+ void writeMessageEnd() {}
+
+ void writeStructBegin(TStruct tstruct) {}
+ void writeStructEnd() {}
+
+ void writeFieldBegin(TField field) {
+ writeByte(field.type);
+ writeI16(field.id);
+ }
+ void writeFieldEnd() {}
+
+ void writeFieldStop() {
+ writeByte(TType.STOP);
+ }
+
+ void writeListBegin(TList list) {
+ assert(list.size <= int.max);
+ writeByte(list.elemType);
+ writeI32(cast(int)list.size);
+ }
+ void writeListEnd() {}
+
+ void writeMapBegin(TMap map) {
+ assert(map.size <= int.max);
+ writeByte(map.keyType);
+ writeByte(map.valueType);
+ writeI32(cast(int)map.size);
+ }
+ void writeMapEnd() {}
+
+ void writeSetBegin(TSet set) {
+ assert(set.size <= int.max);
+ writeByte(set.elemType);
+ writeI32(cast(int)set.size);
+ }
+ void writeSetEnd() {}
+
+
+ /*
+ * Reading methods.
+ */
+
+ bool readBool() {
+ return readByte() != 0;
+ }
+
+ byte readByte() {
+ ubyte[1] b = void;
+ trans_.readAll(b);
+ return cast(byte)b[0];
+ }
+
+ short readI16() {
+ IntBuf!short b = void;
+ trans_.readAll(b.bytes);
+ return netToHost(b.value);
+ }
+
+ int readI32() {
+ IntBuf!int b = void;
+ trans_.readAll(b.bytes);
+ return netToHost(b.value);
+ }
+
+ long readI64() {
+ IntBuf!long b = void;
+ trans_.readAll(b.bytes);
+ return netToHost(b.value);
+ }
+
+ double readDouble() {
+ IntBuf!long b = void;
+ trans_.readAll(b.bytes);
+ b.value = netToHost(b.value);
+ return *cast(double*)(&b.value);
+ }
+
+ string readString() {
+ return cast(string)readBinary();
+ }
+
+ ubyte[] readBinary() {
+ return readBinaryBody(readSize(stringSizeLimit));
+ }
+
+ TMessage readMessageBegin() {
+ TMessage msg = void;
+
+ int size = readI32();
+ if (size < 0) {
+ int versn = size & VERSION_MASK;
+ if (versn != VERSION_1) {
+ throw new TProtocolException("Bad protocol version.",
+ TProtocolException.Type.BAD_VERSION);
+ }
+
+ msg.type = cast(TMessageType)(size & MESSAGE_TYPE_MASK);
+ msg.name = readString();
+ msg.seqid = readI32();
+ } else {
+ if (strictRead) {
+ throw new TProtocolException(
+ "Protocol version missing, old client?",
+ TProtocolException.Type.BAD_VERSION);
+ } else {
+ if (size < 0) {
+ throw new TProtocolException(TProtocolException.Type.NEGATIVE_SIZE);
+ }
+ msg.name = cast(string)readBinaryBody(size);
+ msg.type = cast(TMessageType)(readByte());
+ msg.seqid = readI32();
+ }
+ }
+
+ return msg;
+ }
+ void readMessageEnd() {}
+
+ TStruct readStructBegin() {
+ return TStruct();
+ }
+ void readStructEnd() {}
+
+ TField readFieldBegin() {
+ TField f = void;
+ f.name = null;
+ f.type = cast(TType)readByte();
+ if (f.type == TType.STOP) return f;
+ f.id = readI16();
+ return f;
+ }
+ void readFieldEnd() {}
+
+ TList readListBegin() {
+ return TList(cast(TType)readByte(), readSize(containerSizeLimit));
+ }
+ void readListEnd() {}
+
+ TMap readMapBegin() {
+ return TMap(cast(TType)readByte(), cast(TType)readByte(),
+ readSize(containerSizeLimit));
+ }
+ void readMapEnd() {}
+
+ TSet readSetBegin() {
+ return TSet(cast(TType)readByte(), readSize(containerSizeLimit));
+ }
+ void readSetEnd() {}
+
+private:
+ ubyte[] readBinaryBody(int size) {
+ if (size == 0) {
+ return null;
+ }
+
+ auto buf = uninitializedArray!(ubyte[])(size);
+ trans_.readAll(buf);
+ return buf;
+ }
+
+ int readSize(int limit) {
+ auto size = readI32();
+ if (size < 0) {
+ throw new TProtocolException(TProtocolException.Type.NEGATIVE_SIZE);
+ } else if (limit > 0 && size > limit) {
+ throw new TProtocolException(TProtocolException.Type.SIZE_LIMIT);
+ }
+ return size;
+ }
+
+ enum MESSAGE_TYPE_MASK = 0x000000ff;
+ enum VERSION_MASK = 0xffff0000;
+ enum VERSION_1 = 0x80010000;
+
+ Transport trans_;
+}
+
+/**
+ * TBinaryProtocol construction helper to avoid having to explicitly specify
+ * the transport type, i.e. to allow the constructor being called using IFTI
+ * (see $(LINK2 http://d.puremagic.com/issues/show_bug.cgi?id=6082, D Bugzilla
+ * enhancement requet 6082)).
+ */
+TBinaryProtocol!Transport tBinaryProtocol(Transport)(Transport trans,
+ int containerSizeLimit = 0, int stringSizeLimit = 0,
+ bool strictRead = false, bool strictWrite = true
+) if (isTTransport!Transport) {
+ return new TBinaryProtocol!Transport(trans, containerSizeLimit,
+ stringSizeLimit, strictRead, strictWrite);
+}
+
+unittest {
+ import std.exception;
+ import thrift.transport.memory;
+
+ // Check the message header format.
+ auto buf = new TMemoryBuffer;
+ auto binary = tBinaryProtocol(buf);
+ binary.writeMessageBegin(TMessage("foo", TMessageType.CALL, 0));
+
+ auto header = new ubyte[15];
+ buf.readAll(header);
+ enforce(header == [
+ 128, 1, 0, 1, // Version 1, TMessageType.CALL
+ 0, 0, 0, 3, // Method name length
+ 102, 111, 111, // Method name ("foo")
+ 0, 0, 0, 0, // Sequence id
+ ]);
+}
+
+unittest {
+ import thrift.internal.test.protocol;
+ testContainerSizeLimit!(TBinaryProtocol!())();
+ testStringSizeLimit!(TBinaryProtocol!())();
+}
+
+/**
+ * TProtocolFactory creating a TBinaryProtocol instance for passed in
+ * transports.
+ *
+ * The optional Transports template tuple parameter can be used to specify
+ * one or more TTransport implementations to specifically instantiate
+ * TBinaryProtocol for. If the actual transport types encountered at
+ * runtime match one of the transports in the list, a specialized protocol
+ * instance is created. Otherwise, a generic TTransport version is used.
+ */
+class TBinaryProtocolFactory(Transports...) if (
+ allSatisfy!(isTTransport, Transports)
+) : TProtocolFactory {
+ ///
+ this (int containerSizeLimit = 0, int stringSizeLimit = 0,
+ bool strictRead = false, bool strictWrite = true
+ ) {
+ strictRead_ = strictRead;
+ strictWrite_ = strictWrite;
+ containerSizeLimit_ = containerSizeLimit;
+ stringSizeLimit_ = stringSizeLimit;
+ }
+
+ TProtocol getProtocol(TTransport trans) const {
+ foreach (Transport; TypeTuple!(Transports, TTransport)) {
+ auto concreteTrans = cast(Transport)trans;
+ if (concreteTrans) {
+ return new TBinaryProtocol!Transport(concreteTrans,
+ containerSizeLimit_, stringSizeLimit_, strictRead_, strictWrite_);
+ }
+ }
+ throw new TProtocolException(
+ "Passed null transport to TBinaryProtocolFactoy.");
+ }
+
+protected:
+ bool strictRead_;
+ bool strictWrite_;
+ int containerSizeLimit_;
+ int stringSizeLimit_;
+}
diff --git a/lib/d/src/thrift/protocol/compact.d b/lib/d/src/thrift/protocol/compact.d
new file mode 100644
index 000000000..5122f61c2
--- /dev/null
+++ b/lib/d/src/thrift/protocol/compact.d
@@ -0,0 +1,695 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.protocol.compact;
+
+import std.array : uninitializedArray;
+import std.typetuple : allSatisfy, TypeTuple;
+import thrift.protocol.base;
+import thrift.transport.base;
+import thrift.internal.endian;
+
+/**
+ * D implementation of the Compact protocol.
+ *
+ * See THRIFT-110 for a protocol description. This implementation is based on
+ * the C++ one.
+ */
+final class TCompactProtocol(Transport = TTransport) if (
+ isTTransport!Transport
+) : TProtocol {
+ /**
+ * Constructs a new instance.
+ *
+ * Params:
+ * trans = The transport to use.
+ * containerSizeLimit = If positive, the container size is limited to the
+ * given number of items.
+ * stringSizeLimit = If positive, the string length is limited to the
+ * given number of bytes.
+ */
+ this(Transport trans, int containerSizeLimit = 0, int stringSizeLimit = 0) {
+ trans_ = trans;
+ this.containerSizeLimit = containerSizeLimit;
+ this.stringSizeLimit = stringSizeLimit;
+ }
+
+ Transport transport() @property {
+ return trans_;
+ }
+
+ void reset() {
+ lastFieldId_ = 0;
+ fieldIdStack_ = null;
+ booleanField_ = TField.init;
+ hasBoolValue_ = false;
+ }
+
+ /**
+ * If positive, limits the number of items of deserialized containers to the
+ * given amount.
+ *
+ * This is useful to avoid allocating excessive amounts of memory when broken
+ * data is received. If the limit is exceeded, a SIZE_LIMIT-type
+ * TProtocolException is thrown.
+ *
+ * Defaults to zero (no limit).
+ */
+ int containerSizeLimit;
+
+ /**
+ * If positive, limits the length of deserialized strings/binary data to the
+ * given number of bytes.
+ *
+ * This is useful to avoid allocating excessive amounts of memory when broken
+ * data is received. If the limit is exceeded, a SIZE_LIMIT-type
+ * TProtocolException is thrown.
+ *
+ * Defaults to zero (no limit).
+ */
+ int stringSizeLimit;
+
+ /*
+ * Writing methods.
+ */
+
+ void writeBool(bool b) {
+ if (booleanField_.name !is null) {
+ // we haven't written the field header yet
+ writeFieldBeginInternal(booleanField_,
+ b ? CType.BOOLEAN_TRUE : CType.BOOLEAN_FALSE);
+ booleanField_.name = null;
+ } else {
+ // we're not part of a field, so just write the value
+ writeByte(b ? CType.BOOLEAN_TRUE : CType.BOOLEAN_FALSE);
+ }
+ }
+
+ void writeByte(byte b) {
+ trans_.write((cast(ubyte*)&b)[0..1]);
+ }
+
+ void writeI16(short i16) {
+ writeVarint32(i32ToZigzag(i16));
+ }
+
+ void writeI32(int i32) {
+ writeVarint32(i32ToZigzag(i32));
+ }
+
+ void writeI64(long i64) {
+ writeVarint64(i64ToZigzag(i64));
+ }
+
+ void writeDouble(double dub) {
+ ulong bits = hostToLe(*cast(ulong*)(&dub));
+ trans_.write((cast(ubyte*)&bits)[0 .. 8]);
+ }
+
+ void writeString(string str) {
+ writeBinary(cast(ubyte[])str);
+ }
+
+ void writeBinary(ubyte[] buf) {
+ assert(buf.length <= int.max);
+ writeVarint32(cast(int)buf.length);
+ trans_.write(buf);
+ }
+
+ void writeMessageBegin(TMessage msg) {
+ writeByte(cast(byte)PROTOCOL_ID);
+ writeByte((VERSION_N & VERSION_MASK) |
+ ((cast(int)msg.type << TYPE_SHIFT_AMOUNT) & TYPE_MASK));
+ writeVarint32(msg.seqid);
+ writeString(msg.name);
+ }
+ void writeMessageEnd() {}
+
+ void writeStructBegin(TStruct tstruct) {
+ fieldIdStack_ ~= lastFieldId_;
+ lastFieldId_ = 0;
+ }
+
+ void writeStructEnd() {
+ lastFieldId_ = fieldIdStack_[$ - 1];
+ fieldIdStack_ = fieldIdStack_[0 .. $ - 1];
+ fieldIdStack_.assumeSafeAppend();
+ }
+
+ void writeFieldBegin(TField field) {
+ if (field.type == TType.BOOL) {
+ booleanField_.name = field.name;
+ booleanField_.type = field.type;
+ booleanField_.id = field.id;
+ } else {
+ return writeFieldBeginInternal(field);
+ }
+ }
+ void writeFieldEnd() {}
+
+ void writeFieldStop() {
+ writeByte(TType.STOP);
+ }
+
+ void writeListBegin(TList list) {
+ writeCollectionBegin(list.elemType, list.size);
+ }
+ void writeListEnd() {}
+
+ void writeMapBegin(TMap map) {
+ if (map.size == 0) {
+ writeByte(0);
+ } else {
+ assert(map.size <= int.max);
+ writeVarint32(cast(int)map.size);
+ writeByte(cast(byte)(toCType(map.keyType) << 4 | toCType(map.valueType)));
+ }
+ }
+ void writeMapEnd() {}
+
+ void writeSetBegin(TSet set) {
+ writeCollectionBegin(set.elemType, set.size);
+ }
+ void writeSetEnd() {}
+
+
+ /*
+ * Reading methods.
+ */
+
+ bool readBool() {
+ if (hasBoolValue_ == true) {
+ hasBoolValue_ = false;
+ return boolValue_;
+ }
+
+ return readByte() == CType.BOOLEAN_TRUE;
+ }
+
+ byte readByte() {
+ ubyte[1] b = void;
+ trans_.readAll(b);
+ return cast(byte)b[0];
+ }
+
+ short readI16() {
+ return cast(short)zigzagToI32(readVarint32());
+ }
+
+ int readI32() {
+ return zigzagToI32(readVarint32());
+ }
+
+ long readI64() {
+ return zigzagToI64(readVarint64());
+ }
+
+ double readDouble() {
+ IntBuf!long b = void;
+ trans_.readAll(b.bytes);
+ b.value = leToHost(b.value);
+ return *cast(double*)(&b.value);
+ }
+
+ string readString() {
+ return cast(string)readBinary();
+ }
+
+ ubyte[] readBinary() {
+ auto size = readVarint32();
+ checkSize(size, stringSizeLimit);
+
+ if (size == 0) {
+ return null;
+ }
+
+ auto buf = uninitializedArray!(ubyte[])(size);
+ trans_.readAll(buf);
+ return buf;
+ }
+
+ TMessage readMessageBegin() {
+ TMessage msg = void;
+
+ auto protocolId = readByte();
+ if (protocolId != cast(byte)PROTOCOL_ID) {
+ throw new TProtocolException("Bad protocol identifier",
+ TProtocolException.Type.BAD_VERSION);
+ }
+
+ auto versionAndType = readByte();
+ auto ver = versionAndType & VERSION_MASK;
+ if (ver != VERSION_N) {
+ throw new TProtocolException("Bad protocol version",
+ TProtocolException.Type.BAD_VERSION);
+ }
+
+ msg.type = cast(TMessageType)((versionAndType >> TYPE_SHIFT_AMOUNT) & 0x03);
+ msg.seqid = readVarint32();
+ msg.name = readString();
+
+ return msg;
+ }
+ void readMessageEnd() {}
+
+ TStruct readStructBegin() {
+ fieldIdStack_ ~= lastFieldId_;
+ lastFieldId_ = 0;
+ return TStruct();
+ }
+
+ void readStructEnd() {
+ lastFieldId_ = fieldIdStack_[$ - 1];
+ fieldIdStack_ = fieldIdStack_[0 .. $ - 1];
+ }
+
+ TField readFieldBegin() {
+ TField f = void;
+ f.name = null;
+
+ auto bite = readByte();
+ auto type = cast(CType)(bite & 0x0f);
+
+ if (type == CType.STOP) {
+ // Struct stop byte, nothing more to do.
+ f.id = 0;
+ f.type = TType.STOP;
+ return f;
+ }
+
+ // Mask off the 4 MSB of the type header, which could contain a field id
+ // delta.
+ auto modifier = cast(short)((bite & 0xf0) >> 4);
+ if (modifier > 0) {
+ f.id = cast(short)(lastFieldId_ + modifier);
+ } else {
+ // Delta encoding not used, just read the id as usual.
+ f.id = readI16();
+ }
+ f.type = getTType(type);
+
+ if (type == CType.BOOLEAN_TRUE || type == CType.BOOLEAN_FALSE) {
+ // For boolean fields, the value is encoded in the type – keep it around
+ // for the readBool() call.
+ hasBoolValue_ = true;
+ boolValue_ = (type == CType.BOOLEAN_TRUE ? true : false);
+ }
+
+ lastFieldId_ = f.id;
+ return f;
+ }
+ void readFieldEnd() {}
+
+ TList readListBegin() {
+ auto sizeAndType = readByte();
+
+ auto lsize = (sizeAndType >> 4) & 0xf;
+ if (lsize == 0xf) {
+ lsize = readVarint32();
+ }
+ checkSize(lsize, containerSizeLimit);
+
+ TList l = void;
+ l.elemType = getTType(cast(CType)(sizeAndType & 0x0f));
+ l.size = cast(size_t)lsize;
+
+ return l;
+ }
+ void readListEnd() {}
+
+ TMap readMapBegin() {
+ TMap m = void;
+
+ auto size = readVarint32();
+ ubyte kvType;
+ if (size != 0) {
+ kvType = readByte();
+ }
+ checkSize(size, containerSizeLimit);
+
+ m.size = size;
+ m.keyType = getTType(cast(CType)(kvType >> 4));
+ m.valueType = getTType(cast(CType)(kvType & 0xf));
+
+ return m;
+ }
+ void readMapEnd() {}
+
+ TSet readSetBegin() {
+ auto sizeAndType = readByte();
+
+ auto lsize = (sizeAndType >> 4) & 0xf;
+ if (lsize == 0xf) {
+ lsize = readVarint32();
+ }
+ checkSize(lsize, containerSizeLimit);
+
+ TSet s = void;
+ s.elemType = getTType(cast(CType)(sizeAndType & 0xf));
+ s.size = cast(size_t)lsize;
+
+ return s;
+ }
+ void readSetEnd() {}
+
+private:
+ void writeFieldBeginInternal(TField field, byte typeOverride = -1) {
+ // If there's a type override, use that.
+ auto typeToWrite = (typeOverride == -1 ? toCType(field.type) : typeOverride);
+
+ // check if we can use delta encoding for the field id
+ if (field.id > lastFieldId_ && (field.id - lastFieldId_) <= 15) {
+ // write them together
+ writeByte(cast(byte)((field.id - lastFieldId_) << 4 | typeToWrite));
+ } else {
+ // write them separate
+ writeByte(cast(byte)typeToWrite);
+ writeI16(field.id);
+ }
+
+ lastFieldId_ = field.id;
+ }
+
+
+ void writeCollectionBegin(TType elemType, size_t size) {
+ if (size <= 14) {
+ writeByte(cast(byte)(size << 4 | toCType(elemType)));
+ } else {
+ assert(size <= int.max);
+ writeByte(0xf0 | toCType(elemType));
+ writeVarint32(cast(int)size);
+ }
+ }
+
+ void writeVarint32(uint n) {
+ ubyte[5] buf = void;
+ ubyte wsize;
+
+ while (true) {
+ if ((n & ~0x7F) == 0) {
+ buf[wsize++] = cast(ubyte)n;
+ break;
+ } else {
+ buf[wsize++] = cast(ubyte)((n & 0x7F) | 0x80);
+ n >>= 7;
+ }
+ }
+
+ trans_.write(buf[0 .. wsize]);
+ }
+
+ /*
+ * Write an i64 as a varint. Results in 1-10 bytes on the wire.
+ */
+ void writeVarint64(ulong n) {
+ ubyte[10] buf = void;
+ ubyte wsize;
+
+ while (true) {
+ if ((n & ~0x7FL) == 0) {
+ buf[wsize++] = cast(ubyte)n;
+ break;
+ } else {
+ buf[wsize++] = cast(ubyte)((n & 0x7F) | 0x80);
+ n >>= 7;
+ }
+ }
+
+ trans_.write(buf[0 .. wsize]);
+ }
+
+ /*
+ * Convert l into a zigzag long. This allows negative numbers to be
+ * represented compactly as a varint.
+ */
+ ulong i64ToZigzag(long l) {
+ return (l << 1) ^ (l >> 63);
+ }
+
+ /*
+ * Convert n into a zigzag int. This allows negative numbers to be
+ * represented compactly as a varint.
+ */
+ uint i32ToZigzag(int n) {
+ return (n << 1) ^ (n >> 31);
+ }
+
+ CType toCType(TType type) {
+ final switch (type) {
+ case TType.STOP:
+ return CType.STOP;
+ case TType.BOOL:
+ return CType.BOOLEAN_TRUE;
+ case TType.BYTE:
+ return CType.BYTE;
+ case TType.DOUBLE:
+ return CType.DOUBLE;
+ case TType.I16:
+ return CType.I16;
+ case TType.I32:
+ return CType.I32;
+ case TType.I64:
+ return CType.I64;
+ case TType.STRING:
+ return CType.BINARY;
+ case TType.STRUCT:
+ return CType.STRUCT;
+ case TType.MAP:
+ return CType.MAP;
+ case TType.SET:
+ return CType.SET;
+ case TType.LIST:
+ return CType.LIST;
+ }
+ }
+
+ int readVarint32() {
+ return cast(int)readVarint64();
+ }
+
+ long readVarint64() {
+ ulong val;
+ ubyte shift;
+ ubyte[10] buf = void; // 64 bits / (7 bits/byte) = 10 bytes.
+ auto bufSize = buf.sizeof;
+ auto borrowed = trans_.borrow(buf.ptr, bufSize);
+
+ ubyte rsize;
+
+ if (borrowed) {
+ // Fast path.
+ while (true) {
+ auto bite = borrowed[rsize];
+ rsize++;
+ val |= cast(ulong)(bite & 0x7f) << shift;
+ shift += 7;
+ if (!(bite & 0x80)) {
+ trans_.consume(rsize);
+ return val;
+ }
+ // Have to check for invalid data so we don't crash.
+ if (rsize == buf.sizeof) {
+ throw new TProtocolException(TProtocolException.Type.INVALID_DATA,
+ "Variable-length int over 10 bytes.");
+ }
+ }
+ } else {
+ // Slow path.
+ while (true) {
+ ubyte[1] bite;
+ trans_.readAll(bite);
+ ++rsize;
+
+ val |= cast(ulong)(bite[0] & 0x7f) << shift;
+ shift += 7;
+ if (!(bite[0] & 0x80)) {
+ return val;
+ }
+
+ // Might as well check for invalid data on the slow path too.
+ if (rsize >= buf.sizeof) {
+ throw new TProtocolException(TProtocolException.Type.INVALID_DATA,
+ "Variable-length int over 10 bytes.");
+ }
+ }
+ }
+ }
+
+ /*
+ * Convert from zigzag int to int.
+ */
+ int zigzagToI32(uint n) {
+ return (n >> 1) ^ -(n & 1);
+ }
+
+ /*
+ * Convert from zigzag long to long.
+ */
+ long zigzagToI64(ulong n) {
+ return (n >> 1) ^ -(n & 1);
+ }
+
+ TType getTType(CType type) {
+ final switch (type) {
+ case CType.STOP:
+ return TType.STOP;
+ case CType.BOOLEAN_FALSE:
+ return TType.BOOL;
+ case CType.BOOLEAN_TRUE:
+ return TType.BOOL;
+ case CType.BYTE:
+ return TType.BYTE;
+ case CType.I16:
+ return TType.I16;
+ case CType.I32:
+ return TType.I32;
+ case CType.I64:
+ return TType.I64;
+ case CType.DOUBLE:
+ return TType.DOUBLE;
+ case CType.BINARY:
+ return TType.STRING;
+ case CType.LIST:
+ return TType.LIST;
+ case CType.SET:
+ return TType.SET;
+ case CType.MAP:
+ return TType.MAP;
+ case CType.STRUCT:
+ return TType.STRUCT;
+ }
+ }
+
+ void checkSize(int size, int limit) {
+ if (size < 0) {
+ throw new TProtocolException(TProtocolException.Type.NEGATIVE_SIZE);
+ } else if (limit > 0 && size > limit) {
+ throw new TProtocolException(TProtocolException.Type.SIZE_LIMIT);
+ }
+ }
+
+ enum PROTOCOL_ID = 0x82;
+ enum VERSION_N = 1;
+ enum VERSION_MASK = 0b0001_1111;
+ enum TYPE_MASK = 0b1110_0000;
+ enum TYPE_SHIFT_AMOUNT = 5;
+
+ // Probably need to implement a better stack at some point.
+ short[] fieldIdStack_;
+ short lastFieldId_;
+
+ TField booleanField_;
+
+ bool hasBoolValue_;
+ bool boolValue_;
+
+ Transport trans_;
+}
+
+/**
+ * TCompactProtocol construction helper to avoid having to explicitly specify
+ * the transport type, i.e. to allow the constructor being called using IFTI
+ * (see $(LINK2 http://d.puremagic.com/issues/show_bug.cgi?id=6082, D Bugzilla
+ * enhancement requet 6082)).
+ */
+TCompactProtocol!Transport tCompactProtocol(Transport)(Transport trans,
+ int containerSizeLimit = 0, int stringSizeLimit = 0
+) if (isTTransport!Transport)
+{
+ return new TCompactProtocol!Transport(trans,
+ containerSizeLimit, stringSizeLimit);
+}
+
+private {
+ enum CType : ubyte {
+ STOP = 0x0,
+ BOOLEAN_TRUE = 0x1,
+ BOOLEAN_FALSE = 0x2,
+ BYTE = 0x3,
+ I16 = 0x4,
+ I32 = 0x5,
+ I64 = 0x6,
+ DOUBLE = 0x7,
+ BINARY = 0x8,
+ LIST = 0x9,
+ SET = 0xa,
+ MAP = 0xb,
+ STRUCT = 0xc
+ }
+ static assert(CType.max <= 0xf,
+ "Compact protocol wire type representation must fit into 4 bits.");
+}
+
+unittest {
+ import std.exception;
+ import thrift.transport.memory;
+
+ // Check the message header format.
+ auto buf = new TMemoryBuffer;
+ auto compact = tCompactProtocol(buf);
+ compact.writeMessageBegin(TMessage("foo", TMessageType.CALL, 0));
+
+ auto header = new ubyte[7];
+ buf.readAll(header);
+ enforce(header == [
+ 130, // Protocol id.
+ 33, // Version/type byte.
+ 0, // Sequence id.
+ 3, 102, 111, 111 // Method name.
+ ]);
+}
+
+unittest {
+ import thrift.internal.test.protocol;
+ testContainerSizeLimit!(TCompactProtocol!())();
+ testStringSizeLimit!(TCompactProtocol!())();
+}
+
+/**
+ * TProtocolFactory creating a TCompactProtocol instance for passed in
+ * transports.
+ *
+ * The optional Transports template tuple parameter can be used to specify
+ * one or more TTransport implementations to specifically instantiate
+ * TCompactProtocol for. If the actual transport types encountered at
+ * runtime match one of the transports in the list, a specialized protocol
+ * instance is created. Otherwise, a generic TTransport version is used.
+ */
+class TCompactProtocolFactory(Transports...) if (
+ allSatisfy!(isTTransport, Transports)
+) : TProtocolFactory {
+ ///
+ this(int containerSizeLimit = 0, int stringSizeLimit = 0) {
+ containerSizeLimit_ = 0;
+ stringSizeLimit_ = 0;
+ }
+
+ TProtocol getProtocol(TTransport trans) const {
+ foreach (Transport; TypeTuple!(Transports, TTransport)) {
+ auto concreteTrans = cast(Transport)trans;
+ if (concreteTrans) {
+ return new TCompactProtocol!Transport(concreteTrans);
+ }
+ }
+ throw new TProtocolException(
+ "Passed null transport to TCompactProtocolFactory.");
+ }
+
+ int containerSizeLimit_;
+ int stringSizeLimit_;
+}
diff --git a/lib/d/src/thrift/protocol/json.d b/lib/d/src/thrift/protocol/json.d
new file mode 100644
index 000000000..7d35ba258
--- /dev/null
+++ b/lib/d/src/thrift/protocol/json.d
@@ -0,0 +1,979 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.protocol.json;
+
+import std.algorithm;
+import std.array;
+import std.base64;
+import std.conv;
+import std.range;
+import std.string : format;
+import std.traits : isIntegral;
+import std.typetuple : allSatisfy, TypeTuple;
+import thrift.protocol.base;
+import thrift.transport.base;
+
+alias Base64Impl!('+', '/', Base64.NoPadding) Base64NoPad;
+
+/**
+ * Implementation of the Thrift JSON protocol.
+ */
+final class TJsonProtocol(Transport = TTransport) if (
+ isTTransport!Transport
+) : TProtocol {
+ /**
+ * Constructs a new instance.
+ *
+ * Params:
+ * trans = The transport to use.
+ * containerSizeLimit = If positive, the container size is limited to the
+ * given number of items.
+ * stringSizeLimit = If positive, the string length is limited to the
+ * given number of bytes.
+ */
+ this(Transport trans, int containerSizeLimit = 0, int stringSizeLimit = 0) {
+ trans_ = trans;
+ this.containerSizeLimit = containerSizeLimit;
+ this.stringSizeLimit = stringSizeLimit;
+
+ context_ = new Context();
+ reader_ = new LookaheadReader(trans);
+ }
+
+ Transport transport() @property {
+ return trans_;
+ }
+
+ void reset() {
+ contextStack_.clear();
+ context_ = new Context();
+ reader_ = new LookaheadReader(trans_);
+ }
+
+ /**
+ * If positive, limits the number of items of deserialized containers to the
+ * given amount.
+ *
+ * This is useful to avoid allocating excessive amounts of memory when broken
+ * data is received. If the limit is exceeded, a SIZE_LIMIT-type
+ * TProtocolException is thrown.
+ *
+ * Defaults to zero (no limit).
+ */
+ int containerSizeLimit;
+
+ /**
+ * If positive, limits the length of deserialized strings/binary data to the
+ * given number of bytes.
+ *
+ * This is useful to avoid allocating excessive amounts of memory when broken
+ * data is received. If the limit is exceeded, a SIZE_LIMIT-type
+ * TProtocolException is thrown.
+ *
+ * Note: For binary data, the limit applies to the length of the
+ * Base64-encoded string data, not the resulting byte array.
+ *
+ * Defaults to zero (no limit).
+ */
+ int stringSizeLimit;
+
+ /*
+ * Writing methods.
+ */
+
+ void writeBool(bool b) {
+ writeJsonInteger(b ? 1 : 0);
+ }
+
+ void writeByte(byte b) {
+ writeJsonInteger(b);
+ }
+
+ void writeI16(short i16) {
+ writeJsonInteger(i16);
+ }
+
+ void writeI32(int i32) {
+ writeJsonInteger(i32);
+ }
+
+ void writeI64(long i64) {
+ writeJsonInteger(i64);
+ }
+
+ void writeDouble(double dub) {
+ context_.write(trans_);
+
+ string value;
+ if (dub is double.nan) {
+ value = NAN_STRING;
+ } else if (dub is double.infinity) {
+ value = INFINITY_STRING;
+ } else if (dub is -double.infinity) {
+ value = NEG_INFINITY_STRING;
+ }
+
+ bool escapeNum = value !is null || context_.escapeNum;
+
+ if (value is null) {
+ value = format("%.16g", dub);
+ }
+
+ if (escapeNum) trans_.write(STRING_DELIMITER);
+ trans_.write(cast(ubyte[])value);
+ if (escapeNum) trans_.write(STRING_DELIMITER);
+ }
+
+ void writeString(string str) {
+ context_.write(trans_);
+ trans_.write(STRING_DELIMITER);
+ foreach (c; str) {
+ writeJsonChar(c);
+ }
+ trans_.write(STRING_DELIMITER);
+ }
+
+ void writeBinary(ubyte[] buf) {
+ context_.write(trans_);
+
+ trans_.write(STRING_DELIMITER);
+ ubyte[4] b;
+ while (!buf.empty) {
+ auto toWrite = take(buf, 3);
+ Base64NoPad.encode(toWrite, b[]);
+ trans_.write(b[0 .. toWrite.length + 1]);
+ buf.popFrontN(toWrite.length);
+ }
+ trans_.write(STRING_DELIMITER);
+ }
+
+ void writeMessageBegin(TMessage msg) {
+ writeJsonArrayBegin();
+ writeJsonInteger(THRIFT_JSON_VERSION);
+ writeString(msg.name);
+ writeJsonInteger(cast(byte)msg.type);
+ writeJsonInteger(msg.seqid);
+ }
+
+ void writeMessageEnd() {
+ writeJsonArrayEnd();
+ }
+
+ void writeStructBegin(TStruct tstruct) {
+ writeJsonObjectBegin();
+ }
+
+ void writeStructEnd() {
+ writeJsonObjectEnd();
+ }
+
+ void writeFieldBegin(TField field) {
+ writeJsonInteger(field.id);
+ writeJsonObjectBegin();
+ writeString(getNameFromTType(field.type));
+ }
+
+ void writeFieldEnd() {
+ writeJsonObjectEnd();
+ }
+
+ void writeFieldStop() {}
+
+ void writeListBegin(TList list) {
+ writeJsonArrayBegin();
+ writeString(getNameFromTType(list.elemType));
+ writeJsonInteger(list.size);
+ }
+
+ void writeListEnd() {
+ writeJsonArrayEnd();
+ }
+
+ void writeMapBegin(TMap map) {
+ writeJsonArrayBegin();
+ writeString(getNameFromTType(map.keyType));
+ writeString(getNameFromTType(map.valueType));
+ writeJsonInteger(map.size);
+ writeJsonObjectBegin();
+ }
+
+ void writeMapEnd() {
+ writeJsonObjectEnd();
+ writeJsonArrayEnd();
+ }
+
+ void writeSetBegin(TSet set) {
+ writeJsonArrayBegin();
+ writeString(getNameFromTType(set.elemType));
+ writeJsonInteger(set.size);
+ }
+
+ void writeSetEnd() {
+ writeJsonArrayEnd();
+ }
+
+
+ /*
+ * Reading methods.
+ */
+
+ bool readBool() {
+ return readJsonInteger!byte() ? true : false;
+ }
+
+ byte readByte() {
+ return readJsonInteger!byte();
+ }
+
+ short readI16() {
+ return readJsonInteger!short();
+ }
+
+ int readI32() {
+ return readJsonInteger!int();
+ }
+
+ long readI64() {
+ return readJsonInteger!long();
+ }
+
+ double readDouble() {
+ context_.read(reader_);
+
+ if (reader_.peek() == STRING_DELIMITER) {
+ auto str = readJsonString(true);
+ if (str == NAN_STRING) {
+ return double.nan;
+ }
+ if (str == INFINITY_STRING) {
+ return double.infinity;
+ }
+ if (str == NEG_INFINITY_STRING) {
+ return -double.infinity;
+ }
+
+ if (!context_.escapeNum) {
+ // Throw exception -- we should not be in a string in this case
+ throw new TProtocolException("Numeric data unexpectedly quoted",
+ TProtocolException.Type.INVALID_DATA);
+ }
+ try {
+ return to!double(str);
+ } catch (ConvException e) {
+ throw new TProtocolException(`Expected numeric value; got "` ~ str ~
+ `".`, TProtocolException.Type.INVALID_DATA);
+ }
+ }
+ else {
+ if (context_.escapeNum) {
+ // This will throw - we should have had a quote if escapeNum == true
+ readJsonSyntaxChar(STRING_DELIMITER);
+ }
+
+ auto str = readJsonNumericChars();
+ try {
+ return to!double(str);
+ } catch (ConvException e) {
+ throw new TProtocolException(`Expected numeric value; got "` ~ str ~
+ `".`, TProtocolException.Type.INVALID_DATA);
+ }
+ }
+ }
+
+ string readString() {
+ return readJsonString(false);
+ }
+
+ ubyte[] readBinary() {
+ return Base64NoPad.decode(readString());
+ }
+
+ TMessage readMessageBegin() {
+ TMessage msg = void;
+
+ readJsonArrayBegin();
+
+ auto ver = readJsonInteger!short();
+ if (ver != THRIFT_JSON_VERSION) {
+ throw new TProtocolException("Message contained bad version.",
+ TProtocolException.Type.BAD_VERSION);
+ }
+
+ msg.name = readString();
+ msg.type = cast(TMessageType)readJsonInteger!byte();
+ msg.seqid = readJsonInteger!short();
+
+ return msg;
+ }
+
+ void readMessageEnd() {
+ readJsonArrayEnd();
+ }
+
+ TStruct readStructBegin() {
+ readJsonObjectBegin();
+ return TStruct();
+ }
+
+ void readStructEnd() {
+ readJsonObjectEnd();
+ }
+
+ TField readFieldBegin() {
+ TField f = void;
+ f.name = null;
+
+ auto ch = reader_.peek();
+ if (ch == OBJECT_END) {
+ f.type = TType.STOP;
+ } else {
+ f.id = readJsonInteger!short();
+ readJsonObjectBegin();
+ f.type = getTTypeFromName(readString());
+ }
+
+ return f;
+ }
+
+ void readFieldEnd() {
+ readJsonObjectEnd();
+ }
+
+ TList readListBegin() {
+ readJsonArrayBegin();
+ auto type = getTTypeFromName(readString());
+ auto size = readContainerSize();
+ return TList(type, size);
+ }
+
+ void readListEnd() {
+ readJsonArrayEnd();
+ }
+
+ TMap readMapBegin() {
+ readJsonArrayBegin();
+ auto keyType = getTTypeFromName(readString());
+ auto valueType = getTTypeFromName(readString());
+ auto size = readContainerSize();
+ readJsonObjectBegin();
+ return TMap(keyType, valueType, size);
+ }
+
+ void readMapEnd() {
+ readJsonObjectEnd();
+ readJsonArrayEnd();
+ }
+
+ TSet readSetBegin() {
+ readJsonArrayBegin();
+ auto type = getTTypeFromName(readString());
+ auto size = readContainerSize();
+ return TSet(type, size);
+ }
+
+ void readSetEnd() {
+ readJsonArrayEnd();
+ }
+
+private:
+ void pushContext(Context c) {
+ contextStack_ ~= context_;
+ context_ = c;
+ }
+
+ void popContext() {
+ context_ = contextStack_.back;
+ contextStack_.popBack();
+ contextStack_.assumeSafeAppend();
+ }
+
+ /*
+ * Writing functions
+ */
+
+ // Write the character ch as a Json escape sequence ("\u00xx")
+ void writeJsonEscapeChar(ubyte ch) {
+ trans_.write(ESCAPE_PREFIX);
+ trans_.write(ESCAPE_PREFIX);
+ auto outCh = hexChar(cast(ubyte)(ch >> 4));
+ trans_.write((&outCh)[0 .. 1]);
+ outCh = hexChar(ch);
+ trans_.write((&outCh)[0 .. 1]);
+ }
+
+ // Write the character ch as part of a Json string, escaping as appropriate.
+ void writeJsonChar(ubyte ch) {
+ if (ch >= 0x30) {
+ if (ch == '\\') { // Only special character >= 0x30 is '\'
+ trans_.write(BACKSLASH);
+ trans_.write(BACKSLASH);
+ } else {
+ trans_.write((&ch)[0 .. 1]);
+ }
+ }
+ else {
+ auto outCh = kJsonCharTable[ch];
+ // Check if regular character, backslash escaped, or Json escaped
+ if (outCh == 1) {
+ trans_.write((&ch)[0 .. 1]);
+ } else if (outCh > 1) {
+ trans_.write(BACKSLASH);
+ trans_.write((&outCh)[0 .. 1]);
+ } else {
+ writeJsonEscapeChar(ch);
+ }
+ }
+ }
+
+ // Convert the given integer type to a Json number, or a string
+ // if the context requires it (eg: key in a map pair).
+ void writeJsonInteger(T)(T num) if (isIntegral!T) {
+ context_.write(trans_);
+
+ auto escapeNum = context_.escapeNum();
+ if (escapeNum) trans_.write(STRING_DELIMITER);
+ trans_.write(cast(ubyte[])to!string(num));
+ if (escapeNum) trans_.write(STRING_DELIMITER);
+ }
+
+ void writeJsonObjectBegin() {
+ context_.write(trans_);
+ trans_.write(OBJECT_BEGIN);
+ pushContext(new PairContext());
+ }
+
+ void writeJsonObjectEnd() {
+ popContext();
+ trans_.write(OBJECT_END);
+ }
+
+ void writeJsonArrayBegin() {
+ context_.write(trans_);
+ trans_.write(ARRAY_BEGIN);
+ pushContext(new ListContext());
+ }
+
+ void writeJsonArrayEnd() {
+ popContext();
+ trans_.write(ARRAY_END);
+ }
+
+ /*
+ * Reading functions
+ */
+
+ int readContainerSize() {
+ auto size = readJsonInteger!int();
+ if (size < 0) {
+ throw new TProtocolException(TProtocolException.Type.NEGATIVE_SIZE);
+ } else if (containerSizeLimit > 0 && size > containerSizeLimit) {
+ throw new TProtocolException(TProtocolException.Type.SIZE_LIMIT);
+ }
+ return size;
+ }
+
+ void readJsonSyntaxChar(ubyte[1] ch) {
+ return readSyntaxChar(reader_, ch);
+ }
+
+ ubyte readJsonEscapeChar() {
+ readJsonSyntaxChar(ZERO_CHAR);
+ readJsonSyntaxChar(ZERO_CHAR);
+ auto a = reader_.read();
+ auto b = reader_.read();
+ return cast(ubyte)((hexVal(a[0]) << 4) + hexVal(b[0]));
+ }
+
+ string readJsonString(bool skipContext = false) {
+ if (!skipContext) context_.read(reader_);
+
+ readJsonSyntaxChar(STRING_DELIMITER);
+ auto buffer = appender!string();
+
+ int bytesRead;
+ while (true) {
+ auto ch = reader_.read();
+ if (ch == STRING_DELIMITER) {
+ break;
+ }
+
+ ++bytesRead;
+ if (stringSizeLimit > 0 && bytesRead > stringSizeLimit) {
+ throw new TProtocolException(TProtocolException.Type.SIZE_LIMIT);
+ }
+
+ if (ch == BACKSLASH) {
+ ch = reader_.read();
+ if (ch == ESCAPE_CHAR) {
+ ch = readJsonEscapeChar();
+ } else {
+ auto pos = countUntil(kEscapeChars[], ch[0]);
+ if (pos == -1) {
+ throw new TProtocolException("Expected control char, got '" ~
+ cast(char)ch[0] ~ "'.", TProtocolException.Type.INVALID_DATA);
+ }
+ ch = kEscapeCharVals[pos];
+ }
+ }
+ buffer.put(ch[0]);
+ }
+
+ return buffer.data;
+ }
+
+ // Reads a sequence of characters, stopping at the first one that is not
+ // a valid Json numeric character.
+ string readJsonNumericChars() {
+ string str;
+ while (true) {
+ auto ch = reader_.peek();
+ if (!isJsonNumeric(ch[0])) {
+ break;
+ }
+ reader_.read();
+ str ~= ch;
+ }
+ return str;
+ }
+
+ // Reads a sequence of characters and assembles them into a number,
+ // returning them via num
+ T readJsonInteger(T)() if (isIntegral!T) {
+ context_.read(reader_);
+ if (context_.escapeNum()) {
+ readJsonSyntaxChar(STRING_DELIMITER);
+ }
+ auto str = readJsonNumericChars();
+ T num;
+ try {
+ num = to!T(str);
+ } catch (ConvException e) {
+ throw new TProtocolException(`Expected numeric value, got "` ~ str ~ `".`,
+ TProtocolException.Type.INVALID_DATA);
+ }
+ if (context_.escapeNum()) {
+ readJsonSyntaxChar(STRING_DELIMITER);
+ }
+ return num;
+ }
+
+ void readJsonObjectBegin() {
+ context_.read(reader_);
+ readJsonSyntaxChar(OBJECT_BEGIN);
+ pushContext(new PairContext());
+ }
+
+ void readJsonObjectEnd() {
+ readJsonSyntaxChar(OBJECT_END);
+ popContext();
+ }
+
+ void readJsonArrayBegin() {
+ context_.read(reader_);
+ readJsonSyntaxChar(ARRAY_BEGIN);
+ pushContext(new ListContext());
+ }
+
+ void readJsonArrayEnd() {
+ readJsonSyntaxChar(ARRAY_END);
+ popContext();
+ }
+
+ static {
+ final class LookaheadReader {
+ this(Transport trans) {
+ trans_ = trans;
+ }
+
+ ubyte[1] read() {
+ if (hasData_) {
+ hasData_ = false;
+ } else {
+ trans_.readAll(data_);
+ }
+ return data_;
+ }
+
+ ubyte[1] peek() {
+ if (!hasData_) {
+ trans_.readAll(data_);
+ hasData_ = true;
+ }
+ return data_;
+ }
+
+ private:
+ Transport trans_;
+ bool hasData_;
+ ubyte[1] data_;
+ }
+
+ /*
+ * Class to serve as base Json context and as base class for other context
+ * implementations
+ */
+ class Context {
+ /**
+ * Write context data to the transport. Default is to do nothing.
+ */
+ void write(Transport trans) {}
+
+ /**
+ * Read context data from the transport. Default is to do nothing.
+ */
+ void read(LookaheadReader reader) {}
+
+ /**
+ * Return true if numbers need to be escaped as strings in this context.
+ * Default behavior is to return false.
+ */
+ bool escapeNum() @property {
+ return false;
+ }
+ }
+
+ // Context class for object member key-value pairs
+ class PairContext : Context {
+ this() {
+ first_ = true;
+ colon_ = true;
+ }
+
+ override void write(Transport trans) {
+ if (first_) {
+ first_ = false;
+ colon_ = true;
+ } else {
+ trans.write(colon_ ? PAIR_SEP : ELEM_SEP);
+ colon_ = !colon_;
+ }
+ }
+
+ override void read(LookaheadReader reader) {
+ if (first_) {
+ first_ = false;
+ colon_ = true;
+ } else {
+ auto ch = (colon_ ? PAIR_SEP : ELEM_SEP);
+ colon_ = !colon_;
+ return readSyntaxChar(reader, ch);
+ }
+ }
+
+ // Numbers must be turned into strings if they are the key part of a pair
+ override bool escapeNum() @property {
+ return colon_;
+ }
+
+ private:
+ bool first_;
+ bool colon_;
+ }
+
+ class ListContext : Context {
+ this() {
+ first_ = true;
+ }
+
+ override void write(Transport trans) {
+ if (first_) {
+ first_ = false;
+ } else {
+ trans.write(ELEM_SEP);
+ }
+ }
+
+ override void read(LookaheadReader reader) {
+ if (first_) {
+ first_ = false;
+ } else {
+ readSyntaxChar(reader, ELEM_SEP);
+ }
+ }
+
+ private:
+ bool first_;
+ }
+
+ // Read 1 character from the transport trans and verify that it is the
+ // expected character ch.
+ // Throw a protocol exception if it is not.
+ void readSyntaxChar(LookaheadReader reader, ubyte[1] ch) {
+ auto ch2 = reader.read();
+ if (ch2 != ch) {
+ throw new TProtocolException("Expected '" ~ cast(char)ch[0] ~ "', got '" ~
+ cast(char)ch2[0] ~ "'.", TProtocolException.Type.INVALID_DATA);
+ }
+ }
+ }
+
+ // Probably need to implement a better stack at some point.
+ Context[] contextStack_;
+ Context context_;
+
+ Transport trans_;
+ LookaheadReader reader_;
+}
+
+/**
+ * TJsonProtocol construction helper to avoid having to explicitly specify
+ * the transport type, i.e. to allow the constructor being called using IFTI
+ * (see $(LINK2 http://d.puremagic.com/issues/show_bug.cgi?id=6082, D Bugzilla
+ * enhancement requet 6082)).
+ */
+TJsonProtocol!Transport tJsonProtocol(Transport)(Transport trans,
+ int containerSizeLimit = 0, int stringSizeLimit = 0
+) if (isTTransport!Transport) {
+ return new TJsonProtocol!Transport(trans, containerSizeLimit, stringSizeLimit);
+}
+
+unittest {
+ import std.exception;
+ import thrift.transport.memory;
+
+ // Check the message header format.
+ auto buf = new TMemoryBuffer;
+ auto json = tJsonProtocol(buf);
+ json.writeMessageBegin(TMessage("foo", TMessageType.CALL, 0));
+ json.writeMessageEnd();
+
+ auto header = new ubyte[13];
+ buf.readAll(header);
+ enforce(cast(char[])header == `[1,"foo",1,0]`);
+}
+
+unittest {
+ import std.exception;
+ import thrift.transport.memory;
+
+ // Check that short binary data is read correctly (the Thrift JSON format
+ // does not include padding chars in the Base64 encoded data).
+ auto buf = new TMemoryBuffer;
+ auto json = tJsonProtocol(buf);
+ json.writeBinary([1, 2]);
+ json.reset();
+ enforce(json.readBinary() == [1, 2]);
+}
+
+unittest {
+ import thrift.internal.test.protocol;
+ testContainerSizeLimit!(TJsonProtocol!())();
+ testStringSizeLimit!(TJsonProtocol!())();
+}
+
+/**
+ * TProtocolFactory creating a TJsonProtocol instance for passed in
+ * transports.
+ *
+ * The optional Transports template tuple parameter can be used to specify
+ * one or more TTransport implementations to specifically instantiate
+ * TJsonProtocol for. If the actual transport types encountered at
+ * runtime match one of the transports in the list, a specialized protocol
+ * instance is created. Otherwise, a generic TTransport version is used.
+ */
+class TJsonProtocolFactory(Transports...) if (
+ allSatisfy!(isTTransport, Transports)
+) : TProtocolFactory {
+ TProtocol getProtocol(TTransport trans) const {
+ foreach (Transport; TypeTuple!(Transports, TTransport)) {
+ auto concreteTrans = cast(Transport)trans;
+ if (concreteTrans) {
+ auto p = new TJsonProtocol!Transport(concreteTrans);
+ return p;
+ }
+ }
+ throw new TProtocolException(
+ "Passed null transport to TJsonProtocolFactoy.");
+ }
+}
+
+private {
+ immutable ubyte[1] OBJECT_BEGIN = '{';
+ immutable ubyte[1] OBJECT_END = '}';
+ immutable ubyte[1] ARRAY_BEGIN = '[';
+ immutable ubyte[1] ARRAY_END = ']';
+ immutable ubyte[1] NEWLINE = '\n';
+ immutable ubyte[1] PAIR_SEP = ':';
+ immutable ubyte[1] ELEM_SEP = ',';
+ immutable ubyte[1] BACKSLASH = '\\';
+ immutable ubyte[1] STRING_DELIMITER = '"';
+ immutable ubyte[1] ZERO_CHAR = '0';
+ immutable ubyte[1] ESCAPE_CHAR = 'u';
+ immutable ubyte[4] ESCAPE_PREFIX = cast(ubyte[4])r"\u00";
+
+ enum THRIFT_JSON_VERSION = 1;
+
+ immutable NAN_STRING = "NaN";
+ immutable INFINITY_STRING = "Infinity";
+ immutable NEG_INFINITY_STRING = "-Infinity";
+
+ string getNameFromTType(TType typeID) {
+ final switch (typeID) {
+ case TType.BOOL:
+ return "tf";
+ case TType.BYTE:
+ return "i8";
+ case TType.I16:
+ return "i16";
+ case TType.I32:
+ return "i32";
+ case TType.I64:
+ return "i64";
+ case TType.DOUBLE:
+ return "dbl";
+ case TType.STRING:
+ return "str";
+ case TType.STRUCT:
+ return "rec";
+ case TType.MAP:
+ return "map";
+ case TType.LIST:
+ return "lst";
+ case TType.SET:
+ return "set";
+ }
+ }
+
+ TType getTTypeFromName(string name) {
+ TType result;
+ if (name.length > 1) {
+ switch (name[0]) {
+ case 'd':
+ result = TType.DOUBLE;
+ break;
+ case 'i':
+ switch (name[1]) {
+ case '8':
+ result = TType.BYTE;
+ break;
+ case '1':
+ result = TType.I16;
+ break;
+ case '3':
+ result = TType.I32;
+ break;
+ case '6':
+ result = TType.I64;
+ break;
+ default:
+ // Do nothing.
+ }
+ break;
+ case 'l':
+ result = TType.LIST;
+ break;
+ case 'm':
+ result = TType.MAP;
+ break;
+ case 'r':
+ result = TType.STRUCT;
+ break;
+ case 's':
+ if (name[1] == 't') {
+ result = TType.STRING;
+ }
+ else if (name[1] == 'e') {
+ result = TType.SET;
+ }
+ break;
+ case 't':
+ result = TType.BOOL;
+ break;
+ default:
+ // Do nothing.
+ }
+ }
+ if (result == TType.STOP) {
+ throw new TProtocolException("Unrecognized type",
+ TProtocolException.Type.NOT_IMPLEMENTED);
+ }
+ return result;
+ }
+
+ // This table describes the handling for the first 0x30 characters
+ // 0 : escape using "\u00xx" notation
+ // 1 : just output index
+ // <other> : escape using "\<other>" notation
+ immutable ubyte[0x30] kJsonCharTable = [
+ // 0 1 2 3 4 5 6 7 8 9 A B C D E F
+ 0, 0, 0, 0, 0, 0, 0, 0,'b','t','n', 0,'f','r', 0, 0, // 0
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1
+ 1, 1,'"', 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2
+ ];
+
+ // This string's characters must match up with the elements in kEscapeCharVals.
+ // I don't have '/' on this list even though it appears on www.json.org --
+ // it is not in the RFC
+ immutable kEscapeChars = cast(ubyte[7]) `"\\bfnrt`;
+
+ // The elements of this array must match up with the sequence of characters in
+ // kEscapeChars
+ immutable ubyte[7] kEscapeCharVals = [
+ '"', '\\', '\b', '\f', '\n', '\r', '\t',
+ ];
+
+ // Return the integer value of a hex character ch.
+ // Throw a protocol exception if the character is not [0-9a-f].
+ ubyte hexVal(ubyte ch) {
+ if ((ch >= '0') && (ch <= '9')) {
+ return cast(ubyte)(ch - '0');
+ } else if ((ch >= 'a') && (ch <= 'f')) {
+ return cast(ubyte)(ch - 'a' + 10);
+ }
+ else {
+ throw new TProtocolException("Expected hex val ([0-9a-f]), got '" ~
+ ch ~ "'.", TProtocolException.Type.INVALID_DATA);
+ }
+ }
+
+ // Return the hex character representing the integer val. The value is masked
+ // to make sure it is in the correct range.
+ ubyte hexChar(ubyte val) {
+ val &= 0x0F;
+ if (val < 10) {
+ return cast(ubyte)(val + '0');
+ } else {
+ return cast(ubyte)(val - 10 + 'a');
+ }
+ }
+
+ // Return true if the character ch is in [-+0-9.Ee]; false otherwise
+ bool isJsonNumeric(ubyte ch) {
+ switch (ch) {
+ case '+':
+ case '-':
+ case '.':
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ case 'E':
+ case 'e':
+ return true;
+ default:
+ return false;
+ }
+ }
+}
diff --git a/lib/d/src/thrift/protocol/processor.d b/lib/d/src/thrift/protocol/processor.d
new file mode 100644
index 000000000..887421cc8
--- /dev/null
+++ b/lib/d/src/thrift/protocol/processor.d
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.protocol.processor;
+
+// Use selective import once DMD @@BUG314@@ is fixed.
+import std.variant /+ : Variant +/;
+import thrift.protocol.base;
+import thrift.transport.base;
+
+/**
+ * A processor is a generic object which operates upon an input stream and
+ * writes to some output stream.
+ *
+ * The definition of this object is loose, though the typical case is for some
+ * sort of server that either generates responses to an input stream or
+ * forwards data from one pipe onto another.
+ *
+ * An implementation can optionally allow one or more TProcessorEventHandlers
+ * to be attached, providing an interface to hook custom code into the
+ * handling process, which can be used e.g. for gathering statistics.
+ */
+interface TProcessor {
+ ///
+ bool process(TProtocol iprot, TProtocol oprot,
+ Variant connectionContext = Variant()
+ ) in {
+ assert(iprot);
+ assert(oprot);
+ }
+
+ ///
+ final bool process(TProtocol prot, Variant connectionContext = Variant()) {
+ return process(prot, prot, connectionContext);
+ }
+}
+
+/**
+ * Handles events from a processor.
+ */
+interface TProcessorEventHandler {
+ /**
+ * Called before calling other callback methods.
+ *
+ * Expected to return some sort of »call context«, which is passed to all
+ * other callbacks for that function invocation.
+ */
+ Variant createContext(string methodName, Variant connectionContext);
+
+ /**
+ * Called when handling the method associated with a context has been
+ * finished – can be used to perform clean up work.
+ */
+ void deleteContext(Variant callContext, string methodName);
+
+ /**
+ * Called before reading arguments.
+ */
+ void preRead(Variant callContext, string methodName);
+
+ /**
+ * Called between reading arguments and calling the handler.
+ */
+ void postRead(Variant callContext, string methodName);
+
+ /**
+ * Called between calling the handler and writing the response.
+ */
+ void preWrite(Variant callContext, string methodName);
+
+ /**
+ * Called after writing the response.
+ */
+ void postWrite(Variant callContext, string methodName);
+
+ /**
+ * Called when handling a one-way function call is completed successfully.
+ */
+ void onewayComplete(Variant callContext, string methodName);
+
+ /**
+ * Called if the handler throws an undeclared exception.
+ */
+ void handlerError(Variant callContext, string methodName, Exception e);
+}
+
+struct TConnectionInfo {
+ /// The input and output protocols.
+ TProtocol input;
+ TProtocol output; /// Ditto.
+
+ /// The underlying transport used for the connection
+ /// This is the transport that was returned by TServerTransport.accept(),
+ /// and it may be different than the transport pointed to by the input and
+ /// output protocols.
+ TTransport transport;
+}
+
+interface TProcessorFactory {
+ /**
+ * Get the TProcessor to use for a particular connection.
+ *
+ * This method is always invoked in the same thread that the connection was
+ * accepted on, which is always the same thread for all current server
+ * implementations.
+ */
+ TProcessor getProcessor(ref const(TConnectionInfo) connInfo);
+}
+
+/**
+ * The default processor factory which always returns the same instance.
+ */
+class TSingletonProcessorFactory : TProcessorFactory {
+ /**
+ * Creates a new instance.
+ *
+ * Params:
+ * processor = The processor object to return from getProcessor().
+ */
+ this(TProcessor processor) {
+ processor_ = processor;
+ }
+
+ override TProcessor getProcessor(ref const(TConnectionInfo) connInfo) {
+ return processor_;
+ }
+
+private:
+ TProcessor processor_;
+}
diff --git a/lib/d/src/thrift/server/base.d b/lib/d/src/thrift/server/base.d
new file mode 100644
index 000000000..b56ae668f
--- /dev/null
+++ b/lib/d/src/thrift/server/base.d
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.server.base;
+
+import std.variant : Variant;
+import thrift.protocol.base;
+import thrift.protocol.binary;
+import thrift.protocol.processor;
+import thrift.server.transport.base;
+import thrift.transport.base;
+import thrift.util.cancellation;
+
+/**
+ * Base class for all Thrift servers.
+ *
+ * By setting the eventHandler property to a TServerEventHandler
+ * implementation, custom code can be integrated into the processing pipeline,
+ * which can be used e.g. for gathering statistics.
+ */
+class TServer {
+ /**
+ * Starts serving.
+ *
+ * Blocks until the server finishes, i.e. a serious problem occured or the
+ * cancellation request has been triggered.
+ *
+ * Server implementations are expected to implement cancellation in a best-
+ * effort way – usually, it should be possible to immediately stop accepting
+ * connections and return after all currently active clients have been
+ * processed, but this might not be the case for every conceivable
+ * implementation.
+ */
+ abstract void serve(TCancellation cancellation = null);
+
+ /// The server event handler to notify. Null by default.
+ TServerEventHandler eventHandler;
+
+protected:
+ this(
+ TProcessor processor,
+ TServerTransport serverTransport,
+ TTransportFactory transportFactory,
+ TProtocolFactory protocolFactory
+ ) {
+ this(processor, serverTransport, transportFactory, transportFactory,
+ protocolFactory, protocolFactory);
+ }
+
+ this(
+ TProcessorFactory processorFactory,
+ TServerTransport serverTransport,
+ TTransportFactory transportFactory,
+ TProtocolFactory protocolFactory
+ ) {
+ this(processorFactory, serverTransport, transportFactory, transportFactory,
+ protocolFactory, protocolFactory);
+ }
+
+ this(
+ TProcessor processor,
+ TServerTransport serverTransport,
+ TTransportFactory inputTransportFactory,
+ TTransportFactory outputTransportFactory,
+ TProtocolFactory inputProtocolFactory,
+ TProtocolFactory outputProtocolFactory
+ ) {
+ this(new TSingletonProcessorFactory(processor), serverTransport,
+ inputTransportFactory, outputTransportFactory,
+ inputProtocolFactory, outputProtocolFactory);
+ }
+
+ this(
+ TProcessorFactory processorFactory,
+ TServerTransport serverTransport,
+ TTransportFactory inputTransportFactory,
+ TTransportFactory outputTransportFactory,
+ TProtocolFactory inputProtocolFactory,
+ TProtocolFactory outputProtocolFactory
+ ) {
+ import std.exception;
+ import thrift.base;
+ enforce(inputTransportFactory,
+ new TException("Input transport factory must not be null."));
+ enforce(outputTransportFactory,
+ new TException("Output transport factory must not be null."));
+ enforce(inputProtocolFactory,
+ new TException("Input protocol factory must not be null."));
+ enforce(outputProtocolFactory,
+ new TException("Output protocol factory must not be null."));
+
+ processorFactory_ = processorFactory;
+ serverTransport_ = serverTransport;
+ inputTransportFactory_ = inputTransportFactory;
+ outputTransportFactory_ = outputTransportFactory;
+ inputProtocolFactory_ = inputProtocolFactory;
+ outputProtocolFactory_ = outputProtocolFactory;
+ }
+
+ TProcessorFactory processorFactory_;
+ TServerTransport serverTransport_;
+ TTransportFactory inputTransportFactory_;
+ TTransportFactory outputTransportFactory_;
+ TProtocolFactory inputProtocolFactory_;
+ TProtocolFactory outputProtocolFactory_;
+}
+
+/**
+ * Handles events from a TServer core.
+ */
+interface TServerEventHandler {
+ /**
+ * Called before the server starts accepting connections.
+ */
+ void preServe();
+
+ /**
+ * Called when a new client has connected and processing is about to begin.
+ */
+ Variant createContext(TProtocol input, TProtocol output);
+
+ /**
+ * Called when request handling for a client has been finished – can be used
+ * to perform clean up work.
+ */
+ void deleteContext(Variant serverContext, TProtocol input, TProtocol output);
+
+ /**
+ * Called when the processor for a client call is about to be invoked.
+ */
+ void preProcess(Variant serverContext, TTransport transport);
+}
diff --git a/lib/d/src/thrift/server/nonblocking.d b/lib/d/src/thrift/server/nonblocking.d
new file mode 100644
index 000000000..02167996e
--- /dev/null
+++ b/lib/d/src/thrift/server/nonblocking.d
@@ -0,0 +1,1397 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * A non-blocking server implementation that operates a set of I/O threads (by
+ * default only one) and either does processing »in-line« or off-loads it to a
+ * task pool.
+ *
+ * It *requires* TFramedTransport to be used on the client side, as it expects
+ * a 4 byte length indicator and writes out responses using the same framing.
+ *
+ * Because I/O is done asynchronous/event based, unfortunately
+ * TServerTransport can't be used.
+ *
+ * This implementation is based on the C++ one, with the exception of request
+ * timeouts and the drain task queue overload handling strategy not being
+ * implemented yet.
+ */
+// This really should use a D non-blocking I/O library, once one becomes
+// available.
+module thrift.server.nonblocking;
+
+import core.atomic : atomicLoad, atomicStore, atomicOp;
+import core.exception : onOutOfMemoryError;
+import core.memory : GC;
+import core.sync.mutex;
+import core.stdc.stdlib : free, realloc;
+import core.time : Duration, dur;
+import core.thread : Thread, ThreadGroup;
+import deimos.event2.event;
+import std.array : empty;
+import std.conv : emplace, to;
+import std.exception : enforce;
+import std.parallelism : TaskPool, task;
+import std.socket : Socket, socketPair, SocketAcceptException,
+ SocketException, TcpSocket;
+import std.variant : Variant;
+import thrift.base;
+import thrift.internal.endian;
+import thrift.internal.socket;
+import thrift.internal.traits;
+import thrift.protocol.base;
+import thrift.protocol.binary;
+import thrift.protocol.processor;
+import thrift.server.base;
+import thrift.server.transport.socket;
+import thrift.transport.base;
+import thrift.transport.memory;
+import thrift.transport.range;
+import thrift.transport.socket;
+import thrift.util.cancellation;
+
+/**
+ * Possible actions taken on new incoming connections when the server is
+ * overloaded.
+ */
+enum TOverloadAction {
+ /// Do not take any special actions while the server is overloaded, just
+ /// continue accepting connections.
+ NONE,
+
+ /// Immediately drop new connections after they have been accepted if the
+ /// server is overloaded.
+ CLOSE_ON_ACCEPT
+}
+
+///
+class TNonblockingServer : TServer {
+ ///
+ this(TProcessor processor, ushort port, TTransportFactory transportFactory,
+ TProtocolFactory protocolFactory, TaskPool taskPool = null
+ ) {
+ this(new TSingletonProcessorFactory(processor), port, transportFactory,
+ transportFactory, protocolFactory, protocolFactory, taskPool);
+ }
+
+ ///
+ this(TProcessorFactory processorFactory, ushort port,
+ TTransportFactory transportFactory, TProtocolFactory protocolFactory,
+ TaskPool taskPool = null
+ ) {
+ this(processorFactory, port, transportFactory, transportFactory,
+ protocolFactory, protocolFactory, taskPool);
+ }
+
+ ///
+ this(
+ TProcessor processor,
+ ushort port,
+ TTransportFactory inputTransportFactory,
+ TTransportFactory outputTransportFactory,
+ TProtocolFactory inputProtocolFactory,
+ TProtocolFactory outputProtocolFactory,
+ TaskPool taskPool = null
+ ) {
+ this(new TSingletonProcessorFactory(processor), port,
+ inputTransportFactory, outputTransportFactory,
+ inputProtocolFactory, outputProtocolFactory, taskPool);
+ }
+
+ ///
+ this(
+ TProcessorFactory processorFactory,
+ ushort port,
+ TTransportFactory inputTransportFactory,
+ TTransportFactory outputTransportFactory,
+ TProtocolFactory inputProtocolFactory,
+ TProtocolFactory outputProtocolFactory,
+ TaskPool taskPool = null
+ ) {
+ super(processorFactory, null, inputTransportFactory,
+ outputTransportFactory, inputProtocolFactory, outputProtocolFactory);
+ port_ = port;
+
+ this.taskPool = taskPool;
+
+ connectionMutex_ = new Mutex;
+
+ connectionStackLimit = DEFAULT_CONNECTION_STACK_LIMIT;
+ maxActiveProcessors = DEFAULT_MAX_ACTIVE_PROCESSORS;
+ maxConnections = DEFAULT_MAX_CONNECTIONS;
+ overloadHysteresis = DEFAULT_OVERLOAD_HYSTERESIS;
+ overloadAction = DEFAULT_OVERLOAD_ACTION;
+ writeBufferDefaultSize = DEFAULT_WRITE_BUFFER_DEFAULT_SIZE;
+ idleReadBufferLimit = DEFAULT_IDLE_READ_BUFFER_LIMIT;
+ idleWriteBufferLimit = DEFAULT_IDLE_WRITE_BUFFER_LIMIT;
+ resizeBufferEveryN = DEFAULT_RESIZE_BUFFER_EVERY_N;
+ maxFrameSize = DEFAULT_MAX_FRAME_SIZE;
+ numIOThreads_ = DEFAULT_NUM_IO_THREADS;
+ }
+
+ override void serve(TCancellation cancellation = null) {
+ if (cancellation && cancellation.triggered) return;
+
+ // Initialize the listening socket.
+ // TODO: SO_KEEPALIVE, TCP_LOW_MIN_RTO, etc.
+ listenSocket_ = makeSocketAndListen(port_, TServerSocket.ACCEPT_BACKLOG,
+ BIND_RETRY_LIMIT, BIND_RETRY_DELAY, 0, 0, ipv6Only_);
+ listenSocket_.blocking = false;
+
+ logInfo("Using %s I/O thread(s).", numIOThreads_);
+ if (taskPool_) {
+ logInfo("Using task pool with size: %s.", numIOThreads_, taskPool_.size);
+ }
+
+ assert(numIOThreads_ > 0);
+ assert(ioLoops_.empty);
+ foreach (id; 0 .. numIOThreads_) {
+ // The IO loop on the first IO thread (this thread, i.e. the one serve()
+ // is called from) also accepts new connections.
+ auto listenSocket = (id == 0 ? listenSocket_ : null);
+ ioLoops_ ~= new IOLoop(this, listenSocket);
+ }
+
+ if (cancellation) {
+ cancellation.triggering.addCallback({
+ foreach (i, loop; ioLoops_) loop.stop();
+
+ // Stop accepting new connections right away.
+ listenSocket_.close();
+ listenSocket_ = null;
+ });
+ }
+
+ // Start the IO helper threads for all but the first loop, which we will run
+ // ourselves. Note that the threads run forever, only terminating if stop()
+ // is called.
+ auto threads = new ThreadGroup();
+ foreach (loop; ioLoops_[1 .. $]) {
+ auto t = new Thread(&loop.run);
+ threads.add(t);
+ t.start();
+ }
+
+ if (eventHandler) eventHandler.preServe();
+
+ // Run the primary (listener) IO thread loop in our main thread; this will
+ // block until the server is shutting down.
+ ioLoops_[0].run();
+
+ // Ensure all threads are finished before leaving serve().
+ threads.joinAll();
+
+ ioLoops_ = null;
+ }
+
+ /**
+ * Returns the number of currently active connections, i.e. open sockets.
+ */
+ size_t numConnections() const @property {
+ return numConnections_;
+ }
+
+ /**
+ * Returns the number of connection objects allocated, but not in use.
+ */
+ size_t numIdleConnections() const @property {
+ return connectionStack_.length;
+ }
+
+ /**
+ * Return count of number of connections which are currently processing.
+ *
+ * This is defined as a connection where all data has been received, and the
+ * processor was invoked but has not yet completed.
+ */
+ size_t numActiveProcessors() const @property {
+ return numActiveProcessors_;
+ }
+
+ /// Number of bind() retries.
+ enum BIND_RETRY_LIMIT = 0;
+
+ /// Duration between bind() retries.
+ enum BIND_RETRY_DELAY = dur!"hnsecs"(0);
+
+ /// Whether to listen on IPv6 only, if IPv6 support is detected
+ // (default: false).
+ void ipv6Only(bool value) @property {
+ ipv6Only_ = value;
+ }
+
+ /**
+ * The task pool to use for processing requests. If null, no additional
+ * threads are used and request are processed »inline«.
+ *
+ * Can safely be set even when the server is already running.
+ */
+ TaskPool taskPool() @property {
+ return taskPool_;
+ }
+
+ /// ditto
+ void taskPool(TaskPool pool) @property {
+ taskPool_ = pool;
+ }
+
+ /**
+ * Hysteresis for overload state.
+ *
+ * This is the fraction of the overload value that needs to be reached
+ * before the overload state is cleared. It must be between 0 and 1,
+ * practical choices probably lie between 0.5 and 0.9.
+ */
+ double overloadHysteresis() const @property {
+ return overloadHysteresis_;
+ }
+
+ /// Ditto
+ void overloadHysteresis(double value) @property {
+ enforce(0 < value && value <= 1,
+ "Invalid value for overload hysteresis: " ~ to!string(value));
+ overloadHysteresis_ = value;
+ }
+
+ /// Ditto
+ enum DEFAULT_OVERLOAD_HYSTERESIS = 0.8;
+
+ /**
+ * The action which will be taken on overload.
+ */
+ TOverloadAction overloadAction;
+
+ /// Ditto
+ enum DEFAULT_OVERLOAD_ACTION = TOverloadAction.NONE;
+
+ /**
+ * The write buffer is initialized (and when idleWriteBufferLimit_ is checked
+ * and found to be exceeded, reinitialized) to this size.
+ */
+ size_t writeBufferDefaultSize;
+
+ /// Ditto
+ enum size_t DEFAULT_WRITE_BUFFER_DEFAULT_SIZE = 1024;
+
+ /**
+ * Max read buffer size for an idle Connection. When we place an idle
+ * Connection into connectionStack_ or on every resizeBufferEveryN_ calls,
+ * we will free the buffer (such that it will be reinitialized by the next
+ * received frame) if it has exceeded this limit. 0 disables this check.
+ */
+ size_t idleReadBufferLimit;
+
+ /// Ditto
+ enum size_t DEFAULT_IDLE_READ_BUFFER_LIMIT = 1024;
+
+ /**
+ * Max write buffer size for an idle connection. When we place an idle
+ * Connection into connectionStack_ or on every resizeBufferEveryN_ calls,
+ * we ensure that its write buffer is <= to this size; otherwise we
+ * replace it with a new one of writeBufferDefaultSize_ bytes to ensure that
+ * idle connections don't hog memory. 0 disables this check.
+ */
+ size_t idleWriteBufferLimit;
+
+ /// Ditto
+ enum size_t DEFAULT_IDLE_WRITE_BUFFER_LIMIT = 1024;
+
+ /**
+ * Every N calls we check the buffer size limits on a connected Connection.
+ * 0 disables (i.e. the checks are only done when a connection closes).
+ */
+ uint resizeBufferEveryN;
+
+ /// Ditto
+ enum uint DEFAULT_RESIZE_BUFFER_EVERY_N = 512;
+
+ /// Limit for how many Connection objects to cache.
+ size_t connectionStackLimit;
+
+ /// Ditto
+ enum size_t DEFAULT_CONNECTION_STACK_LIMIT = 1024;
+
+ /// Limit for number of open connections before server goes into overload
+ /// state.
+ size_t maxConnections;
+
+ /// Ditto
+ enum size_t DEFAULT_MAX_CONNECTIONS = int.max;
+
+ /// Limit for number of connections processing or waiting to process
+ size_t maxActiveProcessors;
+
+ /// Ditto
+ enum size_t DEFAULT_MAX_ACTIVE_PROCESSORS = int.max;
+
+ /// Maximum frame size, in bytes.
+ ///
+ /// If a client tries to send a message larger than this limit, its
+ /// connection will be closed. This helps to avoid allocating huge buffers
+ /// on bogous input.
+ uint maxFrameSize;
+
+ /// Ditto
+ enum uint DEFAULT_MAX_FRAME_SIZE = 256 * 1024 * 1024;
+
+
+ size_t numIOThreads() @property {
+ return numIOThreads_;
+ }
+
+ void numIOThreads(size_t value) @property {
+ enforce(value >= 1, new TException("Must use at least one I/O thread."));
+ numIOThreads_ = value;
+ }
+
+ enum DEFAULT_NUM_IO_THREADS = 1;
+
+private:
+ /**
+ * C callback wrapper around acceptConnections(). Expects the custom argument
+ * to be the this pointer of the associated server instance.
+ */
+ extern(C) static void acceptConnectionsCallback(int fd, short which,
+ void* serverThis
+ ) {
+ (cast(TNonblockingServer)serverThis).acceptConnections(fd, which);
+ }
+
+ /**
+ * Called by libevent (IO loop 0/serve() thread only) when something
+ * happened on the listening socket.
+ */
+ void acceptConnections(int fd, short eventFlags) {
+ if (atomicLoad(ioLoops_[0].shuttingDown_)) return;
+
+ assert(!!listenSocket_,
+ "Server should be shutting down if listen socket is null.");
+ assert(fd == listenSocket_.handle);
+ assert(eventFlags & EV_READ);
+
+ // Accept as many new clients as possible, even though libevent signaled
+ // only one. This helps the number of calls into libevent space.
+ while (true) {
+ // It is lame to use exceptions for regular control flow (failing is
+ // excepted due to non-blocking mode of operation), but that's the
+ // interface std.socket offers…
+ Socket clientSocket;
+ try {
+ clientSocket = listenSocket_.accept();
+ } catch (SocketAcceptException e) {
+ if (e.errorCode != WOULD_BLOCK_ERRNO) {
+ logError("Error accepting connection: %s", e);
+ }
+ break;
+ }
+
+ // If the server is overloaded, this is the point to take the specified
+ // action.
+ if (overloadAction != TOverloadAction.NONE && checkOverloaded()) {
+ nConnectionsDropped_++;
+ nTotalConnectionsDropped_++;
+ if (overloadAction == TOverloadAction.CLOSE_ON_ACCEPT) {
+ clientSocket.close();
+ return;
+ }
+ }
+
+ try {
+ clientSocket.blocking = false;
+ } catch (SocketException e) {
+ logError("Couldn't set client socket to non-blocking mode: %s", e);
+ clientSocket.close();
+ return;
+ }
+
+ // Create a new Connection for this client socket.
+ Connection conn = void;
+ IOLoop loop = void;
+ bool thisThread = void;
+ synchronized (connectionMutex_) {
+ // Assign an I/O loop to the connection (round-robin).
+ assert(nextIOLoop_ >= 0);
+ assert(nextIOLoop_ < ioLoops_.length);
+ auto selectedThreadIdx = nextIOLoop_;
+ nextIOLoop_ = (nextIOLoop_ + 1) % ioLoops_.length;
+
+ loop = ioLoops_[selectedThreadIdx];
+ thisThread = (selectedThreadIdx == 0);
+
+ // Check the connection stack to see if we can re-use an existing one.
+ if (connectionStack_.empty) {
+ ++numConnections_;
+ conn = new Connection(clientSocket, loop);
+
+ // Make sure the connection does not get collected while it is active,
+ // i.e. hooked up with libevent.
+ GC.addRoot(cast(void*)conn);
+ } else {
+ conn = connectionStack_[$ - 1];
+ connectionStack_ = connectionStack_[0 .. $ - 1];
+ connectionStack_.assumeSafeAppend();
+ conn.init(clientSocket, loop);
+ }
+ }
+
+ loop.addConnection();
+
+ // Either notify the ioThread that is assigned this connection to
+ // start processing, or if it is us, we'll just ask this
+ // connection to do its initial state change here.
+ //
+ // (We need to avoid writing to our own notification pipe, to
+ // avoid possible deadlocks if the pipe is full.)
+ if (thisThread) {
+ conn.transition();
+ } else {
+ loop.notifyCompleted(conn);
+ }
+ }
+ }
+
+ /// Increment the count of connections currently processing.
+ void incrementActiveProcessors() {
+ atomicOp!"+="(numActiveProcessors_, 1);
+ }
+
+ /// Decrement the count of connections currently processing.
+ void decrementActiveProcessors() {
+ assert(numActiveProcessors_ > 0);
+ atomicOp!"-="(numActiveProcessors_, 1);
+ }
+
+ /**
+ * Determines if the server is currently overloaded.
+ *
+ * If the number of open connections or »processing« connections is over the
+ * respective limit, the server will enter overload handling mode and a
+ * warning will be logged. If below values are below the hysteresis curve,
+ * this will cause the server to exit it again.
+ *
+ * Returns: Whether the server is currently overloaded.
+ */
+ bool checkOverloaded() {
+ auto activeConnections = numConnections_ - connectionStack_.length;
+ if (numActiveProcessors_ > maxActiveProcessors ||
+ activeConnections > maxConnections) {
+ if (!overloaded_) {
+ logInfo("Entering overloaded state.");
+ overloaded_ = true;
+ }
+ } else {
+ if (overloaded_ &&
+ (numActiveProcessors_ <= overloadHysteresis_ * maxActiveProcessors) &&
+ (activeConnections <= overloadHysteresis_ * maxConnections))
+ {
+ logInfo("Exiting overloaded state, %s connection(s) dropped (% total).",
+ nConnectionsDropped_, nTotalConnectionsDropped_);
+ nConnectionsDropped_ = 0;
+ overloaded_ = false;
+ }
+ }
+
+ return overloaded_;
+ }
+
+ /**
+ * Marks a connection as inactive and either puts it back into the
+ * connection pool or leaves it for garbage collection.
+ */
+ void disposeConnection(Connection connection) {
+ synchronized (connectionMutex_) {
+ if (!connectionStackLimit ||
+ (connectionStack_.length < connectionStackLimit))
+ {
+ connection.checkIdleBufferLimit(idleReadBufferLimit,
+ idleWriteBufferLimit);
+ connectionStack_ ~= connection;
+ } else {
+ assert(numConnections_ > 0);
+ --numConnections_;
+
+ // Leave the connection object for collection now.
+ GC.removeRoot(cast(void*)connection);
+ }
+ }
+ }
+
+ /// Socket used to listen for connections and accepting them.
+ Socket listenSocket_;
+
+ /// Port to listen on.
+ ushort port_;
+
+ /// Whether to listen on IPv6 only.
+ bool ipv6Only_;
+
+ /// The total number of connections existing, both active and idle.
+ size_t numConnections_;
+
+ /// The number of connections which are currently waiting for the processor
+ /// to return.
+ shared size_t numActiveProcessors_;
+
+ /// Hysteresis for leaving overload state.
+ double overloadHysteresis_;
+
+ /// Whether the server is currently overloaded.
+ bool overloaded_;
+
+ /// Number of connections dropped since the server entered the current
+ /// overloaded state.
+ uint nConnectionsDropped_;
+
+ /// Number of connections dropped due to overload since the server started.
+ ulong nTotalConnectionsDropped_;
+
+ /// The task pool used for processing requests.
+ TaskPool taskPool_;
+
+ /// Number of IO threads this server will use (>= 1).
+ size_t numIOThreads_;
+
+ /// The IOLoops among which socket handling work is distributed.
+ IOLoop[] ioLoops_;
+
+ /// The index of the loop in ioLoops_ which will handle the next accepted
+ /// connection.
+ size_t nextIOLoop_;
+
+ /// All the connection objects which have been created but are not currently
+ /// in use. When a connection is closed, it it placed here to enable object
+ /// (resp. buffer) reuse.
+ Connection[] connectionStack_;
+
+ /// This mutex protects the connection stack.
+ Mutex connectionMutex_;
+}
+
+private {
+ /*
+ * Encapsulates a libevent event loop.
+ *
+ * The design is a bit of a mess, since the first loop is actually run on the
+ * server thread itself and is special because it is the only instance for
+ * which listenSocket_ is not null.
+ */
+ final class IOLoop {
+ /**
+ * Creates a new instance and set up the event base.
+ *
+ * If listenSocket is not null, the thread will also accept new
+ * connections itself.
+ */
+ this(TNonblockingServer server, Socket listenSocket) {
+ server_ = server;
+ listenSocket_ = listenSocket;
+ initMutex_ = new Mutex;
+ }
+
+ /**
+ * Runs the event loop; only returns after a call to stop().
+ */
+ void run() {
+ assert(!atomicLoad(initialized_), "IOLoop already running?!");
+
+ synchronized (initMutex_) {
+ if (atomicLoad(shuttingDown_)) return;
+ atomicStore(initialized_, true);
+
+ assert(!eventBase_);
+ eventBase_ = event_base_new();
+
+ if (listenSocket_) {
+ // Log the libevent version and backend.
+ logInfo("libevent version %s, using method %s.",
+ to!string(event_get_version()), to!string(event_base_get_method(eventBase_)));
+
+ // Register the event for the listening socket.
+ listenEvent_ = event_new(eventBase_, listenSocket_.handle,
+ EV_READ | EV_PERSIST | EV_ET,
+ assumeNothrow(&TNonblockingServer.acceptConnectionsCallback),
+ cast(void*)server_);
+ if (event_add(listenEvent_, null) == -1) {
+ throw new TException("event_add for the listening socket event failed.");
+ }
+ }
+
+ auto pair = socketPair();
+ foreach (s; pair) s.blocking = false;
+ completionSendSocket_ = pair[0];
+ completionReceiveSocket_ = pair[1];
+
+ // Register an event for the task completion notification socket.
+ completionEvent_ = event_new(eventBase_, completionReceiveSocket_.handle,
+ EV_READ | EV_PERSIST | EV_ET, assumeNothrow(&completedCallback),
+ cast(void*)this);
+
+ if (event_add(completionEvent_, null) == -1) {
+ throw new TException("event_add for the notification socket failed.");
+ }
+ }
+
+ // Run libevent engine, returns only after stop().
+ event_base_dispatch(eventBase_);
+
+ if (listenEvent_) {
+ event_free(listenEvent_);
+ listenEvent_ = null;
+ }
+
+ event_free(completionEvent_);
+ completionEvent_ = null;
+
+ completionSendSocket_.close();
+ completionSendSocket_ = null;
+
+ completionReceiveSocket_.close();
+ completionReceiveSocket_ = null;
+
+ event_base_free(eventBase_);
+ eventBase_ = null;
+
+ atomicStore(shuttingDown_, false);
+
+ initialized_ = false;
+ }
+
+ /**
+ * Adds a new connection handled by this loop.
+ */
+ void addConnection() {
+ ++numActiveConnections_;
+ }
+
+ /**
+ * Disposes a connection object (typically after it has been closed).
+ */
+ void disposeConnection(Connection conn) {
+ server_.disposeConnection(conn);
+ assert(numActiveConnections_ > 0);
+ --numActiveConnections_;
+ if (numActiveConnections_ == 0) {
+ if (atomicLoad(shuttingDown_)) {
+ event_base_loopbreak(eventBase_);
+ }
+ }
+ }
+
+ /**
+ * Notifies the event loop that the current step (initialization,
+ * processing of a request) on a certain connection has been completed.
+ *
+ * This function is thread-safe, but should never be called from the
+ * thread running the loop itself.
+ */
+ void notifyCompleted(Connection conn) {
+ assert(!!completionSendSocket_);
+ auto bytesSent = completionSendSocket_.send(cast(ubyte[])((&conn)[0 .. 1]));
+
+ if (bytesSent != Connection.sizeof) {
+ logError("Sending completion notification failed, connection will " ~
+ "not be properly terminated.");
+ }
+ }
+
+ /**
+ * Exits the event loop after all currently active connections have been
+ * closed.
+ *
+ * This function is thread-safe.
+ */
+ void stop() {
+ // There is a bug in either libevent or its documentation, having no
+ // events registered doesn't actually terminate the loop, because
+ // event_base_new() registers some internal one by calling
+ // evthread_make_base_notifiable().
+ // Due to this, we can't simply remove all events and expect the event
+ // loop to terminate. Instead, we ping the event loop using a null
+ // completion message. This way, we make sure to wake up the libevent
+ // thread if it not currently processing any connections. It will break
+ // out of the loop in disposeConnection() after the last active
+ // connection has been closed.
+ synchronized (initMutex_) {
+ atomicStore(shuttingDown_, true);
+ if (atomicLoad(initialized_)) notifyCompleted(null);
+ }
+ }
+
+ private:
+ /**
+ * C callback to call completed() from libevent.
+ *
+ * Expects the custom argument to be the this pointer of the associated
+ * IOLoop instance.
+ */
+ extern(C) static void completedCallback(int fd, short what, void* loopThis) {
+ assert(what & EV_READ);
+ auto loop = cast(IOLoop)loopThis;
+ assert(fd == loop.completionReceiveSocket_.handle);
+ loop.completed();
+ }
+
+ /**
+ * Reads from the completion receive socket and appropriately transitions
+ * the connections and shuts down the loop if requested.
+ */
+ void completed() {
+ Connection connection;
+ ptrdiff_t bytesRead;
+ while (true) {
+ bytesRead = completionReceiveSocket_.receive(
+ cast(ubyte[])((&connection)[0 .. 1]));
+ if (bytesRead < 0) {
+ auto errno = getSocketErrno();
+
+ if (errno != WOULD_BLOCK_ERRNO) {
+ logError("Reading from completion socket failed, some connection " ~
+ "will never be properly terminated: %s", socketErrnoString(errno));
+ }
+ }
+
+ if (bytesRead != Connection.sizeof) break;
+
+ if (!connection) {
+ assert(atomicLoad(shuttingDown_));
+ if (numActiveConnections_ == 0) {
+ event_base_loopbreak(eventBase_);
+ }
+ continue;
+ }
+
+ connection.transition();
+ }
+
+ if (bytesRead > 0) {
+ logError("Unexpected partial read from completion socket " ~
+ "(%s bytes instead of %s).", bytesRead, Connection.sizeof);
+ }
+ }
+
+ /// associated server
+ TNonblockingServer server_;
+
+ /// The managed listening socket, if any.
+ Socket listenSocket_;
+
+ /// The libevent event base for the loop.
+ event_base* eventBase_;
+
+ /// Triggered on listen socket events.
+ event* listenEvent_;
+
+ /// Triggered on completion receive socket events.
+ event* completionEvent_;
+
+ /// Socket used to send completion notification messages. Paired with
+ /// completionReceiveSocket_.
+ Socket completionSendSocket_;
+
+ /// Socket used to send completion notification messages. Paired with
+ /// completionSendSocket_.
+ Socket completionReceiveSocket_;
+
+ /// Whether the server is currently shutting down (i.e. the cancellation has
+ /// been triggered, but not all client connections have been closed yet).
+ shared bool shuttingDown_;
+
+ /// The number of currently active client connections.
+ size_t numActiveConnections_;
+
+ /// Guards loop startup so that the loop can be reliably shut down even if
+ /// another thread has just started to execute run(). Locked during
+ /// initialization in run(). When unlocked, the completion mechanism is
+ /// expected to be fully set up.
+ Mutex initMutex_;
+ shared bool initialized_; /// Ditto
+ }
+
+ /*
+ * I/O states a socket can be in.
+ */
+ enum SocketState {
+ RECV_FRAME_SIZE, /// The frame size is received.
+ RECV, /// The payload is received.
+ SEND /// The response is written back out.
+ }
+
+ /*
+ * States a connection can be in.
+ */
+ enum ConnectionState {
+ INIT, /// The connection will be initialized.
+ READ_FRAME_SIZE, /// The four frame size bytes are being read.
+ READ_REQUEST, /// The request payload itself is being read.
+ WAIT_PROCESSOR, /// The connection waits for the processor to finish.
+ SEND_RESULT /// The result is written back out.
+ }
+
+ /*
+ * A connection that is handled via libevent.
+ *
+ * Data received is buffered until the request is complete (returning back to
+ * libevent if not), at which point the processor is invoked.
+ */
+ final class Connection {
+ /**
+ * Constructs a new instance.
+ *
+ * To reuse a connection object later on, the init() function can be used
+ * to the same effect on the internal state.
+ */
+ this(Socket socket, IOLoop loop) {
+ // The input and output transport objects are reused between clients
+ // connections, so initialize them here rather than in init().
+ inputTransport_ = new TInputRangeTransport!(ubyte[])([]);
+ outputTransport_ = new TMemoryBuffer(loop.server_.writeBufferDefaultSize);
+
+ init(socket, loop);
+ }
+
+ /**
+ * Initializes the connection.
+ *
+ * Params:
+ * socket = The socket to work on.
+ * eventFlags = Any flags to pass to libevent.
+ * s = The server this connection is part of.
+ */
+ void init(Socket socket, IOLoop loop) {
+ // TODO: This allocation could be avoided.
+ socket_ = new TSocket(socket);
+
+ loop_ = loop;
+ server_ = loop_.server_;
+ connState_ = ConnectionState.INIT;
+ eventFlags_ = 0;
+
+ readBufferPos_ = 0;
+ readWant_ = 0;
+
+ writeBuffer_ = null;
+ writeBufferPos_ = 0;
+ largestWriteBufferSize_ = 0;
+
+ socketState_ = SocketState.RECV_FRAME_SIZE;
+ callsSinceResize_ = 0;
+
+ factoryInputTransport_ =
+ server_.inputTransportFactory_.getTransport(inputTransport_);
+ factoryOutputTransport_ =
+ server_.outputTransportFactory_.getTransport(outputTransport_);
+
+ inputProtocol_ =
+ server_.inputProtocolFactory_.getProtocol(factoryInputTransport_);
+ outputProtocol_ =
+ server_.outputProtocolFactory_.getProtocol(factoryOutputTransport_);
+
+ if (server_.eventHandler) {
+ connectionContext_ =
+ server_.eventHandler.createContext(inputProtocol_, outputProtocol_);
+ }
+
+ auto info = TConnectionInfo(inputProtocol_, outputProtocol_, socket_);
+ processor_ = server_.processorFactory_.getProcessor(info);
+ }
+
+ ~this() {
+ free(readBuffer_);
+ if (event_) {
+ event_free(event_);
+ event_ = null;
+ }
+ }
+
+ /**
+ * Check buffers against the size limits and shrink them if exceeded.
+ *
+ * Params:
+ * readLimit = Read buffer size limit (in bytes, 0 to ignore).
+ * writeLimit = Write buffer size limit (in bytes, 0 to ignore).
+ */
+ void checkIdleBufferLimit(size_t readLimit, size_t writeLimit) {
+ if (readLimit > 0 && readBufferSize_ > readLimit) {
+ free(readBuffer_);
+ readBuffer_ = null;
+ readBufferSize_ = 0;
+ }
+
+ if (writeLimit > 0 && largestWriteBufferSize_ > writeLimit) {
+ // just start over
+ outputTransport_.reset(server_.writeBufferDefaultSize);
+ largestWriteBufferSize_ = 0;
+ }
+ }
+
+ /**
+ * Transitions the connection to the next state.
+ *
+ * This is called e.g. when the request has been read completely or all
+ * the data has been written back.
+ */
+ void transition() {
+ assert(!!loop_);
+ assert(!!server_);
+
+ // Switch upon the state that we are currently in and move to a new state
+ final switch (connState_) {
+ case ConnectionState.READ_REQUEST:
+ // We are done reading the request, package the read buffer into transport
+ // and get back some data from the dispatch function
+ inputTransport_.reset(readBuffer_[0 .. readBufferPos_]);
+ outputTransport_.reset();
+
+ // Prepend four bytes of blank space to the buffer so we can
+ // write the frame size there later.
+ // Strictly speaking, we wouldn't have to write anything, just
+ // increment the TMemoryBuffer writeOffset_. This would yield a tiny
+ // performance gain.
+ ubyte[4] space = void;
+ outputTransport_.write(space);
+
+ server_.incrementActiveProcessors();
+
+ taskPool_ = server_.taskPool;
+ if (taskPool_) {
+ // Create a new task and add it to the task pool queue.
+ auto processingTask = task!processRequest(this);
+ connState_ = ConnectionState.WAIT_PROCESSOR;
+ taskPool_.put(processingTask);
+
+ // We don't want to process any more data while the task is active.
+ unregisterEvent();
+ return;
+ }
+
+ // Just process it right now if there is no task pool set.
+ processRequest(this);
+ goto case;
+ case ConnectionState.WAIT_PROCESSOR:
+ // We have now finished processing the request, set the frame size
+ // for the outputTransport_ contents and set everything up to write
+ // it out via libevent.
+ server_.decrementActiveProcessors();
+
+ // Acquire the data written to the transport.
+ // KLUDGE: To avoid copying, we simply cast the const away and
+ // modify the internal buffer of the TMemoryBuffer – works with the
+ // current implementation, but isn't exactly beautiful.
+ writeBuffer_ = cast(ubyte[])outputTransport_.getContents();
+
+ assert(writeBuffer_.length >= 4, "The write buffer should have " ~
+ "least the initially added dummy length bytes.");
+ if (writeBuffer_.length == 4) {
+ // The request was one-way, no response to write.
+ goto case ConnectionState.INIT;
+ }
+
+ // Write the frame size into the four bytes reserved for it.
+ auto size = hostToNet(cast(uint)(writeBuffer_.length - 4));
+ writeBuffer_[0 .. 4] = cast(ubyte[])((&size)[0 .. 1]);
+
+ writeBufferPos_ = 0;
+ socketState_ = SocketState.SEND;
+ connState_ = ConnectionState.SEND_RESULT;
+ registerEvent(EV_WRITE | EV_PERSIST);
+
+ return;
+ case ConnectionState.SEND_RESULT:
+ // The result has been sent back to the client, we don't need the
+ // buffers anymore.
+ if (writeBuffer_.length > largestWriteBufferSize_) {
+ largestWriteBufferSize_ = writeBuffer_.length;
+ }
+
+ if (server_.resizeBufferEveryN > 0 &&
+ ++callsSinceResize_ >= server_.resizeBufferEveryN
+ ) {
+ checkIdleBufferLimit(server_.idleReadBufferLimit,
+ server_.idleWriteBufferLimit);
+ callsSinceResize_ = 0;
+ }
+
+ goto case;
+ case ConnectionState.INIT:
+ writeBuffer_ = null;
+ writeBufferPos_ = 0;
+ socketState_ = SocketState.RECV_FRAME_SIZE;
+ connState_ = ConnectionState.READ_FRAME_SIZE;
+ readBufferPos_ = 0;
+ registerEvent(EV_READ | EV_PERSIST);
+
+ return;
+ case ConnectionState.READ_FRAME_SIZE:
+ // We just read the request length, set up the buffers for reading
+ // the payload.
+ if (readWant_ > readBufferSize_) {
+ // The current buffer is too small, exponentially grow the buffer
+ // until it is big enough.
+
+ if (readBufferSize_ == 0) {
+ readBufferSize_ = 1;
+ }
+
+ auto newSize = readBufferSize_;
+ while (readWant_ > newSize) {
+ newSize *= 2;
+ }
+
+ auto newBuffer = cast(ubyte*)realloc(readBuffer_, newSize);
+ if (!newBuffer) onOutOfMemoryError();
+
+ readBuffer_ = newBuffer;
+ readBufferSize_ = newSize;
+ }
+
+ readBufferPos_= 0;
+
+ socketState_ = SocketState.RECV;
+ connState_ = ConnectionState.READ_REQUEST;
+
+ return;
+ }
+ }
+
+ private:
+ /**
+ * C callback to call workSocket() from libevent.
+ *
+ * Expects the custom argument to be the this pointer of the associated
+ * connection.
+ */
+ extern(C) static void workSocketCallback(int fd, short flags, void* connThis) {
+ auto conn = cast(Connection)connThis;
+ assert(fd == conn.socket_.socketHandle);
+ conn.workSocket();
+ }
+
+ /**
+ * Invoked by libevent when something happens on the socket.
+ */
+ void workSocket() {
+ final switch (socketState_) {
+ case SocketState.RECV_FRAME_SIZE:
+ // If some bytes have already been read, they have been kept in
+ // readWant_.
+ auto frameSize = readWant_;
+
+ try {
+ // Read from the socket
+ auto bytesRead = socket_.read(
+ (cast(ubyte[])((&frameSize)[0 .. 1]))[readBufferPos_ .. $]);
+ if (bytesRead == 0) {
+ // Couldn't read anything, but we have been notified – client
+ // has disconnected.
+ close();
+ return;
+ }
+
+ readBufferPos_ += bytesRead;
+ } catch (TTransportException te) {
+ logError("Failed to read frame size from client connection: %s", te);
+ close();
+ return;
+ }
+
+ if (readBufferPos_ < frameSize.sizeof) {
+ // Frame size not complete yet, save the current buffer in
+ // readWant_ so that the remaining bytes can be read later.
+ readWant_ = frameSize;
+ return;
+ }
+
+ auto size = netToHost(frameSize);
+ if (size > server_.maxFrameSize) {
+ logError("Frame size too large (%s > %s), client %s not using " ~
+ "TFramedTransport?", size, server_.maxFrameSize,
+ socket_.getPeerAddress().toHostNameString());
+ close();
+ return;
+ }
+ readWant_ = size;
+
+ // Now we know the frame size, set everything up for reading the
+ // payload.
+ transition();
+ return;
+
+ case SocketState.RECV:
+ // If we already got all the data, we should be in the SEND state.
+ assert(readBufferPos_ < readWant_);
+
+ size_t bytesRead;
+ try {
+ // Read as much as possible from the socket.
+ bytesRead = socket_.read(readBuffer_[readBufferPos_ .. readWant_]);
+ } catch (TTransportException te) {
+ logError("Failed to read from client socket: %s", te);
+ close();
+ return;
+ }
+
+ if (bytesRead == 0) {
+ // We were notified, but no bytes could be read -> the client
+ // disconnected.
+ close();
+ return;
+ }
+
+ readBufferPos_ += bytesRead;
+ assert(readBufferPos_ <= readWant_);
+
+ if (readBufferPos_ == readWant_) {
+ // The payload has been read completely, move on.
+ transition();
+ }
+
+ return;
+ case SocketState.SEND:
+ assert(writeBufferPos_ <= writeBuffer_.length);
+
+ if (writeBufferPos_ == writeBuffer_.length) {
+ // Nothing left to send – this shouldn't happen, just move on.
+ logInfo("WARNING: In send state, but no data to send.\n");
+ transition();
+ return;
+ }
+
+ size_t bytesSent;
+ try {
+ bytesSent = socket_.writeSome(writeBuffer_[writeBufferPos_ .. $]);
+ } catch (TTransportException te) {
+ logError("Failed to write to client socket: %s", te);
+ close();
+ return;
+ }
+
+ writeBufferPos_ += bytesSent;
+ assert(writeBufferPos_ <= writeBuffer_.length);
+
+ if (writeBufferPos_ == writeBuffer_.length) {
+ // The whole response has been written out, we are done.
+ transition();
+ }
+
+ return;
+ }
+ }
+
+ /**
+ * Registers a libevent event for workSocket() with the passed flags,
+ * unregistering the previous one (if any).
+ */
+ void registerEvent(short eventFlags) {
+ if (eventFlags_ == eventFlags) {
+ // Nothing to do if flags are the same.
+ return;
+ }
+
+ // Delete the previously existing event.
+ unregisterEvent();
+
+ eventFlags_ = eventFlags;
+
+ if (eventFlags == 0) return;
+
+ if (!event_) {
+ // If the event was not already allocated, do it now.
+ event_ = event_new(loop_.eventBase_, socket_.socketHandle,
+ eventFlags_, assumeNothrow(&workSocketCallback), cast(void*)this);
+ } else {
+ event_assign(event_, loop_.eventBase_, socket_.socketHandle,
+ eventFlags_, assumeNothrow(&workSocketCallback), cast(void*)this);
+ }
+
+ // Add the event
+ if (event_add(event_, null) == -1) {
+ logError("event_add() for client socket failed.");
+ }
+ }
+
+ /**
+ * Unregisters the current libevent event, if any.
+ */
+ void unregisterEvent() {
+ if (event_ && eventFlags_ != 0) {
+ eventFlags_ = 0;
+ if (event_del(event_) == -1) {
+ logError("event_del() for client socket failed.");
+ return;
+ }
+ }
+ }
+
+ /**
+ * Closes this connection and returns it back to the server.
+ */
+ void close() {
+ unregisterEvent();
+
+ if (server_.eventHandler) {
+ server_.eventHandler.deleteContext(
+ connectionContext_, inputProtocol_, outputProtocol_);
+ }
+
+ // Close the socket
+ socket_.close();
+
+ // close any factory produced transports.
+ factoryInputTransport_.close();
+ factoryOutputTransport_.close();
+
+ // This connection object can now be reused.
+ loop_.disposeConnection(this);
+ }
+
+ /// The server this connection belongs to.
+ TNonblockingServer server_;
+
+ /// The task pool used for this connection. This is cached instead of
+ /// directly using server_.taskPool to avoid confusion if it is changed in
+ /// another thread while the request is processed.
+ TaskPool taskPool_;
+
+ /// The I/O thread handling this connection.
+ IOLoop loop_;
+
+ /// The socket managed by this connection.
+ TSocket socket_;
+
+ /// The libevent object used for registering the workSocketCallback.
+ event* event_;
+
+ /// Libevent flags
+ short eventFlags_;
+
+ /// Socket mode
+ SocketState socketState_;
+
+ /// Application state
+ ConnectionState connState_;
+
+ /// The size of the frame to read. If still in READ_FRAME_SIZE state, some
+ /// of the bytes might not have been written, and the value might still be
+ /// in network byte order. An uint (not a size_t) because the frame size on
+ /// the wire is specified as one.
+ uint readWant_;
+
+ /// The position in the read buffer, i.e. the number of payload bytes
+ /// already received from the socket in READ_REQUEST state, resp. the
+ /// number of size bytes in READ_FRAME_SIZE state.
+ uint readBufferPos_;
+
+ /// Read buffer
+ ubyte* readBuffer_;
+
+ /// Read buffer size
+ size_t readBufferSize_;
+
+ /// Write buffer
+ ubyte[] writeBuffer_;
+
+ /// How far through writing are we?
+ size_t writeBufferPos_;
+
+ /// Largest size of write buffer seen since buffer was constructed
+ size_t largestWriteBufferSize_;
+
+ /// Number of calls since the last time checkIdleBufferLimit has been
+ /// invoked (see TServer.resizeBufferEveryN).
+ uint callsSinceResize_;
+
+ /// Base transports the processor reads from/writes to.
+ TInputRangeTransport!(ubyte[]) inputTransport_;
+ TMemoryBuffer outputTransport_;
+
+ /// The actual transports passed to the processor obtained via the
+ /// transport factory.
+ TTransport factoryInputTransport_;
+ TTransport factoryOutputTransport_; /// Ditto
+
+ /// Input/output protocols, connected to factory{Input, Output}Transport.
+ TProtocol inputProtocol_;
+ TProtocol outputProtocol_; /// Ditto.
+
+ /// Connection context optionally created by the server event handler.
+ Variant connectionContext_;
+
+ /// The processor used for this connection.
+ TProcessor processor_;
+ }
+}
+
+/*
+ * The request processing function, which invokes the processor for the server
+ * for all the RPC messages received over a connection.
+ *
+ * Must be public because it is passed as alias to std.parallelism.task().
+ */
+void processRequest(Connection connection) {
+ try {
+ while (true) {
+ with (connection) {
+ if (server_.eventHandler) {
+ server_.eventHandler.preProcess(connectionContext_, socket_);
+ }
+
+ if (!processor_.process(inputProtocol_, outputProtocol_,
+ connectionContext_) || !inputProtocol_.transport.peek()
+ ) {
+ // Something went fundamentally wrong or there is nothing more to
+ // process, close the connection.
+ break;
+ }
+ }
+ }
+ } catch (TTransportException ttx) {
+ logError("Client died: %s", ttx);
+ } catch (Exception e) {
+ logError("Uncaught exception: %s", e);
+ }
+
+ if (connection.taskPool_) connection.loop_.notifyCompleted(connection);
+}
+
+unittest {
+ import thrift.internal.test.server;
+
+ // Temporarily disable info log output in order not to spam the test results
+ // with startup info messages.
+ auto oldInfoLogSink = g_infoLogSink;
+ g_infoLogSink = null;
+ scope (exit) g_infoLogSink = oldInfoLogSink;
+
+ // Test in-line processing shutdown with one as well as several I/O threads.
+ testServeCancel!(TNonblockingServer)();
+ testServeCancel!(TNonblockingServer)((TNonblockingServer s) {
+ s.numIOThreads = 4;
+ });
+
+ // Test task pool processing shutdown with one as well as several I/O threads.
+ auto tp = new TaskPool(4);
+ tp.isDaemon = true;
+ testServeCancel!(TNonblockingServer)((TNonblockingServer s) {
+ s.taskPool = tp;
+ });
+ testServeCancel!(TNonblockingServer)((TNonblockingServer s) {
+ s.taskPool = tp;
+ s.numIOThreads = 4;
+ });
+}
diff --git a/lib/d/src/thrift/server/simple.d b/lib/d/src/thrift/server/simple.d
new file mode 100644
index 000000000..f7183a751
--- /dev/null
+++ b/lib/d/src/thrift/server/simple.d
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.server.simple;
+
+import std.variant : Variant;
+import thrift.base;
+import thrift.protocol.base;
+import thrift.protocol.processor;
+import thrift.server.base;
+import thrift.server.transport.base;
+import thrift.transport.base;
+import thrift.util.cancellation;
+
+/**
+ * The most basic server.
+ *
+ * It is single-threaded and after it accepts a connections, it processes
+ * requests on it until it closes, then waiting for the next connection.
+ *
+ * It is not so much of use in production than it is for writing unittests, or
+ * as an example on how to provide a custom TServer implementation.
+ */
+class TSimpleServer : TServer {
+ ///
+ this(
+ TProcessor processor,
+ TServerTransport serverTransport,
+ TTransportFactory transportFactory,
+ TProtocolFactory protocolFactory
+ ) {
+ super(processor, serverTransport, transportFactory, protocolFactory);
+ }
+
+ ///
+ this(
+ TProcessorFactory processorFactory,
+ TServerTransport serverTransport,
+ TTransportFactory transportFactory,
+ TProtocolFactory protocolFactory
+ ) {
+ super(processorFactory, serverTransport, transportFactory, protocolFactory);
+ }
+
+ ///
+ this(
+ TProcessor processor,
+ TServerTransport serverTransport,
+ TTransportFactory inputTransportFactory,
+ TTransportFactory outputTransportFactory,
+ TProtocolFactory inputProtocolFactory,
+ TProtocolFactory outputProtocolFactory
+ ) {
+ super(processor, serverTransport, inputTransportFactory,
+ outputTransportFactory, inputProtocolFactory, outputProtocolFactory);
+ }
+
+ this(
+ TProcessorFactory processorFactory,
+ TServerTransport serverTransport,
+ TTransportFactory inputTransportFactory,
+ TTransportFactory outputTransportFactory,
+ TProtocolFactory inputProtocolFactory,
+ TProtocolFactory outputProtocolFactory
+ ) {
+ super(processorFactory, serverTransport, inputTransportFactory,
+ outputTransportFactory, inputProtocolFactory, outputProtocolFactory);
+ }
+
+ override void serve(TCancellation cancellation = null) {
+ serverTransport_.listen();
+
+ if (eventHandler) eventHandler.preServe();
+
+ while (true) {
+ TTransport client;
+ TTransport inputTransport;
+ TTransport outputTransport;
+ TProtocol inputProtocol;
+ TProtocol outputProtocol;
+
+ try {
+ client = serverTransport_.accept(cancellation);
+ scope(failure) client.close();
+
+ inputTransport = inputTransportFactory_.getTransport(client);
+ scope(failure) inputTransport.close();
+
+ outputTransport = outputTransportFactory_.getTransport(client);
+ scope(failure) outputTransport.close();
+
+ inputProtocol = inputProtocolFactory_.getProtocol(inputTransport);
+ outputProtocol = outputProtocolFactory_.getProtocol(outputTransport);
+ } catch (TCancelledException tcx) {
+ break;
+ } catch (TTransportException ttx) {
+ logError("TServerTransport failed on accept: %s", ttx);
+ continue;
+ } catch (TException tx) {
+ logError("Caught TException on accept: %s", tx);
+ continue;
+ }
+
+ auto info = TConnectionInfo(inputProtocol, outputProtocol, client);
+ auto processor = processorFactory_.getProcessor(info);
+
+ Variant connectionContext;
+ if (eventHandler) {
+ connectionContext =
+ eventHandler.createContext(inputProtocol, outputProtocol);
+ }
+
+ try {
+ while (true) {
+ if (eventHandler) {
+ eventHandler.preProcess(connectionContext, client);
+ }
+
+ if (!processor.process(inputProtocol, outputProtocol,
+ connectionContext) || !inputProtocol.transport.peek()
+ ) {
+ // Something went fundamentlly wrong or there is nothing more to
+ // process, close the connection.
+ break;
+ }
+ }
+ } catch (TTransportException ttx) {
+ logError("Client died: %s", ttx);
+ } catch (Exception e) {
+ logError("Uncaught exception: %s", e);
+ }
+
+ if (eventHandler) {
+ eventHandler.deleteContext(connectionContext, inputProtocol,
+ outputProtocol);
+ }
+
+ try {
+ inputTransport.close();
+ } catch (TTransportException ttx) {
+ logError("Input close failed: %s", ttx);
+ }
+ try {
+ outputTransport.close();
+ } catch (TTransportException ttx) {
+ logError("Output close failed: %s", ttx);
+ }
+ try {
+ client.close();
+ } catch (TTransportException ttx) {
+ logError("Client close failed: %s", ttx);
+ }
+ }
+
+ try {
+ serverTransport_.close();
+ } catch (TServerTransportException e) {
+ logError("Server transport failed to close(): %s", e);
+ }
+ }
+}
+
+unittest {
+ import thrift.internal.test.server;
+ testServeCancel!TSimpleServer();
+}
diff --git a/lib/d/src/thrift/server/taskpool.d b/lib/d/src/thrift/server/taskpool.d
new file mode 100644
index 000000000..b4720a48e
--- /dev/null
+++ b/lib/d/src/thrift/server/taskpool.d
@@ -0,0 +1,302 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.server.taskpool;
+
+import core.sync.condition;
+import core.sync.mutex;
+import std.exception : enforce;
+import std.parallelism;
+import std.variant : Variant;
+import thrift.base;
+import thrift.protocol.base;
+import thrift.protocol.processor;
+import thrift.server.base;
+import thrift.server.transport.base;
+import thrift.transport.base;
+import thrift.util.cancellation;
+
+/**
+ * A server which dispatches client requests to a std.parallelism TaskPool.
+ */
+class TTaskPoolServer : TServer {
+ ///
+ this(
+ TProcessor processor,
+ TServerTransport serverTransport,
+ TTransportFactory transportFactory,
+ TProtocolFactory protocolFactory,
+ TaskPool taskPool = null
+ ) {
+ this(processor, serverTransport, transportFactory, transportFactory,
+ protocolFactory, protocolFactory, taskPool);
+ }
+
+ ///
+ this(
+ TProcessorFactory processorFactory,
+ TServerTransport serverTransport,
+ TTransportFactory transportFactory,
+ TProtocolFactory protocolFactory,
+ TaskPool taskPool = null
+ ) {
+ this(processorFactory, serverTransport, transportFactory, transportFactory,
+ protocolFactory, protocolFactory, taskPool);
+ }
+
+ ///
+ this(
+ TProcessor processor,
+ TServerTransport serverTransport,
+ TTransportFactory inputTransportFactory,
+ TTransportFactory outputTransportFactory,
+ TProtocolFactory inputProtocolFactory,
+ TProtocolFactory outputProtocolFactory,
+ TaskPool taskPool = null
+ ) {
+ this(new TSingletonProcessorFactory(processor), serverTransport,
+ inputTransportFactory, outputTransportFactory,
+ inputProtocolFactory, outputProtocolFactory);
+ }
+
+ ///
+ this(
+ TProcessorFactory processorFactory,
+ TServerTransport serverTransport,
+ TTransportFactory inputTransportFactory,
+ TTransportFactory outputTransportFactory,
+ TProtocolFactory inputProtocolFactory,
+ TProtocolFactory outputProtocolFactory,
+ TaskPool taskPool = null
+ ) {
+ super(processorFactory, serverTransport, inputTransportFactory,
+ outputTransportFactory, inputProtocolFactory, outputProtocolFactory);
+
+ if (taskPool) {
+ this.taskPool = taskPool;
+ } else {
+ auto ptp = std.parallelism.taskPool;
+ if (ptp.size > 0) {
+ taskPool_ = ptp;
+ } else {
+ // If the global task pool is empty (default on a single-core machine),
+ // create a new one with a single worker thread. The rationale for this
+ // is to avoid that an application which worked fine with no task pool
+ // explicitly set on the multi-core developer boxes suddenly fails on a
+ // single-core user machine.
+ taskPool_ = new TaskPool(1);
+ taskPool_.isDaemon = true;
+ }
+ }
+ }
+
+ override void serve(TCancellation cancellation = null) {
+ serverTransport_.listen();
+
+ if (eventHandler) eventHandler.preServe();
+
+ auto queueState = QueueState();
+
+ while (true) {
+ // Check if we can still handle more connections.
+ if (maxActiveConns) {
+ synchronized (queueState.mutex) {
+ while (queueState.activeConns >= maxActiveConns) {
+ queueState.connClosed.wait();
+ }
+ }
+ }
+
+ TTransport client;
+ TTransport inputTransport;
+ TTransport outputTransport;
+ TProtocol inputProtocol;
+ TProtocol outputProtocol;
+
+ try {
+ client = serverTransport_.accept(cancellation);
+ scope(failure) client.close();
+
+ inputTransport = inputTransportFactory_.getTransport(client);
+ scope(failure) inputTransport.close();
+
+ outputTransport = outputTransportFactory_.getTransport(client);
+ scope(failure) outputTransport.close();
+
+ inputProtocol = inputProtocolFactory_.getProtocol(inputTransport);
+ outputProtocol = outputProtocolFactory_.getProtocol(outputTransport);
+ } catch (TCancelledException tce) {
+ break;
+ } catch (TTransportException ttx) {
+ logError("TServerTransport failed on accept: %s", ttx);
+ continue;
+ } catch (TException tx) {
+ logError("Caught TException on accept: %s", tx);
+ continue;
+ }
+
+ auto info = TConnectionInfo(inputProtocol, outputProtocol, client);
+ auto processor = processorFactory_.getProcessor(info);
+
+ synchronized (queueState.mutex) {
+ ++queueState.activeConns;
+ }
+ taskPool_.put(task!worker(queueState, client, inputProtocol,
+ outputProtocol, processor, eventHandler));
+ }
+
+ // First, stop accepting new connections.
+ try {
+ serverTransport_.close();
+ } catch (TServerTransportException e) {
+ logError("Server transport failed to close: %s", e);
+ }
+
+ // Then, wait until all active connections are finished.
+ synchronized (queueState.mutex) {
+ while (queueState.activeConns > 0) {
+ queueState.connClosed.wait();
+ }
+ }
+ }
+
+ /**
+ * Sets the task pool to use.
+ *
+ * By default, the global std.parallelism taskPool instance is used, which
+ * might not be appropriate for many applications, e.g. where tuning the
+ * number of worker threads is desired. (On single-core systems, a private
+ * task pool with a single thread is used by default, since the global
+ * taskPool instance has no worker threads then.)
+ *
+ * Note: TTaskPoolServer expects that tasks are never dropped from the pool,
+ * e.g. by calling TaskPool.close() while there are still tasks in the
+ * queue. If this happens, serve() will never return.
+ */
+ void taskPool(TaskPool pool) @property {
+ enforce(pool !is null, "Cannot use a null task pool.");
+ enforce(pool.size > 0, "Cannot use a task pool with no worker threads.");
+ taskPool_ = pool;
+ }
+
+ /**
+ * The maximum number of client connections open at the same time. Zero for
+ * no limit, which is the default.
+ *
+ * If this limit is reached, no clients are accept()ed from the server
+ * transport any longer until another connection has been closed again.
+ */
+ size_t maxActiveConns;
+
+protected:
+ TaskPool taskPool_;
+}
+
+// Cannot be private as worker has to be passed as alias parameter to
+// another module.
+// private {
+ /*
+ * The state of the »connection queue«, i.e. used for keeping track of how
+ * many client connections are currently processed.
+ */
+ struct QueueState {
+ /// Protects the queue state.
+ Mutex mutex;
+
+ /// The number of active connections (from the time they are accept()ed
+ /// until they are closed when the worked task finishes).
+ size_t activeConns;
+
+ /// Signals that the number of active connections has been decreased, i.e.
+ /// that a connection has been closed.
+ Condition connClosed;
+
+ /// Returns an initialized instance.
+ static QueueState opCall() {
+ QueueState q;
+ q.mutex = new Mutex;
+ q.connClosed = new Condition(q.mutex);
+ return q;
+ }
+ }
+
+ void worker(ref QueueState queueState, TTransport client,
+ TProtocol inputProtocol, TProtocol outputProtocol,
+ TProcessor processor, TServerEventHandler eventHandler)
+ {
+ scope (exit) {
+ synchronized (queueState.mutex) {
+ assert(queueState.activeConns > 0);
+ --queueState.activeConns;
+ queueState.connClosed.notifyAll();
+ }
+ }
+
+ Variant connectionContext;
+ if (eventHandler) {
+ connectionContext =
+ eventHandler.createContext(inputProtocol, outputProtocol);
+ }
+
+ try {
+ while (true) {
+ if (eventHandler) {
+ eventHandler.preProcess(connectionContext, client);
+ }
+
+ if (!processor.process(inputProtocol, outputProtocol,
+ connectionContext) || !inputProtocol.transport.peek()
+ ) {
+ // Something went fundamentlly wrong or there is nothing more to
+ // process, close the connection.
+ break;
+ }
+ }
+ } catch (TTransportException ttx) {
+ logError("Client died: %s", ttx);
+ } catch (Exception e) {
+ logError("Uncaught exception: %s", e);
+ }
+
+ if (eventHandler) {
+ eventHandler.deleteContext(connectionContext, inputProtocol,
+ outputProtocol);
+ }
+
+ try {
+ inputProtocol.transport.close();
+ } catch (TTransportException ttx) {
+ logError("Input close failed: %s", ttx);
+ }
+ try {
+ outputProtocol.transport.close();
+ } catch (TTransportException ttx) {
+ logError("Output close failed: %s", ttx);
+ }
+ try {
+ client.close();
+ } catch (TTransportException ttx) {
+ logError("Client close failed: %s", ttx);
+ }
+ }
+// }
+
+unittest {
+ import thrift.internal.test.server;
+ testServeCancel!TTaskPoolServer();
+}
diff --git a/lib/d/src/thrift/server/threaded.d b/lib/d/src/thrift/server/threaded.d
new file mode 100644
index 000000000..1cde983a4
--- /dev/null
+++ b/lib/d/src/thrift/server/threaded.d
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.server.threaded;
+
+import core.thread;
+import std.variant : Variant;
+import thrift.base;
+import thrift.protocol.base;
+import thrift.protocol.processor;
+import thrift.server.base;
+import thrift.server.transport.base;
+import thrift.transport.base;
+import thrift.util.cancellation;
+
+/**
+ * A simple threaded server which spawns a new thread per connection.
+ */
+class TThreadedServer : TServer {
+ ///
+ this(
+ TProcessor processor,
+ TServerTransport serverTransport,
+ TTransportFactory transportFactory,
+ TProtocolFactory protocolFactory
+ ) {
+ super(processor, serverTransport, transportFactory, protocolFactory);
+ }
+
+ ///
+ this(
+ TProcessorFactory processorFactory,
+ TServerTransport serverTransport,
+ TTransportFactory transportFactory,
+ TProtocolFactory protocolFactory
+ ) {
+ super(processorFactory, serverTransport, transportFactory, protocolFactory);
+ }
+
+ ///
+ this(
+ TProcessor processor,
+ TServerTransport serverTransport,
+ TTransportFactory inputTransportFactory,
+ TTransportFactory outputTransportFactory,
+ TProtocolFactory inputProtocolFactory,
+ TProtocolFactory outputProtocolFactory
+ ) {
+ super(processor, serverTransport, inputTransportFactory,
+ outputTransportFactory, inputProtocolFactory, outputProtocolFactory);
+ }
+
+ ///
+ this(
+ TProcessorFactory processorFactory,
+ TServerTransport serverTransport,
+ TTransportFactory inputTransportFactory,
+ TTransportFactory outputTransportFactory,
+ TProtocolFactory inputProtocolFactory,
+ TProtocolFactory outputProtocolFactory
+ ) {
+ super(processorFactory, serverTransport, inputTransportFactory,
+ outputTransportFactory, inputProtocolFactory, outputProtocolFactory);
+ }
+
+ override void serve(TCancellation cancellation = null) {
+ try {
+ // Start the server listening
+ serverTransport_.listen();
+ } catch (TTransportException ttx) {
+ logError("listen() failed: %s", ttx);
+ return;
+ }
+
+ if (eventHandler) eventHandler.preServe();
+
+ auto workerThreads = new ThreadGroup();
+
+ while (true) {
+ TTransport client;
+ TTransport inputTransport;
+ TTransport outputTransport;
+ TProtocol inputProtocol;
+ TProtocol outputProtocol;
+
+ try {
+ client = serverTransport_.accept(cancellation);
+ scope(failure) client.close();
+
+ inputTransport = inputTransportFactory_.getTransport(client);
+ scope(failure) inputTransport.close();
+
+ outputTransport = outputTransportFactory_.getTransport(client);
+ scope(failure) outputTransport.close();
+
+ inputProtocol = inputProtocolFactory_.getProtocol(inputTransport);
+ outputProtocol = outputProtocolFactory_.getProtocol(outputTransport);
+ } catch (TCancelledException tce) {
+ break;
+ } catch (TTransportException ttx) {
+ logError("TServerTransport failed on accept: %s", ttx);
+ continue;
+ } catch (TException tx) {
+ logError("Caught TException on accept: %s", tx);
+ continue;
+ }
+
+ auto info = TConnectionInfo(inputProtocol, outputProtocol, client);
+ auto processor = processorFactory_.getProcessor(info);
+ auto worker = new WorkerThread(client, inputProtocol, outputProtocol,
+ processor, eventHandler);
+ workerThreads.add(worker);
+ worker.start();
+ }
+
+ try {
+ serverTransport_.close();
+ } catch (TServerTransportException e) {
+ logError("Server transport failed to close: %s", e);
+ }
+ workerThreads.joinAll();
+ }
+}
+
+// The worker thread handling a client connection.
+private class WorkerThread : Thread {
+ this(TTransport client, TProtocol inputProtocol, TProtocol outputProtocol,
+ TProcessor processor, TServerEventHandler eventHandler)
+ {
+ client_ = client;
+ inputProtocol_ = inputProtocol;
+ outputProtocol_ = outputProtocol;
+ processor_ = processor;
+ eventHandler_ = eventHandler;
+
+ super(&run);
+ }
+
+ void run() {
+ Variant connectionContext;
+ if (eventHandler_) {
+ connectionContext =
+ eventHandler_.createContext(inputProtocol_, outputProtocol_);
+ }
+
+ try {
+ while (true) {
+ if (eventHandler_) {
+ eventHandler_.preProcess(connectionContext, client_);
+ }
+
+ if (!processor_.process(inputProtocol_, outputProtocol_,
+ connectionContext) || !inputProtocol_.transport.peek()
+ ) {
+ // Something went fundamentlly wrong or there is nothing more to
+ // process, close the connection.
+ break;
+ }
+ }
+ } catch (TTransportException ttx) {
+ logError("Client died: %s", ttx);
+ } catch (Exception e) {
+ logError("Uncaught exception: %s", e);
+ }
+
+ if (eventHandler_) {
+ eventHandler_.deleteContext(connectionContext, inputProtocol_,
+ outputProtocol_);
+ }
+
+ try {
+ inputProtocol_.transport.close();
+ } catch (TTransportException ttx) {
+ logError("Input close failed: %s", ttx);
+ }
+ try {
+ outputProtocol_.transport.close();
+ } catch (TTransportException ttx) {
+ logError("Output close failed: %s", ttx);
+ }
+ try {
+ client_.close();
+ } catch (TTransportException ttx) {
+ logError("Client close failed: %s", ttx);
+ }
+ }
+
+private:
+ TTransport client_;
+ TProtocol inputProtocol_;
+ TProtocol outputProtocol_;
+ TProcessor processor_;
+ TServerEventHandler eventHandler_;
+}
+
+unittest {
+ import thrift.internal.test.server;
+ testServeCancel!TThreadedServer();
+}
+
diff --git a/lib/d/src/thrift/server/transport/base.d b/lib/d/src/thrift/server/transport/base.d
new file mode 100644
index 000000000..da165d3d3
--- /dev/null
+++ b/lib/d/src/thrift/server/transport/base.d
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.server.transport.base;
+
+import thrift.base;
+import thrift.transport.base;
+import thrift.util.cancellation;
+
+/**
+ * Some kind of I/O device enabling servers to listen for incoming client
+ * connections and communicate with them via a TTransport interface.
+ */
+interface TServerTransport {
+ /**
+ * Starts listening for server connections.
+ *
+ * Just as simliar functions commonly found in socket libraries, this
+ * function does not block.
+ *
+ * If the socket is already listening, nothing happens.
+ *
+ * Throws: TServerTransportException if listening failed or the transport
+ * was already listening.
+ */
+ void listen();
+
+ /**
+ * Closes the server transport, causing it to stop listening.
+ *
+ * Throws: TServerTransportException if the transport was not listening.
+ */
+ void close();
+
+ /**
+ * Returns whether the server transport is currently listening.
+ */
+ bool isListening() @property;
+
+ /**
+ * Accepts a client connection and returns an opened TTransport for it,
+ * never returning null.
+ *
+ * Blocks until a client connection is available.
+ *
+ * Params:
+ * cancellation = If triggered, requests the call to stop blocking and
+ * return with a TCancelledException. Implementations are free to
+ * ignore this if they cannot provide a reasonable.
+ *
+ * Throws: TServerTransportException if accepting failed,
+ * TCancelledException if it was cancelled.
+ */
+ TTransport accept(TCancellation cancellation = null) out (result) {
+ assert(result !is null);
+ }
+}
+
+/**
+ * Server transport exception.
+ */
+class TServerTransportException : TException {
+ /**
+ * Error codes for the various types of exceptions.
+ */
+ enum Type {
+ ///
+ UNKNOWN,
+
+ /// The server socket is not listening, but excepted to be.
+ NOT_LISTENING,
+
+ /// The server socket is already listening, but expected not to be.
+ ALREADY_LISTENING,
+
+ /// An operation on the primary underlying resource, e.g. a socket used
+ /// for accepting connections, failed.
+ RESOURCE_FAILED
+ }
+
+ ///
+ this(Type type, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
+ string msg = "TTransportException: ";
+ switch (type) {
+ case Type.UNKNOWN: msg ~= "Unknown server transport exception"; break;
+ case Type.NOT_LISTENING: msg ~= "Server transport not listening"; break;
+ case Type.ALREADY_LISTENING: msg ~= "Server transport already listening"; break;
+ case Type.RESOURCE_FAILED: msg ~= "An underlying resource failed"; break;
+ default: msg ~= "(Invalid exception type)"; break;
+ }
+
+ this(msg, type, file, line, next);
+ }
+
+ ///
+ this(string msg, string file = __FILE__, size_t line = __LINE__,
+ Throwable next = null)
+ {
+ this(msg, Type.UNKNOWN, file, line, next);
+ }
+
+ ///
+ this(string msg, Type type, string file = __FILE__, size_t line = __LINE__,
+ Throwable next = null)
+ {
+ super(msg, file, line, next);
+ type_ = type;
+ }
+
+ ///
+ Type type() const nothrow @property {
+ return type_;
+ }
+
+protected:
+ Type type_;
+}
+
diff --git a/lib/d/src/thrift/server/transport/socket.d b/lib/d/src/thrift/server/transport/socket.d
new file mode 100644
index 000000000..0cbca41a5
--- /dev/null
+++ b/lib/d/src/thrift/server/transport/socket.d
@@ -0,0 +1,380 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.server.transport.socket;
+
+import core.thread : dur, Duration, Thread;
+import core.stdc.string : strerror;
+import std.array : empty;
+import std.conv : text, to;
+import std.exception : enforce;
+import std.socket;
+import thrift.base;
+import thrift.internal.socket;
+import thrift.server.transport.base;
+import thrift.transport.base;
+import thrift.transport.socket;
+import thrift.util.awaitable;
+import thrift.util.cancellation;
+
+private alias TServerTransportException STE;
+
+/**
+ * Server socket implementation of TServerTransport.
+ *
+ * Maps to std.socket listen()/accept(); only provides TCP/IP sockets (i.e. no
+ * Unix sockets) for now, because they are not supported in std.socket.
+ */
+class TServerSocket : TServerTransport {
+ /**
+ * Constructs a new instance.
+ *
+ * Params:
+ * port = The TCP port to listen at (host is always 0.0.0.0).
+ * sendTimeout = The socket sending timeout.
+ * recvTimout = The socket receiving timeout.
+ */
+ this(ushort port, Duration sendTimeout = dur!"hnsecs"(0),
+ Duration recvTimeout = dur!"hnsecs"(0))
+ {
+ port_ = port;
+ sendTimeout_ = sendTimeout;
+ recvTimeout_ = recvTimeout;
+
+ cancellationNotifier_ = new TSocketNotifier;
+
+ socketSet_ = new SocketSet;
+ }
+
+ /// The port the server socket listens at.
+ ushort port() const @property {
+ return port_;
+ }
+
+ /// The socket sending timeout, zero to block infinitely.
+ void sendTimeout(Duration sendTimeout) @property {
+ sendTimeout_ = sendTimeout;
+ }
+
+ /// The socket receiving timeout, zero to block infinitely.
+ void recvTimeout(Duration recvTimeout) @property {
+ recvTimeout_ = recvTimeout;
+ }
+
+ /// The maximum number of listening retries if it fails.
+ void retryLimit(ushort retryLimit) @property {
+ retryLimit_ = retryLimit;
+ }
+
+ /// The delay between a listening attempt failing and retrying it.
+ void retryDelay(Duration retryDelay) @property {
+ retryDelay_ = retryDelay;
+ }
+
+ /// The size of the TCP send buffer, in bytes.
+ void tcpSendBuffer(int tcpSendBuffer) @property {
+ tcpSendBuffer_ = tcpSendBuffer;
+ }
+
+ /// The size of the TCP receiving buffer, in bytes.
+ void tcpRecvBuffer(int tcpRecvBuffer) @property {
+ tcpRecvBuffer_ = tcpRecvBuffer;
+ }
+
+ /// Whether to listen on IPv6 only, if IPv6 support is detected
+ /// (default: false).
+ void ipv6Only(bool value) @property {
+ ipv6Only_ = value;
+ }
+
+ override void listen() {
+ enforce(!isListening, new STE(STE.Type.ALREADY_LISTENING));
+
+ serverSocket_ = makeSocketAndListen(port_, ACCEPT_BACKLOG, retryLimit_,
+ retryDelay_, tcpSendBuffer_, tcpRecvBuffer_, ipv6Only_);
+ }
+
+ override void close() {
+ enforce(isListening, new STE(STE.Type.NOT_LISTENING));
+
+ serverSocket_.shutdown(SocketShutdown.BOTH);
+ serverSocket_.close();
+ serverSocket_ = null;
+ }
+
+ override bool isListening() @property {
+ return serverSocket_ !is null;
+ }
+
+ /// Number of connections listen() backlogs.
+ enum ACCEPT_BACKLOG = 1024;
+
+ override TTransport accept(TCancellation cancellation = null) {
+ enforce(isListening, new STE(STE.Type.NOT_LISTENING));
+
+ if (cancellation) cancellationNotifier_.attach(cancellation.triggering);
+ scope (exit) if (cancellation) cancellationNotifier_.detach();
+
+
+ // Too many EINTRs is a fault condition and would need to be handled
+ // manually by our caller, but we can tolerate a certain number.
+ enum MAX_EINTRS = 10;
+ uint numEintrs;
+
+ while (true) {
+ socketSet_.reset();
+ socketSet_.add(serverSocket_);
+ socketSet_.add(cancellationNotifier_.socket);
+
+ auto ret = Socket.select(socketSet_, null, null);
+ enforce(ret != 0, new STE("Socket.select() returned 0.",
+ STE.Type.RESOURCE_FAILED));
+
+ if (ret < 0) {
+ // Select itself failed, check if it was just due to an interrupted
+ // syscall.
+ if (getSocketErrno() == INTERRUPTED_ERRNO) {
+ if (numEintrs++ < MAX_EINTRS) {
+ continue;
+ } else {
+ throw new STE("Socket.select() was interrupted by a signal (EINTR) " ~
+ "more than " ~ to!string(MAX_EINTRS) ~ " times.",
+ STE.Type.RESOURCE_FAILED
+ );
+ }
+ }
+ throw new STE("Unknown error on Socket.select(): " ~
+ socketErrnoString(getSocketErrno()), STE.Type.RESOURCE_FAILED);
+ } else {
+ // Check for a ping on the interrupt socket.
+ if (socketSet_.isSet(cancellationNotifier_.socket)) {
+ cancellation.throwIfTriggered();
+ }
+
+ // Check for the actual server socket having a connection waiting.
+ if (socketSet_.isSet(serverSocket_)) {
+ break;
+ }
+ }
+ }
+
+ try {
+ auto client = createTSocket(serverSocket_.accept());
+ client.sendTimeout = sendTimeout_;
+ client.recvTimeout = recvTimeout_;
+ return client;
+ } catch (SocketException e) {
+ throw new STE("Unknown error on accepting: " ~ to!string(e),
+ STE.Type.RESOURCE_FAILED);
+ }
+ }
+
+protected:
+ /**
+ * Allows derived classes to create a different TSocket type.
+ */
+ TSocket createTSocket(Socket socket) {
+ return new TSocket(socket);
+ }
+
+private:
+ ushort port_;
+ Duration sendTimeout_;
+ Duration recvTimeout_;
+ ushort retryLimit_;
+ Duration retryDelay_;
+ uint tcpSendBuffer_;
+ uint tcpRecvBuffer_;
+ bool ipv6Only_;
+
+ Socket serverSocket_;
+ TSocketNotifier cancellationNotifier_;
+
+ // Keep socket set between accept() calls to avoid reallocating.
+ SocketSet socketSet_;
+}
+
+Socket makeSocketAndListen(ushort port, int backlog, ushort retryLimit,
+ Duration retryDelay, uint tcpSendBuffer = 0, uint tcpRecvBuffer = 0,
+ bool ipv6Only = false
+) {
+ Address localAddr;
+ try {
+ // null represents the wildcard address.
+ auto addrInfos = getAddressInfo(null, to!string(port),
+ AddressInfoFlags.PASSIVE, SocketType.STREAM, ProtocolType.TCP);
+ foreach (i, ai; addrInfos) {
+ // Prefer to bind to IPv6 addresses, because then IPv4 is listened to as
+ // well, but not the other way round.
+ if (ai.family == AddressFamily.INET6 || i == (addrInfos.length - 1)) {
+ localAddr = ai.address;
+ break;
+ }
+ }
+ } catch (Exception e) {
+ throw new STE("Could not determine local address to listen on.",
+ STE.Type.RESOURCE_FAILED, __FILE__, __LINE__, e);
+ }
+
+ Socket socket;
+ try {
+ socket = new Socket(localAddr.addressFamily, SocketType.STREAM,
+ ProtocolType.TCP);
+ } catch (SocketException e) {
+ throw new STE("Could not create accepting socket: " ~ to!string(e),
+ STE.Type.RESOURCE_FAILED);
+ }
+
+ try {
+ socket.setOption(SocketOptionLevel.IPV6, SocketOption.IPV6_V6ONLY, ipv6Only);
+ } catch (SocketException e) {
+ // This is somewhat expected on older systems (e.g. pre-Vista Windows),
+ // which do not support the IPV6_V6ONLY flag yet. Racy flag just to avoid
+ // log spew in unit tests.
+ shared static warned = false;
+ if (!warned) {
+ logError("Could not set IPV6_V6ONLY socket option: %s", e);
+ warned = true;
+ }
+ }
+
+ alias SocketOptionLevel.SOCKET lvlSock;
+
+ // Prevent 2 maximum segement lifetime delay on accept.
+ try {
+ socket.setOption(lvlSock, SocketOption.REUSEADDR, true);
+ } catch (SocketException e) {
+ throw new STE("Could not set REUSEADDR socket option: " ~ to!string(e),
+ STE.Type.RESOURCE_FAILED);
+ }
+
+ // Set TCP buffer sizes.
+ if (tcpSendBuffer > 0) {
+ try {
+ socket.setOption(lvlSock, SocketOption.SNDBUF, tcpSendBuffer);
+ } catch (SocketException e) {
+ throw new STE("Could not set socket send buffer size: " ~ to!string(e),
+ STE.Type.RESOURCE_FAILED);
+ }
+ }
+
+ if (tcpRecvBuffer > 0) {
+ try {
+ socket.setOption(lvlSock, SocketOption.RCVBUF, tcpRecvBuffer);
+ } catch (SocketException e) {
+ throw new STE("Could not set receive send buffer size: " ~ to!string(e),
+ STE.Type.RESOURCE_FAILED);
+ }
+ }
+
+ // Turn linger off to avoid blocking on socket close.
+ try {
+ linger l;
+ l.on = 0;
+ l.time = 0;
+ socket.setOption(lvlSock, SocketOption.LINGER, l);
+ } catch (SocketException e) {
+ throw new STE("Could not disable socket linger: " ~ to!string(e),
+ STE.Type.RESOURCE_FAILED);
+ }
+
+ // Set TCP_NODELAY.
+ try {
+ socket.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, true);
+ } catch (SocketException e) {
+ throw new STE("Could not disable Nagle's algorithm: " ~ to!string(e),
+ STE.Type.RESOURCE_FAILED);
+ }
+
+ ushort retries;
+ while (true) {
+ try {
+ socket.bind(localAddr);
+ break;
+ } catch (SocketException) {}
+
+ // If bind() worked, we breaked outside the loop above.
+ retries++;
+ if (retries < retryLimit) {
+ Thread.sleep(retryDelay);
+ } else {
+ throw new STE(text("Could not bind to address: ", localAddr),
+ STE.Type.RESOURCE_FAILED);
+ }
+ }
+
+ socket.listen(backlog);
+ return socket;
+}
+
+unittest {
+ // Test interrupt().
+ {
+ auto sock = new TServerSocket(0);
+ sock.listen();
+ scope (exit) sock.close();
+
+ auto cancellation = new TCancellationOrigin;
+
+ auto intThread = new Thread({
+ // Sleep for a bit until the socket is accepting.
+ Thread.sleep(dur!"msecs"(50));
+ cancellation.trigger();
+ });
+ intThread.start();
+
+ import std.exception;
+ assertThrown!TCancelledException(sock.accept(cancellation));
+ }
+
+ // Test receive() timeout on accepted client sockets.
+ {
+ immutable port = 11122;
+ auto timeout = dur!"msecs"(500);
+ auto serverSock = new TServerSocket(port, timeout, timeout);
+ serverSock.listen();
+ scope (exit) serverSock.close();
+
+ auto clientSock = new TSocket("127.0.0.1", port);
+ clientSock.open();
+ scope (exit) clientSock.close();
+
+ shared bool hasTimedOut;
+ auto recvThread = new Thread({
+ auto sock = serverSock.accept();
+ ubyte[1] data;
+ try {
+ sock.read(data);
+ } catch (TTransportException e) {
+ if (e.type == TTransportException.Type.TIMED_OUT) {
+ hasTimedOut = true;
+ } else {
+ import std.stdio;
+ stderr.writeln(e);
+ }
+ }
+ });
+ recvThread.isDaemon = true;
+ recvThread.start();
+
+ // Wait for the timeout, with a little bit of spare time.
+ Thread.sleep(timeout + dur!"msecs"(50));
+ enforce(hasTimedOut,
+ "Client socket receive() blocked for longer than recvTimeout.");
+ }
+}
diff --git a/lib/d/src/thrift/server/transport/ssl.d b/lib/d/src/thrift/server/transport/ssl.d
new file mode 100644
index 000000000..2dd9d2366
--- /dev/null
+++ b/lib/d/src/thrift/server/transport/ssl.d
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.server.transport.ssl;
+
+import std.datetime : Duration;
+import std.exception : enforce;
+import std.socket : Socket;
+import thrift.server.transport.socket;
+import thrift.transport.base;
+import thrift.transport.socket;
+import thrift.transport.ssl;
+
+/**
+ * A server transport implementation using SSL-encrypted sockets.
+ *
+ * Note:
+ * On Posix systems which do not have the BSD-specific SO_NOSIGPIPE flag, you
+ * might want to ignore the SIGPIPE signal, as OpenSSL might try to write to
+ * a closed socket if the peer disconnects abruptly:
+ * ---
+ * import core.stdc.signal;
+ * import core.sys.posix.signal;
+ * signal(SIGPIPE, SIG_IGN);
+ * ---
+ *
+ * See: thrift.transport.ssl.
+ */
+class TSSLServerSocket : TServerSocket {
+ /**
+ * Creates a new TSSLServerSocket.
+ *
+ * Params:
+ * port = The port on which to listen.
+ * sslContext = The TSSLContext to use for creating client
+ * sockets. Must be in server-side mode.
+ */
+ this(ushort port, TSSLContext sslContext) {
+ super(port);
+ setSSLContext(sslContext);
+ }
+
+ /**
+ * Creates a new TSSLServerSocket.
+ *
+ * Params:
+ * port = The port on which to listen.
+ * sendTimeout = The send timeout to set on the client sockets.
+ * recvTimeout = The receive timeout to set on the client sockets.
+ * sslContext = The TSSLContext to use for creating client
+ * sockets. Must be in server-side mode.
+ */
+ this(ushort port, Duration sendTimeout, Duration recvTimeout,
+ TSSLContext sslContext)
+ {
+ super(port, sendTimeout, recvTimeout);
+ setSSLContext(sslContext);
+ }
+
+protected:
+ override TSocket createTSocket(Socket socket) {
+ return new TSSLSocket(sslContext_, socket);
+ }
+
+private:
+ void setSSLContext(TSSLContext sslContext) {
+ enforce(sslContext.serverSide, new TTransportException(
+ "Need server-side SSL socket factory for TSSLServerSocket"));
+ sslContext_ = sslContext;
+ }
+
+ TSSLContext sslContext_;
+}
diff --git a/lib/d/src/thrift/transport/base.d b/lib/d/src/thrift/transport/base.d
new file mode 100644
index 000000000..7e76a5948
--- /dev/null
+++ b/lib/d/src/thrift/transport/base.d
@@ -0,0 +1,370 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.transport.base;
+
+import core.stdc.string : strerror;
+import std.conv : text;
+import thrift.base;
+
+/**
+ * An entity data can be read from and/or written to.
+ *
+ * A TTransport implementation may capable of either reading or writing, but
+ * not necessarily both.
+ */
+interface TTransport {
+ /**
+ * Whether this transport is open.
+ *
+ * If a transport is closed, it can be opened by calling open(), and vice
+ * versa for close().
+ *
+ * While a transport should always be open when trying to read/write data,
+ * the related functions do not necessarily fail when called for a closed
+ * transport. Situations like this could occur e.g. with a wrapper
+ * transport which buffers data when the underlying transport has already
+ * been closed (possibly because the connection was abruptly closed), but
+ * there is still data left to be read in the buffers. This choice has been
+ * made to simplify transport implementations, in terms of both code
+ * complexity and runtime overhead.
+ */
+ bool isOpen() @property;
+
+ /**
+ * Tests whether there is more data to read or if the remote side is
+ * still open.
+ *
+ * A typical use case would be a server checking if it should process
+ * another request on the transport.
+ */
+ bool peek();
+
+ /**
+ * Opens the transport for communications.
+ *
+ * If the transport is already open, nothing happens.
+ *
+ * Throws: TTransportException if opening fails.
+ */
+ void open();
+
+ /**
+ * Closes the transport.
+ *
+ * If the transport is not open, nothing happens.
+ *
+ * Throws: TTransportException if closing fails.
+ */
+ void close();
+
+ /**
+ * Attempts to fill the given buffer by reading data.
+ *
+ * For potentially blocking data sources (e.g. sockets), read() will only
+ * block if no data is available at all. If there is some data available,
+ * but waiting for new data to arrive would be required to fill the whole
+ * buffer, the readily available data will be immediately returned – use
+ * readAll() if you want to wait until the whole buffer is filled.
+ *
+ * Params:
+ * buf = Slice to use as buffer.
+ *
+ * Returns: How many bytes were actually read
+ *
+ * Throws: TTransportException if an error occurs.
+ */
+ size_t read(ubyte[] buf);
+
+ /**
+ * Fills the given buffer by reading data into it, failing if not enough
+ * data is available.
+ *
+ * Params:
+ * buf = Slice to use as buffer.
+ *
+ * Throws: TTransportException if insufficient data is available or reading
+ * fails altogether.
+ */
+ void readAll(ubyte[] buf);
+
+ /**
+ * Must be called by clients when read is completed.
+ *
+ * Implementations can choose to perform a transport-specific action, e.g.
+ * logging the request to a file.
+ *
+ * Returns: The number of bytes read if available, 0 otherwise.
+ */
+ size_t readEnd();
+
+ /**
+ * Writes the passed slice of data.
+ *
+ * Note: You must call flush() to ensure the data is actually written,
+ * and available to be read back in the future. Destroying a TTransport
+ * object does not automatically flush pending data – if you destroy a
+ * TTransport object with written but unflushed data, that data may be
+ * discarded.
+ *
+ * Params:
+ * buf = Slice of data to write.
+ *
+ * Throws: TTransportException if an error occurs.
+ */
+ void write(in ubyte[] buf);
+
+ /**
+ * Must be called by clients when write is completed.
+ *
+ * Implementations can choose to perform a transport-specific action, e.g.
+ * logging the request to a file.
+ *
+ * Returns: The number of bytes written if available, 0 otherwise.
+ */
+ size_t writeEnd();
+
+ /**
+ * Flushes any pending data to be written.
+ *
+ * Must be called before destruction to ensure writes are actually complete,
+ * otherwise pending data may be discarded. Typically used with buffered
+ * transport mechanisms.
+ *
+ * Throws: TTransportException if an error occurs.
+ */
+ void flush();
+
+ /**
+ * Attempts to return a slice of <code>len</code> bytes of incoming data,
+ * possibly copied into buf, not consuming them (i.e.: a later read will
+ * return the same data).
+ *
+ * This method is meant to support protocols that need to read variable-
+ * length fields. They can attempt to borrow the maximum amount of data that
+ * they will need, then <code>consume()</code> what they actually use. Some
+ * transports will not support this method and others will fail occasionally,
+ * so protocols must be prepared to fall back to <code>read()</code> if
+ * borrow fails.
+ *
+ * The transport must be open when calling this.
+ *
+ * Params:
+ * buf = A buffer where the data can be stored if needed, or null to
+ * indicate that the caller is not supplying storage, but would like a
+ * slice of an internal buffer, if available.
+ * len = The number of bytes to borrow.
+ *
+ * Returns: If the borrow succeeds, a slice containing the borrowed data,
+ * null otherwise. The slice will be at least as long as requested, but
+ * may be longer if the returned slice points into an internal buffer
+ * rather than buf.
+ *
+ * Throws: TTransportException if an error occurs.
+ */
+ const(ubyte)[] borrow(ubyte* buf, size_t len) out (result) {
+ // FIXME: Commented out because len gets corrupted in
+ // thrift.transport.memory borrow() unittest.
+ version(none) assert(result is null || result.length >= len,
+ "Buffer returned by borrow() too short.");
+ }
+
+ /**
+ * Remove len bytes from the transport. This must always follow a borrow
+ * of at least len bytes, and should always succeed.
+ *
+ * The transport must be open when calling this.
+ *
+ * Params:
+ * len = Number of bytes to consume.
+ *
+ * Throws: TTransportException if an error occurs.
+ */
+ void consume(size_t len);
+}
+
+/**
+ * Provides basic fall-back implementations of the TTransport interface.
+ */
+class TBaseTransport : TTransport {
+ override bool isOpen() @property {
+ return false;
+ }
+
+ override bool peek() {
+ return isOpen;
+ }
+
+ override void open() {
+ throw new TTransportException("Cannot open TBaseTransport.",
+ TTransportException.Type.NOT_IMPLEMENTED);
+ }
+
+ override void close() {
+ throw new TTransportException("Cannot close TBaseTransport.",
+ TTransportException.Type.NOT_IMPLEMENTED);
+ }
+
+ override size_t read(ubyte[] buf) {
+ throw new TTransportException("Cannot read from a TBaseTransport.",
+ TTransportException.Type.NOT_IMPLEMENTED);
+ }
+
+ override void readAll(ubyte[] buf) {
+ size_t have;
+ while (have < buf.length) {
+ size_t get = read(buf[have..$]);
+ if (get <= 0) {
+ throw new TTransportException(text("Could not readAll() ", buf.length,
+ " bytes as no more data was available after ", have, " bytes."),
+ TTransportException.Type.END_OF_FILE);
+ }
+ have += get;
+ }
+ }
+
+ override size_t readEnd() {
+ // Do nothing by default, not needed by all implementations.
+ return 0;
+ }
+
+ override void write(in ubyte[] buf) {
+ throw new TTransportException("Cannot write to a TBaseTransport.",
+ TTransportException.Type.NOT_IMPLEMENTED);
+ }
+
+ override size_t writeEnd() {
+ // Do nothing by default, not needed by all implementations.
+ return 0;
+ }
+
+ override void flush() {
+ // Do nothing by default, not needed by all implementations.
+ }
+
+ override const(ubyte)[] borrow(ubyte* buf, size_t len) {
+ // borrow() is allowed to fail anyway, so just return null.
+ return null;
+ }
+
+ override void consume(size_t len) {
+ throw new TTransportException("Cannot consume from a TBaseTransport.",
+ TTransportException.Type.NOT_IMPLEMENTED);
+ }
+
+protected:
+ this() {}
+}
+
+/**
+ * Makes a TTransport which wraps a given source transport in some way.
+ *
+ * A common use case is inside server implementations, where the raw client
+ * connections accepted from e.g. TServerSocket need to be wrapped into
+ * buffered or compressed transports.
+ */
+class TTransportFactory {
+ /**
+ * Default implementation does nothing, just returns the transport given.
+ */
+ TTransport getTransport(TTransport trans) {
+ return trans;
+ }
+}
+
+/**
+ * Transport factory for transports which simply wrap an underlying TTransport
+ * without requiring additional configuration.
+ */
+class TWrapperTransportFactory(T) if (
+ is(T : TTransport) && __traits(compiles, new T(TTransport.init))
+) : TTransportFactory {
+ override T getTransport(TTransport trans) {
+ return new T(trans);
+ }
+}
+
+/**
+ * Transport-level exception.
+ */
+class TTransportException : TException {
+ /**
+ * Error codes for the various types of exceptions.
+ */
+ enum Type {
+ UNKNOWN, ///
+ NOT_OPEN, ///
+ TIMED_OUT, ///
+ END_OF_FILE, ///
+ INTERRUPTED, ///
+ BAD_ARGS, ///
+ CORRUPTED_DATA, ///
+ INTERNAL_ERROR, ///
+ NOT_IMPLEMENTED ///
+ }
+
+ ///
+ this(Type type, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
+ static string msgForType(Type type) {
+ switch (type) {
+ case Type.UNKNOWN: return "Unknown transport exception";
+ case Type.NOT_OPEN: return "Transport not open";
+ case Type.TIMED_OUT: return "Timed out";
+ case Type.END_OF_FILE: return "End of file";
+ case Type.INTERRUPTED: return "Interrupted";
+ case Type.BAD_ARGS: return "Invalid arguments";
+ case Type.CORRUPTED_DATA: return "Corrupted Data";
+ case Type.INTERNAL_ERROR: return "Internal error";
+ case Type.NOT_IMPLEMENTED: return "Not implemented";
+ default: return "(Invalid exception type)";
+ }
+ }
+ this(msgForType(type), type, file, line, next);
+ }
+
+ ///
+ this(string msg, string file = __FILE__, size_t line = __LINE__,
+ Throwable next = null)
+ {
+ this(msg, Type.UNKNOWN, file, line, next);
+ }
+
+ ///
+ this(string msg, Type type, string file = __FILE__, size_t line = __LINE__,
+ Throwable next = null)
+ {
+ super(msg, file, line, next);
+ type_ = type;
+ }
+
+ ///
+ Type type() const nothrow @property {
+ return type_;
+ }
+
+protected:
+ Type type_;
+}
+
+/**
+ * Meta-programming helper returning whether the passed type is a TTransport
+ * implementation.
+ */
+template isTTransport(T) {
+ enum isTTransport = is(T : TTransport);
+}
diff --git a/lib/d/src/thrift/transport/buffered.d b/lib/d/src/thrift/transport/buffered.d
new file mode 100644
index 000000000..cabfbdc03
--- /dev/null
+++ b/lib/d/src/thrift/transport/buffered.d
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.transport.buffered;
+
+import std.algorithm : min;
+import std.array : empty;
+import std.exception : enforce;
+import thrift.transport.base;
+
+/**
+ * Wraps another transport and buffers reads and writes until the internal
+ * buffers are exhausted, at which point new data is fetched resp. the
+ * accumulated data is written out at once.
+ */
+final class TBufferedTransport : TBaseTransport {
+ /**
+ * Constructs a new instance, using the default buffer sizes.
+ *
+ * Params:
+ * transport = The underlying transport to wrap.
+ */
+ this(TTransport transport) {
+ this(transport, DEFAULT_BUFFER_SIZE);
+ }
+
+ /**
+ * Constructs a new instance, using the specified buffer size.
+ *
+ * Params:
+ * transport = The underlying transport to wrap.
+ * bufferSize = The size of the read and write buffers to use, in bytes.
+ */
+ this(TTransport transport, size_t bufferSize) {
+ this(transport, bufferSize, bufferSize);
+ }
+
+ /**
+ * Constructs a new instance, using the specified buffer size.
+ *
+ * Params:
+ * transport = The underlying transport to wrap.
+ * readBufferSize = The size of the read buffer to use, in bytes.
+ * writeBufferSize = The size of the write buffer to use, in bytes.
+ */
+ this(TTransport transport, size_t readBufferSize, size_t writeBufferSize) {
+ transport_ = transport;
+ readBuffer_ = new ubyte[readBufferSize];
+ writeBuffer_ = new ubyte[writeBufferSize];
+ writeAvail_ = writeBuffer_;
+ }
+
+ /// The default size of the read/write buffers, in bytes.
+ enum int DEFAULT_BUFFER_SIZE = 512;
+
+ override bool isOpen() @property {
+ return transport_.isOpen();
+ }
+
+ override bool peek() {
+ if (readAvail_.empty) {
+ // If there is nothing available to read, see if we can get something
+ // from the underlying transport.
+ auto bytesRead = transport_.read(readBuffer_);
+ readAvail_ = readBuffer_[0 .. bytesRead];
+ }
+
+ return !readAvail_.empty;
+ }
+
+ override void open() {
+ transport_.open();
+ }
+
+ override void close() {
+ if (!isOpen) return;
+ flush();
+ transport_.close();
+ }
+
+ override size_t read(ubyte[] buf) {
+ if (readAvail_.empty) {
+ // No data left in our buffer, fetch some from the underlying transport.
+
+ if (buf.length > readBuffer_.length) {
+ // If the amount of data requested is larger than our reading buffer,
+ // directly read to the passed buffer. This probably doesn't occur too
+ // often in practice (and even if it does, the underlying transport
+ // probably cannot fulfill the request at once anyway), but it can't
+ // harm to try…
+ return transport_.read(buf);
+ }
+
+ auto bytesRead = transport_.read(readBuffer_);
+ readAvail_ = readBuffer_[0 .. bytesRead];
+ }
+
+ // Hand over whatever we have.
+ auto give = min(readAvail_.length, buf.length);
+ buf[0 .. give] = readAvail_[0 .. give];
+ readAvail_ = readAvail_[give .. $];
+ return give;
+ }
+
+ /**
+ * Shortcut version of readAll.
+ */
+ override void readAll(ubyte[] buf) {
+ if (readAvail_.length >= buf.length) {
+ buf[] = readAvail_[0 .. buf.length];
+ readAvail_ = readAvail_[buf.length .. $];
+ return;
+ }
+
+ super.readAll(buf);
+ }
+
+ override void write(in ubyte[] buf) {
+ if (writeAvail_.length >= buf.length) {
+ // If the data fits in the buffer, just save it there.
+ writeAvail_[0 .. buf.length] = buf;
+ writeAvail_ = writeAvail_[buf.length .. $];
+ return;
+ }
+
+ // We have to decide if we copy data from buf to our internal buffer, or
+ // just directly write them out. The same considerations about avoiding
+ // syscalls as for C++ apply here.
+ auto bytesAvail = writeAvail_.ptr - writeBuffer_.ptr;
+ if ((bytesAvail + buf.length >= 2 * writeBuffer_.length) || (bytesAvail == 0)) {
+ // We would immediately need two syscalls anyway (or we don't have
+ // anything) in our buffer to write, so just write out both buffers.
+ if (bytesAvail > 0) {
+ transport_.write(writeBuffer_[0 .. bytesAvail]);
+ writeAvail_ = writeBuffer_;
+ }
+
+ transport_.write(buf);
+ return;
+ }
+
+ // Fill up our internal buffer for a write.
+ writeAvail_[] = buf[0 .. writeAvail_.length];
+ auto left = buf[writeAvail_.length .. $];
+ transport_.write(writeBuffer_);
+
+ // Copy the rest into our buffer.
+ writeBuffer_[0 .. left.length] = left[];
+ writeAvail_ = writeBuffer_[left.length .. $];
+ }
+
+ override void flush() {
+ // Write out any data waiting in the write buffer.
+ auto bytesAvail = writeAvail_.ptr - writeBuffer_.ptr;
+ if (bytesAvail > 0) {
+ // Note that we reset writeAvail_ prior to calling the underlying protocol
+ // to make sure the buffer is cleared even if the transport throws an
+ // exception.
+ writeAvail_ = writeBuffer_;
+ transport_.write(writeBuffer_[0 .. bytesAvail]);
+ }
+
+ // Flush the underlying transport.
+ transport_.flush();
+ }
+
+ override const(ubyte)[] borrow(ubyte* buf, size_t len) {
+ if (len <= readAvail_.length) {
+ return readAvail_;
+ }
+ return null;
+ }
+
+ override void consume(size_t len) {
+ enforce(len <= readBuffer_.length, new TTransportException(
+ "Invalid consume length.", TTransportException.Type.BAD_ARGS));
+ readAvail_ = readAvail_[len .. $];
+ }
+
+ /**
+ * The wrapped transport.
+ */
+ TTransport underlyingTransport() @property {
+ return transport_;
+ }
+
+private:
+ TTransport transport_;
+
+ ubyte[] readBuffer_;
+ ubyte[] writeBuffer_;
+
+ ubyte[] readAvail_;
+ ubyte[] writeAvail_;
+}
+
+/**
+ * Wraps given transports into TBufferedTransports.
+ */
+alias TWrapperTransportFactory!TBufferedTransport TBufferedTransportFactory;
diff --git a/lib/d/src/thrift/transport/file.d b/lib/d/src/thrift/transport/file.d
new file mode 100644
index 000000000..7c6705a6e
--- /dev/null
+++ b/lib/d/src/thrift/transport/file.d
@@ -0,0 +1,1100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Transports for reading from/writing to Thrift »log files«.
+ *
+ * These transports are not »stupid« sources and sinks just reading and
+ * writing bytes from a file verbatim, but organize the contents in the form
+ * of so-called »events«, which refers to the data written between two flush()
+ * calls.
+ *
+ * Chunking is supported, events are guaranteed to never span chunk boundaries.
+ * As a consequence, an event can never be larger than the chunk size. The
+ * chunk size used is not saved with the file, so care has to be taken to make
+ * sure the same chunk size is used for reading and writing.
+ */
+module thrift.transport.file;
+
+import core.thread : Thread;
+import std.array : empty;
+import std.algorithm : min, max;
+import std.concurrency;
+import std.conv : to;
+import std.datetime : AutoStart, dur, Duration, StopWatch;
+import std.exception;
+import std.stdio : File;
+import thrift.base;
+import thrift.transport.base;
+
+/// The default chunk size, in bytes.
+enum DEFAULT_CHUNK_SIZE = 16 * 1024 * 1024;
+
+/// The type used to represent event sizes in the file.
+alias uint EventSize;
+
+version (BigEndian) {
+ static assert(false,
+ "Little endian byte order is assumed in thrift.transport.file.");
+}
+
+/**
+ * A transport used to read log files. It can never be written to, calling
+ * write() throws.
+ *
+ * Contrary to the C++ design, explicitly opening the transport/file before
+ * using is necessary to allow manually closing the file without relying on the
+ * object lifetime. Otherwise, it's a straight port of the C++ implementation.
+ */
+final class TFileReaderTransport : TBaseTransport {
+ /**
+ * Creates a new file writer transport.
+ *
+ * Params:
+ * path = Path of the file to opperate on.
+ */
+ this(string path) {
+ path_ = path;
+ chunkSize_ = DEFAULT_CHUNK_SIZE;
+ readBufferSize_ = DEFAULT_READ_BUFFER_SIZE;
+ readTimeout_ = DEFAULT_READ_TIMEOUT;
+ corruptedEventSleepDuration_ = DEFAULT_CORRUPTED_EVENT_SLEEP_DURATION;
+ maxEventSize = DEFAULT_MAX_EVENT_SIZE;
+ }
+
+ override bool isOpen() @property {
+ return isOpen_;
+ }
+
+ override bool peek() {
+ if (!isOpen) return false;
+
+ // If there is no event currently processed, try fetching one from the
+ // file.
+ if (!currentEvent_) {
+ currentEvent_ = readEvent();
+
+ if (!currentEvent_) {
+ // Still nothing there, couldn't read a new event.
+ return false;
+ }
+ }
+ // check if there is anything to read
+ return (currentEvent_.length - currentEventPos_) > 0;
+ }
+
+ override void open() {
+ if (isOpen) return;
+ try {
+ file_ = File(path_, "rb");
+ } catch (Exception e) {
+ throw new TTransportException("Error on opening input file.",
+ TTransportException.Type.NOT_OPEN, __FILE__, __LINE__, e);
+ }
+ isOpen_ = true;
+ }
+
+ override void close() {
+ if (!isOpen) return;
+
+ file_.close();
+ isOpen_ = false;
+ readState_.resetAllValues();
+ }
+
+ override size_t read(ubyte[] buf) {
+ enforce(isOpen, new TTransportException(
+ "Cannot read if file is not open.", TTransportException.Type.NOT_OPEN));
+
+ // If there is no event currently processed, try fetching one from the
+ // file.
+ if (!currentEvent_) {
+ currentEvent_ = readEvent();
+
+ if (!currentEvent_) {
+ // Still nothing there, couldn't read a new event.
+ return 0;
+ }
+ }
+
+ auto len = buf.length;
+ auto remaining = currentEvent_.length - currentEventPos_;
+
+ if (remaining <= len) {
+ // If less than the requested length is available, read as much as
+ // possible.
+ buf[0 .. remaining] = currentEvent_[currentEventPos_ .. $];
+ currentEvent_ = null;
+ currentEventPos_ = 0;
+ return remaining;
+ }
+
+ // There will still be data left in the buffer after reading, pass out len
+ // bytes.
+ buf[] = currentEvent_[currentEventPos_ .. currentEventPos_ + len];
+ currentEventPos_ += len;
+ return len;
+ }
+
+ ulong getNumChunks() {
+ enforce(isOpen, new TTransportException(
+ "Cannot get number of chunks if file not open.",
+ TTransportException.Type.NOT_OPEN));
+
+ try {
+ auto fileSize = file_.size();
+ if (fileSize == 0) {
+ // Empty files have no chunks.
+ return 0;
+ }
+ return ((fileSize)/chunkSize_) + 1;
+ } catch (Exception e) {
+ throw new TTransportException("Error getting file size.", __FILE__,
+ __LINE__, e);
+ }
+ }
+
+ ulong getCurChunk() {
+ return offset_ / chunkSize_;
+ }
+
+ void seekToChunk(long chunk) {
+ enforce(isOpen, new TTransportException(
+ "Cannot get number of chunks if file not open.",
+ TTransportException.Type.NOT_OPEN));
+
+ auto numChunks = getNumChunks();
+
+ if (chunk < 0) {
+ // Count negative indices from the end.
+ chunk += numChunks;
+ }
+
+ if (chunk < 0) {
+ logError("Incorrect chunk number for reverse seek, seeking to " ~
+ "beginning instead: %s", chunk);
+ chunk = 0;
+ }
+
+ bool seekToEnd;
+ long minEndOffset;
+ if (chunk >= numChunks) {
+ logError("Trying to seek to non-existing chunk, seeking to " ~
+ "end of file instead: %s", chunk);
+ seekToEnd = true;
+ chunk = numChunks - 1;
+ // this is the min offset to process events till
+ minEndOffset = file_.size();
+ }
+
+ readState_.resetAllValues();
+ currentEvent_ = null;
+
+ try {
+ file_.seek(chunk * chunkSize_);
+ offset_ = chunk * chunkSize_;
+ } catch (Exception e) {
+ throw new TTransportException("Error seeking to chunk", __FILE__,
+ __LINE__, e);
+ }
+
+ if (seekToEnd) {
+ // Never wait on the end of the file for new content, we just want to
+ // find the last one.
+ auto oldReadTimeout = readTimeout_;
+ scope (exit) readTimeout_ = oldReadTimeout;
+ readTimeout_ = dur!"hnsecs"(0);
+
+ // Keep on reading unti the last event at point of seekToChunk call.
+ while ((offset_ + readState_.bufferPos_) < minEndOffset) {
+ if (readEvent() is null) {
+ break;
+ }
+ }
+ }
+ }
+
+ void seekToEnd() {
+ seekToChunk(getNumChunks());
+ }
+
+ /**
+ * The size of the chunks the file is divided into, in bytes.
+ */
+ ulong chunkSize() @property const {
+ return chunkSize_;
+ }
+
+ /// ditto
+ void chunkSize(ulong value) @property {
+ enforce(!isOpen, new TTransportException(
+ "Cannot set chunk size after TFileReaderTransport has been opened."));
+ enforce(value > EventSize.sizeof, new TTransportException("Chunks must " ~
+ "be large enough to accomodate at least a single byte of payload data."));
+ chunkSize_ = value;
+ }
+
+ /**
+ * If positive, wait the specified duration for new data when arriving at
+ * end of file. If negative, wait forever (tailing mode), waking up to check
+ * in the specified interval. If zero, do not wait at all.
+ *
+ * Defaults to 500 ms.
+ */
+ Duration readTimeout() @property const {
+ return readTimeout_;
+ }
+
+ /// ditto
+ void readTimeout(Duration value) @property {
+ readTimeout_ = value;
+ }
+
+ /// ditto
+ enum DEFAULT_READ_TIMEOUT = dur!"msecs"(500);
+
+ /**
+ * Read buffer size, in bytes.
+ *
+ * Defaults to 1 MiB.
+ */
+ size_t readBufferSize() @property const {
+ return readBufferSize_;
+ }
+
+ /// ditto
+ void readBufferSize(size_t value) @property {
+ if (readBuffer_) {
+ enforce(value <= readBufferSize_,
+ "Cannot shrink read buffer after first read.");
+ readBuffer_.length = value;
+ }
+ readBufferSize_ = value;
+ }
+
+ /// ditto
+ enum DEFAULT_READ_BUFFER_SIZE = 1 * 1024 * 1024;
+
+ /**
+ * Arbitrary event size limit, in bytes. Must be smaller than chunk size.
+ *
+ * Defaults to zero (no limit).
+ */
+ size_t maxEventSize() @property const {
+ return maxEventSize_;
+ }
+
+ /// ditto
+ void maxEventSize(size_t value) @property {
+ enforce(value <= chunkSize_ - EventSize.sizeof, "Events cannot span " ~
+ "mutiple chunks, maxEventSize must be smaller than chunk size.");
+ maxEventSize_ = value;
+ }
+
+ /// ditto
+ enum DEFAULT_MAX_EVENT_SIZE = 0;
+
+ /**
+ * The interval at which the thread wakes up to check for the next chunk
+ * in tailing mode.
+ *
+ * Defaults to one second.
+ */
+ Duration corruptedEventSleepDuration() const {
+ return corruptedEventSleepDuration_;
+ }
+
+ /// ditto
+ void corruptedEventSleepDuration(Duration value) {
+ corruptedEventSleepDuration_ = value;
+ }
+
+ /// ditto
+ enum DEFAULT_CORRUPTED_EVENT_SLEEP_DURATION = dur!"seconds"(1);
+
+ /**
+ * The maximum number of corrupted events tolerated before the whole chunk
+ * is skipped.
+ *
+ * Defaults to zero.
+ */
+ uint maxCorruptedEvents() @property const {
+ return maxCorruptedEvents_;
+ }
+
+ /// ditto
+ void maxCorruptedEvents(uint value) @property {
+ maxCorruptedEvents_ = value;
+ }
+
+ /// ditto
+ enum DEFAULT_MAX_CORRUPTED_EVENTS = 0;
+
+private:
+ ubyte[] readEvent() {
+ if (!readBuffer_) {
+ readBuffer_ = new ubyte[readBufferSize_];
+ }
+
+ bool timeoutExpired;
+ while (1) {
+ // read from the file if read buffer is exhausted
+ if (readState_.bufferPos_ == readState_.bufferLen_) {
+ // advance the offset pointer
+ offset_ += readState_.bufferLen_;
+
+ try {
+ // Need to clear eof flag before reading, otherwise tailing a file
+ // does not work.
+ file_.clearerr();
+
+ auto usedBuf = file_.rawRead(readBuffer_);
+ readState_.bufferLen_ = usedBuf.length;
+ } catch (Exception e) {
+ readState_.resetAllValues();
+ throw new TTransportException("Error while reading from file",
+ __FILE__, __LINE__, e);
+ }
+
+ readState_.bufferPos_ = 0;
+ readState_.lastDispatchPos_ = 0;
+
+ if (readState_.bufferLen_ == 0) {
+ // Reached end of file.
+ if (readTimeout_ < dur!"hnsecs"(0)) {
+ // Tailing mode, sleep for the specified duration and try again.
+ Thread.sleep(-readTimeout_);
+ continue;
+ } else if (readTimeout_ == dur!"hnsecs"(0) || timeoutExpired) {
+ // Either no timeout set, or it has already expired.
+ readState_.resetState(0);
+ return null;
+ } else {
+ // Timeout mode, sleep for the specified amount of time and retry.
+ Thread.sleep(readTimeout_);
+ timeoutExpired = true;
+ continue;
+ }
+ }
+ }
+
+ // Attempt to read an event from the buffer.
+ while (readState_.bufferPos_ < readState_.bufferLen_) {
+ if (readState_.readingSize_) {
+ if (readState_.eventSizeBuffPos_ == 0) {
+ if ((offset_ + readState_.bufferPos_)/chunkSize_ !=
+ ((offset_ + readState_.bufferPos_ + 3)/chunkSize_))
+ {
+ readState_.bufferPos_++;
+ continue;
+ }
+ }
+
+ readState_.eventSizeBuff_[readState_.eventSizeBuffPos_++] =
+ readBuffer_[readState_.bufferPos_++];
+
+ if (readState_.eventSizeBuffPos_ == 4) {
+ auto size = (cast(uint[])readState_.eventSizeBuff_)[0];
+
+ if (size == 0) {
+ // This is part of the zero padding between chunks.
+ readState_.resetState(readState_.lastDispatchPos_);
+ continue;
+ }
+
+ // got a valid event
+ readState_.readingSize_ = false;
+ readState_.eventLen_ = size;
+ readState_.eventPos_ = 0;
+
+ // check if the event is corrupted and perform recovery if required
+ if (isEventCorrupted()) {
+ performRecovery();
+ // start from the top
+ break;
+ }
+ }
+ } else {
+ if (!readState_.event_) {
+ readState_.event_ = new ubyte[readState_.eventLen_];
+ }
+
+ // take either the entire event or the remaining bytes in the buffer
+ auto reclaimBuffer = min(readState_.bufferLen_ - readState_.bufferPos_,
+ readState_.eventLen_ - readState_.eventPos_);
+
+ // copy data from read buffer into event buffer
+ readState_.event_[
+ readState_.eventPos_ .. readState_.eventPos_ + reclaimBuffer
+ ] = readBuffer_[
+ readState_.bufferPos_ .. readState_.bufferPos_ + reclaimBuffer
+ ];
+
+ // increment position ptrs
+ readState_.eventPos_ += reclaimBuffer;
+ readState_.bufferPos_ += reclaimBuffer;
+
+ // check if the event has been read in full
+ if (readState_.eventPos_ == readState_.eventLen_) {
+ // Reset the read state and return the completed event.
+ auto completeEvent = readState_.event_;
+ readState_.event_ = null;
+ readState_.resetState(readState_.bufferPos_);
+ return completeEvent;
+ }
+ }
+ }
+ }
+ }
+
+ bool isEventCorrupted() {
+ if ((maxEventSize_ > 0) && (readState_.eventLen_ > maxEventSize_)) {
+ // Event size is larger than user-speficied max-event size
+ logError("Corrupt event read: Event size (%s) greater than max " ~
+ "event size (%s)", readState_.eventLen_, maxEventSize_);
+ return true;
+ } else if (readState_.eventLen_ > chunkSize_) {
+ // Event size is larger than chunk size
+ logError("Corrupt event read: Event size (%s) greater than chunk " ~
+ "size (%s)", readState_.eventLen_, chunkSize_);
+ return true;
+ } else if (((offset_ + readState_.bufferPos_ - EventSize.sizeof) / chunkSize_) !=
+ ((offset_ + readState_.bufferPos_ + readState_.eventLen_ - EventSize.sizeof) / chunkSize_))
+ {
+ // Size indicates that event crosses chunk boundary
+ logError("Read corrupt event. Event crosses chunk boundary. " ~
+ "Event size: %s. Offset: %s", readState_.eventLen_,
+ (offset_ + readState_.bufferPos_ + EventSize.sizeof)
+ );
+
+ return true;
+ }
+
+ return false;
+ }
+
+ void performRecovery() {
+ // perform some kickass recovery
+ auto curChunk = getCurChunk();
+ if (lastBadChunk_ == curChunk) {
+ numCorruptedEventsInChunk_++;
+ } else {
+ lastBadChunk_ = curChunk;
+ numCorruptedEventsInChunk_ = 1;
+ }
+
+ if (numCorruptedEventsInChunk_ < maxCorruptedEvents_) {
+ // maybe there was an error in reading the file from disk
+ // seek to the beginning of chunk and try again
+ seekToChunk(curChunk);
+ } else {
+ // Just skip ahead to the next chunk if we not already at the last chunk.
+ if (curChunk != (getNumChunks() - 1)) {
+ seekToChunk(curChunk + 1);
+ } else if (readTimeout_ < dur!"hnsecs"(0)) {
+ // We are in tailing mode, wait until there is enough data to start
+ // the next chunk.
+ while(curChunk == (getNumChunks() - 1)) {
+ Thread.sleep(corruptedEventSleepDuration_);
+ }
+ seekToChunk(curChunk + 1);
+ } else {
+ // Pretty hosed at this stage, rewind the file back to the last
+ // successful point and punt on the error.
+ readState_.resetState(readState_.lastDispatchPos_);
+ currentEvent_ = null;
+ currentEventPos_ = 0;
+
+ throw new TTransportException("File corrupted at offset: " ~
+ to!string(offset_ + readState_.lastDispatchPos_),
+ TTransportException.Type.CORRUPTED_DATA);
+ }
+ }
+ }
+
+ string path_;
+ File file_;
+ bool isOpen_;
+ long offset_;
+ ubyte[] currentEvent_;
+ size_t currentEventPos_;
+ ulong chunkSize_;
+ Duration readTimeout_;
+ size_t maxEventSize_;
+
+ // Read buffer – lazily allocated on the first read().
+ ubyte[] readBuffer_;
+ size_t readBufferSize_;
+
+ static struct ReadState {
+ ubyte[] event_;
+ size_t eventLen_;
+ size_t eventPos_;
+
+ // keep track of event size
+ ubyte[4] eventSizeBuff_;
+ ubyte eventSizeBuffPos_;
+ bool readingSize_ = true;
+
+ // read buffer variables
+ size_t bufferPos_;
+ size_t bufferLen_;
+
+ // last successful dispatch point
+ size_t lastDispatchPos_;
+
+ void resetState(size_t lastDispatchPos) {
+ readingSize_ = true;
+ eventSizeBuffPos_ = 0;
+ lastDispatchPos_ = lastDispatchPos;
+ }
+
+ void resetAllValues() {
+ resetState(0);
+ bufferPos_ = 0;
+ bufferLen_ = 0;
+ event_ = null;
+ }
+ }
+ ReadState readState_;
+
+ ulong lastBadChunk_;
+ uint maxCorruptedEvents_;
+ uint numCorruptedEventsInChunk_;
+ Duration corruptedEventSleepDuration_;
+}
+
+/**
+ * A transport used to write log files. It can never be read from, calling
+ * read() throws.
+ *
+ * Contrary to the C++ design, explicitly opening the transport/file before
+ * using is necessary to allow manually closing the file without relying on the
+ * object lifetime.
+ */
+final class TFileWriterTransport : TBaseTransport {
+ /**
+ * Creates a new file writer transport.
+ *
+ * Params:
+ * path = Path of the file to opperate on.
+ */
+ this(string path) {
+ path_ = path;
+
+ chunkSize_ = DEFAULT_CHUNK_SIZE;
+ eventBufferSize_ = DEFAULT_EVENT_BUFFER_SIZE;
+ ioErrorSleepDuration = DEFAULT_IO_ERROR_SLEEP_DURATION;
+ maxFlushBytes_ = DEFAULT_MAX_FLUSH_BYTES;
+ maxFlushInterval_ = DEFAULT_MAX_FLUSH_INTERVAL;
+ }
+
+ override bool isOpen() @property {
+ return isOpen_;
+ }
+
+ /**
+ * A file writer transport can never be read from.
+ */
+ override bool peek() {
+ return false;
+ }
+
+ override void open() {
+ if (isOpen) return;
+
+ writerThread_ = spawn(
+ &writerThread,
+ path_,
+ chunkSize_,
+ maxFlushBytes_,
+ maxFlushInterval_,
+ ioErrorSleepDuration_
+ );
+ setMaxMailboxSize(writerThread_, eventBufferSize_, OnCrowding.block);
+ isOpen_ = true;
+ }
+
+ /**
+ * Closes the transport, i.e. the underlying file and the writer thread.
+ */
+ override void close() {
+ if (!isOpen) return;
+
+ prioritySend(writerThread_, ShutdownMessage(), thisTid); // FIXME: Should use normal send here.
+ receive((ShutdownMessage msg, Tid tid){});
+ isOpen_ = false;
+ }
+
+ /**
+ * Enqueues the passed slice of data for writing and immediately returns.
+ * write() only blocks if the event buffer has been exhausted.
+ *
+ * The transport must be open when calling this.
+ *
+ * Params:
+ * buf = Slice of data to write.
+ */
+ override void write(in ubyte[] buf) {
+ enforce(isOpen, new TTransportException(
+ "Cannot write to non-open file.", TTransportException.Type.NOT_OPEN));
+
+ if (buf.empty) {
+ logError("Cannot write empty event, skipping.");
+ return;
+ }
+
+ auto maxSize = chunkSize - EventSize.sizeof;
+ enforce(buf.length <= maxSize, new TTransportException(
+ "Cannot write more than " ~ to!string(maxSize) ~
+ "bytes at once due to chunk size."));
+
+ send(writerThread_, buf.idup);
+ }
+
+ /**
+ * Flushes any pending data to be written.
+ *
+ * The transport must be open when calling this.
+ *
+ * Throws: TTransportException if an error occurs.
+ */
+ override void flush() {
+ enforce(isOpen, new TTransportException(
+ "Cannot flush file if not open.", TTransportException.Type.NOT_OPEN));
+
+ send(writerThread_, FlushMessage(), thisTid);
+ receive((FlushMessage msg, Tid tid){});
+ }
+
+ /**
+ * The size of the chunks the file is divided into, in bytes.
+ *
+ * A single event (write call) never spans multiple chunks – this
+ * effectively limits the event size to chunkSize - EventSize.sizeof.
+ */
+ ulong chunkSize() @property {
+ return chunkSize_;
+ }
+
+ /// ditto
+ void chunkSize(ulong value) @property {
+ enforce(!isOpen, new TTransportException(
+ "Cannot set chunk size after TFileWriterTransport has been opened."));
+ chunkSize_ = value;
+ }
+
+ /**
+ * The maximum number of write() calls buffered, or zero for no limit.
+ *
+ * If the buffer is exhausted, write() will block until space becomes
+ * available.
+ */
+ size_t eventBufferSize() @property {
+ return eventBufferSize_;
+ }
+
+ /// ditto
+ void eventBufferSize(size_t value) @property {
+ eventBufferSize_ = value;
+ if (isOpen) {
+ setMaxMailboxSize(writerThread_, value, OnCrowding.throwException);
+ }
+ }
+
+ /// ditto
+ enum DEFAULT_EVENT_BUFFER_SIZE = 10_000;
+
+ /**
+ * Maximum number of bytes buffered before writing and flushing the file
+ * to disk.
+ *
+ * Currently cannot be set after the first call to write().
+ */
+ size_t maxFlushBytes() @property {
+ return maxFlushBytes_;
+ }
+
+ /// ditto
+ void maxFlushBytes(size_t value) @property {
+ maxFlushBytes_ = value;
+ if (isOpen) {
+ send(writerThread_, FlushBytesMessage(value));
+ }
+ }
+
+ /// ditto
+ enum DEFAULT_MAX_FLUSH_BYTES = 1000 * 1024;
+
+ /**
+ * Maximum interval between flushing the file to disk.
+ *
+ * Currenlty cannot be set after the first call to write().
+ */
+ Duration maxFlushInterval() @property {
+ return maxFlushInterval_;
+ }
+
+ /// ditto
+ void maxFlushInterval(Duration value) @property {
+ maxFlushInterval_ = value;
+ if (isOpen) {
+ send(writerThread_, FlushIntervalMessage(value));
+ }
+ }
+
+ /// ditto
+ enum DEFAULT_MAX_FLUSH_INTERVAL = dur!"seconds"(3);
+
+ /**
+ * When the writer thread encounteres an I/O error, it goes pauses for a
+ * short time before trying to reopen the output file. This controls the
+ * sleep duration.
+ */
+ Duration ioErrorSleepDuration() @property {
+ return ioErrorSleepDuration_;
+ }
+
+ /// ditto
+ void ioErrorSleepDuration(Duration value) @property {
+ ioErrorSleepDuration_ = value;
+ if (isOpen) {
+ send(writerThread_, FlushIntervalMessage(value));
+ }
+ }
+
+ /// ditto
+ enum DEFAULT_IO_ERROR_SLEEP_DURATION = dur!"msecs"(500);
+
+private:
+ string path_;
+ ulong chunkSize_;
+ size_t eventBufferSize_;
+ Duration ioErrorSleepDuration_;
+ size_t maxFlushBytes_;
+ Duration maxFlushInterval_;
+ bool isOpen_;
+ Tid writerThread_;
+}
+
+private {
+ // Signals that the file should be flushed on disk. Sent to the writer
+ // thread and sent back along with the tid for confirmation.
+ struct FlushMessage {}
+
+ // Signals that the writer thread should close the file and shut down. Sent
+ // to the writer thread and sent back along with the tid for confirmation.
+ struct ShutdownMessage {}
+
+ struct FlushBytesMessage {
+ size_t value;
+ }
+
+ struct FlushIntervalMessage {
+ Duration value;
+ }
+
+ struct IoErrorSleepDurationMessage {
+ Duration value;
+ }
+
+ void writerThread(
+ string path,
+ ulong chunkSize,
+ size_t maxFlushBytes,
+ Duration maxFlushInterval,
+ Duration ioErrorSleepDuration
+ ) {
+ bool errorOpening;
+ File file;
+ ulong offset;
+ try {
+ // Open file in appending and binary mode.
+ file = File(path, "ab");
+ offset = file.tell();
+ } catch (Exception e) {
+ logError("Error on opening output file in writer thread: %s", e);
+ errorOpening = true;
+ }
+
+ auto flushTimer = StopWatch(AutoStart.yes);
+ size_t unflushedByteCount;
+
+ Tid shutdownRequestTid;
+ bool shutdownRequested;
+ while (true) {
+ if (shutdownRequested) break;
+
+ bool forceFlush;
+ Tid flushRequestTid;
+ receiveTimeout(max(dur!"hnsecs"(0), maxFlushInterval - flushTimer.peek()),
+ (immutable(ubyte)[] data) {
+ while (errorOpening) {
+ logError("Writer thread going to sleep for %s µs due to IO errors",
+ ioErrorSleepDuration.fracSec.usecs);
+
+ // Sleep for ioErrorSleepDuration, being ready to be interrupted
+ // by shutdown requests.
+ auto timedOut = receiveTimeout(ioErrorSleepDuration,
+ (ShutdownMessage msg, Tid tid){ shutdownRequestTid = tid; });
+ if (!timedOut) {
+ // We got a shutdown request, just drop all events and exit the
+ // main loop as to not block application shutdown with our tries
+ // which we must assume to fail.
+ break;
+ }
+
+ try {
+ file = File(path, "ab");
+ unflushedByteCount = 0;
+ errorOpening = false;
+ logError("Output file %s reopened during writer thread error " ~
+ "recovery", path);
+ } catch (Exception e) {
+ logError("Unable to reopen output file %s during writer " ~
+ "thread error recovery", path);
+ }
+ }
+
+ // Make sure the event does not cross the chunk boundary by writing
+ // a padding consisting of zeroes if it would.
+ auto chunk1 = offset / chunkSize;
+ auto chunk2 = (offset + EventSize.sizeof + data.length - 1) / chunkSize;
+
+ if (chunk1 != chunk2) {
+ // TODO: The C++ implementation refetches the offset here to »keep
+ // in sync« – why would this be needed?
+ auto padding = cast(size_t)
+ ((((offset / chunkSize) + 1) * chunkSize) - offset);
+ auto zeroes = new ubyte[padding];
+ file.rawWrite(zeroes);
+ unflushedByteCount += padding;
+ offset += padding;
+ }
+
+ // TODO: 2 syscalls here, is this a problem performance-wise?
+ // Probably abysmal performance on Windows due to rawWrite
+ // implementation.
+ uint len = cast(uint)data.length;
+ file.rawWrite(cast(ubyte[])(&len)[0..1]);
+ file.rawWrite(data);
+
+ auto bytesWritten = EventSize.sizeof + data.length;
+ unflushedByteCount += bytesWritten;
+ offset += bytesWritten;
+ }, (FlushBytesMessage msg) {
+ maxFlushBytes = msg.value;
+ }, (FlushIntervalMessage msg) {
+ maxFlushInterval = msg.value;
+ }, (IoErrorSleepDurationMessage msg) {
+ ioErrorSleepDuration = msg.value;
+ }, (FlushMessage msg, Tid tid) {
+ forceFlush = true;
+ flushRequestTid = tid;
+ }, (OwnerTerminated msg) {
+ shutdownRequested = true;
+ }, (ShutdownMessage msg, Tid tid) {
+ shutdownRequested = true;
+ shutdownRequestTid = tid;
+ }
+ );
+
+ if (errorOpening) continue;
+
+ bool flush;
+ if (forceFlush || shutdownRequested || unflushedByteCount > maxFlushBytes) {
+ flush = true;
+ } else if (cast(Duration)flushTimer.peek() > maxFlushInterval) {
+ if (unflushedByteCount == 0) {
+ // If the flush timer is due, but no data has been written, don't
+ // needlessly fsync, but do reset the timer.
+ flushTimer.reset();
+ } else {
+ flush = true;
+ }
+ }
+
+ if (flush) {
+ file.flush();
+ flushTimer.reset();
+ unflushedByteCount = 0;
+ if (forceFlush) send(flushRequestTid, FlushMessage(), thisTid);
+ }
+ }
+
+ file.close();
+
+ if (shutdownRequestTid != Tid.init) {
+ send(shutdownRequestTid, ShutdownMessage(), thisTid);
+ }
+ }
+}
+
+version (unittest) {
+ import core.memory : GC;
+ import std.file;
+}
+
+unittest {
+ void tryRemove(string fileName) {
+ try {
+ remove(fileName);
+ } catch (Exception) {}
+ }
+
+ immutable fileName = "unittest.dat.tmp";
+ enforce(!exists(fileName), "Unit test output file " ~ fileName ~
+ " already exists.");
+
+ /*
+ * Check the most basic reading/writing operations.
+ */
+ {
+ scope (exit) tryRemove(fileName);
+
+ auto writer = new TFileWriterTransport(fileName);
+ writer.open();
+ scope (exit) writer.close();
+
+ writer.write([1, 2]);
+ writer.write([3, 4]);
+ writer.write([5, 6, 7]);
+ writer.flush();
+
+ auto reader = new TFileReaderTransport(fileName);
+ reader.open();
+ scope (exit) reader.close();
+
+ auto buf = new ubyte[7];
+ reader.readAll(buf);
+ enforce(buf == [1, 2, 3, 4, 5, 6, 7]);
+ }
+
+ /*
+ * Check that chunking works as expected.
+ */
+ {
+ scope (exit) tryRemove(fileName);
+
+ static assert(EventSize.sizeof == 4);
+ enum CHUNK_SIZE = 10;
+
+ // Write some contents to the file.
+ {
+ auto writer = new TFileWriterTransport(fileName);
+ writer.chunkSize = CHUNK_SIZE;
+ writer.open();
+ scope (exit) writer.close();
+
+ writer.write([0xde]);
+ writer.write([0xad]);
+ // Chunk boundary here.
+ writer.write([0xbe]);
+ // The next write doesn't fit in the five bytes remaining, so we expect
+ // padding zero bytes to be written.
+ writer.write([0xef, 0x12]);
+
+ try {
+ writer.write(new ubyte[CHUNK_SIZE]);
+ enforce(false, "Could write event not fitting in a single chunk.");
+ } catch (TTransportException e) {}
+
+ writer.flush();
+ }
+
+ // Check the raw contents of the file to see if chunk padding was written
+ // as expected.
+ auto file = File(fileName, "r");
+ enforce(file.size == 26);
+ auto written = new ubyte[26];
+ file.rawRead(written);
+ enforce(written == [
+ 1, 0, 0, 0, 0xde,
+ 1, 0, 0, 0, 0xad,
+ 1, 0, 0, 0, 0xbe,
+ 0, 0, 0, 0, 0,
+ 2, 0, 0, 0, 0xef, 0x12
+ ]);
+
+ // Read the data back in, getting all the events at once.
+ {
+ auto reader = new TFileReaderTransport(fileName);
+ reader.chunkSize = CHUNK_SIZE;
+ reader.open();
+ scope (exit) reader.close();
+
+ auto buf = new ubyte[5];
+ reader.readAll(buf);
+ enforce(buf == [0xde, 0xad, 0xbe, 0xef, 0x12]);
+ }
+ }
+
+ /*
+ * Make sure that close() exits "quickly", i.e. that there is no problem
+ * with the worker thread waking up.
+ */
+ {
+ import std.conv : text;
+ enum NUM_ITERATIONS = 1000;
+
+ uint numOver = 0;
+ foreach (n; 0 .. NUM_ITERATIONS) {
+ scope (exit) tryRemove(fileName);
+
+ auto transport = new TFileWriterTransport(fileName);
+ transport.open();
+
+ // Write something so that the writer thread gets started.
+ transport.write(cast(ubyte[])"foo");
+
+ // Every other iteration, also call flush(), just in case that potentially
+ // has any effect on how the writer thread wakes up.
+ if (n & 0x1) {
+ transport.flush();
+ }
+
+ // Time the call to close().
+ auto sw = StopWatch(AutoStart.yes);
+ transport.close();
+ sw.stop();
+
+ // If any attempt takes more than 500ms, treat that as a fatal failure to
+ // avoid looping over a potentially very slow operation.
+ enforce(sw.peek().msecs < 500,
+ text("close() took ", sw.peek().msecs, "ms."));
+
+ // Normally, it takes less than 5ms on my dev box.
+ // However, if the box is heavily loaded, some of the test runs can take
+ // longer. Additionally, on a Windows Server 2008 instance running in
+ // a VirtualBox VM, it has been observed that about a quarter of the runs
+ // takes (217 ± 1) ms, for reasons not yet known.
+ if (sw.peek().msecs > 5) {
+ ++numOver;
+ }
+
+ // Force garbage collection runs every now and then to make sure we
+ // don't run out of OS thread handles.
+ if (!(n % 100)) GC.collect();
+ }
+
+ // Make sure fewer than a third of the runs took longer than 5ms.
+ enforce(numOver < NUM_ITERATIONS / 3,
+ text(numOver, " iterations took more than 10 ms."));
+ }
+}
diff --git a/lib/d/src/thrift/transport/framed.d b/lib/d/src/thrift/transport/framed.d
new file mode 100644
index 000000000..94effbbaf
--- /dev/null
+++ b/lib/d/src/thrift/transport/framed.d
@@ -0,0 +1,334 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module thrift.transport.framed;
+
+import core.bitop : bswap;
+import std.algorithm : min;
+import std.array : empty;
+import std.exception : enforce;
+import thrift.transport.base;
+
+/**
+ * Framed transport.
+ *
+ * All writes go into an in-memory buffer until flush is called, at which point
+ * the transport writes the length of the entire binary chunk followed by the
+ * data payload. The receiver on the other end then performs a single
+ * »fixed-length« read to get the whole message off the wire.
+ */
+final class TFramedTransport : TBaseTransport {
+ /**
+ * Constructs a new framed transport.
+ *
+ * Params:
+ * transport = The underlying transport to wrap.
+ */
+ this(TTransport transport) {
+ transport_ = transport;
+ }
+
+ /**
+ * Returns the wrapped transport.
+ */
+ TTransport underlyingTransport() @property {
+ return transport_;
+ }
+
+ override bool isOpen() @property {
+ return transport_.isOpen;
+ }
+
+ override bool peek() {
+ return rBuf_.length > 0 || transport_.peek();
+ }
+
+ override void open() {
+ transport_.open();
+ }
+
+ override void close() {
+ flush();
+ transport_.close();
+ }
+
+ /**
+ * Attempts to read data into the given buffer, stopping when the buffer is
+ * exhausted or the frame end is reached.
+ *
+ * TODO: Contrary to the C++ implementation, this never does cross-frame
+ * reads – is there actually a valid use case for that?
+ *
+ * Params:
+ * buf = Slice to use as buffer.
+ *
+ * Returns: How many bytes were actually read.
+ *
+ * Throws: TTransportException if an error occurs.
+ */
+ override size_t read(ubyte[] buf) {
+ // If the buffer is empty, read a new frame off the wire.
+ if (rBuf_.empty) {
+ bool gotFrame = readFrame();
+ if (!gotFrame) return 0;
+ }
+
+ auto size = min(rBuf_.length, buf.length);
+ buf[0..size] = rBuf_[0..size];
+ rBuf_ = rBuf_[size..$];
+ return size;
+ }
+
+ override void write(in ubyte[] buf) {
+ wBuf_ ~= buf;
+ }
+
+ override void flush() {
+ if (wBuf_.empty) return;
+
+ // Properly reset the write buffer even some of the protocol operations go
+ // wrong.
+ scope (exit) {
+ wBuf_.length = 0;
+ wBuf_.assumeSafeAppend();
+ }
+
+ int len = bswap(cast(int)wBuf_.length);
+ transport_.write(cast(ubyte[])(&len)[0..1]);
+ transport_.write(wBuf_);
+ transport_.flush();
+ }
+
+ override const(ubyte)[] borrow(ubyte* buf, size_t len) {
+ if (len <= rBuf_.length) {
+ return rBuf_;
+ } else {
+ // Don't try attempting cross-frame borrows, trying that does not make
+ // much sense anyway.
+ return null;
+ }
+ }
+
+ override void consume(size_t len) {
+ enforce(len <= rBuf_.length, new TTransportException(
+ "Invalid consume length", TTransportException.Type.BAD_ARGS));
+ rBuf_ = rBuf_[len .. $];
+ }
+
+private:
+ bool readFrame() {
+ // Read the size of the next frame. We can't use readAll() since that
+ // always throws an exception on EOF, but want to throw an exception only
+ // if EOF occurs after partial size data.
+ int size;
+ size_t size_read;
+ while (size_read < size.sizeof) {
+ auto data = (cast(ubyte*)&size)[size_read..size.sizeof];
+ auto read = transport_.read(data);
+ if (read == 0) {
+ if (size_read == 0) {
+ // EOF before any data was read.
+ return false;
+ } else {
+ // EOF after a partial frame header – illegal.
+ throw new TTransportException(
+ "No more data to read after partial frame header",
+ TTransportException.Type.END_OF_FILE
+ );
+ }
+ }
+ size_read += read;
+ }
+
+ size = bswap(size);
+ enforce(size >= 0, new TTransportException("Frame size has negative value",
+ TTransportException.Type.CORRUPTED_DATA));
+
+ // TODO: Benchmark this.
+ rBuf_.length = size;
+ rBuf_.assumeSafeAppend();
+
+ transport_.readAll(rBuf_);
+ return true;
+ }
+
+ TTransport transport_;
+ ubyte[] rBuf_;
+ ubyte[] wBuf_;
+}
+
+/**
+ * Wraps given transports into TFramedTransports.
+ */
+alias TWrapperTransportFactory!TFramedTransport TFramedTransportFactory;
+
+version (unittest) {
+ import std.random : Mt19937, uniform;
+ import thrift.transport.memory;
+}
+
+// Some basic random testing, always starting with the same seed for
+// deterministic unit test results – more tests in transport_test.
+unittest {
+ auto randGen = Mt19937(42);
+
+ // 32 kiB of data to work with.
+ auto data = new ubyte[1 << 15];
+ foreach (ref b; data) {
+ b = uniform!"[]"(cast(ubyte)0, cast(ubyte)255, randGen);
+ }
+
+ // Generate a list of chunk sizes to split the data into. A uniform
+ // distribution is not quite realistic, but std.random doesn't have anything
+ // else yet.
+ enum MAX_FRAME_LENGTH = 512;
+ auto chunkSizesList = new size_t[][2];
+ foreach (ref chunkSizes; chunkSizesList) {
+ size_t sum;
+ while (true) {
+ auto curLen = uniform(0, MAX_FRAME_LENGTH, randGen);
+ sum += curLen;
+ if (sum > data.length) break;
+ chunkSizes ~= curLen;
+ }
+ }
+ chunkSizesList ~= [data.length]; // Also test whole chunk at once.
+
+ // Test writing data.
+ {
+ foreach (chunkSizes; chunkSizesList) {
+ auto buf = new TMemoryBuffer;
+ auto framed = new TFramedTransport(buf);
+
+ auto remainingData = data;
+ foreach (chunkSize; chunkSizes) {
+ framed.write(remainingData[0..chunkSize]);
+ remainingData = remainingData[chunkSize..$];
+ }
+ framed.flush();
+
+ auto writtenData = data[0..($ - remainingData.length)];
+ auto actualData = buf.getContents();
+
+ // Check frame size.
+ int frameSize = bswap((cast(int[])(actualData[0..int.sizeof]))[0]);
+ enforce(frameSize == writtenData.length);
+
+ // Check actual data.
+ enforce(actualData[int.sizeof..$] == writtenData);
+ }
+ }
+
+ // Test reading data.
+ {
+ foreach (chunkSizes; chunkSizesList) {
+ auto buf = new TMemoryBuffer;
+
+ auto size = bswap(cast(int)data.length);
+ buf.write(cast(ubyte[])(&size)[0..1]);
+ buf.write(data);
+
+ auto framed = new TFramedTransport(buf);
+ ubyte[] readData;
+ readData.reserve(data.length);
+ foreach (chunkSize; chunkSizes) {
+ // This should work with read because we have one huge frame.
+ auto oldReadLen = readData.length;
+ readData.length += chunkSize;
+ framed.read(readData[oldReadLen..$]);
+ }
+
+ enforce(readData == data[0..readData.length]);
+ }
+ }
+
+ // Test combined reading/writing of multiple frames.
+ foreach (flushProbability; [1, 2, 4, 8, 16, 32]) {
+ foreach (chunkSizes; chunkSizesList) {
+ auto buf = new TMemoryBuffer;
+ auto framed = new TFramedTransport(buf);
+
+ size_t[] frameSizes;
+
+ // Write the data.
+ size_t frameSize;
+ auto remainingData = data;
+ foreach (chunkSize; chunkSizes) {
+ framed.write(remainingData[0..chunkSize]);
+ remainingData = remainingData[chunkSize..$];
+
+ frameSize += chunkSize;
+ if (frameSize > 0 && uniform(0, flushProbability, randGen) == 0) {
+ frameSizes ~= frameSize;
+ frameSize = 0;
+ framed.flush();
+ }
+ }
+ if (frameSize > 0) {
+ frameSizes ~= frameSize;
+ frameSize = 0;
+ framed.flush();
+ }
+
+ // Read it back.
+ auto readData = new ubyte[data.length - remainingData.length];
+ auto remainToRead = readData;
+ foreach (fSize; frameSizes) {
+ // We are exploiting an implementation detail of TFramedTransport:
+ // The read buffer starts empty and it will never return more than one
+ // frame per read, so by just requesting all of the data, we should
+ // always get exactly one frame.
+ auto got = framed.read(remainToRead);
+ enforce(got == fSize);
+ remainToRead = remainToRead[fSize..$];
+ }
+
+ enforce(remainToRead.empty);
+ enforce(readData == data[0..readData.length]);
+ }
+ }
+}
+
+// Test flush()ing an empty buffer.
+unittest {
+ auto buf = new TMemoryBuffer();
+ auto framed = new TFramedTransport(buf);
+ immutable out1 = [0, 0, 0, 1, 'a'];
+ immutable out2 = [0, 0, 0, 1, 'a', 0, 0, 0, 2, 'b', 'c'];
+
+ framed.flush();
+ enforce(buf.getContents() == []);
+ framed.flush();
+ framed.flush();
+ enforce(buf.getContents() == []);
+ framed.write(cast(ubyte[])"a");
+ enforce(buf.getContents() == []);
+ framed.flush();
+ enforce(buf.getContents() == out1);
+ framed.flush();
+ framed.flush();
+ enforce(buf.getContents() == out1);
+ framed.write(cast(ubyte[])"bc");
+ enforce(buf.getContents() == out1);
+ framed.flush();
+ enforce(buf.getContents() == out2);
+ framed.flush();
+ framed.flush();
+ enforce(buf.getContents() == out2);
+}
diff --git a/lib/d/src/thrift/transport/http.d b/lib/d/src/thrift/transport/http.d
new file mode 100644
index 000000000..5186f3d8b
--- /dev/null
+++ b/lib/d/src/thrift/transport/http.d
@@ -0,0 +1,459 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * HTTP tranpsort implementation, modelled after the C++ one.
+ *
+ * Unfortunately, libcurl is quite heavyweight and supports only client-side
+ * applications. This is an implementation of the basic HTTP/1.1 parts
+ * supporting HTTP 100 Continue, chunked transfer encoding, keepalive, etc.
+ */
+module thrift.transport.http;
+
+import std.algorithm : canFind, countUntil, endsWith, findSplit, min, startsWith;
+import std.ascii : toLower;
+import std.array : empty;
+import std.conv : parse, to;
+import std.datetime : Clock, UTC;
+import std.string : stripLeft;
+import thrift.base : VERSION;
+import thrift.transport.base;
+import thrift.transport.memory;
+import thrift.transport.socket;
+
+/**
+ * Base class for both client- and server-side HTTP transports.
+ */
+abstract class THttpTransport : TBaseTransport {
+ this(TTransport transport) {
+ transport_ = transport;
+ readHeaders_ = true;
+ httpBuf_ = new ubyte[HTTP_BUFFER_SIZE];
+ httpBufRemaining_ = httpBuf_[0 .. 0];
+ readBuffer_ = new TMemoryBuffer;
+ writeBuffer_ = new TMemoryBuffer;
+ }
+
+ override bool isOpen() {
+ return transport_.isOpen();
+ }
+
+ override bool peek() {
+ return transport_.peek();
+ }
+
+ override void open() {
+ transport_.open();
+ }
+
+ override void close() {
+ transport_.close();
+ }
+
+ override size_t read(ubyte[] buf) {
+ if (!readBuffer_.peek()) {
+ readBuffer_.reset();
+
+ if (!refill()) return 0;
+
+ if (readHeaders_) {
+ readHeaders();
+ }
+
+ size_t got;
+ if (chunked_) {
+ got = readChunked();
+ } else {
+ got = readContent(contentLength_);
+ }
+ readHeaders_ = true;
+
+ if (got == 0) return 0;
+ }
+ return readBuffer_.read(buf);
+ }
+
+ override size_t readEnd() {
+ // Read any pending chunked data (footers etc.)
+ if (chunked_) {
+ while (!chunkedDone_) {
+ readChunked();
+ }
+ }
+ return 0;
+ }
+
+ override void write(in ubyte[] buf) {
+ writeBuffer_.write(buf);
+ }
+
+ override void flush() {
+ auto data = writeBuffer_.getContents();
+ string header = getHeader(data.length);
+
+ transport_.write(cast(const(ubyte)[]) header);
+ transport_.write(data);
+ transport_.flush();
+
+ // Reset the buffer and header variables.
+ writeBuffer_.reset();
+ readHeaders_ = true;
+ }
+
+ /**
+ * The size of the buffer to read HTTP requests into, in bytes. Will expand
+ * as required.
+ */
+ enum HTTP_BUFFER_SIZE = 1024;
+
+protected:
+ abstract string getHeader(size_t dataLength);
+ abstract bool parseStatusLine(const(ubyte)[] status);
+
+ void parseHeader(const(ubyte)[] header) {
+ auto split = findSplit(header, [':']);
+ if (split[1].empty) {
+ // No colon found.
+ return;
+ }
+
+ static bool compToLower(ubyte a, ubyte b) {
+ return a == toLower(cast(char)b);
+ }
+
+ if (startsWith!compToLower(split[0], cast(ubyte[])"transfer-encoding")) {
+ if (endsWith!compToLower(split[2], cast(ubyte[])"chunked")) {
+ chunked_ = true;
+ }
+ } else if (startsWith!compToLower(split[0], cast(ubyte[])"content-length")) {
+ chunked_ = false;
+ auto lengthString = stripLeft(cast(const(char)[])split[2]);
+ contentLength_ = parse!size_t(lengthString);
+ }
+ }
+
+private:
+ ubyte[] readLine() {
+ while (true) {
+ auto split = findSplit(httpBufRemaining_, cast(ubyte[])"\r\n");
+
+ if (split[1].empty) {
+ // No CRLF yet, move whatever we have now to front and refill.
+ if (httpBufRemaining_.empty) {
+ httpBufRemaining_ = httpBuf_[0 .. 0];
+ } else {
+ httpBuf_[0 .. httpBufRemaining_.length] = httpBufRemaining_;
+ httpBufRemaining_ = httpBuf_[0 .. httpBufRemaining_.length];
+ }
+
+ if (!refill()) {
+ auto buf = httpBufRemaining_;
+ httpBufRemaining_ = httpBufRemaining_[$ - 1 .. $ - 1];
+ return buf;
+ }
+ } else {
+ // Set the remaining buffer to the part after \r\n and return the part
+ // (line) before it.
+ httpBufRemaining_ = split[2];
+ return split[0];
+ }
+ }
+ }
+
+ void readHeaders() {
+ // Initialize headers state variables
+ contentLength_ = 0;
+ chunked_ = false;
+ chunkedDone_ = false;
+ chunkSize_ = 0;
+
+ // Control state flow
+ bool statusLine = true;
+ bool finished;
+
+ // Loop until headers are finished
+ while (true) {
+ auto line = readLine();
+
+ if (line.length == 0) {
+ if (finished) {
+ readHeaders_ = false;
+ return;
+ } else {
+ // Must have been an HTTP 100, keep going for another status line
+ statusLine = true;
+ }
+ } else {
+ if (statusLine) {
+ statusLine = false;
+ finished = parseStatusLine(line);
+ } else {
+ parseHeader(line);
+ }
+ }
+ }
+ }
+
+ size_t readChunked() {
+ size_t length;
+
+ auto line = readLine();
+ size_t chunkSize;
+ try {
+ auto charLine = cast(char[])line;
+ chunkSize = parse!size_t(charLine, 16);
+ } catch (Exception e) {
+ throw new TTransportException("Invalid chunk size: " ~ to!string(line),
+ TTransportException.Type.CORRUPTED_DATA);
+ }
+
+ if (chunkSize == 0) {
+ readChunkedFooters();
+ } else {
+ // Read data content
+ length += readContent(chunkSize);
+ // Read trailing CRLF after content
+ readLine();
+ }
+ return length;
+ }
+
+ void readChunkedFooters() {
+ while (true) {
+ auto line = readLine();
+ if (line.length == 0) {
+ chunkedDone_ = true;
+ break;
+ }
+ }
+ }
+
+ size_t readContent(size_t size) {
+ auto need = size;
+ while (need > 0) {
+ if (httpBufRemaining_.length == 0) {
+ // We have given all the data, reset position to head of the buffer.
+ httpBufRemaining_ = httpBuf_[0 .. 0];
+ if (!refill()) return size - need;
+ }
+
+ auto give = min(httpBufRemaining_.length, need);
+ readBuffer_.write(cast(ubyte[])httpBufRemaining_[0 .. give]);
+ httpBufRemaining_ = httpBufRemaining_[give .. $];
+ need -= give;
+ }
+ return size;
+ }
+
+ bool refill() {
+ // Is there a nicer way to do this?
+ auto indexBegin = httpBufRemaining_.ptr - httpBuf_.ptr;
+ auto indexEnd = indexBegin + httpBufRemaining_.length;
+
+ if (httpBuf_.length - indexEnd <= (httpBuf_.length / 4)) {
+ httpBuf_.length *= 2;
+ }
+
+ // Read more data.
+ auto got = transport_.read(cast(ubyte[])httpBuf_[indexEnd .. $]);
+ if (got == 0) return false;
+ httpBufRemaining_ = httpBuf_[indexBegin .. indexEnd + got];
+ return true;
+ }
+
+ TTransport transport_;
+
+ TMemoryBuffer writeBuffer_;
+ TMemoryBuffer readBuffer_;
+
+ bool readHeaders_;
+ bool chunked_;
+ bool chunkedDone_;
+ size_t chunkSize_;
+ size_t contentLength_;
+
+ ubyte[] httpBuf_;
+ ubyte[] httpBufRemaining_;
+}
+
+/**
+ * HTTP client transport.
+ */
+final class TClientHttpTransport : THttpTransport {
+ /**
+ * Constructs a client http transport operating on the passed underlying
+ * transport.
+ *
+ * Params:
+ * transport = The underlying transport used for the actual I/O.
+ * host = The HTTP host string.
+ * path = The HTTP path string.
+ */
+ this(TTransport transport, string host, string path) {
+ super(transport);
+ host_ = host;
+ path_ = path;
+ }
+
+ /**
+ * Convenience overload for constructing a client HTTP transport using a
+ * TSocket connecting to the specified host and port.
+ *
+ * Params:
+ * host = The server to connect to, also used as HTTP host string.
+ * port = The port to connect to.
+ * path = The HTTP path string.
+ */
+ this(string host, ushort port, string path) {
+ this(new TSocket(host, port), host, path);
+ }
+
+protected:
+ override string getHeader(size_t dataLength) {
+ return "POST " ~ path_ ~ " HTTP/1.1\r\n" ~
+ "Host: " ~ host_ ~ "\r\n" ~
+ "Content-Type: application/x-thrift\r\n" ~
+ "Content-Length: " ~ to!string(dataLength) ~ "\r\n" ~
+ "Accept: application/x-thrift\r\n"
+ "User-Agent: Thrift/" ~ VERSION ~ " (D/TClientHttpTransport)\r\n" ~
+ "\r\n";
+ }
+
+ override bool parseStatusLine(const(ubyte)[] status) {
+ // HTTP-Version SP Status-Code SP Reason-Phrase CRLF
+ auto firstSplit = findSplit(status, [' ']);
+ if (firstSplit[1].empty) {
+ throw new TTransportException("Bad status: " ~ to!string(status),
+ TTransportException.Type.CORRUPTED_DATA);
+ }
+
+ auto codeReason = firstSplit[2][countUntil!"a != b"(firstSplit[2], ' ') .. $];
+ auto secondSplit = findSplit(codeReason, [' ']);
+ if (secondSplit[1].empty) {
+ throw new TTransportException("Bad status: " ~ to!string(status),
+ TTransportException.Type.CORRUPTED_DATA);
+ }
+
+ if (secondSplit[0] == "200") {
+ // HTTP 200 = OK, we got the response
+ return true;
+ } else if (secondSplit[0] == "100") {
+ // HTTP 100 = continue, just keep reading
+ return false;
+ }
+
+ throw new TTransportException("Bad status (unhandled status code): " ~
+ to!string(cast(const(char[]))status), TTransportException.Type.CORRUPTED_DATA);
+ }
+
+private:
+ string host_;
+ string path_;
+}
+
+/**
+ * HTTP server transport.
+ */
+final class TServerHttpTransport : THttpTransport {
+ /**
+ * Constructs a new instance.
+ *
+ * Param:
+ * transport = The underlying transport used for the actual I/O.
+ */
+ this(TTransport transport) {
+ super(transport);
+ }
+
+protected:
+ override string getHeader(size_t dataLength) {
+ return "HTTP/1.1 200 OK\r\n" ~
+ "Date: " ~ getRFC1123Time() ~ "\r\n" ~
+ "Server: Thrift/" ~ VERSION ~ "\r\n" ~
+ "Content-Type: application/x-thrift\r\n" ~
+ "Content-Length: " ~ to!string(dataLength) ~ "\r\n" ~
+ "Connection: Keep-Alive\r\n" ~
+ "\r\n";
+ }
+
+ override bool parseStatusLine(const(ubyte)[] status) {
+ // Method SP Request-URI SP HTTP-Version CRLF.
+ auto split = findSplit(status, [' ']);
+ if (split[1].empty) {
+ throw new TTransportException("Bad status: " ~ to!string(status),
+ TTransportException.Type.CORRUPTED_DATA);
+ }
+
+ auto uriVersion = split[2][countUntil!"a != b"(split[2], ' ') .. $];
+ if (!canFind(uriVersion, ' ')) {
+ throw new TTransportException("Bad status: " ~ to!string(status),
+ TTransportException.Type.CORRUPTED_DATA);
+ }
+
+ if (split[0] == "POST") {
+ // POST method ok, looking for content.
+ return true;
+ }
+
+ throw new TTransportException("Bad status (unsupported method): " ~
+ to!string(status), TTransportException.Type.CORRUPTED_DATA);
+ }
+}
+
+/**
+ * Wraps a transport into a HTTP server protocol.
+ */
+alias TWrapperTransportFactory!TServerHttpTransport TServerHttpTransportFactory;
+
+private {
+ import std.string : format;
+ string getRFC1123Time() {
+ auto sysTime = Clock.currTime(UTC());
+
+ auto dayName = capMemberName(sysTime.dayOfWeek);
+ auto monthName = capMemberName(sysTime.month);
+
+ return format("%s, %s %s %s %s:%s:%s GMT", dayName, sysTime.day,
+ monthName, sysTime.year, sysTime.hour, sysTime.minute, sysTime.second);
+ }
+
+ import std.ascii : toUpper;
+ import std.traits : EnumMembers;
+ string capMemberName(T)(T val) if (is(T == enum)) {
+ foreach (i, e; EnumMembers!T) {
+ enum name = __traits(derivedMembers, T)[i];
+ enum capName = cast(char) toUpper(name[0]) ~ name [1 .. $];
+ if (val == e) {
+ return capName;
+ }
+ }
+ throw new Exception("Not a member of " ~ T.stringof ~ ": " ~ to!string(val));
+ }
+
+ unittest {
+ enum Foo {
+ bar,
+ bAZ
+ }
+
+ import std.exception;
+ enforce(capMemberName(Foo.bar) == "Bar");
+ enforce(capMemberName(Foo.bAZ) == "BAZ");
+ }
+}
diff --git a/lib/d/src/thrift/transport/memory.d b/lib/d/src/thrift/transport/memory.d
new file mode 100644
index 000000000..cdf0807ab
--- /dev/null
+++ b/lib/d/src/thrift/transport/memory.d
@@ -0,0 +1,233 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.transport.memory;
+
+import core.exception : onOutOfMemoryError;
+import core.stdc.stdlib : free, realloc;
+import std.algorithm : min;
+import std.conv : text;
+import thrift.transport.base;
+
+/**
+ * A transport that simply reads from and writes to an in-memory buffer. Every
+ * time you call write on it, the data is simply placed into a buffer, and
+ * every time you call read, data is consumed from that buffer.
+ *
+ * Currently, the storage for written data is never reclaimed, even if the
+ * buffer contents have already been read out again.
+ */
+final class TMemoryBuffer : TBaseTransport {
+ /**
+ * Constructs a new memory transport with an empty internal buffer.
+ */
+ this() {}
+
+ /**
+ * Constructs a new memory transport with an empty internal buffer,
+ * reserving space for capacity bytes in advance.
+ *
+ * If the amount of data which will be written to the buffer is already
+ * known on construction, this can better performance over the default
+ * constructor because reallocations can be avoided.
+ *
+ * If the preallocated buffer is exhausted, data can still be written to the
+ * transport, but reallocations will happen.
+ *
+ * Params:
+ * capacity = Size of the initially reserved buffer (in bytes).
+ */
+ this(size_t capacity) {
+ reset(capacity);
+ }
+
+ /**
+ * Constructs a new memory transport initially containing the passed data.
+ *
+ * For now, the passed buffer is not intelligently used, the data is just
+ * copied to the internal buffer.
+ *
+ * Params:
+ * buffer = Initial contents available to be read.
+ */
+ this(in ubyte[] contents) {
+ auto size = contents.length;
+ reset(size);
+ buffer_[0 .. size] = contents[];
+ writeOffset_ = size;
+ }
+
+ /**
+ * Destructor, frees the internally allocated buffer.
+ */
+ ~this() {
+ free(buffer_);
+ }
+
+ /**
+ * Returns a read-only view of the current buffer contents.
+ *
+ * Note: For performance reasons, the returned slice is only valid for the
+ * life of this object, and may be invalidated on the next write() call at
+ * will – you might want to immediately .dup it if you intend to keep it
+ * around.
+ */
+ const(ubyte)[] getContents() {
+ return buffer_[readOffset_ .. writeOffset_];
+ }
+
+ /**
+ * A memory transport is always open.
+ */
+ override bool isOpen() @property {
+ return true;
+ }
+
+ override bool peek() {
+ return writeOffset_ - readOffset_ > 0;
+ }
+
+ /**
+ * Opening is a no-op() for a memory buffer.
+ */
+ override void open() {}
+
+ /**
+ * Closing is a no-op() for a memory buffer, it is always open.
+ */
+ override void close() {}
+
+ override size_t read(ubyte[] buf) {
+ auto size = min(buf.length, writeOffset_ - readOffset_);
+ buf[0 .. size] = buffer_[readOffset_ .. readOffset_ + size];
+ readOffset_ += size;
+ return size;
+ }
+
+ /**
+ * Shortcut version of readAll() – using this over TBaseTransport.readAll()
+ * can give us a nice speed increase because gives us a nice speed increase
+ * because it is typically a very hot path during deserialization.
+ */
+ override void readAll(ubyte[] buf) {
+ auto available = writeOffset_ - readOffset_;
+ if (buf.length > available) {
+ throw new TTransportException(text("Cannot readAll() ", buf.length,
+ " bytes of data because only ", available, " bytes are available."),
+ TTransportException.Type.END_OF_FILE);
+ }
+
+ buf[] = buffer_[readOffset_ .. readOffset_ + buf.length];
+ readOffset_ += buf.length;
+ }
+
+ override void write(in ubyte[] buf) {
+ auto need = buf.length;
+ if (bufferLen_ - writeOffset_ < need) {
+ // Exponential growth.
+ auto newLen = bufferLen_ + 1;
+ while (newLen - writeOffset_ < need) newLen *= 2;
+ cRealloc(buffer_, newLen);
+ bufferLen_ = newLen;
+ }
+
+ buffer_[writeOffset_ .. writeOffset_ + need] = buf[];
+ writeOffset_ += need;
+ }
+
+ override const(ubyte)[] borrow(ubyte* buf, size_t len) {
+ if (len <= writeOffset_ - readOffset_) {
+ return buffer_[readOffset_ .. writeOffset_];
+ } else {
+ return null;
+ }
+ }
+
+ override void consume(size_t len) {
+ readOffset_ += len;
+ }
+
+ void reset() {
+ readOffset_ = 0;
+ writeOffset_ = 0;
+ }
+
+ void reset(size_t capacity) {
+ readOffset_ = 0;
+ writeOffset_ = 0;
+ if (bufferLen_ < capacity) {
+ cRealloc(buffer_, capacity);
+ bufferLen_ = capacity;
+ }
+ }
+
+private:
+ ubyte* buffer_;
+ size_t bufferLen_;
+ size_t readOffset_;
+ size_t writeOffset_;
+}
+
+private {
+ void cRealloc(ref ubyte* data, size_t newSize) {
+ auto result = realloc(data, newSize);
+ if (result is null) onOutOfMemoryError();
+ data = cast(ubyte*)result;
+ }
+}
+
+version (unittest) {
+ import std.exception;
+}
+
+unittest {
+ auto a = new TMemoryBuffer(5);
+ immutable(ubyte[]) testData = [1, 2, 3, 4];
+ auto buf = new ubyte[testData.length];
+ enforce(a.isOpen);
+
+ // a should be empty.
+ enforce(!a.peek());
+ enforce(a.read(buf) == 0);
+ assertThrown!TTransportException(a.readAll(buf));
+
+ // Write some data and read it back again.
+ a.write(testData);
+ enforce(a.peek());
+ enforce(a.getContents() == testData);
+ enforce(a.read(buf) == testData.length);
+ enforce(buf == testData);
+
+ // a should be empty again.
+ enforce(!a.peek());
+ enforce(a.read(buf) == 0);
+ assertThrown!TTransportException(a.readAll(buf));
+
+ // Test the constructor which directly accepts initial data.
+ auto b = new TMemoryBuffer(testData);
+ enforce(b.isOpen);
+ enforce(b.peek());
+ enforce(b.getContents() == testData);
+
+ // Test borrow().
+ auto borrowed = b.borrow(null, testData.length);
+ enforce(borrowed == testData);
+ enforce(b.peek());
+ b.consume(testData.length);
+ enforce(!b.peek());
+}
diff --git a/lib/d/src/thrift/transport/piped.d b/lib/d/src/thrift/transport/piped.d
new file mode 100644
index 000000000..9fe143278
--- /dev/null
+++ b/lib/d/src/thrift/transport/piped.d
@@ -0,0 +1,219 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.transport.piped;
+
+import thrift.transport.base;
+import thrift.transport.memory;
+
+/**
+ * Pipes data request from one transport to another when readEnd()
+ * or writeEnd() is called.
+ *
+ * A typical use case would be to log requests on e.g. a socket to
+ * disk (i. e. pipe them to a TFileWriterTransport).
+ *
+ * The implementation keeps an internal buffer which expands to
+ * hold the whole amount of data read/written until the corresponding *End()
+ * method is called.
+ *
+ * Contrary to the C++ implementation, this doesn't introduce yet another layer
+ * of input/output buffering, all calls are passed to the underlying source
+ * transport verbatim.
+ */
+final class TPipedTransport(Source = TTransport) if (
+ isTTransport!Source
+) : TBaseTransport {
+ /// The default initial buffer size if not explicitly specified, in bytes.
+ enum DEFAULT_INITIAL_BUFFER_SIZE = 512;
+
+ /**
+ * Constructs a new instance.
+ *
+ * By default, only reads are piped (pipeReads = true, pipeWrites = false).
+ *
+ * Params:
+ * srcTrans = The transport to which all requests are forwarded.
+ * dstTrans = The transport the read/written data is copied to.
+ * initialBufferSize = The default size of the read/write buffers, for
+ * performance tuning.
+ */
+ this(Source srcTrans, TTransport dstTrans,
+ size_t initialBufferSize = DEFAULT_INITIAL_BUFFER_SIZE
+ ) {
+ srcTrans_ = srcTrans;
+ dstTrans_ = dstTrans;
+
+ readBuffer_ = new TMemoryBuffer(initialBufferSize);
+ writeBuffer_ = new TMemoryBuffer(initialBufferSize);
+
+ pipeReads_ = true;
+ pipeWrites_ = false;
+ }
+
+ bool pipeReads() @property const {
+ return pipeReads_;
+ }
+
+ void pipeReads(bool value) @property {
+ if (!value) {
+ readBuffer_.reset();
+ }
+ pipeReads_ = value;
+ }
+
+ bool pipeWrites() @property const {
+ return pipeWrites_;
+ }
+
+ void pipeWrites(bool value) @property {
+ if (!value) {
+ writeBuffer_.reset();
+ }
+ pipeWrites_ = value;
+ }
+
+ override bool isOpen() {
+ return srcTrans_.isOpen();
+ }
+
+ override bool peek() {
+ return srcTrans_.peek();
+ }
+
+ override void open() {
+ srcTrans_.open();
+ }
+
+ override void close() {
+ srcTrans_.close();
+ }
+
+ override size_t read(ubyte[] buf) {
+ auto bytesRead = srcTrans_.read(buf);
+
+ if (pipeReads_) {
+ readBuffer_.write(buf[0 .. bytesRead]);
+ }
+
+ return bytesRead;
+ }
+
+ override size_t readEnd() {
+ if (pipeReads_) {
+ auto data = readBuffer_.getContents();
+ dstTrans_.write(data);
+ dstTrans_.flush();
+ readBuffer_.reset();
+
+ srcTrans_.readEnd();
+
+ // Return data.length instead of the readEnd() result of the source
+ // transports because it might not be available from it.
+ return data.length;
+ }
+
+ return srcTrans_.readEnd();
+ }
+
+ override void write(in ubyte[] buf) {
+ if (pipeWrites_) {
+ writeBuffer_.write(buf);
+ }
+
+ srcTrans_.write(buf);
+ }
+
+ override size_t writeEnd() {
+ if (pipeWrites_) {
+ auto data = writeBuffer_.getContents();
+ dstTrans_.write(data);
+ dstTrans_.flush();
+ writeBuffer_.reset();
+
+ srcTrans_.writeEnd();
+
+ // Return data.length instead of the readEnd() result of the source
+ // transports because it might not be available from it.
+ return data.length;
+ }
+
+ return srcTrans_.writeEnd();
+ }
+
+ override void flush() {
+ srcTrans_.flush();
+ }
+
+private:
+ Source srcTrans_;
+ TTransport dstTrans_;
+
+ TMemoryBuffer readBuffer_;
+ TMemoryBuffer writeBuffer_;
+
+ bool pipeReads_;
+ bool pipeWrites_;
+}
+
+/**
+ * TPipedTransport construction helper to avoid having to explicitly
+ * specify the transport types, i.e. to allow the constructor being called
+ * using IFTI (see $(DMDBUG 6082, D Bugzilla enhancement request 6082)).
+ */
+TPipedTransport!Source tPipedTransport(Source)(
+ Source srcTrans, TTransport dstTrans
+) if (isTTransport!Source) {
+ return new typeof(return)(srcTrans, dstTrans);
+}
+
+version (unittest) {
+ // DMD @@BUG@@: UFCS for std.array.empty doesn't work when import is moved
+ // into unittest block.
+ import std.array;
+ import std.exception : enforce;
+}
+
+unittest {
+ auto underlying = new TMemoryBuffer;
+ auto pipeTarget = new TMemoryBuffer;
+ auto trans = tPipedTransport(underlying, pipeTarget);
+
+ underlying.write(cast(ubyte[])"abcd");
+
+ ubyte[4] buffer;
+ trans.readAll(buffer[0 .. 2]);
+ enforce(buffer[0 .. 2] == "ab");
+ enforce(pipeTarget.getContents().empty);
+
+ trans.readEnd();
+ enforce(pipeTarget.getContents() == "ab");
+ pipeTarget.reset();
+
+ underlying.write(cast(ubyte[])"ef");
+ trans.readAll(buffer[0 .. 2]);
+ enforce(buffer[0 .. 2] == "cd");
+ enforce(pipeTarget.getContents().empty);
+
+ trans.readAll(buffer[0 .. 2]);
+ enforce(buffer[0 .. 2] == "ef");
+ enforce(pipeTarget.getContents().empty);
+
+ trans.readEnd();
+ enforce(pipeTarget.getContents() == "cdef");
+}
diff --git a/lib/d/src/thrift/transport/range.d b/lib/d/src/thrift/transport/range.d
new file mode 100644
index 000000000..761cea132
--- /dev/null
+++ b/lib/d/src/thrift/transport/range.d
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Transports which operate on generic D ranges.
+ */
+module thrift.transport.range;
+
+import std.array : empty;
+import std.range;
+import std.traits : Unqual;
+import thrift.transport.base;
+
+/**
+ * Adapts an ubyte input range for reading via the TTransport interface.
+ *
+ * The case where R is a plain ubyte[] is reasonably optimized, so a possible
+ * use case for TInputRangeTransport would be to deserialize some data held in
+ * a memory buffer.
+ */
+final class TInputRangeTransport(R) if (
+ isInputRange!(Unqual!R) && is(ElementType!R : const(ubyte))
+) : TBaseTransport {
+ /**
+ * Constructs a new instance.
+ *
+ * Params:
+ * data = The input range to use as data.
+ */
+ this(R data) {
+ data_ = data;
+ }
+
+ /**
+ * An input range transport is always open.
+ */
+ override bool isOpen() @property {
+ return true;
+ }
+
+ override bool peek() {
+ return !data_.empty;
+ }
+
+ /**
+ * Opening is a no-op() for an input range transport.
+ */
+ override void open() {}
+
+ /**
+ * Closing is a no-op() for a memory buffer.
+ */
+ override void close() {}
+
+ override size_t read(ubyte[] buf) {
+ auto data = data_.take(buf.length);
+ auto bytes = data.length;
+
+ static if (is(typeof(R.init[1 .. 2]) : const(ubyte)[])) {
+ // put() is currently unnecessarily slow if both ranges are sliceable.
+ buf[0 .. bytes] = data[];
+ data_ = data_[bytes .. $];
+ } else {
+ buf.put(data);
+ }
+
+ return bytes;
+ }
+
+ /**
+ * Shortcut version of readAll() for slicable ranges.
+ *
+ * Because readAll() is typically a very hot path during deserialization,
+ * using this over TBaseTransport.readAll() gives us a nice increase in
+ * speed due to the reduced amount of indirections.
+ */
+ override void readAll(ubyte[] buf) {
+ static if (is(typeof(R.init[1 .. 2]) : const(ubyte)[])) {
+ if (buf.length <= data_.length) {
+ buf[] = data_[0 .. buf.length];
+ data_ = data_[buf.length .. $];
+ return;
+ }
+ }
+ super.readAll(buf);
+ }
+
+ override const(ubyte)[] borrow(ubyte* buf, size_t len) {
+ static if (is(R : const(ubyte)[])) {
+ // Can only borrow if our data type is actually an ubyte array.
+ if (len <= data_.length) {
+ return data_;
+ }
+ }
+ return null;
+ }
+
+ override void consume(size_t len) {
+ static if (is(R : const(ubyte)[])) {
+ if (len > data_.length) {
+ throw new TTransportException("Invalid consume length",
+ TTransportException.Type.BAD_ARGS);
+ }
+ data_ = data_[len .. $];
+ } else {
+ super.consume(len);
+ }
+ }
+
+ /**
+ * Sets a new data range to use.
+ */
+ void reset(R data) {
+ data_ = data;
+ }
+
+private:
+ R data_;
+}
+
+/**
+ * TInputRangeTransport construction helper to avoid having to explicitly
+ * specify the argument type, i.e. to allow the constructor being called using
+ * IFTI (see $(LINK2 http://d.puremagic.com/issues/show_bug.cgi?id=6082, D
+ * Bugzilla enhancement requet 6082)).
+ */
+TInputRangeTransport!R tInputRangeTransport(R)(R data) if (
+ is (TInputRangeTransport!R)
+) {
+ return new TInputRangeTransport!R(data);
+}
diff --git a/lib/d/src/thrift/transport/socket.d b/lib/d/src/thrift/transport/socket.d
new file mode 100644
index 000000000..2b252c26b
--- /dev/null
+++ b/lib/d/src/thrift/transport/socket.d
@@ -0,0 +1,453 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.transport.socket;
+
+import core.thread : Thread;
+import core.time : dur, Duration;
+import std.array : empty;
+import std.conv : text, to;
+import std.exception : enforce;
+import std.socket;
+import thrift.base;
+import thrift.transport.base;
+import thrift.internal.socket;
+
+/**
+ * Common parts of a socket TTransport implementation, regardless of how the
+ * actual I/O is performed (sync/async).
+ */
+abstract class TSocketBase : TBaseTransport {
+ /**
+ * Constructor that takes an already created, connected (!) socket.
+ *
+ * Params:
+ * socket = Already created, connected socket object.
+ */
+ this(Socket socket) {
+ socket_ = socket;
+ setSocketOpts();
+ }
+
+ /**
+ * Creates a new unconnected socket that will connect to the given host
+ * on the given port.
+ *
+ * Params:
+ * host = Remote host.
+ * port = Remote port.
+ */
+ this(string host, ushort port) {
+ host_ = host;
+ port_ = port;
+ }
+
+ /**
+ * Checks whether the socket is connected.
+ */
+ override bool isOpen() @property {
+ return socket_ !is null;
+ }
+
+ /**
+ * Writes as much data to the socket as there can be in a single OS call.
+ *
+ * Params:
+ * buf = Data to write.
+ *
+ * Returns: The actual number of bytes written. Never more than buf.length.
+ */
+ abstract size_t writeSome(in ubyte[] buf) out (written) {
+ // DMD @@BUG@@: Enabling this e.g. fails the contract in the
+ // async_test_server, because buf.length evaluates to 0 here, even though
+ // in the method body it correctly is 27 (equal to the return value).
+ version (none) assert(written <= buf.length, text("Implementation wrote " ~
+ "more data than requested to?! (", written, " vs. ", buf.length, ")"));
+ } body {
+ assert(0, "DMD bug? – Why would contracts work for interfaces, but not "
+ "for abstract methods? "
+ "(Error: function […] in and out contracts require function body");
+ }
+
+ /**
+ * Returns the actual address of the peer the socket is connected to.
+ *
+ * In contrast, the host and port properties contain the address used to
+ * establish the connection, and are not updated after the connection.
+ *
+ * The socket must be open when calling this.
+ */
+ Address getPeerAddress() {
+ enforce(isOpen, new TTransportException("Cannot get peer host for " ~
+ "closed socket.", TTransportException.Type.NOT_OPEN));
+
+ if (!peerAddress_) {
+ peerAddress_ = socket_.remoteAddress();
+ assert(peerAddress_);
+ }
+
+ return peerAddress_;
+ }
+
+ /**
+ * The host the socket is connected to or will connect to. Null if an
+ * already connected socket was used to construct the object.
+ */
+ string host() const @property {
+ return host_;
+ }
+
+ /**
+ * The port the socket is connected to or will connect to. Zero if an
+ * already connected socket was used to construct the object.
+ */
+ ushort port() const @property {
+ return port_;
+ }
+
+ /// The socket send timeout.
+ Duration sendTimeout() const @property {
+ return sendTimeout_;
+ }
+
+ /// Ditto
+ void sendTimeout(Duration value) @property {
+ sendTimeout_ = value;
+ }
+
+ /// The socket receiving timeout. Values smaller than 500 ms are not
+ /// supported on Windows.
+ Duration recvTimeout() const @property {
+ return recvTimeout_;
+ }
+
+ /// Ditto
+ void recvTimeout(Duration value) @property {
+ recvTimeout_ = value;
+ }
+
+ /**
+ * Returns the OS handle of the underlying socket.
+ *
+ * Should not usually be used directly, but access to it can be necessary
+ * to interface with C libraries.
+ */
+ typeof(socket_.handle()) socketHandle() @property {
+ return socket_.handle();
+ }
+
+protected:
+ /**
+ * Sets the needed socket options.
+ */
+ void setSocketOpts() {
+ try {
+ alias SocketOptionLevel.SOCKET lvlSock;
+ linger l;
+ l.on = 0;
+ l.time = 0;
+ socket_.setOption(lvlSock, SocketOption.LINGER, l);
+ } catch (SocketException e) {
+ logError("Could not set socket option: %s", e);
+ }
+
+ // Just try to disable Nagle's algorithm – this will fail if we are passed
+ // in a non-TCP socket via the Socket-accepting constructor.
+ try {
+ socket_.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, true);
+ } catch (SocketException e) {}
+ }
+
+ /// Remote host.
+ string host_;
+
+ /// Remote port.
+ ushort port_;
+
+ /// Timeout for sending.
+ Duration sendTimeout_;
+
+ /// Timeout for receiving.
+ Duration recvTimeout_;
+
+ /// Cached peer address.
+ Address peerAddress_;
+
+ /// Cached peer host name.
+ string peerHost_;
+
+ /// Cached peer port.
+ ushort peerPort_;
+
+ /// Wrapped socket object.
+ Socket socket_;
+}
+
+/**
+ * Socket implementation of the TTransport interface.
+ *
+ * Due to the limitations of std.socket, currently only TCP/IP sockets are
+ * supported (i.e. Unix domain sockets are not).
+ */
+class TSocket : TSocketBase {
+ ///
+ this(Socket socket) {
+ super(socket);
+ }
+
+ ///
+ this(string host, ushort port) {
+ super(host, port);
+ }
+
+ /**
+ * Connects the socket.
+ */
+ override void open() {
+ if (isOpen) return;
+
+ enforce(!host_.empty, new TTransportException(
+ "Cannot open socket to null host.", TTransportException.Type.NOT_OPEN));
+ enforce(port_ != 0, new TTransportException(
+ "Cannot open socket to port zero.", TTransportException.Type.NOT_OPEN));
+
+ Address[] addrs;
+ try {
+ addrs = getAddress(host_, port_);
+ } catch (SocketException e) {
+ throw new TTransportException("Could not resolve given host string.",
+ TTransportException.Type.NOT_OPEN, __FILE__, __LINE__, e);
+ }
+
+ Exception[] errors;
+ foreach (addr; addrs) {
+ try {
+ socket_ = new TcpSocket(addr.addressFamily);
+ setSocketOpts();
+ socket_.connect(addr);
+ break;
+ } catch (SocketException e) {
+ errors ~= e;
+ }
+ }
+ if (errors.length == addrs.length) {
+ socket_ = null;
+ // Need to throw a TTransportException to abide the TTransport API.
+ import std.algorithm, std.range;
+ throw new TTransportException(
+ text("Failed to connect to ", host_, ":", port_, "."),
+ TTransportException.Type.NOT_OPEN,
+ __FILE__, __LINE__,
+ new TCompoundOperationException(
+ text(
+ "All addresses tried failed (",
+ joiner(map!q{text(a._0, `: "`, a._1.msg, `"`)}(zip(addrs, errors)), ", "),
+ ")."
+ ),
+ errors
+ )
+ );
+ }
+ }
+
+ /**
+ * Closes the socket.
+ */
+ override void close() {
+ if (!isOpen) return;
+
+ socket_.close();
+ socket_ = null;
+ }
+
+ override bool peek() {
+ if (!isOpen) return false;
+
+ ubyte buf;
+ auto r = socket_.receive((&buf)[0 .. 1], SocketFlags.PEEK);
+ if (r == -1) {
+ auto lastErrno = getSocketErrno();
+ static if (connresetOnPeerShutdown) {
+ if (lastErrno == ECONNRESET) {
+ close();
+ return false;
+ }
+ }
+ throw new TTransportException("Peeking into socket failed: " ~
+ socketErrnoString(lastErrno), TTransportException.Type.UNKNOWN);
+ }
+ return (r > 0);
+ }
+
+ override size_t read(ubyte[] buf) {
+ enforce(isOpen, new TTransportException(
+ "Cannot read if socket is not open.", TTransportException.Type.NOT_OPEN));
+
+ typeof(getSocketErrno()) lastErrno;
+ ushort tries;
+ while (tries++ <= maxRecvRetries_) {
+ auto r = socket_.receive(cast(void[])buf);
+
+ // If recv went fine, immediately return.
+ if (r >= 0) return r;
+
+ // Something went wrong, find out how to handle it.
+ lastErrno = getSocketErrno();
+
+ if (lastErrno == INTERRUPTED_ERRNO) {
+ // If the syscall was interrupted, just try again.
+ continue;
+ }
+
+ static if (connresetOnPeerShutdown) {
+ // See top comment.
+ if (lastErrno == ECONNRESET) {
+ return 0;
+ }
+ }
+
+ // Not an error which is handled in a special way, just leave the loop.
+ break;
+ }
+
+ if (isSocketCloseErrno(lastErrno)) {
+ close();
+ throw new TTransportException("Receiving failed, closing socket: " ~
+ socketErrnoString(lastErrno), TTransportException.Type.NOT_OPEN);
+ } else if (lastErrno == TIMEOUT_ERRNO) {
+ throw new TTransportException(TTransportException.Type.TIMED_OUT);
+ } else {
+ throw new TTransportException("Receiving from socket failed: " ~
+ socketErrnoString(lastErrno), TTransportException.Type.UNKNOWN);
+ }
+ }
+
+ override void write(in ubyte[] buf) {
+ size_t sent;
+ while (sent < buf.length) {
+ auto b = writeSome(buf[sent .. $]);
+ if (b == 0) {
+ // This should only happen if the timeout set with SO_SNDTIMEO expired.
+ throw new TTransportException("send() timeout expired.",
+ TTransportException.Type.TIMED_OUT);
+ }
+ sent += b;
+ }
+ assert(sent == buf.length);
+ }
+
+ override size_t writeSome(in ubyte[] buf) {
+ enforce(isOpen, new TTransportException(
+ "Cannot write if file is not open.", TTransportException.Type.NOT_OPEN));
+
+ auto r = socket_.send(buf);
+
+ // Everything went well, just return the number of bytes written.
+ if (r > 0) return r;
+
+ // Handle error conditions.
+ if (r < 0) {
+ auto lastErrno = getSocketErrno();
+
+ if (lastErrno == WOULD_BLOCK_ERRNO) {
+ // Not an exceptional error per se – even with blocking sockets,
+ // EAGAIN apparently is returned sometimes on out-of-resource
+ // conditions (see the C++ implementation for details). Also, this
+ // allows using TSocket with non-blocking sockets e.g. in
+ // TNonblockingServer.
+ return 0;
+ }
+
+ auto type = TTransportException.Type.UNKNOWN;
+ if (isSocketCloseErrno(lastErrno)) {
+ type = TTransportException.Type.NOT_OPEN;
+ close();
+ }
+
+ throw new TTransportException("Sending to socket failed: " ~
+ socketErrnoString(lastErrno), type);
+ }
+
+ // send() should never return 0.
+ throw new TTransportException("Sending to socket failed (0 bytes written).",
+ TTransportException.Type.UNKNOWN);
+ }
+
+ override void sendTimeout(Duration value) @property {
+ super.sendTimeout(value);
+ setTimeout(SocketOption.SNDTIMEO, value);
+ }
+
+ override void recvTimeout(Duration value) @property {
+ super.recvTimeout(value);
+ setTimeout(SocketOption.RCVTIMEO, value);
+ }
+
+ /**
+ * Maximum number of retries for receiving from socket on read() in case of
+ * EAGAIN/EINTR.
+ */
+ ushort maxRecvRetries() @property const {
+ return maxRecvRetries_;
+ }
+
+ /// Ditto
+ void maxRecvRetries(ushort value) @property {
+ maxRecvRetries_ = value;
+ }
+
+ /// Ditto
+ enum DEFAULT_MAX_RECV_RETRIES = 5;
+
+protected:
+ override void setSocketOpts() {
+ super.setSocketOpts();
+ setTimeout(SocketOption.SNDTIMEO, sendTimeout_);
+ setTimeout(SocketOption.RCVTIMEO, recvTimeout_);
+ }
+
+ void setTimeout(SocketOption type, Duration value) {
+ assert(type == SocketOption.SNDTIMEO || type == SocketOption.RCVTIMEO);
+ version (Win32) {
+ if (value > dur!"hnsecs"(0) && value < dur!"msecs"(500)) {
+ logError(
+ "Socket %s timeout of %s ms might be raised to 500 ms on Windows.",
+ (type == SocketOption.SNDTIMEO) ? "send" : "receive",
+ value.total!"msecs"
+ );
+ }
+ }
+
+ if (socket_) {
+ try {
+ socket_.setOption(SocketOptionLevel.SOCKET, type, value);
+ } catch (SocketException e) {
+ throw new TTransportException(
+ "Could not set send timeout: " ~ socketErrnoString(e.errorCode),
+ TTransportException.Type.UNKNOWN,
+ __FILE__,
+ __LINE__,
+ e
+ );
+ }
+ }
+ }
+
+ /// Maximum number of recv() retries.
+ ushort maxRecvRetries_ = DEFAULT_MAX_RECV_RETRIES;
+}
diff --git a/lib/d/src/thrift/transport/ssl.d b/lib/d/src/thrift/transport/ssl.d
new file mode 100644
index 000000000..da0eb274a
--- /dev/null
+++ b/lib/d/src/thrift/transport/ssl.d
@@ -0,0 +1,680 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * OpenSSL socket implementation, in large parts ported from C++.
+ */
+module thrift.transport.ssl;
+
+import core.exception : onOutOfMemoryError;
+import core.stdc.errno : getErrno, EINTR;
+import core.sync.mutex : Mutex;
+import core.memory : GC;
+import core.stdc.config;
+import core.stdc.stdlib : free, malloc;
+import std.ascii : toUpper;
+import std.array : empty, front, popFront;
+import std.conv : emplace, to;
+import std.exception : enforce;
+import std.socket : Address, InternetAddress, Internet6Address, Socket;
+import std.string : toStringz;
+import deimos.openssl.err;
+import deimos.openssl.rand;
+import deimos.openssl.ssl;
+import deimos.openssl.x509v3;
+import thrift.base;
+import thrift.internal.ssl;
+import thrift.transport.base;
+import thrift.transport.socket;
+
+/**
+ * SSL encrypted socket implementation using OpenSSL.
+ *
+ * Note:
+ * On Posix systems which do not have the BSD-specific SO_NOSIGPIPE flag, you
+ * might want to ignore the SIGPIPE signal, as OpenSSL might try to write to
+ * a closed socket if the peer disconnects abruptly:
+ * ---
+ * import core.stdc.signal;
+ * import core.sys.posix.signal;
+ * signal(SIGPIPE, SIG_IGN);
+ * ---
+ */
+final class TSSLSocket : TSocket {
+ /**
+ * Creates an instance that wraps an already created, connected (!) socket.
+ *
+ * Params:
+ * context = The SSL socket context to use. A reference to it is stored so
+ * that it doesn't get cleaned up while the socket is used.
+ * socket = Already created, connected socket object.
+ */
+ this(TSSLContext context, Socket socket) {
+ super(socket);
+ context_ = context;
+ serverSide_ = context.serverSide;
+ accessManager_ = context.accessManager;
+ }
+
+ /**
+ * Creates a new unconnected socket that will connect to the given host
+ * on the given port.
+ *
+ * Params:
+ * context = The SSL socket context to use. A reference to it is stored so
+ * that it doesn't get cleaned up while the socket is used.
+ * host = Remote host.
+ * port = Remote port.
+ */
+ this(TSSLContext context, string host, ushort port) {
+ super(host, port);
+ context_ = context;
+ serverSide_ = context.serverSide;
+ accessManager_ = context.accessManager;
+ }
+
+ override bool isOpen() @property {
+ if (ssl_ is null || !super.isOpen()) return false;
+
+ auto shutdown = SSL_get_shutdown(ssl_);
+ bool shutdownReceived = (shutdown & SSL_RECEIVED_SHUTDOWN) != 0;
+ bool shutdownSent = (shutdown & SSL_SENT_SHUTDOWN) != 0;
+ return !(shutdownReceived && shutdownSent);
+ }
+
+ override bool peek() {
+ if (!isOpen) return false;
+ checkHandshake();
+
+ byte bt;
+ auto rc = SSL_peek(ssl_, &bt, bt.sizeof);
+ enforce(rc >= 0, getSSLException("SSL_peek"));
+
+ if (rc == 0) {
+ ERR_clear_error();
+ }
+ return (rc > 0);
+ }
+
+ override void open() {
+ enforce(!serverSide_, "Cannot open a server-side SSL socket.");
+ if (isOpen) return;
+ super.open();
+ }
+
+ override void close() {
+ if (!isOpen) return;
+
+ if (ssl_ !is null) {
+ // Two-step SSL shutdown.
+ auto rc = SSL_shutdown(ssl_);
+ if (rc == 0) {
+ rc = SSL_shutdown(ssl_);
+ }
+ if (rc < 0) {
+ // Do not throw an exception here as leaving the transport "open" will
+ // probably produce only more errors, and the chance we can do
+ // something about the error e.g. by retrying is very low.
+ logError("Error shutting down SSL: %s", getSSLException());
+ }
+
+ SSL_free(ssl_);
+ ssl_ = null;
+ ERR_remove_state(0);
+ }
+ super.close();
+ }
+
+ override size_t read(ubyte[] buf) {
+ checkHandshake();
+
+ int bytes;
+ foreach (_; 0 .. maxRecvRetries) {
+ bytes = SSL_read(ssl_, buf.ptr, cast(int)buf.length);
+ if (bytes >= 0) break;
+
+ auto errnoCopy = getErrno();
+ if (SSL_get_error(ssl_, bytes) == SSL_ERROR_SYSCALL) {
+ if (ERR_get_error() == 0 && errnoCopy == EINTR) {
+ // FIXME: Windows.
+ continue;
+ }
+ }
+ throw getSSLException("SSL_read");
+ }
+ return bytes;
+ }
+
+ override void write(in ubyte[] buf) {
+ checkHandshake();
+
+ // Loop in case SSL_MODE_ENABLE_PARTIAL_WRITE is set in SSL_CTX.
+ size_t written = 0;
+ while (written < buf.length) {
+ auto bytes = SSL_write(ssl_, buf.ptr + written,
+ cast(int)(buf.length - written));
+ if (bytes <= 0) {
+ throw getSSLException("SSL_write");
+ }
+ written += bytes;
+ }
+ }
+
+ override void flush() {
+ checkHandshake();
+
+ auto bio = SSL_get_wbio(ssl_);
+ enforce(bio !is null, new TSSLException("SSL_get_wbio returned null"));
+
+ auto rc = BIO_flush(bio);
+ enforce(rc == 1, getSSLException("BIO_flush"));
+ }
+
+ /**
+ * Whether to use client or server side SSL handshake protocol.
+ */
+ bool serverSide() @property const {
+ return serverSide_;
+ }
+
+ /// Ditto
+ void serverSide(bool value) @property {
+ serverSide_ = value;
+ }
+
+ /**
+ * The access manager to use.
+ */
+ void accessManager(TAccessManager value) @property {
+ accessManager_ = value;
+ }
+
+private:
+ void checkHandshake() {
+ enforce(super.isOpen(), new TTransportException(
+ TTransportException.Type.NOT_OPEN));
+
+ if (ssl_ !is null) return;
+ ssl_ = context_.createSSL();
+
+ SSL_set_fd(ssl_, socketHandle);
+ int rc;
+ if (serverSide_) {
+ rc = SSL_accept(ssl_);
+ } else {
+ rc = SSL_connect(ssl_);
+ }
+ enforce(rc > 0, getSSLException());
+ authorize(ssl_, accessManager_, getPeerAddress(),
+ (serverSide_ ? getPeerAddress().toHostNameString() : host));
+ }
+
+ bool serverSide_;
+ SSL* ssl_;
+ TSSLContext context_;
+ TAccessManager accessManager_;
+}
+
+/**
+ * Represents an OpenSSL context with certification settings, etc. and handles
+ * initialization/teardown.
+ *
+ * OpenSSL is initialized when the first instance of this class is created
+ * and shut down when the last one is destroyed (thread-safe).
+ */
+class TSSLContext {
+ this() {
+ initMutex_.lock();
+ scope(exit) initMutex_.unlock();
+
+ if (count_ == 0) {
+ initializeOpenSSL();
+ randomize();
+ }
+ count_++;
+
+ ctx_ = SSL_CTX_new(TLSv1_method());
+ enforce(ctx_, getSSLException("SSL_CTX_new"));
+ SSL_CTX_set_mode(ctx_, SSL_MODE_AUTO_RETRY);
+ }
+
+ ~this() {
+ initMutex_.lock();
+ scope(exit) initMutex_.unlock();
+
+ if (ctx_ !is null) {
+ SSL_CTX_free(ctx_);
+ ctx_ = null;
+ }
+
+ count_--;
+ if (count_ == 0) {
+ cleanupOpenSSL();
+ }
+ }
+
+ /**
+ * Ciphers to be used in SSL handshake process.
+ *
+ * The string must be in the colon-delimited OpenSSL notation described in
+ * ciphers(1), for example: "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH".
+ */
+ void ciphers(string enable) @property {
+ auto rc = SSL_CTX_set_cipher_list(ctx_, toStringz(enable));
+
+ enforce(ERR_peek_error() == 0, getSSLException("SSL_CTX_set_cipher_list"));
+ enforce(rc > 0, new TSSLException("None of specified ciphers are supported"));
+ }
+
+ /**
+ * Whether peer is required to present a valid certificate.
+ */
+ void authenticate(bool required) @property {
+ int mode;
+ if (required) {
+ mode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT |
+ SSL_VERIFY_CLIENT_ONCE;
+ } else {
+ mode = SSL_VERIFY_NONE;
+ }
+ SSL_CTX_set_verify(ctx_, mode, null);
+ }
+
+ /**
+ * Load server certificate.
+ *
+ * Params:
+ * path = Path to the certificate file.
+ * format = Certificate file format. Defaults to PEM, which is currently
+ * the only one supported.
+ */
+ void loadCertificate(string path, string format = "PEM") {
+ enforce(path !is null && format !is null, new TTransportException(
+ "loadCertificateChain: either <path> or <format> is null",
+ TTransportException.Type.BAD_ARGS));
+
+ if (format == "PEM") {
+ enforce(SSL_CTX_use_certificate_chain_file(ctx_, toStringz(path)),
+ getSSLException(
+ `Could not load SSL server certificate from file "` ~ path ~ `"`
+ )
+ );
+ } else {
+ throw new TSSLException("Unsupported certificate format: " ~ format);
+ }
+ }
+
+ /*
+ * Load private key.
+ *
+ * Params:
+ * path = Path to the certificate file.
+ * format = Private key file format. Defaults to PEM, which is currently
+ * the only one supported.
+ */
+ void loadPrivateKey(string path, string format = "PEM") {
+ enforce(path !is null && format !is null, new TTransportException(
+ "loadPrivateKey: either <path> or <format> is NULL",
+ TTransportException.Type.BAD_ARGS));
+
+ if (format == "PEM") {
+ enforce(SSL_CTX_use_PrivateKey_file(ctx_, toStringz(path), SSL_FILETYPE_PEM),
+ getSSLException(
+ `Could not load SSL private key from file "` ~ path ~ `"`
+ )
+ );
+ } else {
+ throw new TSSLException("Unsupported certificate format: " ~ format);
+ }
+ }
+
+ /**
+ * Load trusted certificates from specified file (in PEM format).
+ *
+ * Params.
+ * path = Path to the file containing the trusted certificates.
+ */
+ void loadTrustedCertificates(string path) {
+ enforce(path !is null, new TTransportException(
+ "loadTrustedCertificates: <path> is NULL",
+ TTransportException.Type.BAD_ARGS));
+
+ enforce(SSL_CTX_load_verify_locations(ctx_, toStringz(path), null),
+ getSSLException(
+ `Could not load SSL trusted certificate list from file "` ~ path ~ `"`
+ )
+ );
+ }
+
+ /**
+ * Called during OpenSSL initialization to seed the OpenSSL entropy pool.
+ *
+ * Defaults to simply calling RAND_poll(), but it can be overwritten if a
+ * different, perhaps more secure implementation is desired.
+ */
+ void randomize() {
+ RAND_poll();
+ }
+
+ /**
+ * Whether to use client or server side SSL handshake protocol.
+ */
+ bool serverSide() @property const {
+ return serverSide_;
+ }
+
+ /// Ditto
+ void serverSide(bool value) @property {
+ serverSide_ = value;
+ }
+
+ /**
+ * The access manager to use.
+ */
+ TAccessManager accessManager() @property {
+ if (!serverSide_ && !accessManager_) {
+ accessManager_ = new TDefaultClientAccessManager;
+ }
+ return accessManager_;
+ }
+
+ /// Ditto
+ void accessManager(TAccessManager value) @property {
+ accessManager_ = value;
+ }
+
+ SSL* createSSL() out (result) {
+ assert(result);
+ } body {
+ auto result = SSL_new(ctx_);
+ enforce(result, getSSLException("SSL_new"));
+ return result;
+ }
+
+protected:
+ /**
+ * Override this method for custom password callback. It may be called
+ * multiple times at any time during a session as necessary.
+ *
+ * Params:
+ * size = Maximum length of password, including null byte.
+ */
+ string getPassword(int size) nothrow out(result) {
+ assert(result.length < size);
+ } body {
+ return "";
+ }
+
+ /**
+ * Notifies OpenSSL to use getPassword() instead of the default password
+ * callback with getPassword().
+ */
+ void overrideDefaultPasswordCallback() {
+ SSL_CTX_set_default_passwd_cb(ctx_, &passwordCallback);
+ SSL_CTX_set_default_passwd_cb_userdata(ctx_, cast(void*)this);
+ }
+
+ SSL_CTX* ctx_;
+
+private:
+ bool serverSide_;
+ TAccessManager accessManager_;
+
+ shared static this() {
+ initMutex_ = new Mutex();
+ }
+
+ static void initializeOpenSSL() {
+ if (initialized_) {
+ return;
+ }
+ initialized_ = true;
+
+ SSL_library_init();
+ SSL_load_error_strings();
+
+ mutexes_ = new Mutex[CRYPTO_num_locks()];
+ foreach (ref m; mutexes_) {
+ m = new Mutex;
+ }
+
+ import thrift.internal.traits;
+ // As per the OpenSSL threads manpage, this isn't needed on Windows.
+ version (Posix) {
+ CRYPTO_set_id_callback(assumeNothrow(&threadIdCallback));
+ }
+ CRYPTO_set_locking_callback(assumeNothrow(&lockingCallback));
+ CRYPTO_set_dynlock_create_callback(assumeNothrow(&dynlockCreateCallback));
+ CRYPTO_set_dynlock_lock_callback(assumeNothrow(&dynlockLockCallback));
+ CRYPTO_set_dynlock_destroy_callback(assumeNothrow(&dynlockDestroyCallback));
+ }
+
+ static void cleanupOpenSSL() {
+ if (!initialized_) return;
+
+ initialized_ = false;
+ CRYPTO_set_locking_callback(null);
+ CRYPTO_set_dynlock_create_callback(null);
+ CRYPTO_set_dynlock_lock_callback(null);
+ CRYPTO_set_dynlock_destroy_callback(null);
+ CRYPTO_cleanup_all_ex_data();
+ ERR_free_strings();
+ ERR_remove_state(0);
+ }
+
+ static extern(C) {
+ version (Posix) {
+ import core.sys.posix.pthread : pthread_self;
+ c_ulong threadIdCallback() {
+ return cast(c_ulong)pthread_self();
+ }
+ }
+
+ void lockingCallback(int mode, int n, const(char)* file, int line) {
+ if (mode & CRYPTO_LOCK) {
+ mutexes_[n].lock();
+ } else {
+ mutexes_[n].unlock();
+ }
+ }
+
+ CRYPTO_dynlock_value* dynlockCreateCallback(const(char)* file, int line) {
+ enum size = __traits(classInstanceSize, Mutex);
+ auto mem = malloc(size)[0 .. size];
+ if (!mem) onOutOfMemoryError();
+ GC.addRange(mem.ptr, size);
+ auto mutex = emplace!Mutex(mem);
+ return cast(CRYPTO_dynlock_value*)mutex;
+ }
+
+ void dynlockLockCallback(int mode, CRYPTO_dynlock_value* l,
+ const(char)* file, int line)
+ {
+ if (l is null) return;
+ if (mode & CRYPTO_LOCK) {
+ (cast(Mutex)l).lock();
+ } else {
+ (cast(Mutex)l).unlock();
+ }
+ }
+
+ void dynlockDestroyCallback(CRYPTO_dynlock_value* l,
+ const(char)* file, int line)
+ {
+ GC.removeRange(l);
+ clear(cast(Mutex)l);
+ free(l);
+ }
+
+ int passwordCallback(char* password, int size, int, void* data) nothrow {
+ auto context = cast(TSSLContext) data;
+ auto userPassword = context.getPassword(size);
+ auto len = userPassword.length;
+ if (len > size) {
+ len = size;
+ }
+ password[0 .. len] = userPassword[0 .. len]; // TODO: \0 handling correct?
+ return cast(int)len;
+ }
+ }
+
+ static __gshared bool initialized_;
+ static __gshared Mutex initMutex_;
+ static __gshared Mutex[] mutexes_;
+ static __gshared uint count_;
+}
+
+/**
+ * Decides whether a remote host is legitimate or not.
+ *
+ * It is usually set at a TSSLContext, which then passes it to all the created
+ * TSSLSockets.
+ */
+class TAccessManager {
+ ///
+ enum Decision {
+ DENY = -1, /// Deny access.
+ SKIP = 0, /// Cannot decide, move on to next check (deny if last).
+ ALLOW = 1 /// Allow access.
+ }
+
+ /**
+ * Determines whether a peer should be granted access or not based on its
+ * IP address.
+ *
+ * Called once after SSL handshake is completes successfully and before peer
+ * certificate is examined.
+ *
+ * If a valid decision (ALLOW or DENY) is returned, the peer certificate
+ * will not be verified.
+ */
+ Decision verify(Address address) {
+ return Decision.DENY;
+ }
+
+ /**
+ * Determines whether a peer should be granted access or not based on a
+ * name from its certificate.
+ *
+ * Called every time a DNS subjectAltName/common name is extracted from the
+ * peer's certificate.
+ *
+ * Params:
+ * host = The actual host name string from the socket connection.
+ * certHost = A host name string from the certificate.
+ */
+ Decision verify(string host, const(char)[] certHost) {
+ return Decision.DENY;
+ }
+
+ /**
+ * Determines whether a peer should be granted access or not based on an IP
+ * address from its certificate.
+ *
+ * Called every time an IP subjectAltName is extracted from the peer's
+ * certificate.
+ *
+ * Params:
+ * address = The actual address from the socket connection.
+ * certHost = A host name string from the certificate.
+ */
+ Decision verify(Address address, ubyte[] certAddress) {
+ return Decision.DENY;
+ }
+}
+
+/**
+ * Default access manager implementation, which just checks the host name
+ * resp. IP address of the connection against the certificate.
+ */
+class TDefaultClientAccessManager : TAccessManager {
+ override Decision verify(Address address) {
+ return Decision.SKIP;
+ }
+
+ override Decision verify(string host, const(char)[] certHost) {
+ if (host.empty || certHost.empty) {
+ return Decision.SKIP;
+ }
+ return (matchName(host, certHost) ? Decision.ALLOW : Decision.SKIP);
+ }
+
+ override Decision verify(Address address, ubyte[] certAddress) {
+ bool match;
+ if (certAddress.length == 4) {
+ if (auto ia = cast(InternetAddress)address) {
+ match = ((cast(ubyte*)ia.addr())[0 .. 4] == certAddress[]);
+ }
+ } else if (certAddress.length == 16) {
+ if (auto ia = cast(Internet6Address)address) {
+ match = (ia.addr() == certAddress[]);
+ }
+ }
+ return (match ? Decision.ALLOW : Decision.SKIP);
+ }
+}
+
+private {
+ /**
+ * Matches a name with a pattern. The pattern may include wildcard. A single
+ * wildcard "*" can match up to one component in the domain name.
+ *
+ * Params:
+ * host = Host name to match, typically the SSL remote peer.
+ * pattern = Host name pattern, typically from the SSL certificate.
+ *
+ * Returns: true if host matches pattern, false otherwise.
+ */
+ bool matchName(const(char)[] host, const(char)[] pattern) {
+ while (!host.empty && !pattern.empty) {
+ if (toUpper(pattern.front) == toUpper(host.front)) {
+ host.popFront;
+ pattern.popFront;
+ } else if (pattern.front == '*') {
+ while (!host.empty && host.front != '.') {
+ host.popFront;
+ }
+ pattern.popFront;
+ } else {
+ break;
+ }
+ }
+ return (host.empty && pattern.empty);
+ }
+
+ unittest {
+ enforce(matchName("thrift.apache.org", "*.apache.org"));
+ enforce(!matchName("thrift.apache.org", "apache.org"));
+ enforce(matchName("thrift.apache.org", "thrift.*.*"));
+ enforce(matchName("", ""));
+ enforce(!matchName("", "*"));
+ }
+}
+
+/**
+ * SSL-level exception.
+ */
+class TSSLException : TTransportException {
+ ///
+ this(string msg, string file = __FILE__, size_t line = __LINE__,
+ Throwable next = null)
+ {
+ super(msg, TTransportException.Type.INTERNAL_ERROR, file, line, next);
+ }
+}
diff --git a/lib/d/src/thrift/transport/zlib.d b/lib/d/src/thrift/transport/zlib.d
new file mode 100644
index 000000000..9496f9bf6
--- /dev/null
+++ b/lib/d/src/thrift/transport/zlib.d
@@ -0,0 +1,497 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module thrift.transport.zlib;
+
+import core.bitop : bswap;
+import etc.c.zlib;
+import std.algorithm : min;
+import std.array : empty;
+import std.conv : to;
+import std.exception : enforce;
+import thrift.base;
+import thrift.transport.base;
+
+/**
+ * zlib transport. Compresses (deflates) data before writing it to the
+ * underlying transport, and decompresses (inflates) it after reading.
+ */
+final class TZlibTransport : TBaseTransport {
+ // These defaults have yet to be optimized.
+ enum DEFAULT_URBUF_SIZE = 128;
+ enum DEFAULT_CRBUF_SIZE = 1024;
+ enum DEFAULT_UWBUF_SIZE = 128;
+ enum DEFAULT_CWBUF_SIZE = 1024;
+
+ /**
+ * Constructs a new zlib transport.
+ *
+ * Params:
+ * transport = The underlying transport to wrap.
+ * urbufSize = The size of the uncompressed reading buffer, in bytes.
+ * crbufSize = The size of the compressed reading buffer, in bytes.
+ * uwbufSize = The size of the uncompressed writing buffer, in bytes.
+ * cwbufSize = The size of the compressed writing buffer, in bytes.
+ */
+ this(
+ TTransport transport,
+ size_t urbufSize = DEFAULT_URBUF_SIZE,
+ size_t crbufSize = DEFAULT_CRBUF_SIZE,
+ size_t uwbufSize = DEFAULT_UWBUF_SIZE,
+ size_t cwbufSize = DEFAULT_CWBUF_SIZE
+ ) {
+ transport_ = transport;
+
+ enforce(uwbufSize >= MIN_DIRECT_DEFLATE_SIZE, new TTransportException(
+ "TZLibTransport: uncompressed write buffer must be at least " ~
+ to!string(MIN_DIRECT_DEFLATE_SIZE) ~ "bytes in size.",
+ TTransportException.Type.BAD_ARGS));
+
+ urbuf_ = new ubyte[urbufSize];
+ crbuf_ = new ubyte[crbufSize];
+ uwbuf_ = new ubyte[uwbufSize];
+ cwbuf_ = new ubyte[cwbufSize];
+
+ rstream_ = new z_stream;
+ rstream_.next_in = crbuf_.ptr;
+ rstream_.avail_in = 0;
+ rstream_.next_out = urbuf_.ptr;
+ rstream_.avail_out = to!uint(urbuf_.length);
+
+ wstream_ = new z_stream;
+ wstream_.next_in = uwbuf_.ptr;
+ wstream_.avail_in = 0;
+ wstream_.next_out = cwbuf_.ptr;
+ wstream_.avail_out = to!uint(crbuf_.length);
+
+ zlibEnforce(inflateInit(rstream_), rstream_);
+ scope (failure) {
+ zlibLogError(inflateEnd(rstream_), rstream_);
+ }
+
+ zlibEnforce(deflateInit(wstream_, Z_DEFAULT_COMPRESSION), wstream_);
+ }
+
+ ~this() {
+ zlibLogError(inflateEnd(rstream_), rstream_);
+
+ auto result = deflateEnd(wstream_);
+ // Z_DATA_ERROR may indicate unflushed data, so just ignore it.
+ if (result != Z_DATA_ERROR) {
+ zlibLogError(result, wstream_);
+ }
+ }
+
+ /**
+ * Returns the wrapped transport.
+ */
+ TTransport underlyingTransport() @property {
+ return transport_;
+ }
+
+ override bool isOpen() @property {
+ return readAvail > 0 || transport_.isOpen;
+ }
+
+ override bool peek() {
+ return readAvail > 0 || transport_.peek();
+ }
+
+ override void open() {
+ transport_.open();
+ }
+
+ override void close() {
+ transport_.close();
+ }
+
+ override size_t read(ubyte[] buf) {
+ // The C++ implementation suggests to skip urbuf on big reads in future
+ // versions, we would benefit from it as well.
+ auto origLen = buf.length;
+ while (true) {
+ auto give = min(readAvail, buf.length);
+
+ // If std.range.put was optimized for slicable ranges, it could be used
+ // here as well.
+ buf[0 .. give] = urbuf_[urpos_ .. urpos_ + give];
+ buf = buf[give .. $];
+ urpos_ += give;
+
+ auto need = buf.length;
+ if (need == 0) {
+ // We could manage to get the all the data requested.
+ return origLen;
+ }
+
+ if (inputEnded_ || (need < origLen && rstream_.avail_in == 0)) {
+ // We didn't fill buf completely, but there is no more data available.
+ return origLen - need;
+ }
+
+ // Refill our buffer by reading more data through zlib.
+ rstream_.next_out = urbuf_.ptr;
+ rstream_.avail_out = to!uint(urbuf_.length);
+ urpos_ = 0;
+
+ if (!readFromZlib()) {
+ // Couldn't get more data from the underlying transport.
+ return origLen - need;
+ }
+ }
+ }
+
+ override void write(in ubyte[] buf) {
+ enforce(!outputFinished_, new TTransportException(
+ "write() called after finish()", TTransportException.Type.BAD_ARGS));
+
+ auto len = buf.length;
+ if (len > MIN_DIRECT_DEFLATE_SIZE) {
+ flushToZlib(uwbuf_[0 .. uwpos_], Z_NO_FLUSH);
+ uwpos_ = 0;
+ flushToZlib(buf, Z_NO_FLUSH);
+ } else if (len > 0) {
+ if (uwbuf_.length - uwpos_ < len) {
+ flushToZlib(uwbuf_[0 .. uwpos_], Z_NO_FLUSH);
+ uwpos_ = 0;
+ }
+ uwbuf_[uwpos_ .. uwpos_ + len] = buf[];
+ uwpos_ += len;
+ }
+ }
+
+ override void flush() {
+ enforce(!outputFinished_, new TTransportException(
+ "flush() called after finish()", TTransportException.Type.BAD_ARGS));
+
+ flushToTransport(Z_SYNC_FLUSH);
+ }
+
+ override const(ubyte)[] borrow(ubyte* buf, size_t len) {
+ if (len <= readAvail) {
+ return urbuf_[urpos_ .. $];
+ }
+ return null;
+ }
+
+ override void consume(size_t len) {
+ enforce(readAvail >= len, new TTransportException(
+ "consume() did not follow a borrow().", TTransportException.Type.BAD_ARGS));
+ urpos_ += len;
+ }
+
+ /**
+ * Finalize the zlib stream.
+ *
+ * This causes zlib to flush any pending write data and write end-of-stream
+ * information, including the checksum. Once finish() has been called, no
+ * new data can be written to the stream.
+ */
+ void finish() {
+ enforce(!outputFinished_, new TTransportException(
+ "flush() called on already finished TZlibTransport",
+ TTransportException.Type.BAD_ARGS));
+ flushToTransport(Z_FINISH);
+ }
+
+ /**
+ * Verify the checksum at the end of the zlib stream (by finish()).
+ *
+ * May only be called after all data has been read.
+ *
+ * Throws: TTransportException when the checksum is corrupted or there is
+ * still unread data left.
+ */
+ void verifyChecksum() {
+ // If zlib has already reported the end of the stream, the checksum has
+ // been verified, no.
+ if (inputEnded_) return;
+
+ enforce(!readAvail, new TTransportException(
+ "verifyChecksum() called before end of zlib stream",
+ TTransportException.Type.CORRUPTED_DATA));
+
+ rstream_.next_out = urbuf_.ptr;
+ rstream_.avail_out = to!uint(urbuf_.length);
+ urpos_ = 0;
+
+ // readFromZlib() will throw an exception if the checksum is bad.
+ enforce(readFromZlib(), new TTransportException(
+ "checksum not available yet in verifyChecksum()",
+ TTransportException.Type.CORRUPTED_DATA));
+
+ enforce(inputEnded_, new TTransportException(
+ "verifyChecksum() called before end of zlib stream",
+ TTransportException.Type.CORRUPTED_DATA));
+
+ // If we get here, we are at the end of the stream and thus zlib has
+ // successfully verified the checksum.
+ }
+
+private:
+ size_t readAvail() const @property {
+ return urbuf_.length - rstream_.avail_out - urpos_;
+ }
+
+ bool readFromZlib() {
+ assert(!inputEnded_);
+
+ if (rstream_.avail_in == 0) {
+ // zlib has used up all the compressed data we provided in crbuf, read
+ // some more from the underlying transport.
+ auto got = transport_.read(crbuf_);
+ if (got == 0) return false;
+ rstream_.next_in = crbuf_.ptr;
+ rstream_.avail_in = to!uint(got);
+ }
+
+ // We have some compressed data now, uncompress it.
+ auto zlib_result = inflate(rstream_, Z_SYNC_FLUSH);
+ if (zlib_result == Z_STREAM_END) {
+ inputEnded_ = true;
+ } else {
+ zlibEnforce(zlib_result, rstream_);
+ }
+
+ return true;
+ }
+
+ void flushToTransport(int type) {
+ // Compress remaining data in uwbuf_ to cwbuf_.
+ flushToZlib(uwbuf_[0 .. uwpos_], type);
+ uwpos_ = 0;
+
+ // Write all compressed data to the transport.
+ transport_.write(cwbuf_[0 .. $ - wstream_.avail_out]);
+ wstream_.next_out = cwbuf_.ptr;
+ wstream_.avail_out = to!uint(cwbuf_.length);
+
+ // Flush the transport.
+ transport_.flush();
+ }
+
+ void flushToZlib(in ubyte[] buf, int type) {
+ wstream_.next_in = cast(ubyte*)buf.ptr; // zlib only reads, cast is safe.
+ wstream_.avail_in = to!uint(buf.length);
+
+ while (true) {
+ if (type == Z_NO_FLUSH && wstream_.avail_in == 0) {
+ break;
+ }
+
+ if (wstream_.avail_out == 0) {
+ // cwbuf has been exhausted by zlib, flush to the underlying transport.
+ transport_.write(cwbuf_);
+ wstream_.next_out = cwbuf_.ptr;
+ wstream_.avail_out = to!uint(cwbuf_.length);
+ }
+
+ auto zlib_result = deflate(wstream_, type);
+
+ if (type == Z_FINISH && zlib_result == Z_STREAM_END) {
+ assert(wstream_.avail_in == 0);
+ outputFinished_ = true;
+ break;
+ }
+
+ zlibEnforce(zlib_result, wstream_);
+
+ if ((type == Z_SYNC_FLUSH || type == Z_FULL_FLUSH) &&
+ wstream_.avail_in == 0 && wstream_.avail_out != 0) {
+ break;
+ }
+ }
+ }
+
+ static void zlibEnforce(int status, z_stream* stream) {
+ if (status != Z_OK) {
+ throw new TZlibException(status, stream.msg);
+ }
+ }
+
+ static void zlibLogError(int status, z_stream* stream) {
+ if (status != Z_OK) {
+ logError("TZlibTransport: zlib failure in destructor: %s",
+ TZlibException.errorMessage(status, stream.msg));
+ }
+ }
+
+ // Writes smaller than this are buffered up (due to zlib handling overhead).
+ // Larger (or equal) writes are dumped straight to zlib.
+ enum MIN_DIRECT_DEFLATE_SIZE = 32;
+
+ TTransport transport_;
+ z_stream* rstream_;
+ z_stream* wstream_;
+
+ /// Whether zlib has reached the end of the input stream.
+ bool inputEnded_;
+
+ /// Whether the output stream was already finish()ed.
+ bool outputFinished_;
+
+ /// Compressed input data buffer.
+ ubyte[] crbuf_;
+
+ /// Uncompressed input data buffer.
+ ubyte[] urbuf_;
+ size_t urpos_;
+
+ /// Uncompressed output data buffer (where small writes are accumulated
+ /// before handing over to zlib).
+ ubyte[] uwbuf_;
+ size_t uwpos_;
+
+ /// Compressed output data buffer (filled by zlib, we flush it to the
+ /// underlying transport).
+ ubyte[] cwbuf_;
+}
+
+/**
+ * Wraps given transports into TZlibTransports.
+ */
+alias TWrapperTransportFactory!TZlibTransport TZlibTransportFactory;
+
+/**
+ * An INTERNAL_ERROR-type TTransportException originating from an error
+ * signaled by zlib.
+ */
+class TZlibException : TTransportException {
+ this(int statusCode, const(char)* msg) {
+ super(errorMessage(statusCode, msg), TTransportException.Type.INTERNAL_ERROR);
+ zlibStatusCode = statusCode;
+ zlibMsg = msg ? to!string(msg) : "(null)";
+ }
+
+ int zlibStatusCode;
+ string zlibMsg;
+
+ static string errorMessage(int statusCode, const(char)* msg) {
+ string result = "zlib error: ";
+
+ if (msg) {
+ result ~= to!string(msg);
+ } else {
+ result ~= "(no message)";
+ }
+
+ result ~= " (status code = " ~ to!string(statusCode) ~ ")";
+ return result;
+ }
+}
+
+version (unittest) {
+ import std.exception : collectException;
+ import thrift.transport.memory;
+}
+
+// Make sure basic reading/writing works.
+unittest {
+ auto buf = new TMemoryBuffer;
+ auto zlib = new TZlibTransport(buf);
+
+ immutable ubyte[] data = [1, 2, 3, 4, 5];
+ zlib.write(data);
+ zlib.finish();
+
+ auto result = new ubyte[data.length];
+ zlib.readAll(result);
+ enforce(data == result);
+ zlib.verifyChecksum();
+}
+
+// Make sure there is no data is written if write() is never called.
+unittest {
+ auto buf = new TMemoryBuffer;
+ {
+ scope zlib = new TZlibTransport(buf);
+ }
+ enforce(buf.getContents().length == 0);
+}
+
+// Make sure calling write()/flush()/finish() again after finish() throws.
+unittest {
+ auto buf = new TMemoryBuffer;
+ auto zlib = new TZlibTransport(buf);
+
+ zlib.write([1, 2, 3, 4, 5]);
+ zlib.finish();
+
+ auto ex = collectException!TTransportException(zlib.write([6]));
+ enforce(ex && ex.type == TTransportException.Type.BAD_ARGS);
+
+ ex = collectException!TTransportException(zlib.flush());
+ enforce(ex && ex.type == TTransportException.Type.BAD_ARGS);
+
+ ex = collectException!TTransportException(zlib.finish());
+ enforce(ex && ex.type == TTransportException.Type.BAD_ARGS);
+}
+
+// Make sure verifying the checksum works even if it requires starting a new
+// reading buffer after reading the payload has already been completed.
+unittest {
+ auto buf = new TMemoryBuffer;
+ auto zlib = new TZlibTransport(buf);
+
+ immutable ubyte[] data = [1, 2, 3, 4, 5];
+ zlib.write(data);
+ zlib.finish();
+
+ zlib = new TZlibTransport(buf, TZlibTransport.DEFAULT_URBUF_SIZE,
+ buf.getContents().length - 1); // The last byte belongs to the checksum.
+
+ auto result = new ubyte[data.length];
+ zlib.readAll(result);
+ enforce(data == result);
+
+ zlib.verifyChecksum();
+}
+
+// Make sure verifyChecksum() throws if we messed with the checksum.
+unittest {
+ import std.stdio;
+ import thrift.transport.range;
+
+ auto buf = new TMemoryBuffer;
+ auto zlib = new TZlibTransport(buf);
+
+ immutable ubyte[] data = [1, 2, 3, 4, 5];
+ zlib.write(data);
+ zlib.finish();
+
+ void testCorrupted(const(ubyte)[] corruptedData) {
+ auto reader = new TZlibTransport(tInputRangeTransport(corruptedData));
+ auto result = new ubyte[data.length];
+ try {
+ reader.readAll(result);
+
+ // If it does read without complaining, the result should be correct.
+ enforce(result == data);
+ } catch (TZlibException e) {}
+
+ auto ex = collectException!TTransportException(reader.verifyChecksum());
+ enforce(ex && ex.type == TTransportException.Type.CORRUPTED_DATA);
+ }
+
+ testCorrupted(buf.getContents()[0 .. $ - 1]);
+
+ auto modified = buf.getContents().dup;
+ ++modified[$ - 1];
+ testCorrupted(modified);
+}
diff --git a/lib/d/src/thrift/util/awaitable.d b/lib/d/src/thrift/util/awaitable.d
new file mode 100644
index 000000000..38436ee38
--- /dev/null
+++ b/lib/d/src/thrift/util/awaitable.d
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.util.awaitable;
+
+import core.sync.condition;
+import core.sync.mutex;
+import core.time : Duration;
+import std.exception : enforce;
+import std.socket/+ : Socket, socketPair+/; // DMD @@BUG314@@
+import thrift.base;
+
+// To avoid DMD @@BUG6395@@.
+import thrift.internal.algorithm;
+
+/**
+ * An event that can occur at some point in the future and which can be
+ * awaited, either by blocking until it occurs, or by registering a callback
+ * delegate.
+ */
+interface TAwaitable {
+ /**
+ * Waits until the event occurs.
+ *
+ * Calling wait() for an event that has already occurred is a no-op.
+ */
+ void wait();
+
+ /**
+ * Waits until the event occurs or the specified timeout expires.
+ *
+ * Calling wait() for an event that has already occurred is a no-op.
+ *
+ * Returns: Whether the event was triggered before the timeout expired.
+ */
+ bool wait(Duration timeout);
+
+ /**
+ * Registers a callback that is called if the event occurs.
+ *
+ * The delegate will likely be invoked from a different thread, and is
+ * expected not to perform expensive work as it will usually be invoked
+ * synchronously by the notifying thread. The order in which registered
+ * callbacks are invoked is not specified.
+ *
+ * The callback must never throw, but nothrow semantics are difficult to
+ * enforce, so currently exceptions are just swallowed by
+ * TAwaitable implementations.
+ *
+ * If the event has already occurred, the delegate is immediately executed
+ * in the current thread.
+ */
+ void addCallback(void delegate() dg);
+
+ /**
+ * Removes a previously added callback.
+ *
+ * Returns: Whether the callback could be found in the list, i.e. whether it
+ * was previously added.
+ */
+ bool removeCallback(void delegate() dg);
+}
+
+/**
+ * A simple TAwaitable event triggered by just calling a trigger() method.
+ */
+class TOneshotEvent : TAwaitable {
+ this() {
+ mutex_ = new Mutex;
+ condition_ = new Condition(mutex_);
+ }
+
+ override void wait() {
+ synchronized (mutex_) {
+ while (!triggered_) condition_.wait();
+ }
+ }
+
+ override bool wait(Duration timeout) {
+ synchronized (mutex_) {
+ if (triggered_) return true;
+ condition_.wait(timeout);
+ return triggered_;
+ }
+ }
+
+ override void addCallback(void delegate() dg) {
+ mutex_.lock();
+ scope (failure) mutex_.unlock();
+
+ callbacks_ ~= dg;
+
+ if (triggered_) {
+ mutex_.unlock();
+ dg();
+ return;
+ }
+
+ mutex_.unlock();
+ }
+
+ override bool removeCallback(void delegate() dg) {
+ synchronized (mutex_) {
+ auto oldLength = callbacks_.length;
+ callbacks_ = removeEqual(callbacks_, dg);
+ return callbacks_.length < oldLength;
+ }
+ }
+
+ /**
+ * Triggers the event.
+ *
+ * Any registered event callbacks are executed synchronously before the
+ * function returns.
+ */
+ void trigger() {
+ synchronized (mutex_) {
+ if (!triggered_) {
+ triggered_ = true;
+ condition_.notifyAll();
+ foreach (c; callbacks_) c();
+ }
+ }
+ }
+
+private:
+ bool triggered_;
+ Mutex mutex_;
+ Condition condition_;
+ void delegate()[] callbacks_;
+}
+
+/**
+ * Translates TAwaitable events into dummy messages on a socket that can be
+ * used e.g. to wake up from a select() call.
+ */
+final class TSocketNotifier {
+ this() {
+ auto socks = socketPair();
+ foreach (s; socks) s.blocking = false;
+ sendSocket_ = socks[0];
+ recvSocket_ = socks[1];
+ }
+
+ /**
+ * The socket the messages will be sent to.
+ */
+ Socket socket() @property {
+ return recvSocket_;
+ }
+
+ /**
+ * Atatches the socket notifier to the specified awaitable, causing it to
+ * write a byte to the notification socket when the awaitable callbacks are
+ * invoked.
+ *
+ * If the event has already been triggered, the dummy byte is written
+ * immediately to the socket.
+ *
+ * A socket notifier can only be attached to a single awaitable at a time.
+ *
+ * Throws: TException if the socket notifier is already attached.
+ */
+ void attach(TAwaitable awaitable) {
+ enforce(!awaitable_, new TException("Already attached."));
+ awaitable.addCallback(&notify);
+ awaitable_ = awaitable;
+ }
+
+ /**
+ * Detaches the socket notifier from the awaitable it is currently attached
+ * to.
+ *
+ * Throws: TException if the socket notifier is not currently attached.
+ */
+ void detach() {
+ enforce(awaitable_, new TException("Not attached."));
+
+ // Soak up any not currently read notification bytes.
+ ubyte[1] dummy = void;
+ while (recvSocket_.receive(dummy) != Socket.ERROR) {}
+
+ auto couldRemove = awaitable_.removeCallback(&notify);
+ assert(couldRemove);
+ awaitable_ = null;
+ }
+
+private:
+ void notify() {
+ ubyte[1] zero;
+ sendSocket_.send(zero);
+ }
+
+ TAwaitable awaitable_;
+ Socket sendSocket_;
+ Socket recvSocket_;
+}
diff --git a/lib/d/src/thrift/util/cancellation.d b/lib/d/src/thrift/util/cancellation.d
new file mode 100644
index 000000000..62552364d
--- /dev/null
+++ b/lib/d/src/thrift/util/cancellation.d
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.util.cancellation;
+
+import core.atomic;
+import thrift.base;
+import thrift.util.awaitable;
+
+/**
+ * A cancellation request for asynchronous or blocking synchronous operations.
+ *
+ * It is passed to the entity creating an operation, which will usually monitor
+ * it either by polling or by adding event handlers, and cancel the operation
+ * if it is triggered.
+ *
+ * For synchronous operations, this usually means either throwing a
+ * TCancelledException or immediately returning, depending on whether
+ * cancellation is an expected part of the task outcome or not. For
+ * asynchronous operations, cancellation typically entails stopping background
+ * work and cancelling a result future, if not already completed.
+ *
+ * An operation accepting a TCancellation does not need to guarantee that it
+ * will actually be able to react to the cancellation request.
+ */
+interface TCancellation {
+ /**
+ * Whether the cancellation request has been triggered.
+ */
+ bool triggered() const @property;
+
+ /**
+ * Throws a TCancelledException if the cancellation request has already been
+ * triggered.
+ */
+ void throwIfTriggered() const;
+
+ /**
+ * A TAwaitable that can be used to wait for cancellation triggering.
+ */
+ TAwaitable triggering() @property;
+}
+
+/**
+ * The origin of a cancellation request, which provides a way to actually
+ * trigger it.
+ *
+ * This design allows operations to pass the TCancellation on to sub-tasks,
+ * while making sure that the cancellation can only be triggered by the
+ * »outermost« instance waiting for the result.
+ */
+final class TCancellationOrigin : TCancellation {
+ this() {
+ event_ = new TOneshotEvent;
+ }
+
+ /**
+ * Triggers the cancellation request.
+ */
+ void trigger() {
+ atomicStore(triggered_, true);
+ event_.trigger();
+ }
+
+ /+override+/ bool triggered() const @property {
+ return atomicLoad(triggered_);
+ }
+
+ /+override+/ void throwIfTriggered() const {
+ if (triggered) throw new TCancelledException;
+ }
+
+ /+override+/ TAwaitable triggering() @property {
+ return event_;
+ }
+
+private:
+ shared bool triggered_;
+ TOneshotEvent event_;
+}
+
+///
+class TCancelledException : TException {
+ ///
+ this(string msg = null, string file = __FILE__, size_t line = __LINE__,
+ Throwable next = null
+ ) {
+ super(msg ? msg : "The operation has been cancelled.", file, line, next);
+ }
+}
diff --git a/lib/d/src/thrift/util/future.d b/lib/d/src/thrift/util/future.d
new file mode 100644
index 000000000..7c127c429
--- /dev/null
+++ b/lib/d/src/thrift/util/future.d
@@ -0,0 +1,549 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.util.future;
+
+import core.atomic;
+import core.sync.condition;
+import core.sync.mutex;
+import core.time : Duration;
+import std.array : empty, front, popFront;
+import std.conv : to;
+import std.exception : enforce;
+import std.traits : BaseTypeTuple, isSomeFunction, ParameterTypeTuple, ReturnType;
+import thrift.base;
+import thrift.util.awaitable;
+import thrift.util.cancellation;
+
+/**
+ * Represents an operation which is executed asynchronously and the result of
+ * which will become available at some point in the future.
+ *
+ * Once a operation is completed, the result of the operation can be fetched
+ * via the get() family of methods. There are three possible cases: Either the
+ * operation succeeded, then its return value is returned, or it failed by
+ * throwing, in which case the exception is rethrown, or it was cancelled
+ * before, then a TCancelledException is thrown. There might be TFuture
+ * implementations which never possibly enter the cancelled state.
+ *
+ * All methods are thread-safe, but keep in mind that any exception object or
+ * result (if it is a reference type, of course) is shared between all
+ * get()-family invocations.
+ */
+interface TFuture(ResultType) {
+ /**
+ * The status the operation is currently in.
+ *
+ * An operation starts out in RUNNING status, and changes state to one of the
+ * others at most once afterwards.
+ */
+ TFutureStatus status() @property;
+
+ /**
+ * A TAwaitable triggered when the operation leaves the RUNNING status.
+ */
+ TAwaitable completion() @property;
+
+ /**
+ * Convenience shorthand for waiting until the result is available and then
+ * get()ing it.
+ *
+ * If the operation has already completed, the result is immediately
+ * returned.
+ *
+ * The result of this method is »alias this«'d to the interface, so that
+ * TFuture can be used as a drop-in replacement for a simple value in
+ * synchronous code.
+ */
+ final ResultType waitGet() {
+ completion.wait();
+ return get();
+ }
+ final @property auto waitGetProperty() { return waitGet(); }
+ alias waitGetProperty this;
+
+ /**
+ * Convenience shorthand for waiting until the result is available and then
+ * get()ing it.
+ *
+ * If the operation completes in time, returns its result (resp. throws an
+ * exception for the failed/cancelled cases). If not, throws a
+ * TFutureException.
+ */
+ final ResultType waitGet(Duration timeout) {
+ enforce(completion.wait(timeout), new TFutureException(
+ "Operation did not complete in time."));
+ return get();
+ }
+
+ /**
+ * Returns the result of the operation.
+ *
+ * Throws: TFutureException if the operation has been cancelled,
+ * TCancelledException if it is not yet done; the set exception if it
+ * failed.
+ */
+ ResultType get();
+
+ /**
+ * Returns the captured exception if the operation failed, or null otherwise.
+ *
+ * Throws: TFutureException if not yet done, TCancelledException if the
+ * operation has been cancelled.
+ */
+ Exception getException();
+}
+
+/**
+ * The states the operation offering a future interface can be in.
+ */
+enum TFutureStatus : byte {
+ RUNNING, /// The operation is still running.
+ SUCCEEDED, /// The operation completed without throwing an exception.
+ FAILED, /// The operation completed by throwing an exception.
+ CANCELLED /// The operation was cancelled.
+}
+
+/**
+ * A TFuture covering the simple but common case where the result is simply
+ * set by a call to succeed()/fail().
+ *
+ * All methods are thread-safe, but usually, succeed()/fail() are only called
+ * from a single thread (different from the thread(s) waiting for the result
+ * using the TFuture interface, though).
+ */
+class TPromise(ResultType) : TFuture!ResultType {
+ this() {
+ statusMutex_ = new Mutex;
+ completionEvent_ = new TOneshotEvent;
+ }
+
+ override S status() const @property {
+ return atomicLoad(status_);
+ }
+
+ override TAwaitable completion() @property {
+ return completionEvent_;
+ }
+
+ override ResultType get() {
+ auto s = atomicLoad(status_);
+ enforce(s != S.RUNNING,
+ new TFutureException("Operation not yet completed."));
+
+ if (s == S.CANCELLED) throw new TCancelledException;
+ if (s == S.FAILED) throw exception_;
+
+ static if (!is(ResultType == void)) {
+ return result_;
+ }
+ }
+
+ override Exception getException() {
+ auto s = atomicLoad(status_);
+ enforce(s != S.RUNNING,
+ new TFutureException("Operation not yet completed."));
+
+ if (s == S.CANCELLED) throw new TCancelledException;
+ if (s == S.SUCCEEDED) return null;
+
+ return exception_;
+ }
+
+ static if (!is(ResultType == void)) {
+ /**
+ * Sets the result of the operation, marks it as done, and notifies any
+ * waiters.
+ *
+ * If the operation has been cancelled before, nothing happens.
+ *
+ * Throws: TFutureException if the operation is already completed.
+ */
+ void succeed(ResultType result) {
+ synchronized (statusMutex_) {
+ auto s = atomicLoad(status_);
+ if (s == S.CANCELLED) return;
+
+ enforce(s == S.RUNNING,
+ new TFutureException("Operation already completed."));
+ result_ = result;
+
+ atomicStore(status_, S.SUCCEEDED);
+ }
+
+ completionEvent_.trigger();
+ }
+ } else {
+ void succeed() {
+ synchronized (statusMutex_) {
+ auto s = atomicLoad(status_);
+ if (s == S.CANCELLED) return;
+
+ enforce(s == S.RUNNING,
+ new TFutureException("Operation already completed."));
+
+ atomicStore(status_, S.SUCCEEDED);
+ }
+
+ completionEvent_.trigger();
+ }
+ }
+
+ /**
+ * Marks the operation as failed with the specified exception and notifies
+ * any waiters.
+ *
+ * If the operation was already cancelled, nothing happens.
+ *
+ * Throws: TFutureException if the operation is already completed.
+ */
+ void fail(Exception exception) {
+ synchronized (statusMutex_) {
+ auto status = atomicLoad(status_);
+ if (status == S.CANCELLED) return;
+
+ enforce(status == S.RUNNING,
+ new TFutureException("Operation already completed."));
+ exception_ = exception;
+
+ atomicStore(status_, S.FAILED);
+ }
+
+ completionEvent_.trigger();
+ }
+
+
+ /**
+ * Marks this operation as completed and takes over the outcome of another
+ * TFuture of the same type.
+ *
+ * If this operation was already cancelled, nothing happens. If the other
+ * operation was cancelled, this operation is marked as failed with a
+ * TCancelledException.
+ *
+ * Throws: TFutureException if the passed in future was not completed or
+ * this operation is already completed.
+ */
+ void complete(TFuture!ResultType future) {
+ synchronized (statusMutex_) {
+ auto status = atomicLoad(status_);
+ if (status == S.CANCELLED) return;
+ enforce(status == S.RUNNING,
+ new TFutureException("Operation already completed."));
+
+ enforce(future.status != S.RUNNING, new TFutureException(
+ "The passed TFuture is not yet completed."));
+
+ status = future.status;
+ if (status == S.CANCELLED) {
+ status = S.FAILED;
+ exception_ = new TCancelledException;
+ } else if (status == S.FAILED) {
+ exception_ = future.getException();
+ } else static if (!is(ResultType == void)) {
+ result_ = future.get();
+ }
+
+ atomicStore(status_, status);
+ }
+
+ completionEvent_.trigger();
+ }
+
+ /**
+ * Marks this operation as cancelled and notifies any waiters.
+ *
+ * If the operation is already completed, nothing happens.
+ */
+ void cancel() {
+ synchronized (statusMutex_) {
+ auto status = atomicLoad(status_);
+ if (status == S.RUNNING) atomicStore(status_, S.CANCELLED);
+ }
+
+ completionEvent_.trigger();
+ }
+
+private:
+ // Convenience alias because TFutureStatus is ubiquitous in this class.
+ alias TFutureStatus S;
+
+ // The status the promise is currently in.
+ shared S status_;
+
+ union {
+ static if (!is(ResultType == void)) {
+ // Set if status_ is SUCCEEDED.
+ ResultType result_;
+ }
+ // Set if status_ is FAILED.
+ Exception exception_;
+ }
+
+ // Protects status_.
+ // As for result_ and exception_: They are only set once, while status_ is
+ // still RUNNING, so given that the operation has already completed, reading
+ // them is safe without holding some kind of lock.
+ Mutex statusMutex_;
+
+ // Triggered when the event completes.
+ TOneshotEvent completionEvent_;
+}
+
+///
+class TFutureException : TException {
+ ///
+ this(string msg = "", string file = __FILE__, size_t line = __LINE__,
+ Throwable next = null)
+ {
+ super(msg, file, line, next);
+ }
+}
+
+/**
+ * Creates an interface that is similiar to a given one, but accepts an
+ * additional, optional TCancellation parameter each method, and returns
+ * TFutures instead of plain return values.
+ *
+ * For example, given the following declarations:
+ * ---
+ * interface Foo {
+ * void bar();
+ * string baz(int a);
+ * }
+ * alias TFutureInterface!Foo FutureFoo;
+ * ---
+ *
+ * FutureFoo would be equivalent to:
+ * ---
+ * interface FutureFoo {
+ * TFuture!void bar(TCancellation cancellation = null);
+ * TFuture!string baz(int a, TCancellation cancellation = null);
+ * }
+ * ---
+ */
+template TFutureInterface(Interface) if (is(Interface _ == interface)) {
+ mixin({
+ string code = "interface TFutureInterface \n";
+
+ static if (is(Interface Bases == super) && Bases.length > 0) {
+ code ~= ": ";
+ foreach (i; 0 .. Bases.length) {
+ if (i > 0) code ~= ", ";
+ code ~= "TFutureInterface!(BaseTypeTuple!Interface[" ~ to!string(i) ~ "]) ";
+ }
+ }
+
+ code ~= "{\n";
+
+ foreach (methodName; __traits(derivedMembers, Interface)) {
+ enum qn = "Interface." ~ methodName;
+ static if (isSomeFunction!(mixin(qn))) {
+ code ~= "TFuture!(ReturnType!(" ~ qn ~ ")) " ~ methodName ~
+ "(ParameterTypeTuple!(" ~ qn ~ "), TCancellation cancellation = null);\n";
+ }
+ }
+
+ code ~= "}\n";
+ return code;
+ }());
+}
+
+/**
+ * An input range that aggregates results from multiple asynchronous operations,
+ * returning them in the order they arrive.
+ *
+ * Additionally, a timeout can be set after which results from not yet finished
+ * futures will no longer be waited for, e.g. to ensure the time it takes to
+ * iterate over a set of results is limited.
+ */
+final class TFutureAggregatorRange(T) {
+ /**
+ * Constructs a new instance.
+ *
+ * Params:
+ * futures = The set of futures to collect results from.
+ * timeout = If positive, not yet finished futures will be cancelled and
+ * their results will not be taken into account.
+ */
+ this(TFuture!T[] futures, TCancellationOrigin childCancellation,
+ Duration timeout = dur!"hnsecs"(0)
+ ) {
+ if (timeout > dur!"hnsecs"(0)) {
+ timeoutSysTick_ = TickDuration.currSystemTick +
+ TickDuration.from!"hnsecs"(timeout.total!"hnsecs");
+ } else {
+ timeoutSysTick_ = TickDuration(0);
+ }
+
+ queueMutex_ = new Mutex;
+ queueNonEmptyCondition_ = new Condition(queueMutex_);
+ futures_ = futures;
+ childCancellation_ = childCancellation;
+
+ foreach (future; futures_) {
+ future.completion.addCallback({
+ auto f = future;
+ return {
+ if (f.status == TFutureStatus.CANCELLED) return;
+ assert(f.status != TFutureStatus.RUNNING);
+
+ synchronized (queueMutex_) {
+ completedQueue_ ~= f;
+
+ if (completedQueue_.length == 1) {
+ queueNonEmptyCondition_.notifyAll();
+ }
+ }
+ };
+ }());
+ }
+ }
+
+ /**
+ * Whether the range is empty.
+ *
+ * This is the case if the results from the completed futures not having
+ * failed have already been popped and either all future have been finished
+ * or the timeout has expired.
+ *
+ * Potentially blocks until a new result is available or the timeout has
+ * expired.
+ */
+ bool empty() @property {
+ if (finished_) return true;
+ if (bufferFilled_) return false;
+
+ while (true) {
+ TFuture!T future;
+ synchronized (queueMutex_) {
+ // The while loop is just being cautious about spurious wakeups, in
+ // case they should be possible.
+ while (completedQueue_.empty) {
+ auto remaining = to!Duration(timeoutSysTick_ -
+ TickDuration.currSystemTick);
+
+ if (remaining <= dur!"hnsecs"(0)) {
+ // No time left, but still no element received – we are empty now.
+ finished_ = true;
+ childCancellation_.trigger();
+ return true;
+ }
+
+ queueNonEmptyCondition_.wait(remaining);
+ }
+
+ future = completedQueue_.front;
+ completedQueue_.popFront();
+ }
+
+ ++completedCount_;
+ if (completedCount_ == futures_.length) {
+ // This was the last future in the list, there is no possiblity
+ // another result could ever become available.
+ finished_ = true;
+ }
+
+ if (future.status == TFutureStatus.FAILED) {
+ // This one failed, loop again and try getting another item from
+ // the queue.
+ exceptions_ ~= future.getException();
+ } else {
+ resultBuffer_ = future.get();
+ bufferFilled_ = true;
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Returns the first element from the range.
+ *
+ * Potentially blocks until a new result is available or the timeout has
+ * expired.
+ *
+ * Throws: TException if the range is empty.
+ */
+ T front() {
+ enforce(!empty, new TException(
+ "Cannot get front of an empty future aggregator range."));
+ return resultBuffer_;
+ }
+
+ /**
+ * Removes the first element from the range.
+ *
+ * Potentially blocks until a new result is available or the timeout has
+ * expired.
+ *
+ * Throws: TException if the range is empty.
+ */
+ void popFront() {
+ enforce(!empty, new TException(
+ "Cannot pop front of an empty future aggregator range."));
+ bufferFilled_ = false;
+ }
+
+ /**
+ * The number of futures the result of which has been returned or which have
+ * failed so far.
+ */
+ size_t completedCount() @property const {
+ return completedCount_;
+ }
+
+ /**
+ * The exceptions collected from failed TFutures so far.
+ */
+ Exception[] exceptions() @property {
+ return exceptions_;
+ }
+
+private:
+ TFuture!T[] futures_;
+ TCancellationOrigin childCancellation_;
+
+ // The system tick this operation will time out, or zero if no timeout has
+ // been set.
+ TickDuration timeoutSysTick_;
+
+ bool finished_;
+
+ bool bufferFilled_;
+ T resultBuffer_;
+
+ Exception[] exceptions_;
+ size_t completedCount_;
+
+ // The queue of completed futures. This (and the associated condition) are
+ // the only parts of this class that are accessed by multiple threads.
+ TFuture!T[] completedQueue_;
+ Mutex queueMutex_;
+ Condition queueNonEmptyCondition_;
+}
+
+/**
+ * TFutureAggregatorRange construction helper to avoid having to explicitly
+ * specify the value type, i.e. to allow the constructor being called using IFTI
+ * (see $(DMDBUG 6082, D Bugzilla enhancement requet 6082)).
+ */
+TFutureAggregatorRange!T tFutureAggregatorRange(T)(TFuture!T[] futures,
+ TCancellationOrigin childCancellation, Duration timeout = dur!"hnsecs"(0)
+) {
+ return new TFutureAggregatorRange!T(futures, childCancellation, timeout);
+}
diff --git a/lib/d/src/thrift/util/hashset.d b/lib/d/src/thrift/util/hashset.d
new file mode 100644
index 000000000..127374bee
--- /dev/null
+++ b/lib/d/src/thrift/util/hashset.d
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift.util.hashset;
+
+import std.algorithm : joiner, map;
+import std.conv : to;
+import std.traits : isImplicitlyConvertible, ParameterTypeTuple;
+import std.range : ElementType, isInputRange;
+
+/**
+ * A quickly hacked together hash set implementation backed by built-in
+ * associative arrays to have something to compile Thrift's set<> to until
+ * std.container gains something suitable.
+ */
+// Note: The funky pointer casts (i.e. *(cast(immutable(E)*)&e) instead of
+// just cast(immutable(E))e) are a workaround for LDC 2 compatibilty.
+final class HashSet(E) {
+ ///
+ this() {}
+
+ ///
+ this(E[] elems...) {
+ insert(elems);
+ }
+
+ ///
+ void insert(Stuff)(Stuff stuff) if (isImplicitlyConvertible!(Stuff, E)) {
+ aa_[*(cast(immutable(E)*)&stuff)] = [];
+ }
+
+ ///
+ void insert(Stuff)(Stuff stuff) if (
+ isInputRange!Stuff && isImplicitlyConvertible!(ElementType!Stuff, E)
+ ) {
+ foreach (e; stuff) {
+ aa_[*(cast(immutable(E)*)&e)] = [];
+ }
+ }
+
+ ///
+ void opOpAssign(string op : "~", Stuff)(Stuff stuff) {
+ insert(stuff);
+ }
+
+ ///
+ void remove(E e) {
+ aa_.remove(*(cast(immutable(E)*)&e));
+ }
+ alias remove removeKey;
+
+ ///
+ void removeAll() {
+ aa_ = null;
+ }
+
+ ///
+ size_t length() @property const {
+ return aa_.length;
+ }
+
+ ///
+ size_t empty() @property const {
+ return !aa_.length;
+ }
+
+ ///
+ bool opBinaryRight(string op : "in")(E e) const {
+ return (e in aa_) !is null;
+ }
+
+ ///
+ auto opSlice() const {
+ // TODO: Implement using AA key range once availabe in release DMD/druntime
+ // to avoid allocation.
+ return cast(E[])(aa_.keys);
+ }
+
+ ///
+ override string toString() const {
+ // Only provide toString() if to!string() is available for E (exceptions are
+ // e.g. delegates).
+ static if (is(typeof(to!string(E.init)) : string)) {
+ return "{" ~ to!string(joiner(map!`to!string(a)`(aa_.keys), ", ")) ~ "}";
+ } else {
+ // Cast to work around Object not being const-correct.
+ return (cast()super).toString();
+ }
+ }
+
+ ///
+ override bool opEquals(Object other) const {
+ auto rhs = cast(const(HashSet))other;
+ if (rhs) {
+ return aa_ == rhs.aa_;
+ }
+
+ // Cast to work around Object not being const-correct.
+ return (cast()super).opEquals(other);
+ }
+
+private:
+ alias void[0] Void;
+ Void[immutable(E)] aa_;
+}
+
+/// Ditto
+auto hashSet(E)(E[] elems...) {
+ return new HashSet!E(elems);
+}
+
+unittest {
+ import std.exception;
+
+ auto a = hashSet(1, 2, 2, 3);
+ enforce(a.length == 3);
+ enforce(2 in a);
+ enforce(5 !in a);
+ enforce(a.toString().length == 9);
+ a.remove(2);
+ enforce(a.length == 2);
+ enforce(2 !in a);
+ a.removeAll();
+ enforce(a.empty);
+ enforce(a.toString() == "{}");
+
+ void delegate() dg;
+ auto b = hashSet(dg);
+ enforce(b.toString() == "thrift.util.hashset.HashSet!(void delegate()).HashSet");
+}
diff --git a/lib/d/test/Makefile.am b/lib/d/test/Makefile.am
new file mode 100644
index 000000000..ff4bd4641
--- /dev/null
+++ b/lib/d/test/Makefile.am
@@ -0,0 +1,123 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+
+# Thrift compiler rules
+
+THRIFT = $(top_builddir)/compiler/cpp/thrift
+
+debug_proto_gen = $(addprefix gen-d/, DebugProtoTest_types.d)
+
+$(debug_proto_gen): $(top_srcdir)/test/DebugProtoTest.thrift
+ $(THRIFT) --gen d -nowarn $<
+
+stress_test_gen = $(addprefix gen-d/thrift/test/stress/, Service.d \
+ StressTest_types.d)
+
+$(stress_test_gen): $(top_srcdir)/test/StressTest.thrift
+ $(THRIFT) --gen d $<
+
+thrift_test_gen = $(addprefix gen-d/thrift/test/, SecondService.d \
+ ThriftTest.d ThriftTest_constants.d ThriftTest_types.d)
+
+$(thrift_test_gen): $(top_srcdir)/test/ThriftTest.thrift
+ $(THRIFT) --gen d $<
+
+
+# The actual test targets.
+# There just must be some way to reassign a variable without warnings in
+# Automake...
+targets__ = async_test client_pool_test serialization_benchmark \
+ stress_test_server thrift_test_client thrift_test_server transport_test
+ran_tests__ = client_pool_test \
+ transport_test \
+ async_test_runner.sh \
+ thrift_test_runner.sh
+
+libevent_dependent_targets = async_test_client client_pool_test \
+ stress_test_server thrift_test_server
+libevent_dependent_ran_tests = async_test_runner.sh thrift_test_runner.sh
+
+openssl_dependent_targets = async_test thrift_test_client thrift_test_server
+openssl_dependent_ran_tests = async_test_runner.sh thrift_test_runner.sh
+
+d_test_flags =
+
+if WITH_D_EVENT_TESTS
+d_test_flags += $(DMD_LIBEVENT_FLAGS) ../$(D_EVENT_LIB_NAME)
+targets_ = $(targets__)
+ran_tests_ = $(ran_tests__)
+else
+targets_ = $(filter-out $(libevent_dependent_targets), $(targets__))
+ran_tests_ = $(filter-out $(libevent_dependent_ran_tests), $(ran_tests__))
+endif
+
+if WITH_D_SSL_TESTS
+d_test_flags += $(DMD_OPENSSL_FLAGS) ../$(D_SSL_LIB_NAME)
+targets = trusted-ca-certificate.pem server-certificate.pem $(targets_)
+ran_tests = $(ran_tests_)
+else
+targets = $(filter-out $(openssl_dependent_targets), $(targets_))
+ran_tests = $(filter-out $(openssl_dependent_ran_tests), $(ran_tests_))
+endif
+
+d_test_flags += -w -wi -O -release -inline -I$(top_srcdir)/lib/d/src -Igen-d \
+ $(top_builddir)/lib/d/$(D_LIB_NAME)
+
+
+async_test client_pool_test transport_test: %: %.d
+ $(DMD) $(d_test_flags) -of$@ $^
+
+serialization_benchmark: %: %.d $(debug_proto_gen)
+ $(DMD) $(d_test_flags) -of$@ $^
+
+stress_test_server: %: %.d test_utils.d $(stress_test_gen)
+ $(DMD) $(d_test_flags) -of$@ $^
+
+thrift_test_client: %: %.d thrift_test_common.d $(thrift_test_gen)
+ $(DMD) $(d_test_flags) -of$@ $^
+
+thrift_test_server: %: %.d thrift_test_common.d test_utils.d $(thrift_test_gen)
+ $(DMD) $(d_test_flags) -of$@ $^
+
+
+# Certificate generation targets (for the SSL tests).
+# Currently, we just assume that the "openssl" tool is on the path, could be
+# replaced by a more elaborate mechanism.
+
+server-certificate.pem: openssl.test.cnf
+ openssl req -new -x509 -nodes -config openssl.test.cnf \
+ -out server-certificate.pem
+
+trusted-ca-certificate.pem: server-certificate.pem
+ cat server-certificate.pem > $@
+
+check-local: $(targets)
+
+clean-local:
+ $(RM) -rf gen-d $(targets) $(addsuffix .o, $(targets))
+
+
+# Tests ran as part of make check.
+
+async_test_runner.sh: async_test trusted-ca-certificate.pem server-certificate.pem
+thrift_test_runner.sh: thrift_test_client thrift_test_server \
+ trusted-ca-certificate.pem server-certificate.pem
+
+TESTS = $(ran_tests)
diff --git a/lib/d/test/async_test.d b/lib/d/test/async_test.d
new file mode 100644
index 000000000..16db51b9e
--- /dev/null
+++ b/lib/d/test/async_test.d
@@ -0,0 +1,396 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless enforced by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module async_test;
+
+import core.atomic;
+import core.sync.condition : Condition;
+import core.sync.mutex : Mutex;
+import core.thread : dur, Thread, ThreadGroup;
+import std.conv : text;
+import std.datetime;
+import std.getopt;
+import std.exception : collectException, enforce;
+import std.parallelism : TaskPool;
+import std.stdio;
+import std.string;
+import std.variant : Variant;
+import thrift.base;
+import thrift.async.base;
+import thrift.async.libevent;
+import thrift.async.socket;
+import thrift.async.ssl;
+import thrift.codegen.async_client;
+import thrift.codegen.async_client_pool;
+import thrift.codegen.base;
+import thrift.codegen.processor;
+import thrift.protocol.base;
+import thrift.protocol.binary;
+import thrift.server.base;
+import thrift.server.simple;
+import thrift.server.transport.socket;
+import thrift.server.transport.ssl;
+import thrift.transport.base;
+import thrift.transport.buffered;
+import thrift.transport.ssl;
+import thrift.util.cancellation;
+
+version (Posix) {
+ import core.stdc.signal;
+ import core.sys.posix.signal;
+
+ // Disable SIGPIPE because SSL server will write to broken socket after
+ // client disconnected (see TSSLSocket docs).
+ shared static this() {
+ signal(SIGPIPE, SIG_IGN);
+ }
+}
+
+interface AsyncTest {
+ string echo(string value);
+ string delayedEcho(string value, long milliseconds);
+
+ void fail(string reason);
+ void delayedFail(string reason, long milliseconds);
+
+ enum methodMeta = [
+ TMethodMeta("fail", [], [TExceptionMeta("ate", 1, "AsyncTestException")]),
+ TMethodMeta("delayedFail", [], [TExceptionMeta("ate", 1, "AsyncTestException")])
+ ];
+ alias .AsyncTestException AsyncTestException;
+}
+
+class AsyncTestException : TException {
+ string reason;
+ mixin TStructHelpers!();
+}
+
+void main(string[] args) {
+ ushort port = 9090;
+ ushort managerCount = 2;
+ ushort serversPerManager = 5;
+ ushort threadsPerServer = 10;
+ uint iterations = 10;
+ bool ssl;
+ bool trace;
+
+ getopt(args,
+ "iterations", &iterations,
+ "managers", &managerCount,
+ "port", &port,
+ "servers-per-manager", &serversPerManager,
+ "ssl", &ssl,
+ "threads-per-server", &threadsPerServer,
+ "trace", &trace,
+ );
+
+ TTransportFactory clientTransportFactory;
+ TSSLContext serverSSLContext;
+ if (ssl) {
+ auto clientSSLContext = new TSSLContext();
+ with (clientSSLContext) {
+ ciphers = "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH";
+ authenticate = true;
+ loadTrustedCertificates("./trusted-ca-certificate.pem");
+ }
+ clientTransportFactory = new TAsyncSSLSocketFactory(clientSSLContext);
+
+ serverSSLContext = new TSSLContext();
+ with (serverSSLContext) {
+ serverSide = true;
+ loadCertificate("./server-certificate.pem");
+ loadPrivateKey("./server-private-key.pem");
+ ciphers = "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH";
+ }
+ } else {
+ clientTransportFactory = new TBufferedTransportFactory;
+ }
+
+
+ auto serverCancel = new TCancellationOrigin;
+ scope(exit) {
+ writeln("Triggering server shutdown...");
+ serverCancel.trigger();
+ writeln("done.");
+ }
+
+ auto managers = new TLibeventAsyncManager[managerCount];
+ scope (exit) foreach (ref m; managers) clear(m);
+
+ auto clientsThreads = new ThreadGroup;
+ foreach (managerIndex, ref manager; managers) {
+ manager = new TLibeventAsyncManager;
+ foreach (serverIndex; 0 .. serversPerManager) {
+ auto currentPort = cast(ushort)
+ (port + managerIndex * serversPerManager + serverIndex);
+
+ // Start the server and wait until it is up and running.
+ auto servingMutex = new Mutex;
+ auto servingCondition = new Condition(servingMutex);
+ auto handler = new PreServeNotifyHandler(servingMutex, servingCondition);
+ synchronized (servingMutex) {
+ (new ServerThread!TSimpleServer(currentPort, serverSSLContext, trace,
+ serverCancel, handler)).start();
+ servingCondition.wait();
+ }
+
+ // We only run the timing tests for the first server on each async
+ // manager, so that we don't get spurious timing errors becaue of
+ // ordering issues.
+ auto runTimingTests = (serverIndex == 0);
+
+ auto c = new ClientsThread(manager, currentPort, clientTransportFactory,
+ threadsPerServer, iterations, runTimingTests, trace);
+ clientsThreads.add(c);
+ c.start();
+ }
+ }
+ clientsThreads.joinAll();
+}
+
+class AsyncTestHandler : AsyncTest {
+ this(bool trace) {
+ trace_ = trace;
+ }
+
+ override string echo(string value) {
+ if (trace_) writefln(`echo("%s")`, value);
+ return value;
+ }
+
+ override string delayedEcho(string value, long milliseconds) {
+ if (trace_) writef(`delayedEcho("%s", %s ms)... `, value, milliseconds);
+ Thread.sleep(dur!"msecs"(milliseconds));
+ if (trace_) writeln("returning.");
+
+ return value;
+ }
+
+ override void fail(string reason) {
+ if (trace_) writefln(`fail("%s")`, reason);
+ auto ate = new AsyncTestException;
+ ate.reason = reason;
+ throw ate;
+ }
+
+ override void delayedFail(string reason, long milliseconds) {
+ if (trace_) writef(`delayedFail("%s", %s ms)... `, reason, milliseconds);
+ Thread.sleep(dur!"msecs"(milliseconds));
+ if (trace_) writeln("returning.");
+
+ auto ate = new AsyncTestException;
+ ate.reason = reason;
+ throw ate;
+ }
+
+private:
+ bool trace_;
+ AsyncTestException ate_;
+}
+
+class PreServeNotifyHandler : TServerEventHandler {
+ this(Mutex servingMutex, Condition servingCondition) {
+ servingMutex_ = servingMutex;
+ servingCondition_ = servingCondition;
+ }
+
+ void preServe() {
+ synchronized (servingMutex_) {
+ servingCondition_.notifyAll();
+ }
+ }
+ Variant createContext(TProtocol input, TProtocol output) { return Variant.init; }
+ void deleteContext(Variant serverContext, TProtocol input, TProtocol output) {}
+ void preProcess(Variant serverContext, TTransport transport) {}
+
+private:
+ Mutex servingMutex_;
+ Condition servingCondition_;
+}
+
+class ServerThread(ServerType) : Thread {
+ this(ushort port, TSSLContext sslContext, bool trace,
+ TCancellation cancellation, TServerEventHandler eventHandler
+ ) {
+ port_ = port;
+ sslContext_ = sslContext;
+ trace_ = trace;
+ cancellation_ = cancellation;
+ eventHandler_ = eventHandler;
+
+ super(&run);
+ }
+
+ void run() {
+ TServerSocket serverSocket;
+ if (sslContext_) {
+ serverSocket = new TSSLServerSocket(port_, sslContext_);
+ } else {
+ serverSocket = new TServerSocket(port_);
+ }
+ auto transportFactory = new TBufferedTransportFactory;
+ auto protocolFactory = new TBinaryProtocolFactory!();
+ auto processor = new TServiceProcessor!AsyncTest(new AsyncTestHandler(trace_));
+
+ auto server = new ServerType(processor, serverSocket, transportFactory,
+ protocolFactory);
+ server.eventHandler = eventHandler_;
+
+ writefln("Starting server on port %s...", port_);
+ server.serve(cancellation_);
+ writefln("Server thread on port %s done.", port_);
+ }
+
+private:
+ ushort port_;
+ bool trace_;
+ TCancellation cancellation_;
+ TSSLContext sslContext_;
+ TServerEventHandler eventHandler_;
+}
+
+class ClientsThread : Thread {
+ this(TAsyncSocketManager manager, ushort port, TTransportFactory tf,
+ ushort threads, uint iterations, bool runTimingTests, bool trace
+ ) {
+ manager_ = manager;
+ port_ = port;
+ transportFactory_ = tf;
+ threads_ = threads;
+ iterations_ = iterations;
+ runTimingTests_ = runTimingTests;
+ trace_ = trace;
+ super(&run);
+ }
+
+ void run() {
+ auto transport = new TAsyncSocket(manager_, "localhost", port_);
+
+ {
+ auto client = new TAsyncClient!AsyncTest(
+ transport,
+ transportFactory_,
+ new TBinaryProtocolFactory!()
+ );
+ transport.open();
+ auto clientThreads = new ThreadGroup;
+ foreach (clientId; 0 .. threads_) {
+ clientThreads.create({
+ auto c = clientId;
+ return {
+ foreach (i; 0 .. iterations_) {
+ immutable id = text(port_, ":", c, ":", i);
+
+ {
+ if (trace_) writefln(`Calling echo("%s")... `, id);
+ auto a = client.echo(id);
+ enforce(a == id);
+ if (trace_) writefln(`echo("%s") done.`, id);
+ }
+
+ {
+ if (trace_) writefln(`Calling fail("%s")... `, id);
+ auto a = cast(AsyncTestException)collectException(client.fail(id).waitGet());
+ enforce(a && a.reason == id);
+ if (trace_) writefln(`fail("%s") done.`, id);
+ }
+ }
+ };
+ }());
+ }
+ clientThreads.joinAll();
+ transport.close();
+ }
+
+ if (runTimingTests_) {
+ auto client = new TAsyncClient!AsyncTest(
+ transport,
+ transportFactory_,
+ new TBinaryProtocolFactory!TBufferedTransport
+ );
+
+ // Temporarily redirect error logs to stdout, as SSL errors on the server
+ // side are expected when the client terminates aburptly (as is the case
+ // in the timeout test).
+ auto oldErrorLogSink = g_errorLogSink;
+ g_errorLogSink = g_infoLogSink;
+ scope (exit) g_errorLogSink = oldErrorLogSink;
+
+ foreach (i; 0 .. iterations_) {
+ transport.open();
+
+ immutable id = text(port_, ":", i);
+
+ {
+ if (trace_) writefln(`Calling delayedEcho("%s", 100 ms)...`, id);
+ auto a = client.delayedEcho(id, 100);
+ enforce(!a.completion.wait(dur!"usecs"(1)),
+ text("wait() succeded early (", a.get(), ", ", id, ")."));
+ enforce(!a.completion.wait(dur!"usecs"(1)),
+ text("wait() succeded early (", a.get(), ", ", id, ")."));
+ enforce(a.completion.wait(dur!"msecs"(200)),
+ text("wait() didn't succeed as expected (", id, ")."));
+ enforce(a.get() == id);
+ if (trace_) writefln(`... delayedEcho("%s") done.`, id);
+ }
+
+ {
+ if (trace_) writefln(`Calling delayedFail("%s", 100 ms)... `, id);
+ auto a = client.delayedFail(id, 100);
+ enforce(!a.completion.wait(dur!"usecs"(1)),
+ text("wait() succeded early (", id, ", ", collectException(a.get()), ")."));
+ enforce(!a.completion.wait(dur!"usecs"(1)),
+ text("wait() succeded early (", id, ", ", collectException(a.get()), ")."));
+ enforce(a.completion.wait(dur!"msecs"(200)),
+ text("wait() didn't succeed as expected (", id, ")."));
+ auto e = cast(AsyncTestException)collectException(a.get());
+ enforce(e && e.reason == id);
+ if (trace_) writefln(`... delayedFail("%s") done.`, id);
+ }
+
+ {
+ transport.recvTimeout = dur!"msecs"(50);
+
+ if (trace_) write(`Calling delayedEcho("socketTimeout", 100 ms)... `);
+ auto a = client.delayedEcho("socketTimeout", 100);
+ auto e = cast(TTransportException)collectException(a.waitGet());
+ enforce(e, text("Operation didn't fail as expected (", id, ")."));
+ enforce(e.type == TTransportException.Type.TIMED_OUT,
+ text("Wrong timeout exception type (", id, "): ", e));
+ if (trace_) writeln(`timed out as expected.`);
+
+ // Wait until the server thread reset before the next iteration.
+ Thread.sleep(dur!"msecs"(50));
+ transport.recvTimeout = dur!"hnsecs"(0);
+ }
+
+ transport.close();
+ }
+ }
+
+ writefln("Clients thread for port %s done.", port_);
+ }
+
+ TAsyncSocketManager manager_;
+ ushort port_;
+ TTransportFactory transportFactory_;
+ ushort threads_;
+ uint iterations_;
+ bool runTimingTests_;
+ bool trace_;
+}
diff --git a/lib/d/test/async_test_runner.sh b/lib/d/test/async_test_runner.sh
new file mode 100644
index 000000000..4b9b7c0a4
--- /dev/null
+++ b/lib/d/test/async_test_runner.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+# Runs the async test in both SSL and non-SSL mode.
+./async_test > /dev/null || exit 1
+echo "Non-SSL tests done."
+./async_test --ssl > /dev/null || exit 1
+echo "SSL tests done."
diff --git a/lib/d/test/client_pool_test.d b/lib/d/test/client_pool_test.d
new file mode 100644
index 000000000..85bcb291d
--- /dev/null
+++ b/lib/d/test/client_pool_test.d
@@ -0,0 +1,416 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module client_pool_test;
+
+import core.time : Duration, dur;
+import core.thread : Thread;
+import std.algorithm;
+import std.array;
+import std.conv;
+import std.exception;
+import std.getopt;
+import std.range;
+import std.stdio;
+import std.typecons;
+import thrift.base;
+import thrift.async.libevent;
+import thrift.async.socket;
+import thrift.codegen.base;
+import thrift.codegen.async_client;
+import thrift.codegen.async_client_pool;
+import thrift.codegen.client;
+import thrift.codegen.client_pool;
+import thrift.codegen.processor;
+import thrift.protocol.binary;
+import thrift.server.simple;
+import thrift.server.transport.socket;
+import thrift.transport.buffered;
+import thrift.transport.socket;
+import thrift.util.cancellation;
+import thrift.util.future;
+
+// We use this as our RPC-layer exception here to make sure socket/… problems
+// (that would usually considered to be RPC layer faults) cause the tests to
+// fail, even though we are testing the RPC exception handling.
+class TestServiceException : TException {
+ int port;
+}
+
+interface TestService {
+ int getPort();
+ alias .TestServiceException TestServiceException;
+ enum methodMeta = [TMethodMeta("getPort", [],
+ [TExceptionMeta("a", 1, "TestServiceException")])];
+}
+
+// Use some derived service, just to check that the pools handle inheritance
+// correctly.
+interface ExTestService : TestService {
+ int[] getPortInArray();
+ enum methodMeta = [TMethodMeta("getPortInArray", [],
+ [TExceptionMeta("a", 1, "TestServiceException")])];
+}
+
+class ExTestHandler : ExTestService {
+ this(ushort port, Duration delay, bool failing, bool trace) {
+ this.port = port;
+ this.delay = delay;
+ this.failing = failing;
+ this.trace = trace;
+ }
+
+ override int getPort() {
+ if (trace) {
+ stderr.writefln("getPort() called on %s (delay: %s, failing: %s)", port,
+ delay, failing);
+ }
+ sleep();
+ failIfEnabled();
+ return port;
+ }
+
+ override int[] getPortInArray() {
+ return [getPort()];
+ }
+
+ ushort port;
+ Duration delay;
+ bool failing;
+ bool trace;
+
+private:
+ void sleep() {
+ if (delay > dur!"hnsecs"(0)) Thread.sleep(delay);
+ }
+
+ void failIfEnabled() {
+ if (!failing) return;
+
+ auto e = new TestServiceException;
+ e.port = port;
+ throw e;
+ }
+}
+
+class ServerThread : Thread {
+ this(ExTestHandler handler, TCancellation cancellation) {
+ super(&run);
+ handler_ = handler;
+ cancellation_ = cancellation;
+ }
+private:
+ void run() {
+ try {
+ auto protocolFactory = new TBinaryProtocolFactory!();
+ auto processor = new TServiceProcessor!ExTestService(handler_);
+ auto serverTransport = new TServerSocket(handler_.port);
+ serverTransport.recvTimeout = dur!"seconds"(3);
+ auto transportFactory = new TBufferedTransportFactory;
+
+ auto server = new TSimpleServer(
+ processor, serverTransport, transportFactory, protocolFactory);
+ server.serve(cancellation_);
+ } catch (Exception e) {
+ writefln("Server thread on port %s failed: %s", handler_.port, e);
+ }
+ }
+
+ TCancellation cancellation_;
+ ExTestHandler handler_;
+}
+
+void main(string[] args) {
+ bool trace;
+ ushort port = 9090;
+ getopt(args, "port", &port, "trace", &trace);
+
+ auto serverCancellation = new TCancellationOrigin;
+ scope (exit) serverCancellation.trigger();
+
+ immutable ports = cast(immutable)array(map!"cast(ushort)a"(iota(port, port + 6)));
+
+version (none) {
+ // Cannot use this due to multiple DMD @@BUG@@s:
+ // 1. »function D main is a nested function and cannot be accessed from array«
+ // when calling array() on the result of the outer map() – would have to
+ // manually do the eager evaluation/array conversion.
+ // 2. »Zip.opSlice cannot get frame pointer to map« for the delay argument,
+ // can be worked around by calling array() on the map result first.
+ // 3. Even when using the workarounds for the last two points, the DMD-built
+ // executable crashes when building without (sic!) inlining enabled,
+ // the backtrace points into the first delegate literal.
+ auto handlers = array(map!((args){
+ return new ExTestHandler(args._0, args._1, args._2, trace);
+ })(zip(
+ ports,
+ map!((a){ return dur!`msecs`(a); })([1, 10, 100, 1, 10, 100]),
+ [false, false, false, true, true, true]
+ )));
+} else {
+ auto handlers = [
+ new ExTestHandler(cast(ushort)(port + 0), dur!"msecs"(1), false, trace),
+ new ExTestHandler(cast(ushort)(port + 1), dur!"msecs"(10), false, trace),
+ new ExTestHandler(cast(ushort)(port + 2), dur!"msecs"(100), false, trace),
+ new ExTestHandler(cast(ushort)(port + 3), dur!"msecs"(1), true, trace),
+ new ExTestHandler(cast(ushort)(port + 4), dur!"msecs"(10), true, trace),
+ new ExTestHandler(cast(ushort)(port + 5), dur!"msecs"(100), true, trace)
+ ];
+}
+
+ // Fire up the server threads.
+ foreach (h; handlers) (new ServerThread(h, serverCancellation)).start();
+
+ // Give the servers some time to get up. This should really be accomplished
+ // via a barrier here and in the preServe() hook.
+ Thread.sleep(dur!"msecs"(10));
+
+ syncClientPoolTest(ports, handlers);
+ asyncClientPoolTest(ports, handlers);
+ asyncFastestClientPoolTest(ports, handlers);
+ asyncAggregatorTest(ports, handlers);
+}
+
+
+void syncClientPoolTest(const(ushort)[] ports, ExTestHandler[] handlers) {
+ auto clients = array(map!((a){
+ return cast(TClientBase!ExTestService)tClient!ExTestService(
+ tBinaryProtocol(new TSocket("127.0.0.1", a))
+ );
+ })(ports));
+
+ scope(exit) foreach (c; clients) c.outputProtocol.transport.close();
+
+ // Try the case where the first client succeeds.
+ {
+ enforce(makePool(clients).getPort() == ports[0]);
+ }
+
+ // Try the case where all clients fail.
+ {
+ auto pool = makePool(clients[3 .. $]);
+ auto e = cast(TCompoundOperationException)collectException(pool.getPort());
+ enforce(e);
+ enforce(equal(map!"a.port"(cast(TestServiceException[])e.exceptions),
+ ports[3 .. $]));
+ }
+
+ // Try the case where the first clients fail, but a later one succeeds.
+ {
+ auto pool = makePool(clients[3 .. $] ~ clients[0 .. 3]);
+ enforce(pool.getPortInArray() == [ports[0]]);
+ }
+
+ // Make sure a client is properly deactivated when it has failed too often.
+ {
+ auto pool = makePool(clients);
+ pool.faultDisableCount = 1;
+ pool.faultDisableDuration = dur!"msecs"(50);
+
+ handlers[0].failing = true;
+ enforce(pool.getPort() == ports[1]);
+
+ handlers[0].failing = false;
+ enforce(pool.getPort() == ports[1]);
+
+ Thread.sleep(dur!"msecs"(50));
+ enforce(pool.getPort() == ports[0]);
+ }
+}
+
+auto makePool(TClientBase!ExTestService[] clients) {
+ auto p = tClientPool(clients);
+ p.permuteClients = false;
+ p.rpcFaultFilter = (Exception e) {
+ return (cast(TestServiceException)e !is null);
+ };
+ return p;
+}
+
+
+void asyncClientPoolTest(const(ushort)[] ports, ExTestHandler[] handlers) {
+ auto manager = new TLibeventAsyncManager;
+ scope (exit) manager.stop(dur!"hnsecs"(0));
+
+ auto clients = makeAsyncClients(manager, ports);
+ scope(exit) foreach (c; clients) c.transport.close();
+
+ // Try the case where the first client succeeds.
+ {
+ enforce(makeAsyncPool(clients).getPort() == ports[0]);
+ }
+
+ // Try the case where all clients fail.
+ {
+ auto pool = makeAsyncPool(clients[3 .. $]);
+ auto e = cast(TCompoundOperationException)collectException(pool.getPort().waitGet());
+ enforce(e);
+ enforce(equal(map!"a.port"(cast(TestServiceException[])e.exceptions),
+ ports[3 .. $]));
+ }
+
+ // Try the case where the first clients fail, but a later one succeeds.
+ {
+ auto pool = makeAsyncPool(clients[3 .. $] ~ clients[0 .. 3]);
+ enforce(pool.getPortInArray() == [ports[0]]);
+ }
+
+ // Make sure a client is properly deactivated when it has failed too often.
+ {
+ auto pool = makeAsyncPool(clients);
+ pool.faultDisableCount = 1;
+ pool.faultDisableDuration = dur!"msecs"(50);
+
+ handlers[0].failing = true;
+ enforce(pool.getPort() == ports[1]);
+
+ handlers[0].failing = false;
+ enforce(pool.getPort() == ports[1]);
+
+ Thread.sleep(dur!"msecs"(50));
+ enforce(pool.getPort() == ports[0]);
+ }
+}
+
+auto makeAsyncPool(TAsyncClientBase!ExTestService[] clients) {
+ auto p = tAsyncClientPool(clients);
+ p.permuteClients = false;
+ p.rpcFaultFilter = (Exception e) {
+ return (cast(TestServiceException)e !is null);
+ };
+ return p;
+}
+
+auto makeAsyncClients(TLibeventAsyncManager manager, in ushort[] ports) {
+ // DMD @@BUG@@ workaround: Using array on the lazyHandlers map result leads
+ // to »function D main is a nested function and cannot be accessed from array«.
+ // Thus, we manually do the array conversion.
+ auto lazyClients = map!((a){
+ return new TAsyncClient!ExTestService(
+ new TAsyncSocket(manager, "127.0.0.1", a),
+ new TBufferedTransportFactory,
+ new TBinaryProtocolFactory!(TBufferedTransport)
+ );
+ })(ports);
+ TAsyncClientBase!ExTestService[] clients;
+ foreach (c; lazyClients) clients ~= c;
+ return clients;
+}
+
+
+void asyncFastestClientPoolTest(const(ushort)[] ports, ExTestHandler[] handlers) {
+ auto manager = new TLibeventAsyncManager;
+ scope (exit) manager.stop(dur!"hnsecs"(0));
+
+ auto clients = makeAsyncClients(manager, ports);
+ scope(exit) foreach (c; clients) c.transport.close();
+
+ // Make sure the fastest client wins, even if they are called in some other
+ // order.
+ {
+ auto result = makeAsyncFastestPool(array(retro(clients))).getPort().waitGet();
+ enforce(result == ports[0]);
+ }
+
+ // Try the case where all clients fail.
+ {
+ auto pool = makeAsyncFastestPool(clients[3 .. $]);
+ auto e = cast(TCompoundOperationException)collectException(pool.getPort().waitGet());
+ enforce(e);
+ enforce(equal(map!"a.port"(cast(TestServiceException[])e.exceptions),
+ ports[3 .. $]));
+ }
+
+ // Try the case where the first clients fail, but a later one succeeds.
+ {
+ auto pool = makeAsyncFastestPool(clients[1 .. $]);
+ enforce(pool.getPortInArray() == [ports[1]]);
+ }
+}
+
+auto makeAsyncFastestPool(TAsyncClientBase!ExTestService[] clients) {
+ auto p = tAsyncFastestClientPool(clients);
+ p.rpcFaultFilter = (Exception e) {
+ return (cast(TestServiceException)e !is null);
+ };
+ return p;
+}
+
+
+void asyncAggregatorTest(const(ushort)[] ports, ExTestHandler[] handlers) {
+ auto manager = new TLibeventAsyncManager;
+ scope (exit) manager.stop(dur!"hnsecs"(0));
+
+ auto clients = makeAsyncClients(manager, ports);
+ scope(exit) foreach (c; clients) c.transport.close();
+
+ auto aggregator = tAsyncAggregator(
+ cast(TAsyncClientBase!ExTestService[])clients);
+
+ // Test aggregator range interface.
+ {
+ auto range = aggregator.getPort().range(dur!"msecs"(50));
+ enforce(equal(range, ports[0 .. 2][]));
+ enforce(equal(map!"a.port"(cast(TestServiceException[])range.exceptions),
+ ports[3 .. $ - 1]));
+ enforce(range.completedCount == 4);
+ }
+
+ // Test default accumulator for scalars.
+ {
+ auto fullResult = aggregator.getPort().accumulate();
+ enforce(fullResult.waitGet() == ports[0 .. 3]);
+
+ auto partialResult = aggregator.getPort().accumulate();
+ Thread.sleep(dur!"msecs"(20));
+ enforce(partialResult.finishGet() == ports[0 .. 2]);
+
+ }
+
+ // Test default accumulator for arrays.
+ {
+ auto fullResult = aggregator.getPortInArray().accumulate();
+ enforce(fullResult.waitGet() == ports[0 .. 3]);
+
+ auto partialResult = aggregator.getPortInArray().accumulate();
+ Thread.sleep(dur!"msecs"(20));
+ enforce(partialResult.finishGet() == ports[0 .. 2]);
+ }
+
+ // Test custom accumulator.
+ {
+ auto fullResult = aggregator.getPort().accumulate!(function(int[] results){
+ return reduce!"a + b"(results);
+ })();
+ enforce(fullResult.waitGet() == ports[0] + ports[1] + ports[2]);
+
+ auto partialResult = aggregator.getPort().accumulate!(
+ function(int[] results, Exception[] exceptions) {
+ // Return a tuple of the parameters so we can check them outside of
+ // this function (to verify the values, we need access to »ports«, but
+ // due to DMD @@BUG5710@@, we can't use a delegate literal).f
+ return tuple(results, exceptions);
+ }
+ )();
+ Thread.sleep(dur!"msecs"(20));
+ auto resultTuple = partialResult.finishGet();
+ enforce(resultTuple._0 == ports[0 .. 2]);
+ enforce(equal(map!"a.port"(cast(TestServiceException[])resultTuple._1),
+ ports[3 .. $ - 1]));
+ }
+}
diff --git a/lib/d/test/openssl.test.cnf b/lib/d/test/openssl.test.cnf
new file mode 100644
index 000000000..2ada30b7c
--- /dev/null
+++ b/lib/d/test/openssl.test.cnf
@@ -0,0 +1,14 @@
+[ req ]
+default_bits = 2048
+default_keyfile = server-private-key.pem
+distinguished_name = req_distinguished_name
+x509_extensions = v3_ca
+prompt = no
+
+[ req_distinguished_name ]
+CN = localhost
+
+[ v3_ca ]
+# Add ::1 to the list of allowed IPs so we can use ::1 to explicitly connect
+# to localhost via IPv6.
+subjectAltName = IP:::1
diff --git a/lib/d/test/serialization_benchmark.d b/lib/d/test/serialization_benchmark.d
new file mode 100644
index 000000000..35515c82a
--- /dev/null
+++ b/lib/d/test/serialization_benchmark.d
@@ -0,0 +1,70 @@
+/**
+ * An implementation of the mini serialization benchmark also available for
+ * C++ and Java.
+ *
+ * For meaningful results, you might want to make sure that
+ * the Thrift library is compiled with release build flags,
+ * e.g. by including the source files with the build instead
+ * of linking libthriftd:
+ *
+ dmd -w -O -release -inline -I../src -Igen-d -ofserialization_benchmark \
+ $(find ../src/thrift -name '*.d' -not -name index.d) \
+ gen-d/DebugProtoTest_types.d serialization_benchmark.d
+ */
+module serialization_benchmark;
+
+import std.datetime : AutoStart, StopWatch;
+import std.math : PI;
+import std.stdio;
+import thrift.protocol.binary;
+import thrift.transport.memory;
+import thrift.transport.range;
+import DebugProtoTest_types;
+
+void main() {
+ auto buf = new TMemoryBuffer;
+ enum ITERATIONS = 10_000_000;
+
+ {
+ auto ooe = OneOfEach();
+ ooe.im_true = true;
+ ooe.im_false = false;
+ ooe.a_bite = 0x7f;
+ ooe.integer16 = 27_000;
+ ooe.integer32 = 1 << 24;
+ ooe.integer64 = 6_000_000_000;
+ ooe.double_precision = PI;
+ ooe.some_characters = "JSON THIS! \"\1";
+ ooe.zomg_unicode = "\xd7\n\a\t";
+ ooe.base64 = "\1\2\3\255";
+
+ auto prot = tBinaryProtocol(buf);
+ auto sw = StopWatch(AutoStart.yes);
+ foreach (i; 0 .. ITERATIONS) {
+ buf.reset(120);
+ ooe.write(prot);
+ }
+ sw.stop();
+
+ auto msecs = sw.peek().msecs;
+ writefln("Write: %s ms (%s kHz)", msecs, ITERATIONS / msecs);
+ }
+
+ auto data = buf.getContents().dup;
+
+ {
+ auto readBuf = tInputRangeTransport(data);
+ auto prot = tBinaryProtocol(readBuf);
+ auto ooe = OneOfEach();
+
+ auto sw = StopWatch(AutoStart.yes);
+ foreach (i; 0 .. ITERATIONS) {
+ readBuf.reset(data);
+ ooe.read(prot);
+ }
+ sw.stop();
+
+ auto msecs = sw.peek().msecs;
+ writefln(" Read: %s ms (%s kHz)", msecs, ITERATIONS / msecs);
+ }
+}
diff --git a/lib/d/test/stress_test_server.d b/lib/d/test/stress_test_server.d
new file mode 100644
index 000000000..ddda098b3
--- /dev/null
+++ b/lib/d/test/stress_test_server.d
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module stress_test_server;
+
+import std.getopt;
+import std.parallelism : totalCPUs;
+import std.stdio;
+import std.typetuple;
+import thrift.codegen.processor;
+import thrift.protocol.binary;
+import thrift.server.base;
+import thrift.server.transport.socket;
+import thrift.transport.buffered;
+import thrift.transport.memory;
+import thrift.transport.socket;
+import thrift.util.hashset;
+import test_utils;
+
+import thrift.test.stress.Service;
+
+class ServiceHandler : Service {
+ void echoVoid() { return; }
+ byte echoByte(byte arg) { return arg; }
+ int echoI32(int arg) { return arg; }
+ long echoI64(long arg) { return arg; }
+ byte[] echoList(byte[] arg) { return arg; }
+ HashSet!byte echoSet(HashSet!byte arg) { return arg; }
+ byte[byte] echoMap(byte[byte] arg) { return arg; }
+
+ string echoString(string arg) {
+ if (arg != "hello") {
+ stderr.writefln(`Wrong string received: %s instead of "hello"`, arg);
+ throw new Exception("Wrong string received.");
+ }
+ return arg;
+ }
+}
+
+void main(string[] args) {
+ ushort port = 9091;
+ auto serverType = ServerType.threaded;
+ TransportType transportType;
+ size_t numIOThreads = 1;
+ size_t taskPoolSize = totalCPUs;
+
+ getopt(args, "port", &port, "server-type", &serverType,
+ "transport-type", &transportType, "task-pool-size", &taskPoolSize,
+ "num-io-threads", &numIOThreads);
+
+ alias TypeTuple!(TBufferedTransport, TMemoryBuffer) AvailableTransports;
+
+ auto processor = new TServiceProcessor!(Service,
+ staticMap!(TBinaryProtocol, AvailableTransports))(new ServiceHandler());
+ auto serverSocket = new TServerSocket(port);
+ auto transportFactory = createTransportFactory(transportType);
+ auto protocolFactory = new TBinaryProtocolFactory!AvailableTransports;
+
+ auto server = createServer(serverType, taskPoolSize, numIOThreads,
+ processor, serverSocket, transportFactory, protocolFactory);
+
+ writefln("Starting %s %s StressTest server on port %s...", transportType,
+ serverType, port);
+ server.serve();
+ writeln("done.");
+}
diff --git a/lib/d/test/test_utils.d b/lib/d/test/test_utils.d
new file mode 100644
index 000000000..174100b79
--- /dev/null
+++ b/lib/d/test/test_utils.d
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Various helpers used by more than a single test.
+ */
+module test_utils;
+
+import std.parallelism : TaskPool;
+import thrift.protocol.base;
+import thrift.protocol.processor;
+import thrift.server.base;
+import thrift.server.nonblocking;
+import thrift.server.simple;
+import thrift.server.taskpool;
+import thrift.server.threaded;
+import thrift.server.transport.socket;
+import thrift.transport.base;
+import thrift.transport.buffered;
+import thrift.transport.framed;
+import thrift.transport.http;
+
+// This is a likely victim of @@BUG4744@@ when used with command argument
+// parsing.
+enum ServerType {
+ simple,
+ nonblocking,
+ pooledNonblocking,
+ taskpool,
+ threaded
+}
+
+TServer createServer(ServerType type, size_t taskPoolSize, size_t numIOThreads,
+ TProcessor processor, TServerSocket serverTransport,
+ TTransportFactory transportFactory, TProtocolFactory protocolFactory)
+{
+ final switch (type) {
+ case ServerType.simple:
+ return new TSimpleServer(processor, serverTransport,
+ transportFactory, protocolFactory);
+ case ServerType.nonblocking:
+ auto nb = new TNonblockingServer(processor, serverTransport.port,
+ transportFactory, protocolFactory);
+ nb.numIOThreads = numIOThreads;
+ return nb;
+ case ServerType.pooledNonblocking:
+ auto nb = new TNonblockingServer(processor, serverTransport.port,
+ transportFactory, protocolFactory, new TaskPool(taskPoolSize));
+ nb.numIOThreads = numIOThreads;
+ return nb;
+ case ServerType.taskpool:
+ auto tps = new TTaskPoolServer(processor, serverTransport,
+ transportFactory, protocolFactory);
+ tps.taskPool = new TaskPool(taskPoolSize);
+ return tps;
+ case ServerType.threaded:
+ return new TThreadedServer(processor, serverTransport,
+ transportFactory, protocolFactory);
+ }
+}
+
+enum TransportType {
+ buffered,
+ framed,
+ http,
+ raw
+}
+
+TTransportFactory createTransportFactory(TransportType type) {
+ final switch (type) {
+ case TransportType.buffered:
+ return new TBufferedTransportFactory;
+ case TransportType.framed:
+ return new TFramedTransportFactory;
+ case TransportType.http:
+ return new TServerHttpTransportFactory;
+ case TransportType.raw:
+ return new TTransportFactory;
+ }
+}
diff --git a/lib/d/test/thrift_test_client.d b/lib/d/test/thrift_test_client.d
new file mode 100644
index 000000000..a258b6450
--- /dev/null
+++ b/lib/d/test/thrift_test_client.d
@@ -0,0 +1,372 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift_test_client;
+
+import std.conv;
+import std.datetime;
+import std.exception : enforce;
+import std.getopt;
+import std.stdio;
+import std.string;
+import std.traits;
+import thrift.codegen.client;
+import thrift.protocol.base;
+import thrift.protocol.binary;
+import thrift.protocol.compact;
+import thrift.protocol.json;
+import thrift.transport.base;
+import thrift.transport.buffered;
+import thrift.transport.framed;
+import thrift.transport.http;
+import thrift.transport.socket;
+import thrift.transport.ssl;
+import thrift.util.hashset;
+
+import thrift_test_common;
+import thrift.test.ThriftTest;
+import thrift.test.ThriftTest_types;
+
+enum TransportType {
+ buffered,
+ framed,
+ http,
+ raw
+}
+
+TProtocol createProtocol(T)(T trans, ProtocolType type) {
+ final switch (type) {
+ case ProtocolType.binary:
+ return tBinaryProtocol(trans);
+ case ProtocolType.compact:
+ return tCompactProtocol(trans);
+ case ProtocolType.json:
+ return tJsonProtocol(trans);
+ }
+}
+
+void main(string[] args) {
+ string host = "localhost";
+ ushort port = 9090;
+ uint numTests = 1;
+ bool ssl;
+ ProtocolType protocolType;
+ TransportType transportType;
+ bool trace;
+
+ getopt(args,
+ "numTests|n", &numTests,
+ "protocol", &protocolType,
+ "ssl", &ssl,
+ "transport", &transportType,
+ "trace", &trace,
+ "host", (string _, string value) {
+ auto parts = split(value, ":");
+ if (parts.length > 1) {
+ // IPv6 addresses can contain colons, so take the last part for the
+ // port.
+ host = join(parts[0 .. $ - 1], ":");
+ port = to!ushort(parts[$ - 1]);
+ } else {
+ host = value;
+ }
+ }
+ );
+
+ TSocket socket;
+ if (ssl) {
+ auto sslContext = new TSSLContext();
+ sslContext.ciphers = "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH";
+ sslContext.authenticate = true;
+ sslContext.loadTrustedCertificates("./trusted-ca-certificate.pem");
+ socket = new TSSLSocket(sslContext, host, port);
+ } else {
+ socket = new TSocket(host, port);
+ }
+
+ TProtocol protocol;
+ final switch (transportType) {
+ case TransportType.buffered:
+ protocol = createProtocol(new TBufferedTransport(socket), protocolType);
+ break;
+ case TransportType.framed:
+ protocol = createProtocol(new TFramedTransport(socket), protocolType);
+ break;
+ case TransportType.http:
+ protocol = createProtocol(
+ new TClientHttpTransport(socket, host, "/service"), protocolType);
+ break;
+ case TransportType.raw:
+ protocol = createProtocol(socket, protocolType);
+ break;
+ }
+
+ auto client = tClient!ThriftTest(protocol);
+
+ ulong time_min;
+ ulong time_max;
+ ulong time_tot;
+
+ StopWatch sw;
+ foreach(test; 0 .. numTests) {
+ sw.start();
+
+ protocol.transport.open();
+
+ if (trace) writefln("Test #%s, connect %s:%s", test + 1, host, port);
+
+ if (trace) write("testVoid()");
+ client.testVoid();
+ if (trace) writeln(" = void");
+
+ if (trace) write("testString(\"Test\")");
+ string s = client.testString("Test");
+ if (trace) writefln(" = \"%s\"", s);
+ enforce(s == "Test");
+
+ if (trace) write("testByte(1)");
+ byte u8 = client.testByte(1);
+ if (trace) writefln(" = %s", u8);
+ enforce(u8 == 1);
+
+ if (trace) write("testI32(-1)");
+ int i32 = client.testI32(-1);
+ if (trace) writefln(" = %s", i32);
+ enforce(i32 == -1);
+
+ if (trace) write("testI64(-34359738368)");
+ long i64 = client.testI64(-34359738368L);
+ if (trace) writefln(" = %s", i64);
+ enforce(i64 == -34359738368L);
+
+ if (trace) write("testDouble(-5.2098523)");
+ double dub = client.testDouble(-5.2098523);
+ if (trace) writefln(" = %s", dub);
+ enforce(dub == -5.2098523);
+
+ Xtruct out1;
+ out1.string_thing = "Zero";
+ out1.byte_thing = 1;
+ out1.i32_thing = -3;
+ out1.i64_thing = -5;
+ if (trace) writef("testStruct(%s)", out1);
+ auto in1 = client.testStruct(out1);
+ if (trace) writefln(" = %s", in1);
+ enforce(in1 == out1);
+
+ if (trace) write("testNest({1, {\"Zero\", 1, -3, -5}), 5}");
+ Xtruct2 out2;
+ out2.byte_thing = 1;
+ out2.struct_thing = out1;
+ out2.i32_thing = 5;
+ auto in2 = client.testNest(out2);
+ in1 = in2.struct_thing;
+ if (trace) writefln(" = {%s, {\"%s\", %s, %s, %s}, %s}", in2.byte_thing,
+ in1.string_thing, in1.byte_thing, in1.i32_thing, in1.i64_thing,
+ in2.i32_thing);
+ enforce(in2 == out2);
+
+ int[int] mapout;
+ for (int i = 0; i < 5; ++i) {
+ mapout[i] = i - 10;
+ }
+ if (trace) writef("testMap({%s})", mapout);
+ auto mapin = client.testMap(mapout);
+ if (trace) writefln(" = {%s}", mapin);
+ enforce(mapin == mapout);
+
+ auto setout = new HashSet!int;
+ for (int i = -2; i < 3; ++i) {
+ setout ~= i;
+ }
+ if (trace) writef("testSet(%s)", setout);
+ auto setin = client.testSet(setout);
+ if (trace) writefln(" = %s", setin);
+ enforce(setin == setout);
+
+ int[] listout;
+ for (int i = -2; i < 3; ++i) {
+ listout ~= i;
+ }
+ if (trace) writef("testList(%s)", listout);
+ auto listin = client.testList(listout);
+ if (trace) writefln(" = %s", listin);
+ enforce(listin == listout);
+
+ {
+ if (trace) write("testEnum(ONE)");
+ auto ret = client.testEnum(Numberz.ONE);
+ if (trace) writefln(" = %s", ret);
+ enforce(ret == Numberz.ONE);
+
+ if (trace) write("testEnum(TWO)");
+ ret = client.testEnum(Numberz.TWO);
+ if (trace) writefln(" = %s", ret);
+ enforce(ret == Numberz.TWO);
+
+ if (trace) write("testEnum(THREE)");
+ ret = client.testEnum(Numberz.THREE);
+ if (trace) writefln(" = %s", ret);
+ enforce(ret == Numberz.THREE);
+
+ if (trace) write("testEnum(FIVE)");
+ ret = client.testEnum(Numberz.FIVE);
+ if (trace) writefln(" = %s", ret);
+ enforce(ret == Numberz.FIVE);
+
+ if (trace) write("testEnum(EIGHT)");
+ ret = client.testEnum(Numberz.EIGHT);
+ if (trace) writefln(" = %s", ret);
+ enforce(ret == Numberz.EIGHT);
+ }
+
+ if (trace) write("testTypedef(309858235082523)");
+ UserId uid = client.testTypedef(309858235082523L);
+ if (trace) writefln(" = %s", uid);
+ enforce(uid == 309858235082523L);
+
+ if (trace) write("testMapMap(1)");
+ auto mm = client.testMapMap(1);
+ if (trace) writefln(" = {%s}", mm);
+ // Simply doing == doesn't seem to work for nested AAs.
+ foreach (key, value; mm) {
+ enforce(testMapMapReturn[key] == value);
+ }
+ foreach (key, value; testMapMapReturn) {
+ enforce(mm[key] == value);
+ }
+
+ Insanity insane;
+ insane.userMap[Numberz.FIVE] = 5000;
+ Xtruct truck;
+ truck.string_thing = "Truck";
+ truck.byte_thing = 8;
+ truck.i32_thing = 8;
+ truck.i64_thing = 8;
+ insane.xtructs ~= truck;
+ if (trace) write("testInsanity()");
+ auto whoa = client.testInsanity(insane);
+ if (trace) writefln(" = %s", whoa);
+
+ // Commented for now, this is cumbersome to write without opEqual getting
+ // called on AA comparison.
+ // enforce(whoa == testInsanityReturn);
+
+ {
+ try {
+ if (trace) write("client.testException(\"Xception\") =>");
+ client.testException("Xception");
+ if (trace) writeln(" void\nFAILURE");
+ throw new Exception("testException failed.");
+ } catch (Xception e) {
+ if (trace) writefln(" {%s, \"%s\"}", e.errorCode, e.message);
+ }
+
+ try {
+ if (trace) write("client.testException(\"success\") =>");
+ client.testException("success");
+ if (trace) writeln(" void");
+ } catch (Exception e) {
+ if (trace) writeln(" exception\nFAILURE");
+ throw new Exception("testException failed.");
+ }
+ }
+
+ {
+ try {
+ if (trace) write("client.testMultiException(\"Xception\", \"test 1\") =>");
+ auto result = client.testMultiException("Xception", "test 1");
+ if (trace) writeln(" result\nFAILURE");
+ throw new Exception("testMultiException failed.");
+ } catch (Xception e) {
+ if (trace) writefln(" {%s, \"%s\"}", e.errorCode, e.message);
+ }
+
+ try {
+ if (trace) write("client.testMultiException(\"Xception2\", \"test 2\") =>");
+ auto result = client.testMultiException("Xception2", "test 2");
+ if (trace) writeln(" result\nFAILURE");
+ throw new Exception("testMultiException failed.");
+ } catch (Xception2 e) {
+ if (trace) writefln(" {%s, {\"%s\"}}",
+ e.errorCode, e.struct_thing.string_thing);
+ }
+
+ try {
+ if (trace) writef("client.testMultiException(\"success\", \"test 3\") =>");
+ auto result = client.testMultiException("success", "test 3");
+ if (trace) writefln(" {{\"%s\"}}", result.string_thing);
+ } catch (Exception e) {
+ if (trace) writeln(" exception\nFAILURE");
+ throw new Exception("testMultiException failed.");
+ }
+ }
+
+ // Do not run oneway test when doing multiple iterations, as it blocks the
+ // server for three seconds.
+ if (numTests == 1) {
+ if (trace) writef("client.testOneway(3) =>");
+ auto onewayWatch = StopWatch(AutoStart.yes);
+ client.testOneway(3);
+ onewayWatch.stop();
+ if (onewayWatch.peek().msecs > 200) {
+ if (trace) {
+ writefln(" FAILURE - took %s ms", onewayWatch.peek().usecs / 1000.0);
+ }
+ throw new Exception("testOneway failed.");
+ } else {
+ if (trace) {
+ writefln(" success - took %s ms", onewayWatch.peek().usecs / 1000.0);
+ }
+ }
+
+ // Redo a simple test after the oneway to make sure we aren't "off by
+ // one", which would be the case if the server treated oneway methods
+ // like normal ones.
+ if (trace) write("re-test testI32(-1)");
+ i32 = client.testI32(-1);
+ if (trace) writefln(" = %s", i32);
+ }
+
+ // Time metering.
+ sw.stop();
+
+ immutable tot = sw.peek().usecs;
+ if (trace) writefln("Total time: %s us\n", tot);
+
+ time_tot += tot;
+ if (time_min == 0 || tot < time_min) {
+ time_min = tot;
+ }
+ if (tot > time_max) {
+ time_max = tot;
+ }
+ protocol.transport.close();
+
+ sw.reset();
+ }
+
+ writeln("All tests done.");
+
+ if (numTests > 1) {
+ auto time_avg = time_tot / numTests;
+ writefln("Min time: %s us", time_min);
+ writefln("Max time: %s us", time_max);
+ writefln("Avg time: %s us", time_avg);
+ }
+}
diff --git a/lib/d/test/thrift_test_common.d b/lib/d/test/thrift_test_common.d
new file mode 100644
index 000000000..13a568613
--- /dev/null
+++ b/lib/d/test/thrift_test_common.d
@@ -0,0 +1,92 @@
+module thrift_test_common;
+
+import std.stdio;
+import thrift.test.ThriftTest_types;
+
+enum ProtocolType {
+ binary,
+ compact,
+ json
+}
+
+void writeInsanityReturn(in Insanity[Numberz][UserId] insane) {
+ write("{");
+ foreach(key1, value1; insane) {
+ writef("%s => {", key1);
+ foreach(key2, value2; value1) {
+ writef("%s => {", key2);
+ write("{");
+ foreach(key3, value3; value2.userMap) {
+ writef("%s => %s, ", key3, value3);
+ }
+ write("}, ");
+
+ write("{");
+ foreach (x; value2.xtructs) {
+ writef("{\"%s\", %s, %s, %s}, ",
+ x.string_thing, x.byte_thing, x.i32_thing, x.i64_thing);
+ }
+ write("}");
+
+ write("}, ");
+ }
+ write("}, ");
+ }
+ write("}");
+}
+
+Insanity[Numberz][UserId] testInsanityReturn;
+int[int][int] testMapMapReturn;
+
+static this() {
+ testInsanityReturn = {
+ Insanity[Numberz][UserId] insane;
+
+ Xtruct hello;
+ hello.string_thing = "Hello2";
+ hello.byte_thing = 2;
+ hello.i32_thing = 2;
+ hello.i64_thing = 2;
+
+ Xtruct goodbye;
+ goodbye.string_thing = "Goodbye4";
+ goodbye.byte_thing = 4;
+ goodbye.i32_thing = 4;
+ goodbye.i64_thing = 4;
+
+ Insanity crazy;
+ crazy.userMap[Numberz.EIGHT] = 8;
+ crazy.xtructs ~= goodbye;
+
+ Insanity looney;
+ // The C++ TestServer also assigns these to crazy, but that is probably
+ // an oversight.
+ looney.userMap[Numberz.FIVE] = 5;
+ looney.xtructs ~= hello;
+
+ Insanity[Numberz] first_map;
+ first_map[Numberz.TWO] = crazy;
+ first_map[Numberz.THREE] = crazy;
+ insane[1] = first_map;
+
+ Insanity[Numberz] second_map;
+ second_map[Numberz.SIX] = looney;
+ insane[2] = second_map;
+ return insane;
+ }();
+
+ testMapMapReturn = {
+ int[int] pos;
+ int[int] neg;
+
+ for (int i = 1; i < 5; i++) {
+ pos[i] = i;
+ neg[-i] = -i;
+ }
+
+ int[int][int] result;
+ result[4] = pos;
+ result[-4] = neg;
+ return result;
+ }();
+}
diff --git a/lib/d/test/thrift_test_runner.sh b/lib/d/test/thrift_test_runner.sh
new file mode 100644
index 000000000..fbe75f00c
--- /dev/null
+++ b/lib/d/test/thrift_test_runner.sh
@@ -0,0 +1,65 @@
+#!/bin/bash
+# Runs the D ThriftTest client and servers for all combinations of transport,
+# protocol, SSL-mode and server type.
+# Pass -k to keep going after failed tests.
+
+protocols="binary compact json"
+transports="buffered framed http raw"
+servers="simple taskpool threaded"
+framed_only_servers="nonblocking pooledNonblocking"
+
+# Don't leave any server instances behind when interrupted (e.g. by Ctrl+C)
+# or terminated.
+trap "kill $(jobs -p) 2>/dev/null" INT TERM
+
+for protocol in $protocols; do
+ for ssl in "" " --ssl"; do
+ for transport in $transports; do
+ for server in $servers $framed_only_servers; do
+ case $framed_only_servers in
+ *$server*) if [ $transport != "framed" ] || [ $ssl != "" ]; then continue; fi;;
+ esac
+
+ args="--transport=$transport --protocol=$protocol$ssl"
+ ./thrift_test_server $args --server-type=$server > /dev/null &
+ server_pid=$!
+
+ # Give the server some time to get up and check if it runs (yes, this
+ # is a huge kludge, should add a connect timeout to test client).
+ client_rc=-1
+ sleep 0.01
+ kill -0 $server_pid 2>/dev/null
+ if [ $? -eq 0 ]; then
+ ./thrift_test_client $args --numTests=10 > /dev/null
+ client_rc=$?
+
+ # Temporarily redirect stderr to null to avoid job control messages,
+ # restore it afterwards.
+ exec 3>&2
+ exec 2>/dev/null
+ kill $server_pid
+ exec 3>&2
+ fi
+
+ # Get the server exit code (wait should immediately return).
+ wait $server_pid
+ server_rc=$?
+
+ if [ $client_rc -ne 0 -o $server_rc -eq 1 ]; then
+ echo -e "\nTests failed for: $args --server-type=$server"
+ failed="true"
+ if [ "$1" != "-k" ]; then
+ exit 1
+ fi
+ else
+ echo -n "."
+ fi
+ done
+ done
+ done
+done
+
+echo
+if [ -z "$failed" ]; then
+ echo "All tests passed."
+fi
diff --git a/lib/d/test/thrift_test_server.d b/lib/d/test/thrift_test_server.d
new file mode 100644
index 000000000..993c06385
--- /dev/null
+++ b/lib/d/test/thrift_test_server.d
@@ -0,0 +1,264 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module thrift_test_server;
+
+import core.thread : dur, Thread;
+import std.algorithm;
+import std.exception : enforce;
+import std.getopt;
+import std.parallelism : totalCPUs;
+import std.string;
+import std.stdio;
+import std.typetuple : TypeTuple, staticMap;
+import thrift.base;
+import thrift.codegen.processor;
+import thrift.protocol.base;
+import thrift.protocol.binary;
+import thrift.protocol.compact;
+import thrift.protocol.json;
+import thrift.server.base;
+import thrift.server.transport.socket;
+import thrift.server.transport.ssl;
+import thrift.transport.base;
+import thrift.transport.buffered;
+import thrift.transport.framed;
+import thrift.transport.http;
+import thrift.transport.ssl;
+import thrift.util.hashset;
+import test_utils;
+
+import thrift_test_common;
+import thrift.test.ThriftTest_types;
+import thrift.test.ThriftTest;
+
+class TestHandler : ThriftTest {
+ this(bool trace) {
+ trace_ = trace;
+ }
+
+ override void testVoid() {
+ if (trace_) writeln("testVoid()");
+ }
+
+ override string testString(string thing) {
+ if (trace_) writefln("testString(\"%s\")", thing);
+ return thing;
+ }
+
+ override byte testByte(byte thing) {
+ if (trace_) writefln("testByte(%s)", thing);
+ return thing;
+ }
+
+ override int testI32(int thing) {
+ if (trace_) writefln("testI32(%s)", thing);
+ return thing;
+ }
+
+ override long testI64(long thing) {
+ if (trace_) writefln("testI64(%s)", thing);
+ return thing;
+ }
+
+ override double testDouble(double thing) {
+ if (trace_) writefln("testDouble(%s)", thing);
+ return thing;
+ }
+
+ override Xtruct testStruct(ref const(Xtruct) thing) {
+ if (trace_) writefln("testStruct({\"%s\", %s, %s, %s})",
+ thing.string_thing, thing.byte_thing, thing.i32_thing, thing.i64_thing);
+ return thing;
+ }
+
+ override Xtruct2 testNest(ref const(Xtruct2) nest) {
+ auto thing = nest.struct_thing;
+ if (trace_) writefln("testNest({%s, {\"%s\", %s, %s, %s}, %s})",
+ nest.byte_thing, thing.string_thing, thing.byte_thing, thing.i32_thing,
+ thing.i64_thing, nest.i32_thing);
+ return nest;
+ }
+
+ override int[int] testMap(int[int] thing) {
+ if (trace_) writefln("testMap({%s})", thing);
+ return thing;
+ }
+
+ override HashSet!int testSet(HashSet!int thing) {
+ if (trace_) writefln("testSet({%s})",
+ join(map!`to!string(a)`(thing[]), ", "));
+ return thing;
+ }
+
+ override int[] testList(int[] thing) {
+ if (trace_) writefln("testList(%s)", thing);
+ return thing;
+ }
+
+ override Numberz testEnum(Numberz thing) {
+ if (trace_) writefln("testEnum(%s)", thing);
+ return thing;
+ }
+
+ override UserId testTypedef(UserId thing) {
+ if (trace_) writefln("testTypedef(%s)", thing);
+ return thing;
+ }
+
+ override string[string] testStringMap(string[string] thing) {
+ if (trace_) writefln("testStringMap(%s)", thing);
+ return thing;
+ }
+
+ override int[int][int] testMapMap(int hello) {
+ if (trace_) writefln("testMapMap(%s)", hello);
+ return testMapMapReturn;
+ }
+
+ override Insanity[Numberz][UserId] testInsanity(ref const(Insanity) argument) {
+ if (trace_) writeln("testInsanity()");
+ return testInsanityReturn;
+ }
+
+ override Xtruct testMulti(byte arg0, int arg1, long arg2, string[short] arg3,
+ Numberz arg4, UserId arg5)
+ {
+ if (trace_) writeln("testMulti()");
+ return Xtruct("Hello2", arg0, arg1, arg2);
+ }
+
+ override void testException(string arg) {
+ if (trace_) writefln("testException(%s)", arg);
+ if (arg == "Xception") {
+ auto e = new Xception();
+ e.errorCode = 1001;
+ e.message = arg;
+ throw e;
+ } else if (arg == "ApplicationException") {
+ throw new TException();
+ }
+ }
+
+ override Xtruct testMultiException(string arg0, string arg1) {
+ if (trace_) writefln("testMultiException(%s, %s)", arg0, arg1);
+
+ if (arg0 == "Xception") {
+ auto e = new Xception();
+ e.errorCode = 1001;
+ e.message = "This is an Xception";
+ throw e;
+ } else if (arg0 == "Xception2") {
+ auto e = new Xception2();
+ e.errorCode = 2002;
+ e.struct_thing.string_thing = "This is an Xception2";
+ throw e;
+ } else {
+ return Xtruct(arg1);
+ }
+ }
+
+ override void testOneway(int sleepFor) {
+ if (trace_) writefln("testOneway(%s): Sleeping...", sleepFor);
+ Thread.sleep(dur!"seconds"(sleepFor));
+ if (trace_) writefln("testOneway(%s): done sleeping!", sleepFor);
+ }
+
+private:
+ bool trace_;
+}
+
+void main(string[] args) {
+ ushort port = 9090;
+ ServerType serverType;
+ ProtocolType protocolType;
+ size_t numIOThreads = 1;
+ TransportType transportType;
+ bool ssl;
+ bool trace;
+ size_t taskPoolSize = totalCPUs;
+
+ getopt(args, "port", &port, "protocol", &protocolType, "server-type",
+ &serverType, "ssl", &ssl, "num-io-threads", &numIOThreads,
+ "task-pool-size", &taskPoolSize, "trace", &trace,
+ "transport", &transportType);
+
+ if (serverType == ServerType.nonblocking ||
+ serverType == ServerType.pooledNonblocking
+ ) {
+ enforce(transportType == TransportType.framed,
+ "Need to use framed transport with non-blocking server.");
+ enforce(!ssl, "The non-blocking server does not support SSL yet.");
+
+ // Don't wrap the contents into another layer of framing.
+ transportType = TransportType.raw;
+ }
+
+ version (ThriftTestTemplates) {
+ // Only exercise the specialized template code paths if explicitly enabled
+ // to reduce memory consumption on regular test suite runs – there should
+ // not be much that can go wrong with that specifically anyway.
+ alias TypeTuple!(TBufferedTransport, TFramedTransport, TServerHttpTransport)
+ AvailableTransports;
+ alias TypeTuple!(
+ staticMap!(TBinaryProtocol, AvailableTransports),
+ staticMap!(TCompactProtocol, AvailableTransports)
+ ) AvailableProtocols;
+ } else {
+ alias TypeTuple!() AvailableTransports;
+ alias TypeTuple!() AvailableProtocols;
+ }
+
+ TProtocolFactory protocolFactory;
+ final switch (protocolType) {
+ case ProtocolType.binary:
+ protocolFactory = new TBinaryProtocolFactory!AvailableTransports;
+ break;
+ case ProtocolType.compact:
+ protocolFactory = new TCompactProtocolFactory!AvailableTransports;
+ break;
+ case ProtocolType.json:
+ protocolFactory = new TJsonProtocolFactory!AvailableTransports;
+ break;
+ }
+
+ auto processor = new TServiceProcessor!(ThriftTest, AvailableProtocols)(
+ new TestHandler(trace));
+
+ TServerSocket serverSocket;
+ if (ssl) {
+ auto sslContext = new TSSLContext();
+ sslContext.serverSide = true;
+ sslContext.loadCertificate("./server-certificate.pem");
+ sslContext.loadPrivateKey("./server-private-key.pem");
+ sslContext.ciphers = "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH";
+ serverSocket = new TSSLServerSocket(port, sslContext);
+ } else {
+ serverSocket = new TServerSocket(port);
+ }
+
+ auto transportFactory = createTransportFactory(transportType);
+
+ auto server = createServer(serverType, numIOThreads, taskPoolSize,
+ processor, serverSocket, transportFactory, protocolFactory);
+
+ writefln("Starting %s/%s %s ThriftTest server %son port %s...", protocolType,
+ transportType, serverType, ssl ? "(using SSL) ": "", port);
+ server.serve();
+ writeln("done.");
+}
diff --git a/lib/d/test/transport_test.d b/lib/d/test/transport_test.d
new file mode 100644
index 000000000..3f61a5d22
--- /dev/null
+++ b/lib/d/test/transport_test.d
@@ -0,0 +1,803 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Exercises various transports, combined with the buffered/framed wrappers.
+ *
+ * Originally ported from the C++ version, with Windows support code added.
+ */
+module transport_test;
+
+import core.atomic;
+import core.time : Duration;
+import core.thread : Thread;
+import std.conv : to;
+import std.datetime;
+import std.exception : enforce;
+static import std.file;
+import std.getopt;
+import std.random : rndGen, uniform, unpredictableSeed;
+import std.socket;
+import std.stdio;
+import std.string;
+import std.typetuple;
+import thrift.transport.base;
+import thrift.transport.buffered;
+import thrift.transport.framed;
+import thrift.transport.file;
+import thrift.transport.http;
+import thrift.transport.memory;
+import thrift.transport.socket;
+import thrift.transport.zlib;
+
+/*
+ * Size generation helpers – used to be able to run the same testing code
+ * with both constant and random total/chunk sizes.
+ */
+
+interface SizeGenerator {
+ size_t nextSize();
+ string toString();
+}
+
+class ConstantSizeGenerator : SizeGenerator {
+ this(size_t value) {
+ value_ = value;
+ }
+
+ override size_t nextSize() {
+ return value_;
+ }
+
+ override string toString() const {
+ return to!string(value_);
+ }
+
+private:
+ size_t value_;
+}
+
+class RandomSizeGenerator : SizeGenerator {
+ this(size_t min, size_t max) {
+ min_ = min;
+ max_ = max;
+ }
+
+ override size_t nextSize() {
+ return uniform!"[]"(min_, max_);
+ }
+
+ override string toString() const {
+ return format("rand(%s, %s)", min_, max_);
+ }
+
+ size_t min() const @property {
+ return min_;
+ }
+
+ size_t max() const @property {
+ return max_;
+ }
+
+private:
+ size_t min_;
+ size_t max_;
+}
+
+
+/*
+ * Classes to set up coupled transports
+ */
+
+/**
+ * Helper class to represent a coupled pair of transports.
+ *
+ * Data written to the output transport can be read from the input transport.
+ *
+ * This is used as the base class for the various coupled transport
+ * implementations. It shouldn't be used directly.
+ */
+class CoupledTransports(Transport) if (isTTransport!Transport) {
+ Transport input;
+ Transport output;
+}
+
+template isCoupledTransports(T) {
+ static if (is(T _ : CoupledTransports!U, U)) {
+ enum isCoupledTransports = true;
+ } else {
+ enum isCoupledTransports = false;
+ }
+}
+
+/**
+ * Helper template class for creating coupled transports that wrap
+ * another transport.
+ */
+class CoupledWrapperTransports(WrapperTransport, InnerCoupledTransports) if (
+ isTTransport!WrapperTransport && isCoupledTransports!InnerCoupledTransports
+) : CoupledTransports!WrapperTransport {
+ this() {
+ inner_ = new InnerCoupledTransports();
+ if (inner_.input) {
+ input = new WrapperTransport(inner_.input);
+ }
+ if (inner_.output) {
+ output = new WrapperTransport(inner_.output);
+ }
+ }
+
+ ~this() {
+ clear(inner_);
+ }
+
+private:
+ InnerCoupledTransports inner_;
+}
+
+import thrift.internal.codegen : PApply;
+alias PApply!(CoupledWrapperTransports, TBufferedTransport) CoupledBufferedTransports;
+alias PApply!(CoupledWrapperTransports, TFramedTransport) CoupledFramedTransports;
+alias PApply!(CoupledWrapperTransports, TZlibTransport) CoupledZlibTransports;
+
+/**
+ * Coupled TMemoryBuffers.
+ */
+class CoupledMemoryBuffers : CoupledTransports!TMemoryBuffer {
+ this() {
+ buf = new TMemoryBuffer;
+ input = buf;
+ output = buf;
+ }
+
+ TMemoryBuffer buf;
+}
+
+/**
+ * Coupled TSockets.
+ */
+class CoupledSocketTransports : CoupledTransports!TSocket {
+ this() {
+ auto sockets = socketPair();
+ input = new TSocket(sockets[0]);
+ output = new TSocket(sockets[1]);
+ }
+
+ ~this() {
+ input.close();
+ output.close();
+ }
+}
+
+/**
+ * Coupled TFileTransports
+ */
+class CoupledFileTransports : CoupledTransports!TTransport {
+ this() {
+ // We actually need the file name of the temp file here, so we can't just
+ // use the usual tempfile facilities.
+ do {
+ fileName_ = tmpDir ~ "/thrift.transport_test." ~ to!string(rndGen().front);
+ rndGen().popFront();
+ } while (std.file.exists(fileName_));
+
+ writefln("Using temp file: %s", fileName_);
+
+ auto writer = new TFileWriterTransport(fileName_);
+ writer.open();
+ output = writer;
+
+ // Wait until the file has been created.
+ writer.flush();
+
+ auto reader = new TFileReaderTransport(fileName_);
+ reader.open();
+ reader.readTimeout(dur!"msecs"(-1));
+ input = reader;
+ }
+
+ ~this() {
+ input.close();
+ output.close();
+ std.file.remove(fileName_);
+ }
+
+ static string tmpDir;
+
+private:
+ string fileName_;
+}
+
+
+/*
+ * Test functions
+ */
+
+/**
+ * Test interleaved write and read calls.
+ *
+ * Generates a buffer totalSize bytes long, then writes it to the transport,
+ * and verifies the written data can be read back correctly.
+ *
+ * Mode of operation:
+ * - call wChunkGenerator to figure out how large of a chunk to write
+ * - call wSizeGenerator to get the size for individual write() calls,
+ * and do this repeatedly until the entire chunk is written.
+ * - call rChunkGenerator to figure out how large of a chunk to read
+ * - call rSizeGenerator to get the size for individual read() calls,
+ * and do this repeatedly until the entire chunk is read.
+ * - repeat until the full buffer is written and read back,
+ * then compare the data read back against the original buffer
+ *
+ *
+ * - If any of the size generators return 0, this means to use the maximum
+ * possible size.
+ *
+ * - If maxOutstanding is non-zero, write chunk sizes will be chosen such that
+ * there are never more than maxOutstanding bytes waiting to be read back.
+ */
+void testReadWrite(CoupledTransports)(
+ size_t totalSize,
+ SizeGenerator wSizeGenerator,
+ SizeGenerator rSizeGenerator,
+ SizeGenerator wChunkGenerator,
+ SizeGenerator rChunkGenerator,
+ size_t maxOutstanding
+) if (
+ isCoupledTransports!CoupledTransports
+) {
+ scope transports = new CoupledTransports;
+ assert(transports.input);
+ assert(transports.output);
+
+ auto wbuf = new ubyte[totalSize];
+ auto rbuf = new ubyte[totalSize];
+
+ // Store some data in wbuf.
+ foreach (i, ref b; wbuf) {
+ b = i & 0xff;
+ }
+
+ size_t totalWritten;
+ size_t totalRead;
+ while (totalRead < totalSize) {
+ // Determine how large a chunk of data to write.
+ auto wChunkSize = wChunkGenerator.nextSize();
+ if (wChunkSize == 0 || wChunkSize > totalSize - totalWritten) {
+ wChunkSize = totalSize - totalWritten;
+ }
+
+ // Make sure (totalWritten - totalRead) + wChunkSize is less than
+ // maxOutstanding.
+ if (maxOutstanding > 0 &&
+ wChunkSize > maxOutstanding - (totalWritten - totalRead)) {
+ wChunkSize = maxOutstanding - (totalWritten - totalRead);
+ }
+
+ // Write the chunk.
+ size_t chunkWritten = 0;
+ while (chunkWritten < wChunkSize) {
+ auto writeSize = wSizeGenerator.nextSize();
+ if (writeSize == 0 || writeSize > wChunkSize - chunkWritten) {
+ writeSize = wChunkSize - chunkWritten;
+ }
+
+ transports.output.write(wbuf[totalWritten .. totalWritten + writeSize]);
+ chunkWritten += writeSize;
+ totalWritten += writeSize;
+ }
+
+ // Flush the data, so it will be available in the read transport
+ // Don't flush if wChunkSize is 0. (This should only happen if
+ // totalWritten == totalSize already, and we're only reading now.)
+ if (wChunkSize > 0) {
+ transports.output.flush();
+ }
+
+ // Determine how large a chunk of data to read back.
+ auto rChunkSize = rChunkGenerator.nextSize();
+ if (rChunkSize == 0 || rChunkSize > totalWritten - totalRead) {
+ rChunkSize = totalWritten - totalRead;
+ }
+
+ // Read the chunk.
+ size_t chunkRead;
+ while (chunkRead < rChunkSize) {
+ auto readSize = rSizeGenerator.nextSize();
+ if (readSize == 0 || readSize > rChunkSize - chunkRead) {
+ readSize = rChunkSize - chunkRead;
+ }
+
+ size_t bytesRead;
+ try {
+ bytesRead = transports.input.read(
+ rbuf[totalRead .. totalRead + readSize]);
+ } catch (TTransportException e) {
+ throw new Exception(format(`read(pos = %s, size = %s) threw ` ~
+ `exception "%s"; written so far: %s/%s bytes`, totalRead, readSize,
+ e.msg, totalWritten, totalSize));
+ }
+
+ enforce(bytesRead > 0, format(`read(pos = %s, size = %s) returned %s; ` ~
+ `written so far: %s/%s bytes`, totalRead, readSize, bytesRead,
+ totalWritten, totalSize));
+
+ chunkRead += bytesRead;
+ totalRead += bytesRead;
+ }
+ }
+
+ // make sure the data read back is identical to the data written
+ if (rbuf != wbuf) {
+ stderr.writefln("%s vs. %s", wbuf[$ - 4 .. $], rbuf[$ - 4 .. $]);
+ stderr.writefln("rbuf: %s vs. wbuf: %s", rbuf.length, wbuf.length);
+ }
+ enforce(rbuf == wbuf);
+}
+
+void testReadPartAvailable(CoupledTransports)() if (
+ isCoupledTransports!CoupledTransports
+) {
+ scope transports = new CoupledTransports;
+ assert(transports.input);
+ assert(transports.output);
+
+ ubyte[10] writeBuf = 'a';
+ ubyte[10] readBuf;
+
+ // Attemping to read 10 bytes when only 9 are available should return 9
+ // immediately.
+ transports.output.write(writeBuf[0 .. 9]);
+ transports.output.flush();
+
+ auto t = Trigger(dur!"seconds"(3), transports.output, 1);
+ auto bytesRead = transports.input.read(readBuf);
+ enforce(t.fired == 0);
+ enforce(bytesRead == 9);
+}
+
+void testReadPartialMidframe(CoupledTransports)() if (
+ isCoupledTransports!CoupledTransports
+) {
+ scope transports = new CoupledTransports;
+ assert(transports.input);
+ assert(transports.output);
+
+ ubyte[13] writeBuf = 'a';
+ ubyte[14] readBuf;
+
+ // Attempt to read 10 bytes, when only 9 are available, but after we have
+ // already read part of the data that is available. This exercises a
+ // different code path for several of the transports.
+ //
+ // For transports that add their own framing (e.g., TFramedTransport and
+ // TFileTransport), the two flush calls break up the data in to a 10 byte
+ // frame and a 3 byte frame. The first read then puts us partway through the
+ // first frame, and then we attempt to read past the end of that frame, and
+ // through the next frame, too.
+ //
+ // For buffered transports that perform read-ahead (e.g.,
+ // TBufferedTransport), the read-ahead will most likely see all 13 bytes
+ // written on the first read. The next read will then attempt to read past
+ // the end of the read-ahead buffer.
+ //
+ // Flush 10 bytes, then 3 bytes. This creates 2 separate frames for
+ // transports that track framing internally.
+ transports.output.write(writeBuf[0 .. 10]);
+ transports.output.flush();
+ transports.output.write(writeBuf[10 .. 13]);
+ transports.output.flush();
+
+ // Now read 4 bytes, so that we are partway through the written data.
+ auto bytesRead = transports.input.read(readBuf[0 .. 4]);
+ enforce(bytesRead == 4);
+
+ // Now attempt to read 10 bytes. Only 9 more are available.
+ //
+ // We should be able to get all 9 bytes, but it might take multiple read
+ // calls, since it is valid for read() to return fewer bytes than requested.
+ // (Most transports do immediately return 9 bytes, but the framing transports
+ // tend to only return to the end of the current frame, which is 6 bytes in
+ // this case.)
+ size_t totalRead = 0;
+ while (totalRead < 9) {
+ auto t = Trigger(dur!"seconds"(3), transports.output, 1);
+ bytesRead = transports.input.read(readBuf[4 + totalRead .. 14]);
+ enforce(t.fired == 0);
+ enforce(bytesRead > 0);
+ totalRead += bytesRead;
+ enforce(totalRead <= 9);
+ }
+
+ enforce(totalRead == 9);
+}
+
+void testBorrowPartAvailable(CoupledTransports)() if (
+ isCoupledTransports!CoupledTransports
+) {
+ scope transports = new CoupledTransports;
+ assert(transports.input);
+ assert(transports.output);
+
+ ubyte[9] writeBuf = 'a';
+ ubyte[10] readBuf;
+
+ // Attemping to borrow 10 bytes when only 9 are available should return NULL
+ // immediately.
+ transports.output.write(writeBuf);
+ transports.output.flush();
+
+ auto t = Trigger(dur!"seconds"(3), transports.output, 1);
+ auto borrowLen = readBuf.length;
+ auto borrowedBuf = transports.input.borrow(readBuf.ptr, borrowLen);
+ enforce(t.fired == 0);
+ enforce(borrowedBuf is null);
+}
+
+void testReadNoneAvailable(CoupledTransports)() if (
+ isCoupledTransports!CoupledTransports
+) {
+ scope transports = new CoupledTransports;
+ assert(transports.input);
+ assert(transports.output);
+
+ // Attempting to read when no data is available should either block until
+ // some data is available, or fail immediately. (e.g., TSocket blocks,
+ // TMemoryBuffer just fails.)
+ //
+ // If the transport blocks, it should succeed once some data is available,
+ // even if less than the amount requested becomes available.
+ ubyte[10] readBuf;
+
+ auto t = Trigger(dur!"seconds"(1), transports.output, 2);
+ t.add(dur!"seconds"(1), transports.output, 8);
+
+ auto bytesRead = transports.input.read(readBuf);
+ if (bytesRead == 0) {
+ enforce(t.fired == 0);
+ } else {
+ enforce(t.fired == 1);
+ enforce(bytesRead == 2);
+ }
+}
+
+void testBorrowNoneAvailable(CoupledTransports)() if (
+ isCoupledTransports!CoupledTransports
+) {
+ scope transports = new CoupledTransports;
+ assert(transports.input);
+ assert(transports.output);
+
+ ubyte[16] writeBuf = 'a';
+
+ // Attempting to borrow when no data is available should fail immediately
+ auto t = Trigger(dur!"seconds"(1), transports.output, 10);
+
+ auto borrowLen = 10;
+ auto borrowedBuf = transports.input.borrow(null, borrowLen);
+ enforce(borrowedBuf is null);
+ enforce(t.fired == 0);
+}
+
+
+void doRwTest(CoupledTransports)(
+ size_t totalSize,
+ SizeGenerator wSizeGen,
+ SizeGenerator rSizeGen,
+ SizeGenerator wChunkSizeGen = new ConstantSizeGenerator(0),
+ SizeGenerator rChunkSizeGen = new ConstantSizeGenerator(0),
+ size_t maxOutstanding = 0
+) if (
+ isCoupledTransports!CoupledTransports
+) {
+ totalSize = cast(size_t)(totalSize * g_sizeMultiplier);
+
+ scope(failure) {
+ writefln("Test failed for %s: testReadWrite(%s, %s, %s, %s, %s, %s)",
+ CoupledTransports.stringof, totalSize, wSizeGen, rSizeGen,
+ wChunkSizeGen, rChunkSizeGen, maxOutstanding);
+ }
+
+ testReadWrite!CoupledTransports(totalSize, wSizeGen, rSizeGen,
+ wChunkSizeGen, rChunkSizeGen, maxOutstanding);
+}
+
+void doBlockingTest(CoupledTransports)() if (
+ isCoupledTransports!CoupledTransports
+) {
+ void writeFailure(string name) {
+ writefln("Test failed for %s: %s()", CoupledTransports.stringof, name);
+ }
+
+ {
+ scope(failure) writeFailure("testReadPartAvailable");
+ testReadPartAvailable!CoupledTransports();
+ }
+
+ {
+ scope(failure) writeFailure("testReadPartialMidframe");
+ testReadPartialMidframe!CoupledTransports();
+ }
+
+ {
+ scope(failure) writeFailure("testReadNoneAvaliable");
+ testReadNoneAvailable!CoupledTransports();
+ }
+
+ {
+ scope(failure) writeFailure("testBorrowPartAvailable");
+ testBorrowPartAvailable!CoupledTransports();
+ }
+
+ {
+ scope(failure) writeFailure("testBorrowNoneAvailable");
+ testBorrowNoneAvailable!CoupledTransports();
+ }
+}
+
+SizeGenerator getGenerator(T)(T t) {
+ static if (is(T : SizeGenerator)) {
+ return t;
+ } else {
+ return new ConstantSizeGenerator(t);
+ }
+}
+
+template WrappedTransports(T) if (isCoupledTransports!T) {
+ alias TypeTuple!(
+ T,
+ CoupledBufferedTransports!T,
+ CoupledFramedTransports!T,
+ CoupledZlibTransports!T
+ ) WrappedTransports;
+}
+
+void testRw(C, R, S)(
+ size_t totalSize,
+ R wSize,
+ S rSize
+) if (
+ isCoupledTransports!C && is(typeof(getGenerator(wSize))) &&
+ is(typeof(getGenerator(rSize)))
+) {
+ testRw!C(totalSize, wSize, rSize, 0, 0, 0);
+}
+
+void testRw(C, R, S, T, U)(
+ size_t totalSize,
+ R wSize,
+ S rSize,
+ T wChunkSize,
+ U rChunkSize,
+ size_t maxOutstanding = 0
+) if (
+ isCoupledTransports!C && is(typeof(getGenerator(wSize))) &&
+ is(typeof(getGenerator(rSize))) && is(typeof(getGenerator(wChunkSize))) &&
+ is(typeof(getGenerator(rChunkSize)))
+) {
+ foreach (T; WrappedTransports!C) {
+ doRwTest!T(
+ totalSize,
+ getGenerator(wSize),
+ getGenerator(rSize),
+ getGenerator(wChunkSize),
+ getGenerator(rChunkSize),
+ maxOutstanding
+ );
+ }
+}
+
+void testBlocking(C)() if (isCoupledTransports!C) {
+ foreach (T; WrappedTransports!C) {
+ doBlockingTest!T();
+ }
+}
+
+// A quick hack, for the sake of brevity…
+float g_sizeMultiplier = 1;
+
+version (Posix) {
+ immutable defaultTempDir = "/tmp";
+} else version (Windows) {
+ import core.sys.windows.windows;
+ extern(Windows) DWORD GetTempPathA(DWORD nBufferLength, LPTSTR lpBuffer);
+
+ string defaultTempDir() @property {
+ char[MAX_PATH + 1] dir;
+ enforce(GetTempPathA(dir.length, dir.ptr));
+ return to!string(dir.ptr)[0 .. $ - 1];
+ }
+} else static assert(false);
+
+void main(string[] args) {
+ int seed = unpredictableSeed();
+ string tmpDir = defaultTempDir;
+
+ getopt(args, "seed", &seed, "size-multiplier", &g_sizeMultiplier,
+ "tmp-dir", &tmpDir);
+ enforce(g_sizeMultiplier >= 0, "Size multiplier must not be negative.");
+
+ writefln("Using seed: %s", seed);
+ rndGen().seed(seed);
+ CoupledFileTransports.tmpDir = tmpDir;
+
+ auto rand4k = new RandomSizeGenerator(1, 4096);
+
+ /*
+ * We do the basically the same set of tests for each transport type,
+ * although we tweak the parameters in some places.
+ */
+
+ // TMemoryBuffer tests
+ testRw!CoupledMemoryBuffers(1024 * 1024, 0, 0);
+ testRw!CoupledMemoryBuffers(1024 * 256, rand4k, rand4k);
+ testRw!CoupledMemoryBuffers(1024 * 256, 167, 163);
+ testRw!CoupledMemoryBuffers(1024 * 16, 1, 1);
+
+ testRw!CoupledMemoryBuffers(1024 * 256, 0, 0, rand4k, rand4k);
+ testRw!CoupledMemoryBuffers(1024 * 256, rand4k, rand4k, rand4k, rand4k);
+ testRw!CoupledMemoryBuffers(1024 * 256, 167, 163, rand4k, rand4k);
+ testRw!CoupledMemoryBuffers(1024 * 16, 1, 1, rand4k, rand4k);
+
+ testBlocking!CoupledMemoryBuffers();
+
+ // TSocket tests
+ enum socketMaxOutstanding = 4096;
+ testRw!CoupledSocketTransports(1024 * 1024, 0, 0,
+ 0, 0, socketMaxOutstanding);
+ testRw!CoupledSocketTransports(1024 * 256, rand4k, rand4k,
+ 0, 0, socketMaxOutstanding);
+ testRw!CoupledSocketTransports(1024 * 256, 167, 163,
+ 0, 0, socketMaxOutstanding);
+ // Doh. Apparently writing to a socket has some additional overhead for
+ // each send() call. If we have more than ~400 outstanding 1-byte write
+ // requests, additional send() calls start blocking.
+ testRw!CoupledSocketTransports(1024 * 16, 1, 1,
+ 0, 0, 400);
+ testRw!CoupledSocketTransports(1024 * 256, 0, 0,
+ rand4k, rand4k, socketMaxOutstanding);
+ testRw!CoupledSocketTransports(1024 * 256, rand4k, rand4k,
+ rand4k, rand4k, socketMaxOutstanding);
+ testRw!CoupledSocketTransports(1024 * 256, 167, 163,
+ rand4k, rand4k, socketMaxOutstanding);
+ testRw!CoupledSocketTransports(1024 * 16, 1, 1,
+ rand4k, rand4k, 400);
+
+ testBlocking!CoupledSocketTransports();
+
+ // File transport tests.
+
+ // Cannot write more than the frame size at once.
+ enum maxWriteAtOnce = 1024 * 1024 * 16 - 4;
+
+ testRw!CoupledFileTransports(1024 * 1024, maxWriteAtOnce, 0);
+ testRw!CoupledFileTransports(1024 * 256, rand4k, rand4k);
+ testRw!CoupledFileTransports(1024 * 256, 167, 163);
+ testRw!CoupledFileTransports(1024 * 16, 1, 1);
+
+ testRw!CoupledFileTransports(1024 * 256, 0, 0, rand4k, rand4k);
+ testRw!CoupledFileTransports(1024 * 256, rand4k, rand4k, rand4k, rand4k);
+ testRw!CoupledFileTransports(1024 * 256, 167, 163, rand4k, rand4k);
+ testRw!CoupledFileTransports(1024 * 16, 1, 1, rand4k, rand4k);
+
+ testBlocking!CoupledFileTransports();
+}
+
+
+/*
+ * Timer handling code for use in tests that check the transport blocking
+ * semantics.
+ *
+ * The implementation has been hacked together in a hurry and wastes a lot of
+ * threads, but speed should not be the concern here.
+ */
+
+struct Trigger {
+ this(Duration timeout, TTransport transport, size_t writeLength) {
+ mutex_ = new Mutex;
+ cancelCondition_ = new Condition(mutex_);
+ info_ = new Info(timeout, transport, writeLength);
+ startThread();
+ }
+
+ ~this() {
+ synchronized (mutex_) {
+ info_ = null;
+ cancelCondition_.notifyAll();
+ }
+ if (thread_) thread_.join();
+ }
+
+ @disable this(this) { assert(0); }
+
+ void add(Duration timeout, TTransport transport, size_t writeLength) {
+ synchronized (mutex_) {
+ auto info = new Info(timeout, transport, writeLength);
+ if (info_) {
+ auto prev = info_;
+ while (prev.next) prev = prev.next;
+ prev.next = info;
+ } else {
+ info_ = info;
+ startThread();
+ }
+ }
+ }
+
+ @property short fired() {
+ return atomicLoad(fired_);
+ }
+
+private:
+ void timerThread() {
+ // KLUDGE: Make sure the std.concurrency mbox is initialized on the timer
+ // thread to be able to unblock the file transport.
+ import std.concurrency;
+ thisTid;
+
+ synchronized (mutex_) {
+ while (info_) {
+ auto cancelled = cancelCondition_.wait(info_.timeout);
+ if (cancelled) {
+ info_ = null;
+ break;
+ }
+
+ atomicOp!"+="(fired_, 1);
+
+ // Write some data to the transport to unblock it.
+ auto buf = new ubyte[info_.writeLength];
+ buf[] = 'b';
+ info_.transport.write(buf);
+ info_.transport.flush();
+
+ info_ = info_.next;
+ }
+ }
+
+ thread_ = null;
+ }
+
+ void startThread() {
+ thread_ = new Thread(&timerThread);
+ thread_.start();
+ }
+
+ struct Info {
+ this(Duration timeout, TTransport transport, size_t writeLength) {
+ this.timeout = timeout;
+ this.transport = transport;
+ this.writeLength = writeLength;
+ }
+
+ Duration timeout;
+ TTransport transport;
+ size_t writeLength;
+ Info* next;
+ }
+
+ Info* info_;
+ Thread thread_;
+ shared short fired_;
+
+ import core.sync.mutex;
+ Mutex mutex_;
+ import core.sync.condition;
+ Condition cancelCondition_;
+}
diff --git a/test/StressTest.thrift b/test/StressTest.thrift
index 87c6e4761..e09a1fc88 100644
--- a/test/StressTest.thrift
+++ b/test/StressTest.thrift
@@ -18,6 +18,7 @@
*/
namespace cpp test.stress
+namespace d thrift.test.stress
service Service {
diff --git a/tutorial/d/Makefile b/tutorial/d/Makefile
new file mode 100644
index 000000000..efbb93e81
--- /dev/null
+++ b/tutorial/d/Makefile
@@ -0,0 +1,37 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+LIB_D_DIR = ../../lib/d
+
+GEN_SRC = ../gen-d/share/SharedService.d ../gen-d/share/shared_types.d \
+ ../gen-d/tutorial/tutorial_types.d ../gen-d/tutorial/Calculator.d
+
+default: server client async_client
+
+server: server.d
+ dmd -I${LIB_D_DIR}/src -L-L${LIB_D_DIR} -L-lthriftd server.d ${GEN_SRC}
+
+client: client.d
+ dmd -I${LIB_D_DIR}/src -L-L${LIB_D_DIR} -L-lthriftd client.d ${GEN_SRC}
+
+async_client: async_client.d
+ dmd -I${LIB_D_DIR}/src -L-L${LIB_D_DIR} -L-lthriftd -L-lthriftd-event -L-levent async_client.d ${GEN_SRC}
+
+clean:
+ $(RM) -f server client async_client
diff --git a/tutorial/d/async_client.d b/tutorial/d/async_client.d
new file mode 100644
index 000000000..1defce082
--- /dev/null
+++ b/tutorial/d/async_client.d
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module async_client;
+
+import std.exception;
+import std.stdio;
+import thrift.async.libevent;
+import thrift.async.socket;
+import thrift.base;
+import thrift.codegen.async_client;
+import thrift.protocol.binary;
+import thrift.transport.buffered;
+
+import tutorial.Calculator;
+import tutorial.tutorial_types;
+
+void main() {
+ auto asyncManager = new TLibeventAsyncManager;
+
+ // If we are done, gracefully stop the async manager to avoid hanging on
+ // appplication shutdown.
+ scope (exit) asyncManager.stop();
+
+ auto socket = new TAsyncSocket(asyncManager, "localhost", 9090);
+ auto client = new TAsyncClient!Calculator(
+ socket,
+ new TBufferedTransportFactory,
+ new TBinaryProtocolFactory!TBufferedTransport
+ );
+
+ socket.open();
+
+ // Invoke all the methods.
+ auto pingResult = client.ping();
+
+ auto addResult = client.add(1, 1);
+
+ auto work = Work();
+ work.op = Operation.DIVIDE;
+ work.num1 = 1;
+ work.num2 = 0;
+ auto quotientResult = client.calculate(1, work);
+
+ work.op = Operation.SUBTRACT;
+ work.num1 = 15;
+ work.num2 = 10;
+ auto diffResult = client.calculate(1, work);
+
+ auto logResult = client.getStruct(1);
+
+ // Await the responses.
+ pingResult.waitGet();
+ writeln("ping()");
+
+ int sum = addResult.waitGet();
+ writefln("1 + 1 = %s", sum);
+
+ try {
+ quotientResult.waitGet();
+ writeln("Whoa we can divide by 0");
+ } catch (InvalidOperation io) {
+ writeln("Invalid operation: " ~ io.why);
+ }
+
+ writefln("15 - 10 = %s", diffResult.waitGet());
+
+ // TFuture is implicitly convertible to the result type via »alias this«,
+ // for which it (eagerly, of course) awaits completion.
+ writefln("Check log: %s", logResult.value);
+}
diff --git a/tutorial/d/client.d b/tutorial/d/client.d
new file mode 100644
index 000000000..1867a17bc
--- /dev/null
+++ b/tutorial/d/client.d
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module client;
+
+import std.stdio;
+import thrift.base;
+import thrift.codegen.client;
+import thrift.protocol.binary;
+import thrift.transport.buffered;
+import thrift.transport.socket;
+
+import tutorial.Calculator;
+import tutorial.tutorial_types;
+
+void main() {
+ auto socket = new TSocket("localhost", 9090);
+ auto transport = new TBufferedTransport(socket);
+ auto protocol = tBinaryProtocol(transport);
+ auto client = tClient!Calculator(protocol);
+
+ transport.open();
+
+ client.ping();
+ writeln("ping()");
+
+ int sum = client.add(1, 1);
+ writefln("1 + 1 = %s", sum);
+
+ auto work = Work();
+ work.op = Operation.DIVIDE;
+ work.num1 = 1;
+ work.num2 = 0;
+ try {
+ int quotient = client.calculate(1, work);
+ writeln("Whoa we can divide by 0");
+ } catch (InvalidOperation io) {
+ writeln("Invalid operation: " ~ io.why);
+ }
+
+ work.op = Operation.SUBTRACT;
+ work.num1 = 15;
+ work.num2 = 10;
+ int diff = client.calculate(1, work);
+ writefln("15 - 10 = %s", diff);
+
+ auto log = client.getStruct(1);
+ writefln("Check log: %s", log.value);
+}
diff --git a/tutorial/d/server.d b/tutorial/d/server.d
new file mode 100644
index 000000000..d32b9374d
--- /dev/null
+++ b/tutorial/d/server.d
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+module server;
+
+import std.conv : to;
+import std.stdio;
+import thrift.codegen.processor;
+import thrift.protocol.binary;
+import thrift.server.simple;
+import thrift.server.transport.socket;
+import thrift.transport.buffered;
+
+import share.SharedService;
+import share.shared_types;
+import tutorial.Calculator;
+import tutorial.tutorial_types;
+
+/**
+ * The actual implementation of the Calculator interface that is called by
+ * the server to answer the requests.
+ */
+class CalculatorHandler : Calculator {
+ void ping() {
+ writeln("ping()");
+ }
+
+ int add(int n1, int n2) {
+ writefln("add(%s,%s)", n1, n2);
+ return n1 + n2;
+ }
+
+ int calculate(int logid, ref const(Work) work) {
+ writefln("calculate(%s, {%s, %s, %s})", logid, work.op, work.num1, work.num2);
+ int val;
+
+ switch (work.op) {
+ case Operation.ADD:
+ val = work.num1 + work.num2;
+ break;
+ case Operation.SUBTRACT:
+ val = work.num1 - work.num2;
+ break;
+ case Operation.MULTIPLY:
+ val = work.num1 * work.num2;
+ break;
+ case Operation.DIVIDE:
+ if (work.num2 == 0) {
+ auto io = new InvalidOperation();
+ io.what = work.op;
+ io.why = "Cannot divide by 0";
+ throw io;
+ }
+ val = work.num1 / work.num2;
+ break;
+ default:
+ auto io = new InvalidOperation();
+ io.what = work.op;
+ io.why = "Invalid Operation";
+ throw io;
+ }
+
+ auto ss = SharedStruct();
+ ss.key = logid;
+ ss.value = to!string(val);
+ log[logid] = ss;
+
+ return val;
+ }
+
+ SharedStruct getStruct(int logid) {
+ writefln("getStruct(%s)", logid);
+ return log[logid];
+ }
+
+ void zip() {
+ writeln("zip()");
+ }
+
+protected:
+ SharedStruct[int] log;
+}
+
+void main() {
+ auto protocolFactory = new TBinaryProtocolFactory!();
+ auto processor = new TServiceProcessor!Calculator(new CalculatorHandler);
+ auto serverTransport = new TServerSocket(9090);
+ auto transportFactory = new TBufferedTransportFactory;
+
+ auto server = new TSimpleServer(
+ processor, serverTransport, transportFactory, protocolFactory);
+
+ writeln("Starting the server on port 9090...");
+ server.serve();
+ writeln("done.");
+}
diff --git a/tutorial/shared.thrift b/tutorial/shared.thrift
index acb107b9a..db4b69443 100644
--- a/tutorial/shared.thrift
+++ b/tutorial/shared.thrift
@@ -23,6 +23,7 @@
*/
namespace cpp shared
+namespace d share // "shared" would collide with the eponymous D keyword.
namespace java shared
namespace perl shared
namespace php shared
diff --git a/tutorial/tutorial.thrift b/tutorial/tutorial.thrift
index 3ea496cab..3150151de 100644
--- a/tutorial/tutorial.thrift
+++ b/tutorial/tutorial.thrift
@@ -63,6 +63,7 @@ include "shared.thrift"
* target languages.
*/
namespace cpp tutorial
+namespace d tutorial
namespace java tutorial
namespace php tutorial
namespace perl tutorial