diff options
Diffstat (limited to 'src')
22 files changed, 711 insertions, 87 deletions
diff --git a/src/core/ext/client_config/subchannel.c b/src/core/ext/client_config/subchannel.c index 3a5af9f53d..bd45d3825c 100644 --- a/src/core/ext/client_config/subchannel.c +++ b/src/core/ext/client_config/subchannel.c @@ -268,7 +268,7 @@ static void disconnect(grpc_exec_ctx *exec_ctx, grpc_subchannel *c) { con = GET_CONNECTED_SUBCHANNEL(c, no_barrier); if (con != NULL) { GRPC_CONNECTED_SUBCHANNEL_UNREF(exec_ctx, con, "connection"); - gpr_atm_no_barrier_store(&c->connected_subchannel, 0xdeadbeef); + gpr_atm_no_barrier_store(&c->connected_subchannel, (gpr_atm)0xdeadbeef); } gpr_mu_unlock(&c->mu); } diff --git a/src/core/ext/transport/chttp2/transport/chttp2_transport.c b/src/core/ext/transport/chttp2/transport/chttp2_transport.c index fcf2abfe66..8c8593748d 100644 --- a/src/core/ext/transport/chttp2/transport/chttp2_transport.c +++ b/src/core/ext/transport/chttp2/transport/chttp2_transport.c @@ -629,9 +629,10 @@ static void finish_global_actions(grpc_exec_ctx *exec_ctx, check_read_ops(exec_ctx, &t->global); gpr_mu_lock(&t->executor.mu); - if (t->executor.pending_actions != NULL) { - hdr = t->executor.pending_actions; - t->executor.pending_actions = NULL; + if (t->executor.pending_actions_head != NULL) { + hdr = t->executor.pending_actions_head; + t->executor.pending_actions_head = t->executor.pending_actions_tail = + NULL; gpr_mu_unlock(&t->executor.mu); while (hdr != NULL) { hdr->action(exec_ctx, t, hdr->stream, hdr->arg); @@ -686,8 +687,14 @@ void grpc_chttp2_run_with_global_lock(grpc_exec_ctx *exec_ctx, gpr_free(hdr); continue; } - hdr->next = t->executor.pending_actions; - t->executor.pending_actions = hdr; + hdr->next = NULL; + if (t->executor.pending_actions_head != NULL) { + t->executor.pending_actions_tail = + t->executor.pending_actions_tail->next = hdr; + } else { + t->executor.pending_actions_tail = t->executor.pending_actions_head = + hdr; + } REF_TRANSPORT(t, "pending_action"); gpr_mu_unlock(&t->executor.mu); } diff --git a/src/core/ext/transport/chttp2/transport/internal.h b/src/core/ext/transport/chttp2/transport/internal.h index 7a8084641d..8c4fa2d34a 100644 --- a/src/core/ext/transport/chttp2/transport/internal.h +++ b/src/core/ext/transport/chttp2/transport/internal.h @@ -236,9 +236,6 @@ struct grpc_chttp2_transport_parsing { /** was a goaway frame received? */ uint8_t goaway_received; - /** the last sent max_table_size setting */ - uint32_t last_sent_max_table_size; - /** initial window change */ int64_t initial_window_update; @@ -272,6 +269,9 @@ struct grpc_chttp2_transport_parsing { uint32_t incoming_frame_size; uint32_t incoming_stream_id; + /* current max frame size */ + uint32_t max_frame_size; + /* active parser */ void *parser_data; grpc_chttp2_stream_parsing *incoming_stream; @@ -282,6 +282,8 @@ struct grpc_chttp2_transport_parsing { /* received settings */ uint32_t settings[GRPC_CHTTP2_NUM_SETTINGS]; + /* last settings that were sent */ + uint32_t last_sent_settings[GRPC_CHTTP2_NUM_SETTINGS]; /* goaway data */ grpc_status_code goaway_error; @@ -321,7 +323,8 @@ struct grpc_chttp2_transport { /** is a thread currently parsing */ bool parsing_active; - grpc_chttp2_executor_action_header *pending_actions; + grpc_chttp2_executor_action_header *pending_actions_head; + grpc_chttp2_executor_action_header *pending_actions_tail; } executor; /** is the transport destroying itself? */ diff --git a/src/core/ext/transport/chttp2/transport/parsing.c b/src/core/ext/transport/chttp2/transport/parsing.c index e827a43f7a..2995066e51 100644 --- a/src/core/ext/transport/chttp2/transport/parsing.c +++ b/src/core/ext/transport/chttp2/transport/parsing.c @@ -79,9 +79,12 @@ void grpc_chttp2_prepare_to_read( GPR_TIMER_BEGIN("grpc_chttp2_prepare_to_read", 0); transport_parsing->next_stream_id = transport_global->next_stream_id; - transport_parsing->last_sent_max_table_size = - transport_global->settings[GRPC_SENT_SETTINGS] - [GRPC_CHTTP2_SETTINGS_HEADER_TABLE_SIZE]; + memcpy(transport_parsing->last_sent_settings, + transport_global->settings[GRPC_SENT_SETTINGS], + sizeof(transport_parsing->last_sent_settings)); + transport_parsing->max_frame_size = + transport_global->settings[GRPC_ACKED_SETTINGS] + [GRPC_CHTTP2_SETTINGS_MAX_FRAME_SIZE]; /* update the parsing view of incoming window */ while (grpc_chttp2_list_pop_unannounced_incoming_window_available( @@ -388,6 +391,12 @@ int grpc_chttp2_perform_read(grpc_exec_ctx *exec_ctx, return 1; } goto dts_fh_0; /* loop */ + } else if (transport_parsing->incoming_frame_size > + transport_parsing->max_frame_size) { + gpr_log(GPR_DEBUG, "Frame size %d is larger than max frame size %d", + transport_parsing->incoming_frame_size, + transport_parsing->max_frame_size); + return 0; } if (++cur == end) { return 1; @@ -840,7 +849,11 @@ static int init_settings_frame_parser( transport_parsing->settings_ack_received = 1; grpc_chttp2_hptbl_set_max_bytes( &transport_parsing->hpack_parser.table, - transport_parsing->last_sent_max_table_size); + transport_parsing + ->last_sent_settings[GRPC_CHTTP2_SETTINGS_HEADER_TABLE_SIZE]); + transport_parsing->max_frame_size = + transport_parsing + ->last_sent_settings[GRPC_CHTTP2_SETTINGS_MAX_FRAME_SIZE]; } transport_parsing->parser = grpc_chttp2_settings_parser_parse; transport_parsing->parser_data = &transport_parsing->simple.settings; diff --git a/src/core/lib/iomgr/tcp_client_windows.c b/src/core/lib/iomgr/tcp_client_windows.c index 7d78beb15a..66f9ff7a46 100644 --- a/src/core/lib/iomgr/tcp_client_windows.c +++ b/src/core/lib/iomgr/tcp_client_windows.c @@ -63,39 +63,45 @@ typedef struct { grpc_endpoint **endpoint; } async_connect; -static void async_connect_unlock_and_cleanup(async_connect *ac) { +static void async_connect_unlock_and_cleanup(async_connect *ac, + grpc_winsocket *socket) { int done = (--ac->refs == 0); gpr_mu_unlock(&ac->mu); if (done) { - if (ac->socket != NULL) grpc_winsocket_destroy(ac->socket); gpr_mu_destroy(&ac->mu); gpr_free(ac->addr_name); gpr_free(ac); } + if (socket != NULL) grpc_winsocket_destroy(socket); } static void on_alarm(grpc_exec_ctx *exec_ctx, void *acp, bool occured) { async_connect *ac = acp; gpr_mu_lock(&ac->mu); - /* If the alarm didn't occur, it got cancelled. */ - if (ac->socket != NULL && occured) { + if (ac->socket != NULL) { grpc_winsocket_shutdown(ac->socket); } - async_connect_unlock_and_cleanup(ac); + async_connect_unlock_and_cleanup(ac, ac->socket); } static void on_connect(grpc_exec_ctx *exec_ctx, void *acp, bool from_iocp) { async_connect *ac = acp; SOCKET sock = ac->socket->socket; grpc_endpoint **ep = ac->endpoint; + GPR_ASSERT(*ep == NULL); grpc_winsocket_callback_info *info = &ac->socket->write_info; grpc_closure *on_done = ac->on_done; + gpr_mu_lock(&ac->mu); + grpc_winsocket *socket = ac->socket; + ac->socket = NULL; + gpr_mu_unlock(&ac->mu); + grpc_timer_cancel(exec_ctx, &ac->alarm); gpr_mu_lock(&ac->mu); - if (from_iocp) { + if (from_iocp && socket != NULL) { DWORD transfered_bytes = 0; DWORD flags; BOOL wsa_success = WSAGetOverlappedResult(sock, &info->overlapped, @@ -107,12 +113,12 @@ static void on_connect(grpc_exec_ctx *exec_ctx, void *acp, bool from_iocp) { ac->addr_name, utf8_message); gpr_free(utf8_message); } else { - *ep = grpc_tcp_create(ac->socket, ac->addr_name); - ac->socket = NULL; + *ep = grpc_tcp_create(socket, ac->addr_name); + socket = NULL; } } - async_connect_unlock_and_cleanup(ac); + async_connect_unlock_and_cleanup(ac, socket); /* If the connection was aborted, the callback was already called when the deadline was met. */ on_done->cb(exec_ctx, on_done->cb_arg, *ep != NULL); @@ -138,6 +144,7 @@ void grpc_tcp_client_connect(grpc_exec_ctx *exec_ctx, grpc_closure *on_done, const char *message = NULL; char *utf8_message; grpc_winsocket_callback_info *info; + int last_error; *endpoint = NULL; @@ -208,8 +215,10 @@ void grpc_tcp_client_connect(grpc_exec_ctx *exec_ctx, grpc_closure *on_done, return; failure: - utf8_message = gpr_format_message(WSAGetLastError()); + last_error = WSAGetLastError(); + utf8_message = gpr_format_message(last_error); gpr_log(GPR_ERROR, message, utf8_message); + gpr_log(GPR_ERROR, "last error = %d", last_error); gpr_free(utf8_message); if (socket != NULL) { grpc_winsocket_destroy(socket); diff --git a/src/csharp/Grpc.Core.Tests/Internal/AsyncCallTest.cs b/src/csharp/Grpc.Core.Tests/Internal/AsyncCallTest.cs index 60530d3250..a678e4dafe 100644 --- a/src/csharp/Grpc.Core.Tests/Internal/AsyncCallTest.cs +++ b/src/csharp/Grpc.Core.Tests/Internal/AsyncCallTest.cs @@ -64,28 +64,115 @@ namespace Grpc.Core.Internal.Tests } [Test] - public void AsyncUnary_CompletionSuccess() + public void AsyncUnary_CanBeStartedOnlyOnce() + { + asyncCall.UnaryCallAsync("request1"); + Assert.Throws(typeof(InvalidOperationException), + () => asyncCall.UnaryCallAsync("abc")); + } + + [Test] + public void AsyncUnary_StreamingOperationsNotAllowed() + { + asyncCall.UnaryCallAsync("request1"); + Assert.Throws(typeof(InvalidOperationException), + () => asyncCall.StartReadMessage((x,y) => {})); + Assert.Throws(typeof(InvalidOperationException), + () => asyncCall.StartSendMessage("abc", new WriteFlags(), (x,y) => {})); + } + + [Test] + public void AsyncUnary_Success() + { + var resultTask = asyncCall.UnaryCallAsync("request1"); + fakeCall.UnaryResponseClientHandler(true, + new ClientSideStatus(Status.DefaultSuccess, new Metadata()), + CreateResponsePayload(), + new Metadata()); + + AssertUnaryResponseSuccess(asyncCall, fakeCall, resultTask); + } + + [Test] + public void AsyncUnary_NonSuccessStatusCode() + { + var resultTask = asyncCall.UnaryCallAsync("request1"); + fakeCall.UnaryResponseClientHandler(true, + CreateClientSideStatus(StatusCode.InvalidArgument), + CreateResponsePayload(), + new Metadata()); + + AssertUnaryResponseError(asyncCall, fakeCall, resultTask, StatusCode.InvalidArgument); + } + + [Test] + public void AsyncUnary_NullResponsePayload() + { + var resultTask = asyncCall.UnaryCallAsync("request1"); + fakeCall.UnaryResponseClientHandler(true, + new ClientSideStatus(Status.DefaultSuccess, new Metadata()), + null, + new Metadata()); + + // failure to deserialize will result in InvalidArgument status. + AssertUnaryResponseError(asyncCall, fakeCall, resultTask, StatusCode.Internal); + } + + [Test] + public void ClientStreaming_NoRequest_Success() + { + var resultTask = asyncCall.ClientStreamingCallAsync(); + fakeCall.UnaryResponseClientHandler(true, + new ClientSideStatus(Status.DefaultSuccess, new Metadata()), + CreateResponsePayload(), + new Metadata()); + + AssertUnaryResponseSuccess(asyncCall, fakeCall, resultTask); + } + + [Test] + public void ClientStreaming_NoRequest_NonSuccessStatusCode() + { + var resultTask = asyncCall.ClientStreamingCallAsync(); + fakeCall.UnaryResponseClientHandler(true, + CreateClientSideStatus(StatusCode.InvalidArgument), + CreateResponsePayload(), + new Metadata()); + + AssertUnaryResponseError(asyncCall, fakeCall, resultTask, StatusCode.InvalidArgument); + } + + ClientSideStatus CreateClientSideStatus(StatusCode statusCode) + { + return new ClientSideStatus(new Status(statusCode, ""), new Metadata()); + } + + byte[] CreateResponsePayload() + { + return Marshallers.StringMarshaller.Serializer("response1"); + } + + static void AssertUnaryResponseSuccess(AsyncCall<string, string> asyncCall, FakeNativeCall fakeCall, Task<string> resultTask) { - var resultTask = asyncCall.UnaryCallAsync("abc"); - fakeCall.UnaryResponseClientHandler(true, new ClientSideStatus(Status.DefaultSuccess, new Metadata()), new byte[] { 1, 2, 3 }, new Metadata()); Assert.IsTrue(resultTask.IsCompleted); Assert.IsTrue(fakeCall.IsDisposed); + Assert.AreEqual(Status.DefaultSuccess, asyncCall.GetStatus()); + Assert.AreEqual(0, asyncCall.ResponseHeadersAsync.Result.Count); + Assert.AreEqual(0, asyncCall.GetTrailers().Count); + Assert.AreEqual("response1", resultTask.Result); } - [Test] - public void AsyncUnary_CompletionFailure() + static void AssertUnaryResponseError(AsyncCall<string, string> asyncCall, FakeNativeCall fakeCall, Task<string> resultTask, StatusCode expectedStatusCode) { - var resultTask = asyncCall.UnaryCallAsync("abc"); - fakeCall.UnaryResponseClientHandler(false, new ClientSideStatus(new Status(StatusCode.Internal, ""), null), new byte[] { 1, 2, 3 }, new Metadata()); - Assert.IsTrue(resultTask.IsCompleted); Assert.IsTrue(fakeCall.IsDisposed); - Assert.AreEqual(StatusCode.Internal, asyncCall.GetStatus().StatusCode); - Assert.IsNull(asyncCall.GetTrailers()); + Assert.AreEqual(expectedStatusCode, asyncCall.GetStatus().StatusCode); var ex = Assert.ThrowsAsync<RpcException>(async () => await resultTask); - Assert.AreEqual(StatusCode.Internal, ex.Status.StatusCode); + Assert.AreEqual(expectedStatusCode, ex.Status.StatusCode); + Assert.AreEqual(0, asyncCall.ResponseHeadersAsync.Result.Count); + Assert.AreEqual(0, asyncCall.GetTrailers().Count); } internal class FakeNativeCall : INativeCall diff --git a/src/csharp/Grpc.Core/Internal/AsyncCall.cs b/src/csharp/Grpc.Core/Internal/AsyncCall.cs index 016e1b8587..50ba617cdb 100644 --- a/src/csharp/Grpc.Core/Internal/AsyncCall.cs +++ b/src/csharp/Grpc.Core/Internal/AsyncCall.cs @@ -409,10 +409,13 @@ namespace Grpc.Core.Internal /// </summary> private void HandleUnaryResponse(bool success, ClientSideStatus receivedStatus, byte[] receivedMessage, Metadata responseHeaders) { + // NOTE: because this event is a result of batch containing GRPC_OP_RECV_STATUS_ON_CLIENT, + // success will be always set to true. + using (Profilers.ForCurrentThread().NewScope("AsyncCall.HandleUnaryResponse")) { TResponse msg = default(TResponse); - var deserializeException = success ? TryDeserialize(receivedMessage, out msg) : null; + var deserializeException = TryDeserialize(receivedMessage, out msg); lock (myLock) { @@ -425,14 +428,13 @@ namespace Grpc.Core.Internal finishedStatus = receivedStatus; ReleaseResourcesIfPossible(); - } responseHeadersTcs.SetResult(responseHeaders); var status = receivedStatus.Status; - if (!success || status.StatusCode != StatusCode.OK) + if (status.StatusCode != StatusCode.OK) { unaryResponseTcs.SetException(new RpcException(status)); return; @@ -447,6 +449,9 @@ namespace Grpc.Core.Internal /// </summary> private void HandleFinished(bool success, ClientSideStatus receivedStatus) { + // NOTE: because this event is a result of batch containing GRPC_OP_RECV_STATUS_ON_CLIENT, + // success will be always set to true. + lock (myLock) { finished = true; @@ -457,7 +462,7 @@ namespace Grpc.Core.Internal var status = receivedStatus.Status; - if (!success || status.StatusCode != StatusCode.OK) + if (status.StatusCode != StatusCode.OK) { streamingCallFinishedTcs.SetException(new RpcException(status)); return; diff --git a/src/node/tools/bin/protoc.js b/src/node/tools/bin/protoc.js index 0c6d7ce017..4d50c94b0f 100755 --- a/src/node/tools/bin/protoc.js +++ b/src/node/tools/bin/protoc.js @@ -43,7 +43,9 @@ var path = require('path'); var execFile = require('child_process').execFile; -var protoc = path.resolve(__dirname, 'protoc'); +var exe_ext = process.platform === 'win32' ? '.exe' : ''; + +var protoc = path.resolve(__dirname, 'protoc' + exe_ext); execFile(protoc, process.argv.slice(2), function(error, stdout, stderr) { if (error) { diff --git a/src/node/tools/bin/protoc_plugin.js b/src/node/tools/bin/protoc_plugin.js index 0e0bb9406e..281ec0d85e 100755 --- a/src/node/tools/bin/protoc_plugin.js +++ b/src/node/tools/bin/protoc_plugin.js @@ -43,9 +43,11 @@ var path = require('path'); var execFile = require('child_process').execFile; -var protoc = path.resolve(__dirname, 'grpc_node_plugin'); +var exe_ext = process.platform === 'win32' ? '.exe' : ''; -execFile(protoc, process.argv.slice(2), function(error, stdout, stderr) { +var plugin = path.resolve(__dirname, 'grpc_node_plugin' + exe_ext); + +execFile(plugin, process.argv.slice(2), function(error, stdout, stderr) { if (error) { throw error; } diff --git a/src/objective-c/GRPCClient/GRPCCall.m b/src/objective-c/GRPCClient/GRPCCall.m index 1847d6016f..0eb10656dd 100644 --- a/src/objective-c/GRPCClient/GRPCCall.m +++ b/src/objective-c/GRPCClient/GRPCCall.m @@ -136,6 +136,10 @@ NSString * const kGRPCTrailersKey = @"io.grpc.TrailersKey"; #pragma mark Finish - (void)finishWithError:(NSError *)errorOrNil { + @synchronized(self) { + _state = GRXWriterStateFinished; + } + // If the call isn't retained anywhere else, it can be deallocated now. _retainSelf = nil; @@ -342,6 +346,10 @@ NSString * const kGRPCTrailersKey = @"io.grpc.TrailersKey"; #pragma mark GRXWriter implementation - (void)startWithWriteable:(id<GRXWriteable>)writeable { + @synchronized(self) { + _state = GRXWriterStateStarted; + } + // Create a retain cycle so that this instance lives until the RPC finishes (or is cancelled). // This makes RPCs in which the call isn't externally retained possible (as long as it is started // before being autoreleased). @@ -375,30 +383,32 @@ NSString * const kGRPCTrailersKey = @"io.grpc.TrailersKey"; } - (void)setState:(GRXWriterState)newState { - // Manual transitions are only allowed from the started or paused states. - if (_state == GRXWriterStateNotStarted || _state == GRXWriterStateFinished) { - return; - } - - switch (newState) { - case GRXWriterStateFinished: - _state = newState; - // Per GRXWriter's contract, setting the state to Finished manually - // means one doesn't wish the writeable to be messaged anymore. - [_responseWriteable cancelSilently]; - _responseWriteable = nil; - return; - case GRXWriterStatePaused: - _state = newState; + @synchronized(self) { + // Manual transitions are only allowed from the started or paused states. + if (_state == GRXWriterStateNotStarted || _state == GRXWriterStateFinished) { return; - case GRXWriterStateStarted: - if (_state == GRXWriterStatePaused) { + } + + switch (newState) { + case GRXWriterStateFinished: _state = newState; - [self startNextRead]; - } - return; - case GRXWriterStateNotStarted: - return; + // Per GRXWriter's contract, setting the state to Finished manually + // means one doesn't wish the writeable to be messaged anymore. + [_responseWriteable cancelSilently]; + _responseWriteable = nil; + return; + case GRXWriterStatePaused: + _state = newState; + return; + case GRXWriterStateStarted: + if (_state == GRXWriterStatePaused) { + _state = newState; + [self startNextRead]; + } + return; + case GRXWriterStateNotStarted: + return; + } } } diff --git a/src/objective-c/tests/InteropTests.m b/src/objective-c/tests/InteropTests.m index 26877b1ae8..4f096b9efa 100644 --- a/src/objective-c/tests/InteropTests.m +++ b/src/objective-c/tests/InteropTests.m @@ -272,8 +272,14 @@ XCTAssertEqual(error.code, GRPC_STATUS_CANCELLED); [expectation fulfill]; }]; + XCTAssertEqual(call.state, GRXWriterStateNotStarted); + [call start]; + XCTAssertEqual(call.state, GRXWriterStateStarted); + [call cancel]; + XCTAssertEqual(call.state, GRXWriterStateFinished); + [self waitForExpectationsWithTimeout:1 handler:nil]; } diff --git a/src/python/grpcio/tests/stress/__init__.py b/src/python/grpcio/tests/stress/__init__.py new file mode 100644 index 0000000000..100a624dc9 --- /dev/null +++ b/src/python/grpcio/tests/stress/__init__.py @@ -0,0 +1,28 @@ +# Copyright 2016, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/python/grpcio/tests/stress/client.py b/src/python/grpcio/tests/stress/client.py new file mode 100644 index 0000000000..a733741b73 --- /dev/null +++ b/src/python/grpcio/tests/stress/client.py @@ -0,0 +1,132 @@ +# Copyright 2016, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Entry point for running stress tests.""" + +import argparse +import Queue +import threading + +from grpc.beta import implementations +from src.proto.grpc.testing import metrics_pb2 +from src.proto.grpc.testing import test_pb2 + +from tests.interop import methods +from tests.qps import histogram +from tests.stress import metrics_server +from tests.stress import test_runner + + +def _args(): + parser = argparse.ArgumentParser(description='gRPC Python stress test client') + parser.add_argument( + '--server_addresses', + help='comma seperated list of hostname:port to run servers on', + default='localhost:8080', type=str) + parser.add_argument( + '--test_cases', + help='comma seperated list of testcase:weighting of tests to run', + default='large_unary:100', + type=str) + parser.add_argument( + '--test_duration_secs', + help='number of seconds to run the stress test', + default=-1, type=int) + parser.add_argument( + '--num_channels_per_server', + help='number of channels per server', + default=1, type=int) + parser.add_argument( + '--num_stubs_per_channel', + help='number of stubs to create per channel', + default=1, type=int) + parser.add_argument( + '--metrics_port', + help='the port to listen for metrics requests on', + default=8081, type=int) + return parser.parse_args() + + +def _test_case_from_arg(test_case_arg): + for test_case in methods.TestCase: + if test_case_arg == test_case.value: + return test_case + else: + raise ValueError('No test case {}!'.format(test_case_arg)) + + +def _parse_weighted_test_cases(test_case_args): + weighted_test_cases = {} + for test_case_arg in test_case_args.split(','): + name, weight = test_case_arg.split(':', 1) + test_case = _test_case_from_arg(name) + weighted_test_cases[test_case] = int(weight) + return weighted_test_cases + + +def run_test(args): + test_cases = _parse_weighted_test_cases(args.test_cases) + test_servers = args.server_addresses.split(',') + # Propagate any client exceptions with a queue + exception_queue = Queue.Queue() + stop_event = threading.Event() + hist = histogram.Histogram(1, 1) + runners = [] + + server = metrics_pb2.beta_create_MetricsService_server( + metrics_server.MetricsServer(hist)) + server.add_insecure_port('[::]:{}'.format(args.metrics_port)) + server.start() + + for test_server in test_servers: + host, port = test_server.split(':', 1) + for _ in xrange(args.num_channels_per_server): + channel = implementations.insecure_channel(host, int(port)) + for _ in xrange(args.num_stubs_per_channel): + stub = test_pb2.beta_create_TestService_stub(channel) + runner = test_runner.TestRunner(stub, test_cases, hist, + exception_queue, stop_event) + runners.append(runner) + + for runner in runners: + runner.start() + try: + raise exception_queue.get(block=True, timeout=args.test_duration_secs) + except Queue.Empty: + # No exceptions thrown, success + pass + finally: + stop_event.set() + for runner in runners: + runner.join() + runner = None + server.stop(0) + +if __name__ == '__main__': + run_test(_args()) diff --git a/src/python/grpcio/tests/stress/metrics_server.py b/src/python/grpcio/tests/stress/metrics_server.py new file mode 100644 index 0000000000..b994e4643e --- /dev/null +++ b/src/python/grpcio/tests/stress/metrics_server.py @@ -0,0 +1,60 @@ +# Copyright 2016, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""MetricsService for publishing stress test qps data.""" + +import time + +from src.proto.grpc.testing import metrics_pb2 + +GAUGE_NAME = 'python_overall_qps' + + +class MetricsServer(metrics_pb2.BetaMetricsServiceServicer): + + def __init__(self, histogram): + self._start_time = time.time() + self._histogram = histogram + + def _get_qps(self): + count = self._histogram.get_data().count + delta = time.time() - self._start_time + self._histogram.reset() + self._start_time = time.time() + return int(count/delta) + + def GetAllGauges(self, request, context): + qps = self._get_qps() + return [metrics_pb2.GaugeResponse(name=GAUGE_NAME, long_value=qps)] + + def GetGauge(self, request, context): + if request.name != GAUGE_NAME: + raise Exception('Gauge {} does not exist'.format(request.name)) + qps = self._get_qps() + return metrics_pb2.GaugeResponse(name=GAUGE_NAME, long_value=qps) diff --git a/src/python/grpcio/tests/stress/test_runner.py b/src/python/grpcio/tests/stress/test_runner.py new file mode 100644 index 0000000000..88f13727e3 --- /dev/null +++ b/src/python/grpcio/tests/stress/test_runner.py @@ -0,0 +1,73 @@ +# Copyright 2016, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Thread that sends random weighted requests on a TestService stub.""" + +import random +import threading +import time +import traceback + + +def _weighted_test_case_generator(weighted_cases): + weight_sum = sum(weighted_cases.itervalues()) + + while True: + val = random.uniform(0, weight_sum) + partial_sum = 0 + for case in weighted_cases: + partial_sum += weighted_cases[case] + if val <= partial_sum: + yield case + break + + +class TestRunner(threading.Thread): + + def __init__(self, stub, test_cases, hist, exception_queue, stop_event): + super(TestRunner, self).__init__() + self._exception_queue = exception_queue + self._stop_event = stop_event + self._stub = stub + self._test_cases = _weighted_test_case_generator(test_cases) + self._histogram = hist + + def run(self): + while not self._stop_event.is_set(): + try: + test_case = next(self._test_cases) + start_time = time.time() + test_case.test_interoperability(self._stub, None) + end_time = time.time() + self._histogram.add((end_time - start_time)*1e9) + except Exception as e: + traceback.print_exc() + self._exception_queue.put( + Exception("An exception occured during test {}" + .format(test_case), e)) diff --git a/src/ruby/ext/grpc/extconf.rb b/src/ruby/ext/grpc/extconf.rb index 82b6d313c8..07f7bb93b8 100644 --- a/src/ruby/ext/grpc/extconf.rb +++ b/src/ruby/ext/grpc/extconf.rb @@ -60,35 +60,27 @@ grpc_root = File.expand_path(File.join(File.dirname(__FILE__), '../../../..')) grpc_config = ENV['GRPC_CONFIG'] || 'opt' -if ENV.key?('GRPC_LIB_DIR') - grpc_lib_dir = File.join(grpc_root, ENV['GRPC_LIB_DIR']) -else - grpc_lib_dir = File.join(grpc_root, 'libs', grpc_config) -end - ENV['MACOSX_DEPLOYMENT_TARGET'] = '10.7' -unless File.exist?(File.join(grpc_lib_dir, 'libgrpc.a')) or windows - ENV['AR'] = RbConfig::CONFIG['AR'] + ' rcs' - ENV['CC'] = RbConfig::CONFIG['CC'] - ENV['LD'] = ENV['CC'] +ENV['AR'] = RbConfig::CONFIG['AR'] + ' rcs' +ENV['CC'] = RbConfig::CONFIG['CC'] +ENV['LD'] = ENV['CC'] - ENV['AR'] = 'libtool -o' if RUBY_PLATFORM =~ /darwin/ +ENV['AR'] = 'libtool -o' if RUBY_PLATFORM =~ /darwin/ - ENV['EMBED_OPENSSL'] = 'true' - ENV['EMBED_ZLIB'] = 'true' - ENV['ARCH_FLAGS'] = RbConfig::CONFIG['ARCH_FLAG'] - ENV['ARCH_FLAGS'] = '-arch i386 -arch x86_64' if RUBY_PLATFORM =~ /darwin/ - ENV['CFLAGS'] = '-DGPR_BACKWARDS_COMPATIBILITY_MODE' +ENV['EMBED_OPENSSL'] = 'true' +ENV['EMBED_ZLIB'] = 'true' +ENV['ARCH_FLAGS'] = RbConfig::CONFIG['ARCH_FLAG'] +ENV['ARCH_FLAGS'] = '-arch i386 -arch x86_64' if RUBY_PLATFORM =~ /darwin/ +ENV['CFLAGS'] = '-DGPR_BACKWARDS_COMPATIBILITY_MODE' - output_dir = File.expand_path(RbConfig::CONFIG['topdir']) - grpc_lib_dir = File.join(output_dir, 'libs', grpc_config) - ENV['BUILDDIR'] = output_dir +output_dir = File.expand_path(RbConfig::CONFIG['topdir']) +grpc_lib_dir = File.join(output_dir, 'libs', grpc_config) +ENV['BUILDDIR'] = output_dir - puts 'Building internal gRPC into ' + grpc_lib_dir - system("make -j -C #{grpc_root} #{grpc_lib_dir}/libgrpc.a CONFIG=#{grpc_config}") - exit 1 unless $? == 0 -end +puts 'Building internal gRPC into ' + grpc_lib_dir +system("make -j -C #{grpc_root} #{grpc_lib_dir}/libgrpc.a CONFIG=#{grpc_config}") +exit 1 unless $? == 0 $CFLAGS << ' -I' + File.join(grpc_root, 'include') $LDFLAGS << ' ' + File.join(grpc_lib_dir, 'libgrpc.a') unless windows diff --git a/src/ruby/tools/README.md b/src/ruby/tools/README.md new file mode 100644 index 0000000000..e43f223c89 --- /dev/null +++ b/src/ruby/tools/README.md @@ -0,0 +1,12 @@ +# Ruby gRPC Tools + +This package distributes protoc and the Ruby gRPC protoc plugin for Windows, Linux, and Mac. + +Before this package is published, the following directories should be filled with the corresponding `protoc` and `grpc_ruby_plugin` executables. + + - `bin/x86-linux` + - `bin/x86_64-linux` + - `bin/x86-macos` + - `bin/x86_64-macos` + - `bin/x86-windows` + - `bin/x86_64-windows` diff --git a/src/ruby/tools/bin/protoc.rb b/src/ruby/tools/bin/protoc.rb new file mode 100755 index 0000000000..3a2a5b8dc9 --- /dev/null +++ b/src/ruby/tools/bin/protoc.rb @@ -0,0 +1,41 @@ +#!/usr/bin/env ruby +# Copyright 2016, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +require 'rbconfig' + +require_relative '../os_check' + +protoc_name = 'protoc' + RbConfig::CONFIG['EXEEXT'] + +protoc_path = File.join(File.dirname(__FILE__), + RbConfig::CONFIG['host_cpu'] + '-' + OS.os_name, + protoc_name) + +exec([ protoc_path, protoc_path ], *ARGV) diff --git a/src/ruby/tools/bin/protoc_grpc_ruby_plugin.rb b/src/ruby/tools/bin/protoc_grpc_ruby_plugin.rb new file mode 100755 index 0000000000..4b296dedc7 --- /dev/null +++ b/src/ruby/tools/bin/protoc_grpc_ruby_plugin.rb @@ -0,0 +1,41 @@ +#!/usr/bin/env ruby +# Copyright 2016, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +require 'rbconfig' + +require_relative '../os_check' + +plugin_name = 'grpc_ruby_plugin' + RbConfig::CONFIG['EXEEXT'] + +plugin_path = File.join(File.dirname(__FILE__), + RbConfig::CONFIG['host_cpu'] + '-' + OS.os_name, + plugin_name) + +exec([ plugin_path, plugin_path ], *ARGV) diff --git a/src/ruby/tools/grpc-tools.gemspec b/src/ruby/tools/grpc-tools.gemspec new file mode 100644 index 0000000000..af904de4a9 --- /dev/null +++ b/src/ruby/tools/grpc-tools.gemspec @@ -0,0 +1,22 @@ +# -*- ruby -*- +# encoding: utf-8 +require_relative 'version.rb' +Gem::Specification.new do |s| + s.name = 'grpc-tools' + s.version = GRPC::Tools::VERSION + s.authors = ['grpc Authors'] + s.email = 'grpc-io@googlegroups.com' + s.homepage = 'https://github.com/google/grpc/tree/master/src/ruby/tools' + s.summary = 'Development tools for Ruby gRPC' + s.description = 'protoc and the Ruby gRPC protoc plugin' + s.license = 'BSD-3-Clause' + + s.files = %w( version.rb os_check.rb README.md ) + s.files += Dir.glob('bin/**/*') + + s.bindir = 'bin' + + s.platform = Gem::Platform::RUBY + + s.executables = %w( protoc.rb protoc_grpc_ruby_plugin.rb ) +end diff --git a/src/ruby/tools/os_check.rb b/src/ruby/tools/os_check.rb new file mode 100644 index 0000000000..2677306457 --- /dev/null +++ b/src/ruby/tools/os_check.rb @@ -0,0 +1,45 @@ +# Copyright 2016, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# This is based on http://stackoverflow.com/a/171011/159388 by Aaron Hinni + +require 'rbconfig' + +module OS + def OS.os_name + case RbConfig::CONFIG['host_os'] + when /cygwin|mswin|mingw|bccwin|wince|emx/ + 'windows' + when /darwin/ + 'macos' + else + 'linux' + end + end +end diff --git a/src/ruby/tools/version.rb b/src/ruby/tools/version.rb new file mode 100644 index 0000000000..12ad21b80e --- /dev/null +++ b/src/ruby/tools/version.rb @@ -0,0 +1,34 @@ +# Copyright 2015, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +module GRPC + module Tools + VERSION = '0.14.0.dev' + end +end |