diff options
author | yang-g <yangg@google.com> | 2015-08-24 15:54:07 -0700 |
---|---|---|
committer | yang-g <yangg@google.com> | 2015-08-24 15:54:07 -0700 |
commit | 3e4bd9598bc8b5740317227c8a7a8e38f519dabe (patch) | |
tree | 6b9399f3fd91ea545bb6cc4ffba3e72eda24303b /src | |
parent | 9fb35a53320a7b958739ce01ed50de087e6c5ee9 (diff) | |
parent | 8c4549aec780e2cdeb72f028d9a44ad9fd853c33 (diff) |
merge with head
Diffstat (limited to 'src')
110 files changed, 5707 insertions, 552 deletions
diff --git a/src/core/channel/channel_args.c b/src/core/channel/channel_args.c index c430b56fa2..54ee75af28 100644 --- a/src/core/channel/channel_args.c +++ b/src/core/channel/channel_args.c @@ -37,6 +37,7 @@ #include <grpc/support/alloc.h> #include <grpc/support/string_util.h> +#include <grpc/support/useful.h> #include <string.h> @@ -146,3 +147,65 @@ grpc_channel_args *grpc_channel_args_set_compression_algorithm( tmp.value.integer = algorithm; return grpc_channel_args_copy_and_add(a, &tmp, 1); } + +/** Returns 1 if the argument for compression algorithm's enabled states bitset + * was found in \a a, returning the arg's value in \a states. Otherwise, returns + * 0. */ +static int find_compression_algorithm_states_bitset( + const grpc_channel_args *a, int **states_arg) { + if (a != NULL) { + size_t i; + for (i = 0; i < a->num_args; ++i) { + if (a->args[i].type == GRPC_ARG_INTEGER && + !strcmp(GRPC_COMPRESSION_ALGORITHM_STATE_ARG, a->args[i].key)) { + *states_arg = &a->args[i].value.integer; + return 1; /* GPR_TRUE */ + } + } + } + return 0; /* GPR_FALSE */ +} + +grpc_channel_args *grpc_channel_args_compression_algorithm_set_state( + grpc_channel_args **a, + grpc_compression_algorithm algorithm, + int state) { + int *states_arg; + grpc_channel_args *result = *a; + const int states_arg_found = + find_compression_algorithm_states_bitset(*a, &states_arg); + + if (states_arg_found) { + if (state != 0) { + GPR_BITSET(states_arg, algorithm); + } else { + GPR_BITCLEAR(states_arg, algorithm); + } + } else { + /* create a new arg */ + grpc_arg tmp; + tmp.type = GRPC_ARG_INTEGER; + tmp.key = GRPC_COMPRESSION_ALGORITHM_STATE_ARG; + /* all enabled by default */ + tmp.value.integer = (1u << GRPC_COMPRESS_ALGORITHMS_COUNT) - 1; + if (state != 0) { + GPR_BITSET(&tmp.value.integer, algorithm); + } else { + GPR_BITCLEAR(&tmp.value.integer, algorithm); + } + result = grpc_channel_args_copy_and_add(*a, &tmp, 1); + grpc_channel_args_destroy(*a); + *a = result; + } + return result; +} + +int grpc_channel_args_compression_algorithm_get_states( + const grpc_channel_args *a) { + int *states_arg; + if (find_compression_algorithm_states_bitset(a, &states_arg)) { + return *states_arg; + } else { + return (1u << GRPC_COMPRESS_ALGORITHMS_COUNT) - 1; /* All algs. enabled */ + } +} diff --git a/src/core/channel/channel_args.h b/src/core/channel/channel_args.h index 7e6ddd3997..06a6012dee 100644 --- a/src/core/channel/channel_args.h +++ b/src/core/channel/channel_args.h @@ -67,4 +67,24 @@ grpc_compression_algorithm grpc_channel_args_get_compression_algorithm( grpc_channel_args *grpc_channel_args_set_compression_algorithm( grpc_channel_args *a, grpc_compression_algorithm algorithm); +/** Sets the support for the given compression algorithm. By default, all + * compression algorithms are enabled. It's an error to disable an algorithm set + * by grpc_channel_args_set_compression_algorithm. + * + * Returns an instance will the updated algorithm states. The \a a pointer is + * modified to point to the returned instance (which may be different from the + * input value of \a a). */ +grpc_channel_args *grpc_channel_args_compression_algorithm_set_state( + grpc_channel_args **a, + grpc_compression_algorithm algorithm, + int enabled); + +/** Returns the bitset representing the support state (true for enabled, false + * for disabled) for compression algorithms. + * + * The i-th bit of the returned bitset corresponds to the i-th entry in the + * grpc_compression_algorithm enum. */ +int grpc_channel_args_compression_algorithm_get_states( + const grpc_channel_args *a); + #endif /* GRPC_INTERNAL_CORE_CHANNEL_CHANNEL_ARGS_H */ diff --git a/src/core/iomgr/pollset.h b/src/core/iomgr/pollset.h index c474e4dbf1..337596cb74 100644 --- a/src/core/iomgr/pollset.h +++ b/src/core/iomgr/pollset.h @@ -74,10 +74,9 @@ void grpc_pollset_destroy(grpc_pollset *pollset); grpc_pollset_work, and it is guaranteed that GRPC_POLLSET_MU(pollset) will not be released by grpc_pollset_work AFTER worker has been destroyed. - Returns true if some work has been done, and false if the deadline - expired. */ -int grpc_pollset_work(grpc_pollset *pollset, grpc_pollset_worker *worker, - gpr_timespec deadline); + Tries not to block past deadline. */ +void grpc_pollset_work(grpc_pollset *pollset, grpc_pollset_worker *worker, + gpr_timespec now, gpr_timespec deadline); /* Break one polling thread out of polling work for this pollset. If specific_worker is GRPC_POLLSET_KICK_BROADCAST, kick ALL the workers. diff --git a/src/core/iomgr/pollset_multipoller_with_epoll.c b/src/core/iomgr/pollset_multipoller_with_epoll.c index 5ea9dd2101..fe66ebed77 100644 --- a/src/core/iomgr/pollset_multipoller_with_epoll.c +++ b/src/core/iomgr/pollset_multipoller_with_epoll.c @@ -181,7 +181,7 @@ static void multipoll_with_epoll_pollset_maybe_work( pfds[1].events = POLLIN; pfds[1].revents = 0; - poll_rv = poll(pfds, 2, timeout_ms); + poll_rv = grpc_poll_function(pfds, 2, timeout_ms); if (poll_rv < 0) { if (errno != EINTR) { diff --git a/src/core/iomgr/pollset_multipoller_with_poll_posix.c b/src/core/iomgr/pollset_multipoller_with_poll_posix.c index 001fcecf76..30ee6e24db 100644 --- a/src/core/iomgr/pollset_multipoller_with_poll_posix.c +++ b/src/core/iomgr/pollset_multipoller_with_poll_posix.c @@ -144,7 +144,7 @@ static void multipoll_with_poll_pollset_maybe_work( POLLOUT, &watchers[i]); } - r = poll(pfds, pfd_count, timeout); + r = grpc_poll_function(pfds, pfd_count, timeout); for (i = 1; i < pfd_count; i++) { grpc_fd_end_poll(&watchers[i], pfds[i].revents & POLLIN, diff --git a/src/core/iomgr/pollset_posix.c b/src/core/iomgr/pollset_posix.c index a01f9ff727..6bd1b61f24 100644 --- a/src/core/iomgr/pollset_posix.c +++ b/src/core/iomgr/pollset_posix.c @@ -38,7 +38,6 @@ #include "src/core/iomgr/pollset_posix.h" #include <errno.h> -#include <poll.h> #include <stdlib.h> #include <string.h> #include <unistd.h> @@ -57,6 +56,8 @@ GPR_TLS_DECL(g_current_thread_poller); GPR_TLS_DECL(g_current_thread_worker); +grpc_poll_function_type grpc_poll_function = poll; + static void remove_worker(grpc_pollset *p, grpc_pollset_worker *worker) { worker->prev->next = worker->next; worker->next->prev = worker->prev; @@ -89,6 +90,7 @@ static void push_front_worker(grpc_pollset *p, grpc_pollset_worker *worker) { } void grpc_pollset_kick(grpc_pollset *p, grpc_pollset_worker *specific_worker) { + /* pollset->mu already held */ if (specific_worker != NULL) { if (specific_worker == GRPC_POLLSET_KICK_BROADCAST) { for (specific_worker = p->root_worker.next; @@ -168,14 +170,10 @@ static void finish_shutdown(grpc_pollset *pollset) { pollset->shutdown_done_cb(pollset->shutdown_done_arg); } -int grpc_pollset_work(grpc_pollset *pollset, grpc_pollset_worker *worker, - gpr_timespec deadline) { +void grpc_pollset_work(grpc_pollset *pollset, grpc_pollset_worker *worker, + gpr_timespec now, gpr_timespec deadline) { /* pollset->mu already held */ - gpr_timespec now = gpr_now(GPR_CLOCK_MONOTONIC); int added_worker = 0; - if (gpr_time_cmp(now, deadline) > 0) { - return 0; - } /* this must happen before we (potentially) drop pollset->mu */ worker->next = worker->prev = NULL; /* TODO(ctiller): pool these */ @@ -217,7 +215,6 @@ done: gpr_mu_lock(&pollset->mu); } } - return 1; } void grpc_pollset_shutdown(grpc_pollset *pollset, @@ -456,7 +453,7 @@ static void basic_pollset_maybe_work(grpc_pollset *pollset, /* poll fd count (argument 2) is shortened by one if we have no events to poll on - such that it only includes the kicker */ - r = poll(pfd, nfds, timeout); + r = grpc_poll_function(pfd, nfds, timeout); GRPC_TIMER_MARK(GRPC_PTAG_POLL_FINISHED, r); if (fd) { diff --git a/src/core/iomgr/pollset_posix.h b/src/core/iomgr/pollset_posix.h index a3ea353de6..69bd9cca8c 100644 --- a/src/core/iomgr/pollset_posix.h +++ b/src/core/iomgr/pollset_posix.h @@ -34,6 +34,8 @@ #ifndef GRPC_INTERNAL_CORE_IOMGR_POLLSET_POSIX_H #define GRPC_INTERNAL_CORE_IOMGR_POLLSET_POSIX_H +#include <poll.h> + #include <grpc/support/sync.h> #include "src/core/iomgr/wakeup_fd_posix.h" @@ -118,4 +120,8 @@ void grpc_poll_become_multipoller(grpc_pollset *pollset, struct grpc_fd **fds, * be locked) */ int grpc_pollset_has_workers(grpc_pollset *pollset); +/* override to allow tests to hook poll() usage */ +typedef int (*grpc_poll_function_type)(struct pollfd *, nfds_t, int); +extern grpc_poll_function_type grpc_poll_function; + #endif /* GRPC_INTERNAL_CORE_IOMGR_POLLSET_POSIX_H */ diff --git a/src/core/iomgr/pollset_windows.c b/src/core/iomgr/pollset_windows.c index 8710395ab3..07522c8a0c 100644 --- a/src/core/iomgr/pollset_windows.c +++ b/src/core/iomgr/pollset_windows.c @@ -99,14 +99,9 @@ void grpc_pollset_destroy(grpc_pollset *pollset) { gpr_mu_destroy(&pollset->mu); } -int grpc_pollset_work(grpc_pollset *pollset, grpc_pollset_worker *worker, - gpr_timespec deadline) { - gpr_timespec now; +void grpc_pollset_work(grpc_pollset *pollset, grpc_pollset_worker *worker, + gpr_timespec now, gpr_timespec deadline) { int added_worker = 0; - now = gpr_now(GPR_CLOCK_MONOTONIC); - if (gpr_time_cmp(now, deadline) > 0) { - return 0 /* GPR_FALSE */; - } worker->next = worker->prev = NULL; gpr_cv_init(&worker->cv); if (grpc_maybe_call_delayed_callbacks(&pollset->mu, 1 /* GPR_TRUE */)) { @@ -127,7 +122,6 @@ done: if (added_worker) { remove_worker(pollset, worker); } - return 1 /* GPR_TRUE */; } void grpc_pollset_kick(grpc_pollset *p, grpc_pollset_worker *specific_worker) { diff --git a/src/core/security/google_default_credentials.c b/src/core/security/google_default_credentials.c index d6092ece32..3631de867a 100644 --- a/src/core/security/google_default_credentials.c +++ b/src/core/security/google_default_credentials.c @@ -115,7 +115,7 @@ static int is_stack_running_on_compute_engine(void) { gpr_mu_lock(GRPC_POLLSET_MU(&detector.pollset)); while (!detector.is_done) { grpc_pollset_worker worker; - grpc_pollset_work(&detector.pollset, &worker, + grpc_pollset_work(&detector.pollset, &worker, gpr_now(GPR_CLOCK_MONOTONIC), gpr_inf_future(GPR_CLOCK_MONOTONIC)); } gpr_mu_unlock(GRPC_POLLSET_MU(&detector.pollset)); diff --git a/src/core/security/security_connector.c b/src/core/security/security_connector.c index a354536dcd..ba9ac68c5f 100644 --- a/src/core/security/security_connector.c +++ b/src/core/security/security_connector.c @@ -575,6 +575,16 @@ grpc_security_status grpc_ssl_channel_security_connector_create( if (!check_request_metadata_creds(request_metadata_creds)) { goto error; } + if (config->pem_root_certs == NULL) { + pem_root_certs_size = grpc_get_default_ssl_roots(&pem_root_certs); + if (pem_root_certs == NULL || pem_root_certs_size == 0) { + gpr_log(GPR_ERROR, "Could not get default pem root certs."); + goto error; + } + } else { + pem_root_certs = config->pem_root_certs; + pem_root_certs_size = config->pem_root_certs_size; + } c = gpr_malloc(sizeof(grpc_ssl_channel_security_connector)); memset(c, 0, sizeof(grpc_ssl_channel_security_connector)); @@ -590,16 +600,6 @@ grpc_security_status grpc_ssl_channel_security_connector_create( if (overridden_target_name != NULL) { c->overridden_target_name = gpr_strdup(overridden_target_name); } - if (config->pem_root_certs == NULL) { - pem_root_certs_size = grpc_get_default_ssl_roots(&pem_root_certs); - if (pem_root_certs == NULL || pem_root_certs_size == 0) { - gpr_log(GPR_ERROR, "Could not get default pem root certs."); - goto error; - } - } else { - pem_root_certs = config->pem_root_certs; - pem_root_certs_size = config->pem_root_certs_size; - } result = tsi_create_ssl_client_handshaker_factory( config->pem_private_key, config->pem_private_key_size, config->pem_cert_chain, config->pem_cert_chain_size, pem_root_certs, diff --git a/src/core/security/server_auth_filter.c b/src/core/security/server_auth_filter.c index 2f42f01f53..6e831431fa 100644 --- a/src/core/security/server_auth_filter.c +++ b/src/core/security/server_auth_filter.c @@ -104,24 +104,34 @@ static grpc_mdelem *remove_consumed_md(void *user_data, grpc_mdelem *md) { return md; } -static void on_md_processing_done(void *user_data, - const grpc_metadata *consumed_md, - size_t num_consumed_md, int success) { +static void on_md_processing_done( + void *user_data, const grpc_metadata *consumed_md, size_t num_consumed_md, + const grpc_metadata *response_md, size_t num_response_md, + grpc_status_code status, const char *error_details) { grpc_call_element *elem = user_data; call_data *calld = elem->call_data; - if (success) { + /* TODO(jboeuf): Implement support for response_md. */ + if (response_md != NULL && num_response_md > 0) { + gpr_log(GPR_INFO, + "response_md in auth metadata processing not supported for now. " + "Ignoring..."); + } + + if (status == GRPC_STATUS_OK) { calld->consumed_md = consumed_md; calld->num_consumed_md = num_consumed_md; grpc_metadata_batch_filter(&calld->md_op->data.metadata, remove_consumed_md, elem); - calld->on_done_recv->cb(calld->on_done_recv->cb_arg, success); + calld->on_done_recv->cb(calld->on_done_recv->cb_arg, 1); } else { - gpr_slice message = gpr_slice_from_copied_string( - "Authentication metadata processing failed."); + gpr_slice message; + error_details = error_details != NULL + ? error_details + : "Authentication metadata processing failed."; + message = gpr_slice_from_copied_string(error_details); grpc_sopb_reset(calld->recv_ops); - grpc_transport_stream_op_add_close(&calld->transport_op, - GRPC_STATUS_UNAUTHENTICATED, &message); + grpc_transport_stream_op_add_close(&calld->transport_op, status, &message); grpc_call_next_op(elem, &calld->transport_op); } } diff --git a/src/core/surface/completion_queue.c b/src/core/surface/completion_queue.c index 77443a7ae8..b58115a93f 100644 --- a/src/core/surface/completion_queue.c +++ b/src/core/surface/completion_queue.c @@ -170,6 +170,9 @@ grpc_event grpc_completion_queue_next(grpc_completion_queue *cc, gpr_timespec deadline, void *reserved) { grpc_event ret; grpc_pollset_worker worker; + int first_loop = 1; + gpr_timespec now; + GPR_ASSERT(!reserved); deadline = gpr_convert_clock_type(deadline, GPR_CLOCK_MONOTONIC); @@ -196,12 +199,15 @@ grpc_event grpc_completion_queue_next(grpc_completion_queue *cc, ret.type = GRPC_QUEUE_SHUTDOWN; break; } - if (!grpc_pollset_work(&cc->pollset, &worker, deadline)) { + now = gpr_now(GPR_CLOCK_MONOTONIC); + if (!first_loop && gpr_time_cmp(now, deadline) >= 0) { gpr_mu_unlock(GRPC_POLLSET_MU(&cc->pollset)); memset(&ret, 0, sizeof(ret)); ret.type = GRPC_QUEUE_TIMEOUT; break; } + first_loop = 0; + grpc_pollset_work(&cc->pollset, &worker, now, deadline); } GRPC_SURFACE_TRACE_RETURNED_EVENT(cc, &ret); GRPC_CQ_INTERNAL_UNREF(cc, "next"); @@ -239,6 +245,9 @@ grpc_event grpc_completion_queue_pluck(grpc_completion_queue *cc, void *tag, grpc_cq_completion *c; grpc_cq_completion *prev; grpc_pollset_worker worker; + gpr_timespec now; + int first_loop = 1; + GPR_ASSERT(!reserved); deadline = gpr_convert_clock_type(deadline, GPR_CLOCK_MONOTONIC); @@ -281,13 +290,16 @@ grpc_event grpc_completion_queue_pluck(grpc_completion_queue *cc, void *tag, ret.type = GRPC_QUEUE_TIMEOUT; break; } - if (!grpc_pollset_work(&cc->pollset, &worker, deadline)) { + now = gpr_now(GPR_CLOCK_MONOTONIC); + if (!first_loop && gpr_time_cmp(now, deadline) >= 0) { del_plucker(cc, tag, &worker); gpr_mu_unlock(GRPC_POLLSET_MU(&cc->pollset)); memset(&ret, 0, sizeof(ret)); ret.type = GRPC_QUEUE_TIMEOUT; break; } + first_loop = 0; + grpc_pollset_work(&cc->pollset, &worker, now, deadline); del_plucker(cc, tag, &worker); } done: diff --git a/src/core/transport/chttp2/stream_encoder.c b/src/core/transport/chttp2/stream_encoder.c index 0f04169741..1ea697f71e 100644 --- a/src/core/transport/chttp2/stream_encoder.c +++ b/src/core/transport/chttp2/stream_encoder.c @@ -66,6 +66,8 @@ typedef struct { size_t header_idx; /* was the last frame emitted a header? (if yes, we'll need a CONTINUATION */ gpr_uint8 last_was_header; + /* have we seen a regular (non-colon-prefixed) header yet? */ + gpr_uint8 seen_regular_header; /* output stream id */ gpr_uint32 stream_id; gpr_slice_buffer *output; @@ -361,6 +363,15 @@ static grpc_mdelem *hpack_enc(grpc_chttp2_hpack_compressor *c, gpr_uint32 indices_key; int should_add_elem; + GPR_ASSERT (GPR_SLICE_LENGTH(elem->key->slice) > 0); + if (GPR_SLICE_START_PTR(elem->key->slice)[0] != ':') { /* regular header */ + st->seen_regular_header = 1; + } else if (st->seen_regular_header != 0) { /* reserved header */ + gpr_log(GPR_ERROR, + "Reserved header (colon-prefixed) happening after regular ones."); + abort(); + } + inc_filter(HASH_FRAGMENT_1(elem_hash), &c->filter_elems_sum, c->filter_elems); /* is this elem currently in the decoders table? */ @@ -566,6 +577,7 @@ void grpc_chttp2_encode(grpc_stream_op *ops, size_t ops_count, int eof, st.cur_frame_type = NONE; st.last_was_header = 0; + st.seen_regular_header = 0; st.stream_id = stream_id; st.output = output; diff --git a/src/cpp/server/create_default_thread_pool.cc b/src/cpp/server/create_default_thread_pool.cc index 0eef7dfffe..f3b07ec8ce 100644 --- a/src/cpp/server/create_default_thread_pool.cc +++ b/src/cpp/server/create_default_thread_pool.cc @@ -32,7 +32,8 @@ */ #include <grpc/support/cpu.h> -#include <grpc++/support/dynamic_thread_pool.h> + +#include "src/cpp/server/dynamic_thread_pool.h" #ifndef GRPC_CUSTOM_DEFAULT_THREAD_POOL diff --git a/src/cpp/server/dynamic_thread_pool.cc b/src/cpp/server/dynamic_thread_pool.cc index d33852364d..4b226c2992 100644 --- a/src/cpp/server/dynamic_thread_pool.cc +++ b/src/cpp/server/dynamic_thread_pool.cc @@ -33,7 +33,8 @@ #include <grpc++/impl/sync.h> #include <grpc++/impl/thd.h> -#include <grpc++/support/dynamic_thread_pool.h> + +#include "src/cpp/server/dynamic_thread_pool.h" namespace grpc { DynamicThreadPool::DynamicThread::DynamicThread(DynamicThreadPool* pool) diff --git a/src/cpp/server/dynamic_thread_pool.h b/src/cpp/server/dynamic_thread_pool.h new file mode 100644 index 0000000000..a4683eefc4 --- /dev/null +++ b/src/cpp/server/dynamic_thread_pool.h @@ -0,0 +1,84 @@ +/* + * + * 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. + * + */ + +#ifndef GRPC_INTERNAL_CPP_DYNAMIC_THREAD_POOL_H +#define GRPC_INTERNAL_CPP_DYNAMIC_THREAD_POOL_H + +#include <grpc++/config.h> + +#include <grpc++/impl/sync.h> +#include <grpc++/impl/thd.h> + +#include <list> +#include <memory> +#include <queue> + +#include "src/cpp/server/thread_pool_interface.h" + +namespace grpc { + +class DynamicThreadPool GRPC_FINAL : public ThreadPoolInterface { + public: + explicit DynamicThreadPool(int reserve_threads); + ~DynamicThreadPool(); + + void Add(const std::function<void()>& callback) GRPC_OVERRIDE; + + private: + class DynamicThread { + public: + DynamicThread(DynamicThreadPool* pool); + ~DynamicThread(); + + private: + DynamicThreadPool* pool_; + std::unique_ptr<grpc::thread> thd_; + void ThreadFunc(); + }; + grpc::mutex mu_; + grpc::condition_variable cv_; + grpc::condition_variable shutdown_cv_; + bool shutdown_; + std::queue<std::function<void()>> callbacks_; + int reserve_threads_; + int nthreads_; + int threads_waiting_; + std::list<DynamicThread*> dead_threads_; + + void ThreadFunc(); + static void ReapThreads(std::list<DynamicThread*>* tlist); +}; + +} // namespace grpc + +#endif // GRPC_INTERNAL_CPP_DYNAMIC_THREAD_POOL_H diff --git a/src/cpp/server/fixed_size_thread_pool.cc b/src/cpp/server/fixed_size_thread_pool.cc index 427e904449..2bdc44be2e 100644 --- a/src/cpp/server/fixed_size_thread_pool.cc +++ b/src/cpp/server/fixed_size_thread_pool.cc @@ -33,7 +33,7 @@ #include <grpc++/impl/sync.h> #include <grpc++/impl/thd.h> -#include <grpc++/support/fixed_size_thread_pool.h> +#include "src/cpp/server/fixed_size_thread_pool.h" namespace grpc { diff --git a/src/cpp/server/fixed_size_thread_pool.h b/src/cpp/server/fixed_size_thread_pool.h new file mode 100644 index 0000000000..65d3134ec4 --- /dev/null +++ b/src/cpp/server/fixed_size_thread_pool.h @@ -0,0 +1,68 @@ +/* + * + * 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. + * + */ + +#ifndef GRPC_INTERNAL_CPP_FIXED_SIZE_THREAD_POOL_H +#define GRPC_INTERNAL_CPP_FIXED_SIZE_THREAD_POOL_H + +#include <grpc++/config.h> + +#include <grpc++/impl/sync.h> +#include <grpc++/impl/thd.h> + +#include <queue> +#include <vector> + +#include "src/cpp/server/thread_pool_interface.h" + +namespace grpc { + +class FixedSizeThreadPool GRPC_FINAL : public ThreadPoolInterface { + public: + explicit FixedSizeThreadPool(int num_threads); + ~FixedSizeThreadPool(); + + void Add(const std::function<void()>& callback) GRPC_OVERRIDE; + + private: + grpc::mutex mu_; + grpc::condition_variable cv_; + bool shutdown_; + std::queue<std::function<void()>> callbacks_; + std::vector<grpc::thread*> threads_; + + void ThreadFunc(); +}; + +} // namespace grpc + +#endif // GRPC_INTERNAL_CPP_FIXED_SIZE_THREAD_POOL_H diff --git a/src/cpp/server/server.cc b/src/cpp/server/server.cc index 568bb8ad5e..66cd27cc33 100644 --- a/src/cpp/server/server.cc +++ b/src/cpp/server/server.cc @@ -44,13 +44,59 @@ #include <grpc++/impl/service_type.h> #include <grpc++/server_context.h> #include <grpc++/server_credentials.h> -#include <grpc++/support/thread_pool_interface.h> #include <grpc++/support/time.h> #include "src/core/profiling/timers.h" +#include "src/cpp/server/thread_pool_interface.h" namespace grpc { +class Server::UnimplementedAsyncRequestContext { + protected: + UnimplementedAsyncRequestContext() : generic_stream_(&server_context_) {} + + GenericServerContext server_context_; + GenericServerAsyncReaderWriter generic_stream_; +}; + +class Server::UnimplementedAsyncRequest GRPC_FINAL + : public UnimplementedAsyncRequestContext, + public GenericAsyncRequest { + public: + UnimplementedAsyncRequest(Server* server, ServerCompletionQueue* cq) + : GenericAsyncRequest(server, &server_context_, &generic_stream_, cq, cq, + NULL, false), + server_(server), + cq_(cq) {} + + bool FinalizeResult(void** tag, bool* status) GRPC_OVERRIDE; + + ServerContext* context() { return &server_context_; } + GenericServerAsyncReaderWriter* stream() { return &generic_stream_; } + + private: + Server* const server_; + ServerCompletionQueue* const cq_; +}; + +typedef SneakyCallOpSet<CallOpSendInitialMetadata, CallOpServerSendStatus> + UnimplementedAsyncResponseOp; +class Server::UnimplementedAsyncResponse GRPC_FINAL + : public UnimplementedAsyncResponseOp { + public: + UnimplementedAsyncResponse(UnimplementedAsyncRequest* request); + ~UnimplementedAsyncResponse() { delete request_; } + + bool FinalizeResult(void** tag, bool* status) GRPC_OVERRIDE { + bool r = UnimplementedAsyncResponseOp::FinalizeResult(tag, status); + delete this; + return r; + } + + private: + UnimplementedAsyncRequest* const request_; +}; + class Server::ShutdownRequest GRPC_FINAL : public CompletionQueueTag { public: bool FinalizeResult(void** tag, bool* status) { @@ -298,18 +344,23 @@ int Server::AddListeningPort(const grpc::string& addr, return creds->AddPortToServer(addr, server_); } -bool Server::Start() { +bool Server::Start(ServerCompletionQueue** cqs, size_t num_cqs) { GPR_ASSERT(!started_); started_ = true; grpc_server_start(server_); if (!has_generic_service_) { - unknown_method_.reset(new RpcServiceMethod( - "unknown", RpcMethod::BIDI_STREAMING, new UnknownMethodHandler)); - // Use of emplace_back with just constructor arguments is not accepted here - // by gcc-4.4 because it can't match the anonymous nullptr with a proper - // constructor implicitly. Construct the object and use push_back. - sync_methods_->push_back(SyncRequest(unknown_method_.get(), nullptr)); + if (!sync_methods_->empty()) { + unknown_method_.reset(new RpcServiceMethod( + "unknown", RpcMethod::BIDI_STREAMING, new UnknownMethodHandler)); + // Use of emplace_back with just constructor arguments is not accepted + // here by gcc-4.4 because it can't match the anonymous nullptr with a + // proper constructor implicitly. Construct the object and use push_back. + sync_methods_->push_back(SyncRequest(unknown_method_.get(), nullptr)); + } + for (size_t i = 0; i < num_cqs; i++) { + new UnimplementedAsyncRequest(this, cqs[i]); + } } // Start processing rpcs. if (!sync_methods_->empty()) { @@ -371,12 +422,14 @@ void Server::PerformOpsOnCall(CallOpSetInterface* ops, Call* call) { Server::BaseAsyncRequest::BaseAsyncRequest( Server* server, ServerContext* context, - ServerAsyncStreamingInterface* stream, CompletionQueue* call_cq, void* tag) + ServerAsyncStreamingInterface* stream, CompletionQueue* call_cq, void* tag, + bool delete_on_finalize) : server_(server), context_(context), stream_(stream), call_cq_(call_cq), tag_(tag), + delete_on_finalize_(delete_on_finalize), call_(nullptr) { memset(&initial_metadata_array_, 0, sizeof(initial_metadata_array_)); } @@ -403,14 +456,16 @@ bool Server::BaseAsyncRequest::FinalizeResult(void** tag, bool* status) { // just the pointers inside call are copied here stream_->BindCall(&call); *tag = tag_; - delete this; + if (delete_on_finalize_) { + delete this; + } return true; } Server::RegisteredAsyncRequest::RegisteredAsyncRequest( Server* server, ServerContext* context, ServerAsyncStreamingInterface* stream, CompletionQueue* call_cq, void* tag) - : BaseAsyncRequest(server, context, stream, call_cq, tag) {} + : BaseAsyncRequest(server, context, stream, call_cq, tag, true) {} void Server::RegisteredAsyncRequest::IssueRequest( void* registered_method, grpc_byte_buffer** payload, @@ -424,8 +479,9 @@ void Server::RegisteredAsyncRequest::IssueRequest( Server::GenericAsyncRequest::GenericAsyncRequest( Server* server, GenericServerContext* context, ServerAsyncStreamingInterface* stream, CompletionQueue* call_cq, - ServerCompletionQueue* notification_cq, void* tag) - : BaseAsyncRequest(server, context, stream, call_cq, tag) { + ServerCompletionQueue* notification_cq, void* tag, bool delete_on_finalize) + : BaseAsyncRequest(server, context, stream, call_cq, tag, + delete_on_finalize) { grpc_call_details_init(&call_details_); GPR_ASSERT(notification_cq); GPR_ASSERT(call_cq); @@ -446,6 +502,25 @@ bool Server::GenericAsyncRequest::FinalizeResult(void** tag, bool* status) { return BaseAsyncRequest::FinalizeResult(tag, status); } +bool Server::UnimplementedAsyncRequest::FinalizeResult(void** tag, + bool* status) { + if (GenericAsyncRequest::FinalizeResult(tag, status) && *status) { + new UnimplementedAsyncRequest(server_, cq_); + new UnimplementedAsyncResponse(this); + } else { + delete this; + } + return false; +} + +Server::UnimplementedAsyncResponse::UnimplementedAsyncResponse( + UnimplementedAsyncRequest* request) + : request_(request) { + Status status(StatusCode::UNIMPLEMENTED, ""); + UnknownMethodHandler::FillOps(request_->context(), this); + request_->stream()->call_.PerformOps(this); +} + void Server::ScheduleCallback() { { grpc::unique_lock<grpc::mutex> lock(mu_); diff --git a/src/cpp/server/server_builder.cc b/src/cpp/server/server_builder.cc index 4e8399405e..b739cbfe62 100644 --- a/src/cpp/server/server_builder.cc +++ b/src/cpp/server/server_builder.cc @@ -37,8 +37,8 @@ #include <grpc/support/log.h> #include <grpc++/impl/service_type.h> #include <grpc++/server.h> -#include <grpc++/support/thread_pool_interface.h> -#include <grpc++/support/fixed_size_thread_pool.h> +#include "src/cpp/server/thread_pool_interface.h" +#include "src/cpp/server/fixed_size_thread_pool.h" namespace grpc { @@ -89,10 +89,6 @@ void ServerBuilder::AddListeningPort(const grpc::string& addr, ports_.push_back(port); } -void ServerBuilder::SetThreadPool(ThreadPoolInterface* thread_pool) { - thread_pool_ = thread_pool; -} - std::unique_ptr<Server> ServerBuilder::BuildAndStart() { bool thread_pool_owned = false; if (!async_services_.empty() && !services_.empty()) { @@ -103,12 +99,6 @@ std::unique_ptr<Server> ServerBuilder::BuildAndStart() { thread_pool_ = CreateDefaultThreadPool(); thread_pool_owned = true; } - // Async services only, create a thread pool to handle requests to unknown - // services. - if (!thread_pool_ && !generic_service_ && !async_services_.empty()) { - thread_pool_ = new FixedSizeThreadPool(1); - thread_pool_owned = true; - } std::unique_ptr<Server> server( new Server(thread_pool_, thread_pool_owned, max_message_size_)); for (auto cq = cqs_.begin(); cq != cqs_.end(); ++cq) { @@ -138,7 +128,7 @@ std::unique_ptr<Server> ServerBuilder::BuildAndStart() { *port->selected_port = r; } } - if (!server->Start()) { + if (!server->Start(&cqs_[0], cqs_.size())) { return nullptr; } return server; diff --git a/src/cpp/server/thread_pool_interface.h b/src/cpp/server/thread_pool_interface.h new file mode 100644 index 0000000000..1ebe30fe2a --- /dev/null +++ b/src/cpp/server/thread_pool_interface.h @@ -0,0 +1,54 @@ +/* + * + * 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. + * + */ + +#ifndef GRPC_INTERNAL_CPP_THREAD_POOL_INTERFACE_H +#define GRPC_INTERNAL_CPP_THREAD_POOL_INTERFACE_H + +#include <functional> + +namespace grpc { + +// A thread pool interface for running callbacks. +class ThreadPoolInterface { + public: + virtual ~ThreadPoolInterface() {} + + // Schedule the given callback for execution. + virtual void Add(const std::function<void()>& callback) = 0; +}; + +ThreadPoolInterface* CreateDefaultThreadPool(); + +} // namespace grpc + +#endif // GRPC_INTERNAL_CPP_THREAD_POOL_INTERFACE_H diff --git a/src/csharp/.gitignore b/src/csharp/.gitignore index ae48956567..48365e32a5 100644 --- a/src/csharp/.gitignore +++ b/src/csharp/.gitignore @@ -5,4 +5,5 @@ test-results packages Grpc.v12.suo TestResult.xml +/TestResults *.nupkg diff --git a/src/csharp/Grpc.Core.Tests/ChannelTest.cs b/src/csharp/Grpc.Core.Tests/ChannelTest.cs index 2787572924..dfbd92879e 100644 --- a/src/csharp/Grpc.Core.Tests/ChannelTest.cs +++ b/src/csharp/Grpc.Core.Tests/ChannelTest.cs @@ -41,12 +41,6 @@ namespace Grpc.Core.Tests { public class ChannelTest { - [TestFixtureTearDown] - public void CleanupClass() - { - GrpcEnvironment.Shutdown(); - } - [Test] public void Constructor_RejectsInvalidParams() { @@ -56,36 +50,33 @@ namespace Grpc.Core.Tests [Test] public void State_IdleAfterCreation() { - using (var channel = new Channel("localhost", Credentials.Insecure)) - { - Assert.AreEqual(ChannelState.Idle, channel.State); - } + var channel = new Channel("localhost", Credentials.Insecure); + Assert.AreEqual(ChannelState.Idle, channel.State); + channel.ShutdownAsync().Wait(); } [Test] public void WaitForStateChangedAsync_InvalidArgument() { - using (var channel = new Channel("localhost", Credentials.Insecure)) - { - Assert.Throws(typeof(ArgumentException), () => channel.WaitForStateChangedAsync(ChannelState.FatalFailure)); - } + var channel = new Channel("localhost", Credentials.Insecure); + Assert.Throws(typeof(ArgumentException), () => channel.WaitForStateChangedAsync(ChannelState.FatalFailure)); + channel.ShutdownAsync().Wait(); } [Test] public void ResolvedTarget() { - using (var channel = new Channel("127.0.0.1", Credentials.Insecure)) - { - Assert.IsTrue(channel.ResolvedTarget.Contains("127.0.0.1")); - } + var channel = new Channel("127.0.0.1", Credentials.Insecure); + Assert.IsTrue(channel.ResolvedTarget.Contains("127.0.0.1")); + channel.ShutdownAsync().Wait(); } [Test] - public void Dispose_IsIdempotent() + public void Shutdown_AllowedOnlyOnce() { var channel = new Channel("localhost", Credentials.Insecure); - channel.Dispose(); - channel.Dispose(); + channel.ShutdownAsync().Wait(); + Assert.Throws(typeof(InvalidOperationException), () => channel.ShutdownAsync().GetAwaiter().GetResult()); } } } diff --git a/src/csharp/Grpc.Core.Tests/ClientServerTest.cs b/src/csharp/Grpc.Core.Tests/ClientServerTest.cs index e49fdb5268..68279a2007 100644 --- a/src/csharp/Grpc.Core.Tests/ClientServerTest.cs +++ b/src/csharp/Grpc.Core.Tests/ClientServerTest.cs @@ -63,16 +63,10 @@ namespace Grpc.Core.Tests [TearDown] public void Cleanup() { - channel.Dispose(); + channel.ShutdownAsync().Wait(); server.ShutdownAsync().Wait(); } - [TestFixtureTearDown] - public void CleanupClass() - { - GrpcEnvironment.Shutdown(); - } - [Test] public async Task UnaryCall() { @@ -208,13 +202,6 @@ namespace Grpc.Core.Tests } [Test] - public void UnaryCall_DisposedChannel() - { - channel.Dispose(); - Assert.Throws(typeof(ObjectDisposedException), () => Calls.BlockingUnaryCall(helper.CreateUnaryCall(), "ABC")); - } - - [Test] public void UnaryCallPerformance() { helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => diff --git a/src/csharp/Grpc.Core.Tests/CompressionTest.cs b/src/csharp/Grpc.Core.Tests/CompressionTest.cs index 9547683f60..378c81851c 100644 --- a/src/csharp/Grpc.Core.Tests/CompressionTest.cs +++ b/src/csharp/Grpc.Core.Tests/CompressionTest.cs @@ -62,16 +62,10 @@ namespace Grpc.Core.Tests [TearDown] public void Cleanup() { - channel.Dispose(); + channel.ShutdownAsync().Wait(); server.ShutdownAsync().Wait(); } - [TestFixtureTearDown] - public void CleanupClass() - { - GrpcEnvironment.Shutdown(); - } - [Test] public void WriteOptions_Unary() { diff --git a/src/csharp/Grpc.Core.Tests/ContextPropagationTest.cs b/src/csharp/Grpc.Core.Tests/ContextPropagationTest.cs index db5f953b0e..2db3f286f7 100644 --- a/src/csharp/Grpc.Core.Tests/ContextPropagationTest.cs +++ b/src/csharp/Grpc.Core.Tests/ContextPropagationTest.cs @@ -62,16 +62,10 @@ namespace Grpc.Core.Tests [TearDown] public void Cleanup() { - channel.Dispose(); + channel.ShutdownAsync().Wait(); server.ShutdownAsync().Wait(); } - [TestFixtureTearDown] - public void CleanupClass() - { - GrpcEnvironment.Shutdown(); - } - [Test] public async Task PropagateCancellation() { diff --git a/src/csharp/Grpc.Core.Tests/Grpc.Core.Tests.csproj b/src/csharp/Grpc.Core.Tests/Grpc.Core.Tests.csproj index 829effc9a2..b571fe9025 100644 --- a/src/csharp/Grpc.Core.Tests/Grpc.Core.Tests.csproj +++ b/src/csharp/Grpc.Core.Tests/Grpc.Core.Tests.csproj @@ -64,6 +64,8 @@ <Link>Version.cs</Link> </Compile> <Compile Include="ClientBaseTest.cs" /> + <Compile Include="ShutdownTest.cs" /> + <Compile Include="Internal\AsyncCallTest.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="ClientServerTest.cs" /> <Compile Include="ServerTest.cs" /> diff --git a/src/csharp/Grpc.Core.Tests/GrpcEnvironmentTest.cs b/src/csharp/Grpc.Core.Tests/GrpcEnvironmentTest.cs index 4ed93c7eca..78295cf6d4 100644 --- a/src/csharp/Grpc.Core.Tests/GrpcEnvironmentTest.cs +++ b/src/csharp/Grpc.Core.Tests/GrpcEnvironmentTest.cs @@ -43,31 +43,40 @@ namespace Grpc.Core.Tests [Test] public void InitializeAndShutdownGrpcEnvironment() { - var env = GrpcEnvironment.GetInstance(); + var env = GrpcEnvironment.AddRef(); Assert.IsNotNull(env.CompletionQueue); - GrpcEnvironment.Shutdown(); + GrpcEnvironment.Release(); } [Test] public void SubsequentInvocations() { - var env1 = GrpcEnvironment.GetInstance(); - var env2 = GrpcEnvironment.GetInstance(); - Assert.IsTrue(object.ReferenceEquals(env1, env2)); - GrpcEnvironment.Shutdown(); - GrpcEnvironment.Shutdown(); + var env1 = GrpcEnvironment.AddRef(); + var env2 = GrpcEnvironment.AddRef(); + Assert.AreSame(env1, env2); + GrpcEnvironment.Release(); + GrpcEnvironment.Release(); } [Test] public void InitializeAfterShutdown() { - var env1 = GrpcEnvironment.GetInstance(); - GrpcEnvironment.Shutdown(); + Assert.AreEqual(0, GrpcEnvironment.GetRefCount()); - var env2 = GrpcEnvironment.GetInstance(); - GrpcEnvironment.Shutdown(); + var env1 = GrpcEnvironment.AddRef(); + GrpcEnvironment.Release(); - Assert.IsFalse(object.ReferenceEquals(env1, env2)); + var env2 = GrpcEnvironment.AddRef(); + GrpcEnvironment.Release(); + + Assert.AreNotSame(env1, env2); + } + + [Test] + public void ReleaseWithoutAddRef() + { + Assert.AreEqual(0, GrpcEnvironment.GetRefCount()); + Assert.Throws(typeof(InvalidOperationException), () => GrpcEnvironment.Release()); } [Test] diff --git a/src/csharp/Grpc.Core.Tests/Internal/AsyncCallTest.cs b/src/csharp/Grpc.Core.Tests/Internal/AsyncCallTest.cs new file mode 100644 index 0000000000..685c5f7d6c --- /dev/null +++ b/src/csharp/Grpc.Core.Tests/Internal/AsyncCallTest.cs @@ -0,0 +1,222 @@ +#region Copyright notice and license + +// 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. + +#endregion + +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +using Grpc.Core.Internal; +using NUnit.Framework; + +namespace Grpc.Core.Internal.Tests +{ + public class AsyncCallTest + { + Channel channel; + FakeNativeCall fakeCall; + AsyncCall<string, string> asyncCall; + + [SetUp] + public void Init() + { + channel = new Channel("localhost", Credentials.Insecure); + + fakeCall = new FakeNativeCall(); + + var callDetails = new CallInvocationDetails<string, string>(channel, "someMethod", null, Marshallers.StringMarshaller, Marshallers.StringMarshaller, new CallOptions()); + asyncCall = new AsyncCall<string, string>(callDetails, fakeCall); + } + + [TearDown] + public void Cleanup() + { + channel.ShutdownAsync().Wait(); + } + + [Test] + public void AsyncUnary_CompletionSuccess() + { + 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()); + } + + [Test] + public void AsyncUnary_CompletionFailure() + { + 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()); + var ex = Assert.Throws<RpcException>(() => resultTask.GetAwaiter().GetResult()); + Assert.AreEqual(StatusCode.Internal, ex.Status.StatusCode); + } + + internal class FakeNativeCall : INativeCall + { + public UnaryResponseClientHandler UnaryResponseClientHandler + { + get; + set; + } + + public ReceivedStatusOnClientHandler ReceivedStatusOnClientHandler + { + get; + set; + } + + public ReceivedMessageHandler ReceivedMessageHandler + { + get; + set; + } + + public ReceivedResponseHeadersHandler ReceivedResponseHeadersHandler + { + get; + set; + } + + public SendCompletionHandler SendCompletionHandler + { + get; + set; + } + + public ReceivedCloseOnServerHandler ReceivedCloseOnServerHandler + { + get; + set; + } + + public bool IsCancelled + { + get; + set; + } + + public bool IsDisposed + { + get; + set; + } + + public void Cancel() + { + IsCancelled = true; + } + + public void CancelWithStatus(Status status) + { + IsCancelled = true; + } + + public string GetPeer() + { + return "PEER"; + } + + public void StartUnary(UnaryResponseClientHandler callback, byte[] payload, MetadataArraySafeHandle metadataArray, WriteFlags writeFlags) + { + UnaryResponseClientHandler = callback; + } + + public void StartUnary(BatchContextSafeHandle ctx, byte[] payload, MetadataArraySafeHandle metadataArray, WriteFlags writeFlags) + { + throw new NotImplementedException(); + } + + public void StartClientStreaming(UnaryResponseClientHandler callback, MetadataArraySafeHandle metadataArray) + { + UnaryResponseClientHandler = callback; + } + + public void StartServerStreaming(ReceivedStatusOnClientHandler callback, byte[] payload, MetadataArraySafeHandle metadataArray, WriteFlags writeFlags) + { + ReceivedStatusOnClientHandler = callback; + } + + public void StartDuplexStreaming(ReceivedStatusOnClientHandler callback, MetadataArraySafeHandle metadataArray) + { + ReceivedStatusOnClientHandler = callback; + } + + public void StartReceiveMessage(ReceivedMessageHandler callback) + { + ReceivedMessageHandler = callback; + } + + public void StartReceiveInitialMetadata(ReceivedResponseHeadersHandler callback) + { + ReceivedResponseHeadersHandler = callback; + } + + public void StartSendInitialMetadata(SendCompletionHandler callback, MetadataArraySafeHandle metadataArray) + { + SendCompletionHandler = callback; + } + + public void StartSendMessage(SendCompletionHandler callback, byte[] payload, WriteFlags writeFlags, bool sendEmptyInitialMetadata) + { + SendCompletionHandler = callback; + } + + public void StartSendCloseFromClient(SendCompletionHandler callback) + { + SendCompletionHandler = callback; + } + + public void StartSendStatusFromServer(SendCompletionHandler callback, Status status, MetadataArraySafeHandle metadataArray, bool sendEmptyInitialMetadata) + { + SendCompletionHandler = callback; + } + + public void StartServerSide(ReceivedCloseOnServerHandler callback) + { + ReceivedCloseOnServerHandler = callback; + } + + public void Dispose() + { + IsDisposed = true; + } + } + } +}
\ No newline at end of file diff --git a/src/csharp/Grpc.Core.Tests/ResponseHeadersTest.cs b/src/csharp/Grpc.Core.Tests/ResponseHeadersTest.cs index 981b8ea3c8..a1648f3671 100644 --- a/src/csharp/Grpc.Core.Tests/ResponseHeadersTest.cs +++ b/src/csharp/Grpc.Core.Tests/ResponseHeadersTest.cs @@ -32,13 +32,16 @@ #endregion using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; + using Grpc.Core; using Grpc.Core.Internal; using Grpc.Core.Utils; + using NUnit.Framework; namespace Grpc.Core.Tests @@ -69,14 +72,82 @@ namespace Grpc.Core.Tests [TearDown] public void Cleanup() { - channel.Dispose(); + channel.ShutdownAsync().Wait(); server.ShutdownAsync().Wait(); } - [TestFixtureTearDown] - public void CleanupClass() + [Test] + public async Task ResponseHeadersAsync_UnaryCall() + { + helper.UnaryHandler = new UnaryServerMethod<string, string>(async (request, context) => + { + await context.WriteResponseHeadersAsync(headers); + return "PASS"; + }); + + var call = Calls.AsyncUnaryCall(helper.CreateUnaryCall(), ""); + var responseHeaders = await call.ResponseHeadersAsync; + + Assert.AreEqual(headers.Count, responseHeaders.Count); + Assert.AreEqual("ascii-header", responseHeaders[0].Key); + Assert.AreEqual("abcdefg", responseHeaders[0].Value); + + Assert.AreEqual("PASS", await call.ResponseAsync); + } + + [Test] + public async Task ResponseHeadersAsync_ClientStreamingCall() + { + helper.ClientStreamingHandler = new ClientStreamingServerMethod<string, string>(async (requestStream, context) => + { + await context.WriteResponseHeadersAsync(headers); + return "PASS"; + }); + + var call = Calls.AsyncClientStreamingCall(helper.CreateClientStreamingCall()); + await call.RequestStream.CompleteAsync(); + var responseHeaders = await call.ResponseHeadersAsync; + + Assert.AreEqual("ascii-header", responseHeaders[0].Key); + Assert.AreEqual("PASS", await call.ResponseAsync); + } + + [Test] + public async Task ResponseHeadersAsync_ServerStreamingCall() + { + helper.ServerStreamingHandler = new ServerStreamingServerMethod<string, string>(async (request, responseStream, context) => + { + await context.WriteResponseHeadersAsync(headers); + await responseStream.WriteAsync("PASS"); + }); + + var call = Calls.AsyncServerStreamingCall(helper.CreateServerStreamingCall(), ""); + var responseHeaders = await call.ResponseHeadersAsync; + + Assert.AreEqual("ascii-header", responseHeaders[0].Key); + CollectionAssert.AreEqual(new[] { "PASS" }, await call.ResponseStream.ToListAsync()); + } + + [Test] + public async Task ResponseHeadersAsync_DuplexStreamingCall() { - GrpcEnvironment.Shutdown(); + helper.DuplexStreamingHandler = new DuplexStreamingServerMethod<string, string>(async (requestStream, responseStream, context) => + { + await context.WriteResponseHeadersAsync(headers); + while (await requestStream.MoveNext()) + { + await responseStream.WriteAsync(requestStream.Current); + } + }); + + var call = Calls.AsyncDuplexStreamingCall(helper.CreateDuplexStreamingCall()); + var responseHeaders = await call.ResponseHeadersAsync; + + var messages = new[] { "PASS" }; + await call.RequestStream.WriteAllAsync(messages); + + Assert.AreEqual("ascii-header", responseHeaders[0].Key); + CollectionAssert.AreEqual(messages, await call.ResponseStream.ToListAsync()); } [Test] diff --git a/src/csharp/Grpc.Core.Tests/ServerTest.cs b/src/csharp/Grpc.Core.Tests/ServerTest.cs index 485006ebac..e7193c843b 100644 --- a/src/csharp/Grpc.Core.Tests/ServerTest.cs +++ b/src/csharp/Grpc.Core.Tests/ServerTest.cs @@ -51,7 +51,6 @@ namespace Grpc.Core.Tests }; server.Start(); server.ShutdownAsync().Wait(); - GrpcEnvironment.Shutdown(); } [Test] @@ -67,8 +66,7 @@ namespace Grpc.Core.Tests Assert.Greater(boundPort.BoundPort, 0); server.Start(); - server.ShutdownAsync(); - GrpcEnvironment.Shutdown(); + server.ShutdownAsync().Wait(); } [Test] @@ -83,7 +81,6 @@ namespace Grpc.Core.Tests Assert.Throws(typeof(InvalidOperationException), () => server.Services.Add(ServerServiceDefinition.CreateBuilder("serviceName").Build())); server.ShutdownAsync().Wait(); - GrpcEnvironment.Shutdown(); } } } diff --git a/src/csharp/Grpc.Core.Tests/ShutdownTest.cs b/src/csharp/Grpc.Core.Tests/ShutdownTest.cs new file mode 100644 index 0000000000..a2be7ddd5e --- /dev/null +++ b/src/csharp/Grpc.Core.Tests/ShutdownTest.cs @@ -0,0 +1,77 @@ +#region Copyright notice and license + +// 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. + +#endregion + +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; +using Grpc.Core.Internal; +using Grpc.Core.Utils; +using NUnit.Framework; + +namespace Grpc.Core.Tests +{ + public class ShutdownTest + { + const string Host = "127.0.0.1"; + + MockServiceHelper helper; + Server server; + Channel channel; + + [SetUp] + public void Init() + { + helper = new MockServiceHelper(Host); + server = helper.GetServer(); + server.Start(); + channel = helper.GetChannel(); + } + + [Test] + public async Task AbandonedCall() + { + helper.DuplexStreamingHandler = new DuplexStreamingServerMethod<string, string>(async (requestStream, responseStream, context) => + { + await requestStream.ToListAsync(); + }); + + var call = Calls.AsyncDuplexStreamingCall(helper.CreateDuplexStreamingCall(new CallOptions(deadline: DateTime.UtcNow.AddMilliseconds(1)))); + + channel.ShutdownAsync().Wait(); + server.ShutdownAsync().Wait(); + } + } +} diff --git a/src/csharp/Grpc.Core.Tests/TimeoutsTest.cs b/src/csharp/Grpc.Core.Tests/TimeoutsTest.cs index d875d601b9..41f661f62d 100644 --- a/src/csharp/Grpc.Core.Tests/TimeoutsTest.cs +++ b/src/csharp/Grpc.Core.Tests/TimeoutsTest.cs @@ -65,16 +65,10 @@ namespace Grpc.Core.Tests [TearDown] public void Cleanup() { - channel.Dispose(); + channel.ShutdownAsync().Wait(); server.ShutdownAsync().Wait(); } - [TestFixtureTearDown] - public void CleanupClass() - { - GrpcEnvironment.Shutdown(); - } - [Test] public void InfiniteDeadline() { diff --git a/src/csharp/Grpc.Core/AsyncClientStreamingCall.cs b/src/csharp/Grpc.Core/AsyncClientStreamingCall.cs index fb9b562c77..dbaa3085c5 100644 --- a/src/csharp/Grpc.Core/AsyncClientStreamingCall.cs +++ b/src/csharp/Grpc.Core/AsyncClientStreamingCall.cs @@ -44,14 +44,16 @@ namespace Grpc.Core { readonly IClientStreamWriter<TRequest> requestStream; readonly Task<TResponse> responseAsync; + readonly Task<Metadata> responseHeadersAsync; readonly Func<Status> getStatusFunc; readonly Func<Metadata> getTrailersFunc; readonly Action disposeAction; - public AsyncClientStreamingCall(IClientStreamWriter<TRequest> requestStream, Task<TResponse> responseAsync, Func<Status> getStatusFunc, Func<Metadata> getTrailersFunc, Action disposeAction) + public AsyncClientStreamingCall(IClientStreamWriter<TRequest> requestStream, Task<TResponse> responseAsync, Task<Metadata> responseHeadersAsync, Func<Status> getStatusFunc, Func<Metadata> getTrailersFunc, Action disposeAction) { this.requestStream = requestStream; this.responseAsync = responseAsync; + this.responseHeadersAsync = responseHeadersAsync; this.getStatusFunc = getStatusFunc; this.getTrailersFunc = getTrailersFunc; this.disposeAction = disposeAction; @@ -69,6 +71,17 @@ namespace Grpc.Core } /// <summary> + /// Asynchronous access to response headers. + /// </summary> + public Task<Metadata> ResponseHeadersAsync + { + get + { + return this.responseHeadersAsync; + } + } + + /// <summary> /// Async stream to send streaming requests. /// </summary> public IClientStreamWriter<TRequest> RequestStream diff --git a/src/csharp/Grpc.Core/AsyncDuplexStreamingCall.cs b/src/csharp/Grpc.Core/AsyncDuplexStreamingCall.cs index 183c84216a..ee7ba29695 100644 --- a/src/csharp/Grpc.Core/AsyncDuplexStreamingCall.cs +++ b/src/csharp/Grpc.Core/AsyncDuplexStreamingCall.cs @@ -32,6 +32,7 @@ #endregion using System; +using System.Threading.Tasks; namespace Grpc.Core { @@ -42,14 +43,16 @@ namespace Grpc.Core { readonly IClientStreamWriter<TRequest> requestStream; readonly IAsyncStreamReader<TResponse> responseStream; + readonly Task<Metadata> responseHeadersAsync; readonly Func<Status> getStatusFunc; readonly Func<Metadata> getTrailersFunc; readonly Action disposeAction; - public AsyncDuplexStreamingCall(IClientStreamWriter<TRequest> requestStream, IAsyncStreamReader<TResponse> responseStream, Func<Status> getStatusFunc, Func<Metadata> getTrailersFunc, Action disposeAction) + public AsyncDuplexStreamingCall(IClientStreamWriter<TRequest> requestStream, IAsyncStreamReader<TResponse> responseStream, Task<Metadata> responseHeadersAsync, Func<Status> getStatusFunc, Func<Metadata> getTrailersFunc, Action disposeAction) { this.requestStream = requestStream; this.responseStream = responseStream; + this.responseHeadersAsync = responseHeadersAsync; this.getStatusFunc = getStatusFunc; this.getTrailersFunc = getTrailersFunc; this.disposeAction = disposeAction; @@ -78,6 +81,17 @@ namespace Grpc.Core } /// <summary> + /// Asynchronous access to response headers. + /// </summary> + public Task<Metadata> ResponseHeadersAsync + { + get + { + return this.responseHeadersAsync; + } + } + + /// <summary> /// Gets the call status if the call has already finished. /// Throws InvalidOperationException otherwise. /// </summary> diff --git a/src/csharp/Grpc.Core/AsyncServerStreamingCall.cs b/src/csharp/Grpc.Core/AsyncServerStreamingCall.cs index ab2049f269..2853a79ce6 100644 --- a/src/csharp/Grpc.Core/AsyncServerStreamingCall.cs +++ b/src/csharp/Grpc.Core/AsyncServerStreamingCall.cs @@ -32,6 +32,7 @@ #endregion using System; +using System.Threading.Tasks; namespace Grpc.Core { @@ -41,13 +42,15 @@ namespace Grpc.Core public sealed class AsyncServerStreamingCall<TResponse> : IDisposable { readonly IAsyncStreamReader<TResponse> responseStream; + readonly Task<Metadata> responseHeadersAsync; readonly Func<Status> getStatusFunc; readonly Func<Metadata> getTrailersFunc; readonly Action disposeAction; - public AsyncServerStreamingCall(IAsyncStreamReader<TResponse> responseStream, Func<Status> getStatusFunc, Func<Metadata> getTrailersFunc, Action disposeAction) + public AsyncServerStreamingCall(IAsyncStreamReader<TResponse> responseStream, Task<Metadata> responseHeadersAsync, Func<Status> getStatusFunc, Func<Metadata> getTrailersFunc, Action disposeAction) { this.responseStream = responseStream; + this.responseHeadersAsync = responseHeadersAsync; this.getStatusFunc = getStatusFunc; this.getTrailersFunc = getTrailersFunc; this.disposeAction = disposeAction; @@ -65,6 +68,17 @@ namespace Grpc.Core } /// <summary> + /// Asynchronous access to response headers. + /// </summary> + public Task<Metadata> ResponseHeadersAsync + { + get + { + return this.responseHeadersAsync; + } + } + + /// <summary> /// Gets the call status if the call has already finished. /// Throws InvalidOperationException otherwise. /// </summary> diff --git a/src/csharp/Grpc.Core/AsyncUnaryCall.cs b/src/csharp/Grpc.Core/AsyncUnaryCall.cs index 224e343916..154a17a33e 100644 --- a/src/csharp/Grpc.Core/AsyncUnaryCall.cs +++ b/src/csharp/Grpc.Core/AsyncUnaryCall.cs @@ -43,13 +43,15 @@ namespace Grpc.Core public sealed class AsyncUnaryCall<TResponse> : IDisposable { readonly Task<TResponse> responseAsync; + readonly Task<Metadata> responseHeadersAsync; readonly Func<Status> getStatusFunc; readonly Func<Metadata> getTrailersFunc; readonly Action disposeAction; - public AsyncUnaryCall(Task<TResponse> responseAsync, Func<Status> getStatusFunc, Func<Metadata> getTrailersFunc, Action disposeAction) + public AsyncUnaryCall(Task<TResponse> responseAsync, Task<Metadata> responseHeadersAsync, Func<Status> getStatusFunc, Func<Metadata> getTrailersFunc, Action disposeAction) { this.responseAsync = responseAsync; + this.responseHeadersAsync = responseHeadersAsync; this.getStatusFunc = getStatusFunc; this.getTrailersFunc = getTrailersFunc; this.disposeAction = disposeAction; @@ -67,6 +69,17 @@ namespace Grpc.Core } /// <summary> + /// Asynchronous access to response headers. + /// </summary> + public Task<Metadata> ResponseHeadersAsync + { + get + { + return this.responseHeadersAsync; + } + } + + /// <summary> /// Allows awaiting this object directly. /// </summary> public TaskAwaiter<TResponse> GetAwaiter() diff --git a/src/csharp/Grpc.Core/Calls.cs b/src/csharp/Grpc.Core/Calls.cs index 7067456638..e57ac89db3 100644 --- a/src/csharp/Grpc.Core/Calls.cs +++ b/src/csharp/Grpc.Core/Calls.cs @@ -74,7 +74,7 @@ namespace Grpc.Core { var asyncCall = new AsyncCall<TRequest, TResponse>(call); var asyncResult = asyncCall.UnaryCallAsync(req); - return new AsyncUnaryCall<TResponse>(asyncResult, asyncCall.GetStatus, asyncCall.GetTrailers, asyncCall.Cancel); + return new AsyncUnaryCall<TResponse>(asyncResult, asyncCall.ResponseHeadersAsync, asyncCall.GetStatus, asyncCall.GetTrailers, asyncCall.Cancel); } /// <summary> @@ -93,7 +93,7 @@ namespace Grpc.Core var asyncCall = new AsyncCall<TRequest, TResponse>(call); asyncCall.StartServerStreamingCall(req); var responseStream = new ClientResponseStream<TRequest, TResponse>(asyncCall); - return new AsyncServerStreamingCall<TResponse>(responseStream, asyncCall.GetStatus, asyncCall.GetTrailers, asyncCall.Cancel); + return new AsyncServerStreamingCall<TResponse>(responseStream, asyncCall.ResponseHeadersAsync, asyncCall.GetStatus, asyncCall.GetTrailers, asyncCall.Cancel); } /// <summary> @@ -110,7 +110,7 @@ namespace Grpc.Core var asyncCall = new AsyncCall<TRequest, TResponse>(call); var resultTask = asyncCall.ClientStreamingCallAsync(); var requestStream = new ClientRequestStream<TRequest, TResponse>(asyncCall); - return new AsyncClientStreamingCall<TRequest, TResponse>(requestStream, resultTask, asyncCall.GetStatus, asyncCall.GetTrailers, asyncCall.Cancel); + return new AsyncClientStreamingCall<TRequest, TResponse>(requestStream, resultTask, asyncCall.ResponseHeadersAsync, asyncCall.GetStatus, asyncCall.GetTrailers, asyncCall.Cancel); } /// <summary> @@ -130,7 +130,7 @@ namespace Grpc.Core asyncCall.StartDuplexStreamingCall(); var requestStream = new ClientRequestStream<TRequest, TResponse>(asyncCall); var responseStream = new ClientResponseStream<TRequest, TResponse>(asyncCall); - return new AsyncDuplexStreamingCall<TRequest, TResponse>(requestStream, responseStream, asyncCall.GetStatus, asyncCall.GetTrailers, asyncCall.Cancel); + return new AsyncDuplexStreamingCall<TRequest, TResponse>(requestStream, responseStream, asyncCall.ResponseHeadersAsync, asyncCall.GetStatus, asyncCall.GetTrailers, asyncCall.Cancel); } } } diff --git a/src/csharp/Grpc.Core/Channel.cs b/src/csharp/Grpc.Core/Channel.cs index 64c6adf2bf..c11b320a64 100644 --- a/src/csharp/Grpc.Core/Channel.cs +++ b/src/csharp/Grpc.Core/Channel.cs @@ -45,15 +45,19 @@ namespace Grpc.Core /// <summary> /// gRPC Channel /// </summary> - public class Channel : IDisposable + public class Channel { static readonly ILogger Logger = GrpcEnvironment.Logger.ForType<Channel>(); + readonly object myLock = new object(); + readonly AtomicCounter activeCallCounter = new AtomicCounter(); + readonly string target; readonly GrpcEnvironment environment; readonly ChannelSafeHandle handle; readonly List<ChannelOption> options; - bool disposed; + + bool shutdownRequested; /// <summary> /// Creates a channel that connects to a specific host. @@ -65,7 +69,7 @@ namespace Grpc.Core public Channel(string target, Credentials credentials, IEnumerable<ChannelOption> options = null) { this.target = Preconditions.CheckNotNull(target, "target"); - this.environment = GrpcEnvironment.GetInstance(); + this.environment = GrpcEnvironment.AddRef(); this.options = options != null ? new List<ChannelOption>(options) : new List<ChannelOption>(); EnsureUserAgentChannelOption(this.options); @@ -172,12 +176,26 @@ namespace Grpc.Core } /// <summary> - /// Destroys the underlying channel. + /// Waits until there are no more active calls for this channel and then cleans up + /// resources used by this channel. /// </summary> - public void Dispose() + public async Task ShutdownAsync() { - Dispose(true); - GC.SuppressFinalize(this); + lock (myLock) + { + Preconditions.CheckState(!shutdownRequested); + shutdownRequested = true; + } + + var activeCallCount = activeCallCounter.Count; + if (activeCallCount > 0) + { + Logger.Warning("Channel shutdown was called but there are still {0} active calls for that channel.", activeCallCount); + } + + handle.Dispose(); + + await Task.Run(() => GrpcEnvironment.Release()); } internal ChannelSafeHandle Handle @@ -196,13 +214,20 @@ namespace Grpc.Core } } - protected virtual void Dispose(bool disposing) + internal void AddCallReference(object call) { - if (disposing && handle != null && !disposed) - { - disposed = true; - handle.Dispose(); - } + activeCallCounter.Increment(); + + bool success = false; + handle.DangerousAddRef(ref success); + Preconditions.CheckState(success); + } + + internal void RemoveCallReference(object call) + { + handle.DangerousRelease(); + + activeCallCounter.Decrement(); } private static void EnsureUserAgentChannelOption(List<ChannelOption> options) diff --git a/src/csharp/Grpc.Core/Grpc.Core.csproj b/src/csharp/Grpc.Core/Grpc.Core.csproj index 055aff1444..ad2af17bc7 100644 --- a/src/csharp/Grpc.Core/Grpc.Core.csproj +++ b/src/csharp/Grpc.Core/Grpc.Core.csproj @@ -49,6 +49,7 @@ <Compile Include="AsyncDuplexStreamingCall.cs" /> <Compile Include="AsyncServerStreamingCall.cs" /> <Compile Include="IClientStreamWriter.cs" /> + <Compile Include="Internal\INativeCall.cs" /> <Compile Include="IServerStreamWriter.cs" /> <Compile Include="IAsyncStreamWriter.cs" /> <Compile Include="IAsyncStreamReader.cs" /> diff --git a/src/csharp/Grpc.Core/GrpcEnvironment.cs b/src/csharp/Grpc.Core/GrpcEnvironment.cs index 30d8c80235..e7c04185c2 100644 --- a/src/csharp/Grpc.Core/GrpcEnvironment.cs +++ b/src/csharp/Grpc.Core/GrpcEnvironment.cs @@ -58,6 +58,7 @@ namespace Grpc.Core static object staticLock = new object(); static GrpcEnvironment instance; + static int refCount; static ILogger logger = new ConsoleLogger(); @@ -67,13 +68,14 @@ namespace Grpc.Core bool isClosed; /// <summary> - /// Returns an instance of initialized gRPC environment. - /// Subsequent invocations return the same instance unless Shutdown has been called first. + /// Returns a reference-counted instance of initialized gRPC environment. + /// Subsequent invocations return the same instance unless reference count has dropped to zero previously. /// </summary> - internal static GrpcEnvironment GetInstance() + internal static GrpcEnvironment AddRef() { lock (staticLock) { + refCount++; if (instance == null) { instance = new GrpcEnvironment(); @@ -83,14 +85,16 @@ namespace Grpc.Core } /// <summary> - /// Shuts down the gRPC environment if it was initialized before. - /// Blocks until the environment has been fully shutdown. + /// Decrements the reference count for currently active environment and shuts down the gRPC environment if reference count drops to zero. + /// (and blocks until the environment has been fully shutdown). /// </summary> - public static void Shutdown() + internal static void Release() { lock (staticLock) { - if (instance != null) + Preconditions.CheckState(refCount > 0); + refCount--; + if (refCount == 0) { instance.Close(); instance = null; @@ -98,6 +102,14 @@ namespace Grpc.Core } } + internal static int GetRefCount() + { + lock (staticLock) + { + return refCount; + } + } + /// <summary> /// Gets application-wide logger used by gRPC. /// </summary> @@ -125,12 +137,10 @@ namespace Grpc.Core private GrpcEnvironment() { NativeLogRedirector.Redirect(); - grpcsharp_init(); + GrpcNativeInit(); completionRegistry = new CompletionRegistry(this); threadPool = new GrpcThreadPool(this, THREAD_POOL_SIZE); threadPool.Start(); - // TODO: use proper logging here - Logger.Info("gRPC initialized."); } /// <summary> @@ -175,6 +185,16 @@ namespace Grpc.Core return Marshal.PtrToStringAnsi(ptr); } + internal static void GrpcNativeInit() + { + grpcsharp_init(); + } + + internal static void GrpcNativeShutdown() + { + grpcsharp_shutdown(); + } + /// <summary> /// Shuts down this environment. /// </summary> @@ -185,12 +205,10 @@ namespace Grpc.Core throw new InvalidOperationException("Close has already been called"); } threadPool.Stop(); - grpcsharp_shutdown(); + GrpcNativeShutdown(); isClosed = true; debugStats.CheckOK(); - - Logger.Info("gRPC shutdown."); } } } diff --git a/src/csharp/Grpc.Core/Internal/AsyncCall.cs b/src/csharp/Grpc.Core/Internal/AsyncCall.cs index 2c3e3d75ea..be5d611a53 100644 --- a/src/csharp/Grpc.Core/Internal/AsyncCall.cs +++ b/src/csharp/Grpc.Core/Internal/AsyncCall.cs @@ -51,22 +51,35 @@ namespace Grpc.Core.Internal static readonly ILogger Logger = GrpcEnvironment.Logger.ForType<AsyncCall<TRequest, TResponse>>(); readonly CallInvocationDetails<TRequest, TResponse> details; + readonly INativeCall injectedNativeCall; // for testing // Completion of a pending unary response if not null. TaskCompletionSource<TResponse> unaryResponseTcs; + // Indicates that steaming call has finished. + TaskCompletionSource<object> streamingCallFinishedTcs = new TaskCompletionSource<object>(); + + // Response headers set here once received. + TaskCompletionSource<Metadata> responseHeadersTcs = new TaskCompletionSource<Metadata>(); + // Set after status is received. Used for both unary and streaming response calls. ClientSideStatus? finishedStatus; - bool readObserverCompleted; // True if readObserver has already been completed. - public AsyncCall(CallInvocationDetails<TRequest, TResponse> callDetails) - : base(callDetails.RequestMarshaller.Serializer, callDetails.ResponseMarshaller.Deserializer) + : base(callDetails.RequestMarshaller.Serializer, callDetails.ResponseMarshaller.Deserializer, callDetails.Channel.Environment) { this.details = callDetails.WithOptions(callDetails.Options.Normalize()); this.initialMetadataSent = true; // we always send metadata at the very beginning of the call. } + /// <summary> + /// This constructor should only be used for testing. + /// </summary> + public AsyncCall(CallInvocationDetails<TRequest, TResponse> callDetails, INativeCall injectedNativeCall) : this(callDetails) + { + this.injectedNativeCall = injectedNativeCall; + } + // TODO: this method is not Async, so it shouldn't be in AsyncCall class, but // it is reusing fair amount of code in this class, so we are leaving it here. /// <summary> @@ -100,7 +113,7 @@ namespace Grpc.Core.Internal bool success = (ev.success != 0); try { - HandleUnaryResponse(success, ctx); + HandleUnaryResponse(success, ctx.GetReceivedStatusOnClient(), ctx.GetReceivedMessage(), ctx.GetReceivedInitialMetadata()); } catch (Exception e) { @@ -125,7 +138,7 @@ namespace Grpc.Core.Internal Preconditions.CheckState(!started); started = true; - Initialize(details.Channel.Environment.CompletionQueue); + Initialize(environment.CompletionQueue); halfcloseRequested = true; readingDone = true; @@ -152,7 +165,7 @@ namespace Grpc.Core.Internal Preconditions.CheckState(!started); started = true; - Initialize(details.Channel.Environment.CompletionQueue); + Initialize(environment.CompletionQueue); readingDone = true; @@ -176,10 +189,9 @@ namespace Grpc.Core.Internal Preconditions.CheckState(!started); started = true; - Initialize(details.Channel.Environment.CompletionQueue); + Initialize(environment.CompletionQueue); halfcloseRequested = true; - halfclosed = true; // halfclose not confirmed yet, but it will be once finishedHandler is called. byte[] payload = UnsafeSerialize(msg); @@ -187,6 +199,7 @@ namespace Grpc.Core.Internal { call.StartServerStreaming(HandleFinished, payload, metadataArray, GetWriteFlagsForCall()); } + call.StartReceiveInitialMetadata(HandleReceivedResponseHeaders); } } @@ -201,12 +214,13 @@ namespace Grpc.Core.Internal Preconditions.CheckState(!started); started = true; - Initialize(details.Channel.Environment.CompletionQueue); + Initialize(environment.CompletionQueue); using (var metadataArray = MetadataArraySafeHandle.Create(details.Options.Headers)) { call.StartDuplexStreaming(HandleFinished, metadataArray); } + call.StartReceiveInitialMetadata(HandleReceivedResponseHeaders); } } @@ -248,6 +262,28 @@ namespace Grpc.Core.Internal } /// <summary> + /// Get the task that completes once if streaming call finishes with ok status and throws RpcException with given status otherwise. + /// </summary> + public Task StreamingCallFinishedTask + { + get + { + return streamingCallFinishedTcs.Task; + } + } + + /// <summary> + /// Get the task that completes once response headers are received. + /// </summary> + public Task<Metadata> ResponseHeadersAsync + { + get + { + return responseHeadersTcs.Task; + } + } + + /// <summary> /// Gets the resulting status if the call has already finished. /// Throws InvalidOperationException otherwise. /// </summary> @@ -281,51 +317,31 @@ namespace Grpc.Core.Internal } } - /// <summary> - /// On client-side, we only fire readCompletionDelegate once all messages have been read - /// and status has been received. - /// </summary> - protected override void ProcessLastRead(AsyncCompletionDelegate<TResponse> completionDelegate) + protected override void OnAfterReleaseResources() { - if (completionDelegate != null && readingDone && finishedStatus.HasValue) - { - bool shouldComplete; - lock (myLock) - { - shouldComplete = !readObserverCompleted; - readObserverCompleted = true; - } - - if (shouldComplete) - { - var status = finishedStatus.Value.Status; - if (status.StatusCode != StatusCode.OK) - { - FireCompletion(completionDelegate, default(TResponse), new RpcException(status)); - } - else - { - FireCompletion(completionDelegate, default(TResponse), null); - } - } - } + details.Channel.RemoveCallReference(this); } - protected override void OnReleaseResources() + private void Initialize(CompletionQueueSafeHandle cq) { - details.Channel.Environment.DebugStats.ActiveClientCalls.Decrement(); + var call = CreateNativeCall(cq); + details.Channel.AddCallReference(this); + InitializeInternal(call); + RegisterCancellationCallback(); } - private void Initialize(CompletionQueueSafeHandle cq) + private INativeCall CreateNativeCall(CompletionQueueSafeHandle cq) { + if (injectedNativeCall != null) + { + return injectedNativeCall; // allows injecting a mock INativeCall in tests. + } + var parentCall = details.Options.PropagationToken != null ? details.Options.PropagationToken.ParentCall : CallSafeHandle.NullInstance; - var call = details.Channel.Handle.CreateCall(details.Channel.Environment.CompletionRegistry, + return details.Channel.Handle.CreateCall(environment.CompletionRegistry, parentCall, ContextPropagationToken.DefaultMask, cq, details.Method, details.Host, Timespec.FromDateTime(details.Options.Deadline.Value)); - details.Channel.Environment.DebugStats.ActiveClientCalls.Increment(); - InitializeInternal(call); - RegisterCancellationCallback(); } // Make sure that once cancellationToken for this call is cancelled, Cancel() will be called. @@ -348,31 +364,31 @@ namespace Grpc.Core.Internal } /// <summary> - /// Handler for unary response completion. + /// Handles receive status completion for calls with streaming response. /// </summary> - private void HandleUnaryResponse(bool success, BatchContextSafeHandle ctx) + private void HandleReceivedResponseHeaders(bool success, Metadata responseHeaders) { - var fullStatus = ctx.GetReceivedStatusOnClient(); + responseHeadersTcs.SetResult(responseHeaders); + } + /// <summary> + /// Handler for unary response completion. + /// </summary> + private void HandleUnaryResponse(bool success, ClientSideStatus receivedStatus, byte[] receivedMessage, Metadata responseHeaders) + { lock (myLock) { finished = true; - finishedStatus = fullStatus; - - halfclosed = true; + finishedStatus = receivedStatus; ReleaseResourcesIfPossible(); } - if (!success) - { - unaryResponseTcs.SetException(new RpcException(new Status(StatusCode.Internal, "Internal error occured."))); - return; - } + responseHeadersTcs.SetResult(responseHeaders); - var status = fullStatus.Status; + var status = receivedStatus.Status; - if (status.StatusCode != StatusCode.OK) + if (!success || status.StatusCode != StatusCode.OK) { unaryResponseTcs.SetException(new RpcException(status)); return; @@ -380,7 +396,7 @@ namespace Grpc.Core.Internal // TODO: handle deserialization error TResponse msg; - TryDeserialize(ctx.GetReceivedMessage(), out msg); + TryDeserialize(receivedMessage, out msg); unaryResponseTcs.SetResult(msg); } @@ -388,22 +404,25 @@ namespace Grpc.Core.Internal /// <summary> /// Handles receive status completion for calls with streaming response. /// </summary> - private void HandleFinished(bool success, BatchContextSafeHandle ctx) + private void HandleFinished(bool success, ClientSideStatus receivedStatus) { - var fullStatus = ctx.GetReceivedStatusOnClient(); - - AsyncCompletionDelegate<TResponse> origReadCompletionDelegate = null; lock (myLock) { finished = true; - finishedStatus = fullStatus; - - origReadCompletionDelegate = readCompletionDelegate; + finishedStatus = receivedStatus; ReleaseResourcesIfPossible(); } - ProcessLastRead(origReadCompletionDelegate); + var status = receivedStatus.Status; + + if (!success || status.StatusCode != StatusCode.OK) + { + streamingCallFinishedTcs.SetException(new RpcException(status)); + return; + } + + streamingCallFinishedTcs.SetResult(null); } } }
\ No newline at end of file diff --git a/src/csharp/Grpc.Core/Internal/AsyncCallBase.cs b/src/csharp/Grpc.Core/Internal/AsyncCallBase.cs index 6ca4bbdafc..4d20394644 100644 --- a/src/csharp/Grpc.Core/Internal/AsyncCallBase.cs +++ b/src/csharp/Grpc.Core/Internal/AsyncCallBase.cs @@ -54,30 +54,30 @@ namespace Grpc.Core.Internal readonly Func<TWrite, byte[]> serializer; readonly Func<byte[], TRead> deserializer; + protected readonly GrpcEnvironment environment; protected readonly object myLock = new object(); - protected CallSafeHandle call; + protected INativeCall call; protected bool disposed; protected bool started; - protected bool errorOccured; protected bool cancelRequested; protected AsyncCompletionDelegate<object> sendCompletionDelegate; // Completion of a pending send or sendclose if not null. protected AsyncCompletionDelegate<TRead> readCompletionDelegate; // Completion of a pending send or sendclose if not null. - protected bool readingDone; - protected bool halfcloseRequested; - protected bool halfclosed; + protected bool readingDone; // True if last read (i.e. read with null payload) was already received. + protected bool halfcloseRequested; // True if send close have been initiated. protected bool finished; // True if close has been received from the peer. protected bool initialMetadataSent; - protected long streamingWritesCounter; + protected long streamingWritesCounter; // Number of streaming send operations started so far. - public AsyncCallBase(Func<TWrite, byte[]> serializer, Func<byte[], TRead> deserializer) + public AsyncCallBase(Func<TWrite, byte[]> serializer, Func<byte[], TRead> deserializer, GrpcEnvironment environment) { this.serializer = Preconditions.CheckNotNull(serializer); this.deserializer = Preconditions.CheckNotNull(deserializer); + this.environment = Preconditions.CheckNotNull(environment); } /// <summary> @@ -114,7 +114,7 @@ namespace Grpc.Core.Internal } } - protected void InitializeInternal(CallSafeHandle call) + protected void InitializeInternal(INativeCall call) { lock (myLock) { @@ -159,16 +159,6 @@ namespace Grpc.Core.Internal } } - // TODO(jtattermusch): find more fitting name for this method. - /// <summary> - /// Default behavior just completes the read observer, but more sofisticated behavior might be required - /// by subclasses. - /// </summary> - protected virtual void ProcessLastRead(AsyncCompletionDelegate<TRead> completionDelegate) - { - FireCompletion(completionDelegate, default(TRead), null); - } - /// <summary> /// If there are no more pending actions and no new actions can be started, releases /// the underlying native resources. @@ -177,7 +167,7 @@ namespace Grpc.Core.Internal { if (!disposed && call != null) { - bool noMoreSendCompletions = halfclosed || (cancelRequested && sendCompletionDelegate == null); + bool noMoreSendCompletions = sendCompletionDelegate == null && (halfcloseRequested || cancelRequested || finished); if (noMoreSendCompletions && readingDone && finished) { ReleaseResources(); @@ -189,34 +179,33 @@ namespace Grpc.Core.Internal private void ReleaseResources() { - OnReleaseResources(); if (call != null) { call.Dispose(); } disposed = true; + OnAfterReleaseResources(); } - protected virtual void OnReleaseResources() + protected virtual void OnAfterReleaseResources() { } protected void CheckSendingAllowed() { Preconditions.CheckState(started); - Preconditions.CheckState(!errorOccured); CheckNotCancelled(); Preconditions.CheckState(!disposed); Preconditions.CheckState(!halfcloseRequested, "Already halfclosed."); + Preconditions.CheckState(!finished, "Already finished."); Preconditions.CheckState(sendCompletionDelegate == null, "Only one write can be pending at a time"); } - protected void CheckReadingAllowed() + protected virtual void CheckReadingAllowed() { Preconditions.CheckState(started); Preconditions.CheckState(!disposed); - Preconditions.CheckState(!errorOccured); Preconditions.CheckState(!readingDone, "Stream has already been closed."); Preconditions.CheckState(readCompletionDelegate == null, "Only one read can be pending at a time"); @@ -280,7 +269,7 @@ namespace Grpc.Core.Internal /// <summary> /// Handles send completion. /// </summary> - protected void HandleSendFinished(bool success, BatchContextSafeHandle ctx) + protected void HandleSendFinished(bool success) { AsyncCompletionDelegate<object> origCompletionDelegate = null; lock (myLock) @@ -304,12 +293,11 @@ namespace Grpc.Core.Internal /// <summary> /// Handles halfclose completion. /// </summary> - protected void HandleHalfclosed(bool success, BatchContextSafeHandle ctx) + protected void HandleHalfclosed(bool success) { AsyncCompletionDelegate<object> origCompletionDelegate = null; lock (myLock) { - halfclosed = true; origCompletionDelegate = sendCompletionDelegate; sendCompletionDelegate = null; @@ -329,23 +317,17 @@ namespace Grpc.Core.Internal /// <summary> /// Handles streaming read completion. /// </summary> - protected void HandleReadFinished(bool success, BatchContextSafeHandle ctx) + protected void HandleReadFinished(bool success, byte[] receivedMessage) { - var payload = ctx.GetReceivedMessage(); - AsyncCompletionDelegate<TRead> origCompletionDelegate = null; lock (myLock) { origCompletionDelegate = readCompletionDelegate; - if (payload != null) - { - readCompletionDelegate = null; - } - else + readCompletionDelegate = null; + + if (receivedMessage == null) { - // This was the last read. Keeping the readCompletionDelegate - // to be either fired by this handler or by client-side finished - // handler. + // This was the last read. readingDone = true; } @@ -354,17 +336,17 @@ namespace Grpc.Core.Internal // TODO: handle the case when error occured... - if (payload != null) + if (receivedMessage != null) { // TODO: handle deserialization error TRead msg; - TryDeserialize(payload, out msg); + TryDeserialize(receivedMessage, out msg); FireCompletion(origCompletionDelegate, msg, null); } else { - ProcessLastRead(origCompletionDelegate); + FireCompletion(origCompletionDelegate, default(TRead), null); } } } diff --git a/src/csharp/Grpc.Core/Internal/AsyncCallServer.cs b/src/csharp/Grpc.Core/Internal/AsyncCallServer.cs index 3710a65d6b..5c47251030 100644 --- a/src/csharp/Grpc.Core/Internal/AsyncCallServer.cs +++ b/src/csharp/Grpc.Core/Internal/AsyncCallServer.cs @@ -49,17 +49,18 @@ namespace Grpc.Core.Internal { readonly TaskCompletionSource<object> finishedServersideTcs = new TaskCompletionSource<object>(); readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - readonly GrpcEnvironment environment; + readonly Server server; - public AsyncCallServer(Func<TResponse, byte[]> serializer, Func<byte[], TRequest> deserializer, GrpcEnvironment environment) : base(serializer, deserializer) + public AsyncCallServer(Func<TResponse, byte[]> serializer, Func<byte[], TRequest> deserializer, GrpcEnvironment environment, Server server) : base(serializer, deserializer, environment) { - this.environment = Preconditions.CheckNotNull(environment); + this.server = Preconditions.CheckNotNull(server); } public void Initialize(CallSafeHandle call) { call.SetCompletionRegistry(environment.CompletionRegistry); - environment.DebugStats.ActiveServerCalls.Increment(); + + server.AddCallReference(this); InitializeInternal(call); } @@ -168,18 +169,22 @@ namespace Grpc.Core.Internal } } - protected override void OnReleaseResources() + protected override void CheckReadingAllowed() { - environment.DebugStats.ActiveServerCalls.Decrement(); + base.CheckReadingAllowed(); + Preconditions.CheckArgument(!cancelRequested); + } + + protected override void OnAfterReleaseResources() + { + server.RemoveCallReference(this); } /// <summary> /// Handles the server side close completion. /// </summary> - private void HandleFinishedServerside(bool success, BatchContextSafeHandle ctx) + private void HandleFinishedServerside(bool success, bool cancelled) { - bool cancelled = ctx.GetReceivedCloseOnServerCancelled(); - lock (myLock) { finished = true; diff --git a/src/csharp/Grpc.Core/Internal/BatchContextSafeHandle.cs b/src/csharp/Grpc.Core/Internal/BatchContextSafeHandle.cs index 6a2add54db..3a96414bea 100644 --- a/src/csharp/Grpc.Core/Internal/BatchContextSafeHandle.cs +++ b/src/csharp/Grpc.Core/Internal/BatchContextSafeHandle.cs @@ -134,7 +134,7 @@ namespace Grpc.Core.Internal } // Gets data of server_rpc_new completion. - public ServerRpcNew GetServerRpcNew() + public ServerRpcNew GetServerRpcNew(Server server) { var call = grpcsharp_batch_context_server_rpc_new_call(this); @@ -145,7 +145,7 @@ namespace Grpc.Core.Internal IntPtr metadataArrayPtr = grpcsharp_batch_context_server_rpc_new_request_metadata(this); var metadata = MetadataArraySafeHandle.ReadMetadataFromPtrUnsafe(metadataArrayPtr); - return new ServerRpcNew(call, method, host, deadline, metadata); + return new ServerRpcNew(server, call, method, host, deadline, metadata); } // Gets data of receive_close_on_server completion. @@ -198,14 +198,16 @@ namespace Grpc.Core.Internal /// </summary> internal struct ServerRpcNew { + readonly Server server; readonly CallSafeHandle call; readonly string method; readonly string host; readonly Timespec deadline; readonly Metadata requestMetadata; - public ServerRpcNew(CallSafeHandle call, string method, string host, Timespec deadline, Metadata requestMetadata) + public ServerRpcNew(Server server, CallSafeHandle call, string method, string host, Timespec deadline, Metadata requestMetadata) { + this.server = server; this.call = call; this.method = method; this.host = host; @@ -213,6 +215,14 @@ namespace Grpc.Core.Internal this.requestMetadata = requestMetadata; } + public Server Server + { + get + { + return this.server; + } + } + public CallSafeHandle Call { get diff --git a/src/csharp/Grpc.Core/Internal/CallSafeHandle.cs b/src/csharp/Grpc.Core/Internal/CallSafeHandle.cs index 3cb01e29bd..0f187529e8 100644 --- a/src/csharp/Grpc.Core/Internal/CallSafeHandle.cs +++ b/src/csharp/Grpc.Core/Internal/CallSafeHandle.cs @@ -40,7 +40,7 @@ namespace Grpc.Core.Internal /// <summary> /// grpc_call from <grpc/grpc.h> /// </summary> - internal class CallSafeHandle : SafeHandleZeroIsInvalid + internal class CallSafeHandle : SafeHandleZeroIsInvalid, INativeCall { public static readonly CallSafeHandle NullInstance = new CallSafeHandle(); @@ -87,6 +87,10 @@ namespace Grpc.Core.Internal BatchContextSafeHandle ctx); [DllImport("grpc_csharp_ext.dll")] + static extern GRPCCallError grpcsharp_call_recv_initial_metadata(CallSafeHandle call, + BatchContextSafeHandle ctx); + + [DllImport("grpc_csharp_ext.dll")] static extern GRPCCallError grpcsharp_call_start_serverside(CallSafeHandle call, BatchContextSafeHandle ctx); @@ -109,10 +113,10 @@ namespace Grpc.Core.Internal this.completionRegistry = completionRegistry; } - public void StartUnary(BatchCompletionDelegate callback, byte[] payload, MetadataArraySafeHandle metadataArray, WriteFlags writeFlags) + public void StartUnary(UnaryResponseClientHandler callback, byte[] payload, MetadataArraySafeHandle metadataArray, WriteFlags writeFlags) { var ctx = BatchContextSafeHandle.Create(); - completionRegistry.RegisterBatchCompletion(ctx, callback); + completionRegistry.RegisterBatchCompletion(ctx, (success, context) => callback(success, context.GetReceivedStatusOnClient(), context.GetReceivedMessage(), context.GetReceivedInitialMetadata())); grpcsharp_call_start_unary(this, ctx, payload, new UIntPtr((ulong)payload.Length), metadataArray, writeFlags) .CheckOk(); } @@ -123,66 +127,73 @@ namespace Grpc.Core.Internal .CheckOk(); } - public void StartClientStreaming(BatchCompletionDelegate callback, MetadataArraySafeHandle metadataArray) + public void StartClientStreaming(UnaryResponseClientHandler callback, MetadataArraySafeHandle metadataArray) { var ctx = BatchContextSafeHandle.Create(); - completionRegistry.RegisterBatchCompletion(ctx, callback); + completionRegistry.RegisterBatchCompletion(ctx, (success, context) => callback(success, context.GetReceivedStatusOnClient(), context.GetReceivedMessage(), context.GetReceivedInitialMetadata())); grpcsharp_call_start_client_streaming(this, ctx, metadataArray).CheckOk(); } - public void StartServerStreaming(BatchCompletionDelegate callback, byte[] payload, MetadataArraySafeHandle metadataArray, WriteFlags writeFlags) + public void StartServerStreaming(ReceivedStatusOnClientHandler callback, byte[] payload, MetadataArraySafeHandle metadataArray, WriteFlags writeFlags) { var ctx = BatchContextSafeHandle.Create(); - completionRegistry.RegisterBatchCompletion(ctx, callback); + completionRegistry.RegisterBatchCompletion(ctx, (success, context) => callback(success, context.GetReceivedStatusOnClient())); grpcsharp_call_start_server_streaming(this, ctx, payload, new UIntPtr((ulong)payload.Length), metadataArray, writeFlags).CheckOk(); } - public void StartDuplexStreaming(BatchCompletionDelegate callback, MetadataArraySafeHandle metadataArray) + public void StartDuplexStreaming(ReceivedStatusOnClientHandler callback, MetadataArraySafeHandle metadataArray) { var ctx = BatchContextSafeHandle.Create(); - completionRegistry.RegisterBatchCompletion(ctx, callback); + completionRegistry.RegisterBatchCompletion(ctx, (success, context) => callback(success, context.GetReceivedStatusOnClient())); grpcsharp_call_start_duplex_streaming(this, ctx, metadataArray).CheckOk(); } - public void StartSendMessage(BatchCompletionDelegate callback, byte[] payload, WriteFlags writeFlags, bool sendEmptyInitialMetadata) + public void StartSendMessage(SendCompletionHandler callback, byte[] payload, WriteFlags writeFlags, bool sendEmptyInitialMetadata) { var ctx = BatchContextSafeHandle.Create(); - completionRegistry.RegisterBatchCompletion(ctx, callback); + completionRegistry.RegisterBatchCompletion(ctx, (success, context) => callback(success)); grpcsharp_call_send_message(this, ctx, payload, new UIntPtr((ulong)payload.Length), writeFlags, sendEmptyInitialMetadata).CheckOk(); } - public void StartSendCloseFromClient(BatchCompletionDelegate callback) + public void StartSendCloseFromClient(SendCompletionHandler callback) { var ctx = BatchContextSafeHandle.Create(); - completionRegistry.RegisterBatchCompletion(ctx, callback); + completionRegistry.RegisterBatchCompletion(ctx, (success, context) => callback(success)); grpcsharp_call_send_close_from_client(this, ctx).CheckOk(); } - public void StartSendStatusFromServer(BatchCompletionDelegate callback, Status status, MetadataArraySafeHandle metadataArray, bool sendEmptyInitialMetadata) + public void StartSendStatusFromServer(SendCompletionHandler callback, Status status, MetadataArraySafeHandle metadataArray, bool sendEmptyInitialMetadata) { var ctx = BatchContextSafeHandle.Create(); - completionRegistry.RegisterBatchCompletion(ctx, callback); + completionRegistry.RegisterBatchCompletion(ctx, (success, context) => callback(success)); grpcsharp_call_send_status_from_server(this, ctx, status.StatusCode, status.Detail, metadataArray, sendEmptyInitialMetadata).CheckOk(); } - public void StartReceiveMessage(BatchCompletionDelegate callback) + public void StartReceiveMessage(ReceivedMessageHandler callback) { var ctx = BatchContextSafeHandle.Create(); - completionRegistry.RegisterBatchCompletion(ctx, callback); + completionRegistry.RegisterBatchCompletion(ctx, (success, context) => callback(success, context.GetReceivedMessage())); grpcsharp_call_recv_message(this, ctx).CheckOk(); } - public void StartServerSide(BatchCompletionDelegate callback) + public void StartReceiveInitialMetadata(ReceivedResponseHeadersHandler callback) + { + var ctx = BatchContextSafeHandle.Create(); + completionRegistry.RegisterBatchCompletion(ctx, (success, context) => callback(success, context.GetReceivedInitialMetadata())); + grpcsharp_call_recv_initial_metadata(this, ctx).CheckOk(); + } + + public void StartServerSide(ReceivedCloseOnServerHandler callback) { var ctx = BatchContextSafeHandle.Create(); - completionRegistry.RegisterBatchCompletion(ctx, callback); + completionRegistry.RegisterBatchCompletion(ctx, (success, context) => callback(success, context.GetReceivedCloseOnServerCancelled())); grpcsharp_call_start_serverside(this, ctx).CheckOk(); } - public void StartSendInitialMetadata(BatchCompletionDelegate callback, MetadataArraySafeHandle metadataArray) + public void StartSendInitialMetadata(SendCompletionHandler callback, MetadataArraySafeHandle metadataArray) { var ctx = BatchContextSafeHandle.Create(); - completionRegistry.RegisterBatchCompletion(ctx, callback); + completionRegistry.RegisterBatchCompletion(ctx, (success, context) => callback(success)); grpcsharp_call_send_initial_metadata(this, ctx, metadataArray).CheckOk(); } diff --git a/src/csharp/Grpc.Core/Internal/ChannelSafeHandle.cs b/src/csharp/Grpc.Core/Internal/ChannelSafeHandle.cs index 7f03bf4ea5..8cef566c14 100644 --- a/src/csharp/Grpc.Core/Internal/ChannelSafeHandle.cs +++ b/src/csharp/Grpc.Core/Internal/ChannelSafeHandle.cs @@ -68,11 +68,17 @@ namespace Grpc.Core.Internal public static ChannelSafeHandle CreateInsecure(string target, ChannelArgsSafeHandle channelArgs) { + // Increment reference count for the native gRPC environment to make sure we don't do grpc_shutdown() before destroying the server handle. + // Doing so would make object finalizer crash if we end up abandoning the handle. + GrpcEnvironment.GrpcNativeInit(); return grpcsharp_insecure_channel_create(target, channelArgs); } public static ChannelSafeHandle CreateSecure(CredentialsSafeHandle credentials, string target, ChannelArgsSafeHandle channelArgs) { + // Increment reference count for the native gRPC environment to make sure we don't do grpc_shutdown() before destroying the server handle. + // Doing so would make object finalizer crash if we end up abandoning the handle. + GrpcEnvironment.GrpcNativeInit(); return grpcsharp_secure_channel_create(credentials, target, channelArgs); } @@ -107,6 +113,7 @@ namespace Grpc.Core.Internal protected override bool ReleaseHandle() { grpcsharp_channel_destroy(handle); + GrpcEnvironment.GrpcNativeShutdown(); return true; } } diff --git a/src/csharp/Grpc.Core/Internal/ClientResponseStream.cs b/src/csharp/Grpc.Core/Internal/ClientResponseStream.cs index 6c44521038..b4a7335c7c 100644 --- a/src/csharp/Grpc.Core/Internal/ClientResponseStream.cs +++ b/src/csharp/Grpc.Core/Internal/ClientResponseStream.cs @@ -72,7 +72,13 @@ namespace Grpc.Core.Internal call.StartReadMessage(taskSource.CompletionDelegate); var result = await taskSource.Task; this.current = result; - return result != null; + + if (result == null) + { + await call.StreamingCallFinishedTask; + return false; + } + return true; } public void Dispose() diff --git a/src/csharp/Grpc.Core/Internal/DebugStats.cs b/src/csharp/Grpc.Core/Internal/DebugStats.cs index 8793450ff3..1bea1adf9e 100644 --- a/src/csharp/Grpc.Core/Internal/DebugStats.cs +++ b/src/csharp/Grpc.Core/Internal/DebugStats.cs @@ -38,10 +38,6 @@ namespace Grpc.Core.Internal { internal class DebugStats { - public readonly AtomicCounter ActiveClientCalls = new AtomicCounter(); - - public readonly AtomicCounter ActiveServerCalls = new AtomicCounter(); - public readonly AtomicCounter PendingBatchCompletions = new AtomicCounter(); /// <summary> @@ -49,16 +45,6 @@ namespace Grpc.Core.Internal /// </summary> public void CheckOK() { - var remainingClientCalls = ActiveClientCalls.Count; - if (remainingClientCalls != 0) - { - DebugWarning(string.Format("Detected {0} client calls that weren't disposed properly.", remainingClientCalls)); - } - var remainingServerCalls = ActiveServerCalls.Count; - if (remainingServerCalls != 0) - { - DebugWarning(string.Format("Detected {0} server calls that weren't disposed properly.", remainingServerCalls)); - } var pendingBatchCompletions = PendingBatchCompletions.Count; if (pendingBatchCompletions != 0) { diff --git a/src/csharp/Grpc.Core/Internal/GrpcThreadPool.cs b/src/csharp/Grpc.Core/Internal/GrpcThreadPool.cs index cb4c7c821e..4b7124ee74 100644 --- a/src/csharp/Grpc.Core/Internal/GrpcThreadPool.cs +++ b/src/csharp/Grpc.Core/Internal/GrpcThreadPool.cs @@ -83,8 +83,6 @@ namespace Grpc.Core.Internal lock (myLock) { cq.Shutdown(); - - Logger.Info("Waiting for GRPC threads to finish."); foreach (var thread in threads) { thread.Join(); @@ -136,7 +134,6 @@ namespace Grpc.Core.Internal } } while (ev.type != GRPCCompletionType.Shutdown); - Logger.Info("Completion queue has shutdown successfully, thread {0} exiting.", Thread.CurrentThread.Name); } } } diff --git a/src/csharp/Grpc.Core/Internal/INativeCall.cs b/src/csharp/Grpc.Core/Internal/INativeCall.cs new file mode 100644 index 0000000000..cbef599139 --- /dev/null +++ b/src/csharp/Grpc.Core/Internal/INativeCall.cs @@ -0,0 +1,85 @@ +#region Copyright notice and license +// 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. + +#endregion + +using System; + +namespace Grpc.Core.Internal +{ + internal delegate void UnaryResponseClientHandler(bool success, ClientSideStatus receivedStatus, byte[] receivedMessage, Metadata responseHeaders); + + // Received status for streaming response calls. + internal delegate void ReceivedStatusOnClientHandler(bool success, ClientSideStatus receivedStatus); + + internal delegate void ReceivedMessageHandler(bool success, byte[] receivedMessage); + + internal delegate void ReceivedResponseHeadersHandler(bool success, Metadata responseHeaders); + + internal delegate void SendCompletionHandler(bool success); + + internal delegate void ReceivedCloseOnServerHandler(bool success, bool cancelled); + + /// <summary> + /// Abstraction of a native call object. + /// </summary> + internal interface INativeCall : IDisposable + { + void Cancel(); + + void CancelWithStatus(Grpc.Core.Status status); + + string GetPeer(); + + void StartUnary(UnaryResponseClientHandler callback, byte[] payload, MetadataArraySafeHandle metadataArray, Grpc.Core.WriteFlags writeFlags); + + void StartUnary(BatchContextSafeHandle ctx, byte[] payload, MetadataArraySafeHandle metadataArray, Grpc.Core.WriteFlags writeFlags); + + void StartClientStreaming(UnaryResponseClientHandler callback, MetadataArraySafeHandle metadataArray); + + void StartServerStreaming(ReceivedStatusOnClientHandler callback, byte[] payload, MetadataArraySafeHandle metadataArray, Grpc.Core.WriteFlags writeFlags); + + void StartDuplexStreaming(ReceivedStatusOnClientHandler callback, MetadataArraySafeHandle metadataArray); + + void StartReceiveMessage(ReceivedMessageHandler callback); + + void StartReceiveInitialMetadata(ReceivedResponseHeadersHandler callback); + + void StartSendInitialMetadata(SendCompletionHandler callback, MetadataArraySafeHandle metadataArray); + + void StartSendMessage(SendCompletionHandler callback, byte[] payload, Grpc.Core.WriteFlags writeFlags, bool sendEmptyInitialMetadata); + + void StartSendCloseFromClient(SendCompletionHandler callback); + + void StartSendStatusFromServer(SendCompletionHandler callback, Grpc.Core.Status status, MetadataArraySafeHandle metadataArray, bool sendEmptyInitialMetadata); + + void StartServerSide(ReceivedCloseOnServerHandler callback); + } +} diff --git a/src/csharp/Grpc.Core/Internal/ServerCallHandler.cs b/src/csharp/Grpc.Core/Internal/ServerCallHandler.cs index 688f9f6fec..59f4c5727c 100644 --- a/src/csharp/Grpc.Core/Internal/ServerCallHandler.cs +++ b/src/csharp/Grpc.Core/Internal/ServerCallHandler.cs @@ -67,7 +67,7 @@ namespace Grpc.Core.Internal var asyncCall = new AsyncCallServer<TRequest, TResponse>( method.ResponseMarshaller.Serializer, method.RequestMarshaller.Deserializer, - environment); + environment, newRpc.Server); asyncCall.Initialize(newRpc.Call); var finishedTask = asyncCall.ServerSideCallAsync(); @@ -123,7 +123,7 @@ namespace Grpc.Core.Internal var asyncCall = new AsyncCallServer<TRequest, TResponse>( method.ResponseMarshaller.Serializer, method.RequestMarshaller.Deserializer, - environment); + environment, newRpc.Server); asyncCall.Initialize(newRpc.Call); var finishedTask = asyncCall.ServerSideCallAsync(); @@ -179,7 +179,7 @@ namespace Grpc.Core.Internal var asyncCall = new AsyncCallServer<TRequest, TResponse>( method.ResponseMarshaller.Serializer, method.RequestMarshaller.Deserializer, - environment); + environment, newRpc.Server); asyncCall.Initialize(newRpc.Call); var finishedTask = asyncCall.ServerSideCallAsync(); @@ -239,7 +239,7 @@ namespace Grpc.Core.Internal var asyncCall = new AsyncCallServer<TRequest, TResponse>( method.ResponseMarshaller.Serializer, method.RequestMarshaller.Deserializer, - environment); + environment, newRpc.Server); asyncCall.Initialize(newRpc.Call); var finishedTask = asyncCall.ServerSideCallAsync(); @@ -278,7 +278,7 @@ namespace Grpc.Core.Internal { // We don't care about the payload type here. var asyncCall = new AsyncCallServer<byte[], byte[]>( - (payload) => payload, (payload) => payload, environment); + (payload) => payload, (payload) => payload, environment, newRpc.Server); asyncCall.Initialize(newRpc.Call); var finishedTask = asyncCall.ServerSideCallAsync(); diff --git a/src/csharp/Grpc.Core/Internal/ServerSafeHandle.cs b/src/csharp/Grpc.Core/Internal/ServerSafeHandle.cs index f9b44b1acf..5ee7ac14e8 100644 --- a/src/csharp/Grpc.Core/Internal/ServerSafeHandle.cs +++ b/src/csharp/Grpc.Core/Internal/ServerSafeHandle.cs @@ -74,6 +74,9 @@ namespace Grpc.Core.Internal public static ServerSafeHandle NewServer(CompletionQueueSafeHandle cq, ChannelArgsSafeHandle args) { + // Increment reference count for the native gRPC environment to make sure we don't do grpc_shutdown() before destroying the server handle. + // Doing so would make object finalizer crash if we end up abandoning the handle. + GrpcEnvironment.GrpcNativeInit(); return grpcsharp_server_create(cq, args); } @@ -109,6 +112,7 @@ namespace Grpc.Core.Internal protected override bool ReleaseHandle() { grpcsharp_server_destroy(handle); + GrpcEnvironment.GrpcNativeShutdown(); return true; } diff --git a/src/csharp/Grpc.Core/Logging/ConsoleLogger.cs b/src/csharp/Grpc.Core/Logging/ConsoleLogger.cs index 382481d871..35561d25d8 100644 --- a/src/csharp/Grpc.Core/Logging/ConsoleLogger.cs +++ b/src/csharp/Grpc.Core/Logging/ConsoleLogger.cs @@ -51,7 +51,19 @@ namespace Grpc.Core.Logging private ConsoleLogger(Type forType) { this.forType = forType; - this.forTypeString = forType != null ? forType.FullName + " " : ""; + if (forType != null) + { + var namespaceStr = forType.Namespace ?? ""; + if (namespaceStr.Length > 0) + { + namespaceStr += "."; + } + this.forTypeString = namespaceStr + forType.Name + " "; + } + else + { + this.forTypeString = ""; + } } /// <summary> diff --git a/src/csharp/Grpc.Core/Server.cs b/src/csharp/Grpc.Core/Server.cs index c76f126026..28f1686e20 100644 --- a/src/csharp/Grpc.Core/Server.cs +++ b/src/csharp/Grpc.Core/Server.cs @@ -50,6 +50,8 @@ namespace Grpc.Core { static readonly ILogger Logger = GrpcEnvironment.Logger.ForType<Server>(); + readonly AtomicCounter activeCallCounter = new AtomicCounter(); + readonly ServiceDefinitionCollection serviceDefinitions; readonly ServerPortCollection ports; readonly GrpcEnvironment environment; @@ -73,7 +75,7 @@ namespace Grpc.Core { this.serviceDefinitions = new ServiceDefinitionCollection(this); this.ports = new ServerPortCollection(this); - this.environment = GrpcEnvironment.GetInstance(); + this.environment = GrpcEnvironment.AddRef(); this.options = options != null ? new List<ChannelOption>(options) : new List<ChannelOption>(); using (var channelArgs = ChannelOptions.CreateChannelArgs(this.options)) { @@ -106,6 +108,17 @@ namespace Grpc.Core } /// <summary> + /// To allow awaiting termination of the server. + /// </summary> + public Task ShutdownTask + { + get + { + return shutdownTcs.Task; + } + } + + /// <summary> /// Starts the server. /// </summary> public void Start() @@ -136,18 +149,9 @@ namespace Grpc.Core handle.ShutdownAndNotify(HandleServerShutdown, environment); await shutdownTcs.Task; - handle.Dispose(); - } + DisposeHandle(); - /// <summary> - /// To allow awaiting termination of the server. - /// </summary> - public Task ShutdownTask - { - get - { - return shutdownTcs.Task; - } + await Task.Run(() => GrpcEnvironment.Release()); } /// <summary> @@ -166,7 +170,22 @@ namespace Grpc.Core handle.ShutdownAndNotify(HandleServerShutdown, environment); handle.CancelAllCalls(); await shutdownTcs.Task; - handle.Dispose(); + DisposeHandle(); + } + + internal void AddCallReference(object call) + { + activeCallCounter.Increment(); + + bool success = false; + handle.DangerousAddRef(ref success); + Preconditions.CheckState(success); + } + + internal void RemoveCallReference(object call) + { + handle.DangerousRelease(); + activeCallCounter.Decrement(); } /// <summary> @@ -227,6 +246,16 @@ namespace Grpc.Core } } + private void DisposeHandle() + { + var activeCallCount = activeCallCounter.Count; + if (activeCallCount > 0) + { + Logger.Warning("Server shutdown has finished but there are still {0} active calls for that server.", activeCallCount); + } + handle.Dispose(); + } + /// <summary> /// Selects corresponding handler for given call and handles the call. /// </summary> @@ -254,7 +283,7 @@ namespace Grpc.Core { if (success) { - ServerRpcNew newRpc = ctx.GetServerRpcNew(); + ServerRpcNew newRpc = ctx.GetServerRpcNew(this); // after server shutdown, the callback returns with null call if (!newRpc.Call.IsInvalid) diff --git a/src/csharp/Grpc.Examples.MathClient/MathClient.cs b/src/csharp/Grpc.Examples.MathClient/MathClient.cs index f9839d99f1..abd95cb905 100644 --- a/src/csharp/Grpc.Examples.MathClient/MathClient.cs +++ b/src/csharp/Grpc.Examples.MathClient/MathClient.cs @@ -39,23 +39,21 @@ namespace math { public static void Main(string[] args) { - using (Channel channel = new Channel("127.0.0.1", 23456, Credentials.Insecure)) - { - Math.IMathClient client = new Math.MathClient(channel); - MathExamples.DivExample(client); + var channel = new Channel("127.0.0.1", 23456, Credentials.Insecure); + Math.IMathClient client = new Math.MathClient(channel); + MathExamples.DivExample(client); - MathExamples.DivAsyncExample(client).Wait(); + MathExamples.DivAsyncExample(client).Wait(); - MathExamples.FibExample(client).Wait(); + MathExamples.FibExample(client).Wait(); - MathExamples.SumExample(client).Wait(); + MathExamples.SumExample(client).Wait(); - MathExamples.DivManyExample(client).Wait(); + MathExamples.DivManyExample(client).Wait(); - MathExamples.DependendRequestsExample(client).Wait(); - } + MathExamples.DependendRequestsExample(client).Wait(); - GrpcEnvironment.Shutdown(); + channel.ShutdownAsync().Wait(); } } } diff --git a/src/csharp/Grpc.Examples.MathServer/MathServer.cs b/src/csharp/Grpc.Examples.MathServer/MathServer.cs index 5f7e717b0c..26bef646ec 100644 --- a/src/csharp/Grpc.Examples.MathServer/MathServer.cs +++ b/src/csharp/Grpc.Examples.MathServer/MathServer.cs @@ -56,7 +56,6 @@ namespace math Console.ReadKey(); server.ShutdownAsync().Wait(); - GrpcEnvironment.Shutdown(); } } } diff --git a/src/csharp/Grpc.Examples.Tests/MathClientServerTests.cs b/src/csharp/Grpc.Examples.Tests/MathClientServerTests.cs index fdef950f09..36c1c947bd 100644 --- a/src/csharp/Grpc.Examples.Tests/MathClientServerTests.cs +++ b/src/csharp/Grpc.Examples.Tests/MathClientServerTests.cs @@ -68,9 +68,8 @@ namespace math.Tests [TestFixtureTearDown] public void Cleanup() { - channel.Dispose(); + channel.ShutdownAsync().Wait(); server.ShutdownAsync().Wait(); - GrpcEnvironment.Shutdown(); } [Test] diff --git a/src/csharp/Grpc.HealthCheck.Tests/HealthClientServerTest.cs b/src/csharp/Grpc.HealthCheck.Tests/HealthClientServerTest.cs index 024377e216..80c35fb197 100644 --- a/src/csharp/Grpc.HealthCheck.Tests/HealthClientServerTest.cs +++ b/src/csharp/Grpc.HealthCheck.Tests/HealthClientServerTest.cs @@ -71,10 +71,9 @@ namespace Grpc.HealthCheck.Tests [TestFixtureTearDown] public void Cleanup() { - channel.Dispose(); + channel.ShutdownAsync().Wait(); server.ShutdownAsync().Wait(); - GrpcEnvironment.Shutdown(); } [Test] diff --git a/src/csharp/Grpc.IntegrationTesting/InteropClient.cs b/src/csharp/Grpc.IntegrationTesting/InteropClient.cs index 423da2801e..24c22273fb 100644 --- a/src/csharp/Grpc.IntegrationTesting/InteropClient.cs +++ b/src/csharp/Grpc.IntegrationTesting/InteropClient.cs @@ -120,12 +120,10 @@ namespace Grpc.IntegrationTesting }; } - using (Channel channel = new Channel(options.serverHost, options.serverPort.Value, credentials, channelOptions)) - { - TestService.TestServiceClient client = new TestService.TestServiceClient(channel); - await RunTestCaseAsync(options.testCase, client); - } - GrpcEnvironment.Shutdown(); + var channel = new Channel(options.serverHost, options.serverPort.Value, credentials, channelOptions); + TestService.TestServiceClient client = new TestService.TestServiceClient(channel); + await RunTestCaseAsync(options.testCase, client); + channel.ShutdownAsync().Wait(); } private async Task RunTestCaseAsync(string testCase, TestService.TestServiceClient client) @@ -171,6 +169,9 @@ namespace Grpc.IntegrationTesting case "cancel_after_first_response": await RunCancelAfterFirstResponseAsync(client); break; + case "timeout_on_sleeping_server": + await RunTimeoutOnSleepingServerAsync(client); + break; case "benchmark_empty_unary": RunBenchmarkEmptyUnary(client); break; @@ -460,6 +461,29 @@ namespace Grpc.IntegrationTesting Console.WriteLine("Passed!"); } + public static async Task RunTimeoutOnSleepingServerAsync(TestService.ITestServiceClient client) + { + Console.WriteLine("running timeout_on_sleeping_server"); + + var deadline = DateTime.UtcNow.AddMilliseconds(1); + using (var call = client.FullDuplexCall(deadline: deadline)) + { + try + { + await call.RequestStream.WriteAsync(StreamingOutputCallRequest.CreateBuilder() + .SetPayload(CreateZerosPayload(27182)).Build()); + } + catch (InvalidOperationException) + { + // Deadline was reached before write has started. Eat the exception and continue. + } + + var ex = Assert.Throws<RpcException>(async () => await call.ResponseStream.MoveNext()); + Assert.AreEqual(StatusCode.DeadlineExceeded, ex.Status.StatusCode); + } + Console.WriteLine("Passed!"); + } + // This is not an official interop test, but it's useful. public static void RunBenchmarkEmptyUnary(TestService.ITestServiceClient client) { diff --git a/src/csharp/Grpc.IntegrationTesting/InteropClientServerTest.cs b/src/csharp/Grpc.IntegrationTesting/InteropClientServerTest.cs index 6fa721bc1c..f3158aeb45 100644 --- a/src/csharp/Grpc.IntegrationTesting/InteropClientServerTest.cs +++ b/src/csharp/Grpc.IntegrationTesting/InteropClientServerTest.cs @@ -75,9 +75,8 @@ namespace Grpc.IntegrationTesting [TestFixtureTearDown] public void Cleanup() { - channel.Dispose(); + channel.ShutdownAsync().Wait(); server.ShutdownAsync().Wait(); - GrpcEnvironment.Shutdown(); } [Test] @@ -127,5 +126,11 @@ namespace Grpc.IntegrationTesting { await InteropClient.RunCancelAfterFirstResponseAsync(client); } + + [Test] + public async Task TimeoutOnSleepingServerAsync() + { + await InteropClient.RunTimeoutOnSleepingServerAsync(client); + } } } diff --git a/src/csharp/Grpc.IntegrationTesting/InteropServer.cs b/src/csharp/Grpc.IntegrationTesting/InteropServer.cs index 504fd11857..0cc8b2cde1 100644 --- a/src/csharp/Grpc.IntegrationTesting/InteropServer.cs +++ b/src/csharp/Grpc.IntegrationTesting/InteropServer.cs @@ -107,8 +107,6 @@ namespace Grpc.IntegrationTesting server.Start(); server.ShutdownTask.Wait(); - - GrpcEnvironment.Shutdown(); } private static ServerOptions ParseArguments(string[] args) diff --git a/src/csharp/Grpc.IntegrationTesting/SslCredentialsTest.cs b/src/csharp/Grpc.IntegrationTesting/SslCredentialsTest.cs index 1c398eb84e..842795374f 100644 --- a/src/csharp/Grpc.IntegrationTesting/SslCredentialsTest.cs +++ b/src/csharp/Grpc.IntegrationTesting/SslCredentialsTest.cs @@ -85,9 +85,8 @@ namespace Grpc.IntegrationTesting [TestFixtureTearDown] public void Cleanup() { - channel.Dispose(); + channel.ShutdownAsync().Wait(); server.ShutdownAsync().Wait(); - GrpcEnvironment.Shutdown(); } [Test] diff --git a/src/csharp/ext/grpc_csharp_ext.c b/src/csharp/ext/grpc_csharp_ext.c index fc9470f93f..489e219c49 100644 --- a/src/csharp/ext/grpc_csharp_ext.c +++ b/src/csharp/ext/grpc_csharp_ext.c @@ -595,7 +595,7 @@ GPR_EXPORT grpc_call_error GPR_CALLTYPE grpcsharp_call_start_server_streaming( grpc_call *call, grpcsharp_batch_context *ctx, const char *send_buffer, size_t send_buffer_len, grpc_metadata_array *initial_metadata, gpr_uint32 write_flags) { /* TODO: don't use magic number */ - grpc_op ops[5]; + grpc_op ops[4]; ops[0].op = GRPC_OP_SEND_INITIAL_METADATA; grpcsharp_metadata_array_move(&(ctx->send_initial_metadata), initial_metadata); @@ -615,23 +615,18 @@ GPR_EXPORT grpc_call_error GPR_CALLTYPE grpcsharp_call_start_server_streaming( ops[2].flags = 0; ops[2].reserved = NULL; - ops[3].op = GRPC_OP_RECV_INITIAL_METADATA; - ops[3].data.recv_initial_metadata = &(ctx->recv_initial_metadata); - ops[3].flags = 0; - ops[3].reserved = NULL; - - ops[4].op = GRPC_OP_RECV_STATUS_ON_CLIENT; - ops[4].data.recv_status_on_client.trailing_metadata = + ops[3].op = GRPC_OP_RECV_STATUS_ON_CLIENT; + ops[3].data.recv_status_on_client.trailing_metadata = &(ctx->recv_status_on_client.trailing_metadata); - ops[4].data.recv_status_on_client.status = + ops[3].data.recv_status_on_client.status = &(ctx->recv_status_on_client.status); /* not using preallocation for status_details */ - ops[4].data.recv_status_on_client.status_details = + ops[3].data.recv_status_on_client.status_details = &(ctx->recv_status_on_client.status_details); - ops[4].data.recv_status_on_client.status_details_capacity = + ops[3].data.recv_status_on_client.status_details_capacity = &(ctx->recv_status_on_client.status_details_capacity); - ops[4].flags = 0; - ops[4].reserved = NULL; + ops[3].flags = 0; + ops[3].reserved = NULL; return grpc_call_start_batch(call, ops, sizeof(ops) / sizeof(ops[0]), ctx, NULL); @@ -642,7 +637,7 @@ grpcsharp_call_start_duplex_streaming(grpc_call *call, grpcsharp_batch_context *ctx, grpc_metadata_array *initial_metadata) { /* TODO: don't use magic number */ - grpc_op ops[3]; + grpc_op ops[2]; ops[0].op = GRPC_OP_SEND_INITIAL_METADATA; grpcsharp_metadata_array_move(&(ctx->send_initial_metadata), initial_metadata); @@ -652,28 +647,36 @@ grpcsharp_call_start_duplex_streaming(grpc_call *call, ops[0].flags = 0; ops[0].reserved = NULL; - ops[1].op = GRPC_OP_RECV_INITIAL_METADATA; - ops[1].data.recv_initial_metadata = &(ctx->recv_initial_metadata); - ops[1].flags = 0; - ops[1].reserved = NULL; - - ops[2].op = GRPC_OP_RECV_STATUS_ON_CLIENT; - ops[2].data.recv_status_on_client.trailing_metadata = + ops[1].op = GRPC_OP_RECV_STATUS_ON_CLIENT; + ops[1].data.recv_status_on_client.trailing_metadata = &(ctx->recv_status_on_client.trailing_metadata); - ops[2].data.recv_status_on_client.status = + ops[1].data.recv_status_on_client.status = &(ctx->recv_status_on_client.status); /* not using preallocation for status_details */ - ops[2].data.recv_status_on_client.status_details = + ops[1].data.recv_status_on_client.status_details = &(ctx->recv_status_on_client.status_details); - ops[2].data.recv_status_on_client.status_details_capacity = + ops[1].data.recv_status_on_client.status_details_capacity = &(ctx->recv_status_on_client.status_details_capacity); - ops[2].flags = 0; - ops[2].reserved = NULL; + ops[1].flags = 0; + ops[1].reserved = NULL; return grpc_call_start_batch(call, ops, sizeof(ops) / sizeof(ops[0]), ctx, NULL); } +GPR_EXPORT grpc_call_error GPR_CALLTYPE grpcsharp_call_recv_initial_metadata( + grpc_call *call, grpcsharp_batch_context *ctx) { + /* TODO: don't use magic number */ + grpc_op ops[1]; + ops[0].op = GRPC_OP_RECV_INITIAL_METADATA; + ops[0].data.recv_initial_metadata = &(ctx->recv_initial_metadata); + ops[0].flags = 0; + ops[0].reserved = NULL; + + return grpc_call_start_batch(call, ops, sizeof(ops) / sizeof(ops[0]), ctx, + NULL); +} + GPR_EXPORT grpc_call_error GPR_CALLTYPE grpcsharp_call_send_message(grpc_call *call, grpcsharp_batch_context *ctx, const char *send_buffer, size_t send_buffer_len, diff --git a/src/node/ext/server_credentials.cc b/src/node/ext/server_credentials.cc index 1b8e7b43fb..6e17197e16 100644 --- a/src/node/ext/server_credentials.cc +++ b/src/node/ext/server_credentials.cc @@ -41,6 +41,7 @@ namespace grpc { namespace node { +using v8::Array; using v8::Exception; using v8::External; using v8::Function; @@ -52,6 +53,7 @@ using v8::Local; using v8::Object; using v8::ObjectTemplate; using v8::Persistent; +using v8::String; using v8::Value; NanCallback *ServerCredentials::constructor; @@ -122,25 +124,66 @@ NAN_METHOD(ServerCredentials::CreateSsl) { // TODO: have the node API support multiple key/cert pairs. NanScope(); char *root_certs = NULL; - grpc_ssl_pem_key_cert_pair key_cert_pair; if (::node::Buffer::HasInstance(args[0])) { root_certs = ::node::Buffer::Data(args[0]); } else if (!(args[0]->IsNull() || args[0]->IsUndefined())) { return NanThrowTypeError( "createSSl's first argument must be a Buffer if provided"); } - if (!::node::Buffer::HasInstance(args[1])) { - return NanThrowTypeError("createSsl's second argument must be a Buffer"); + if (!args[1]->IsArray()) { + return NanThrowTypeError( + "createSsl's second argument must be a list of objects"); + } + int force_client_auth = 0; + if (args[2]->IsBoolean()) { + force_client_auth = (int)args[2]->BooleanValue(); + } else if (!(args[2]->IsUndefined() || args[2]->IsNull())) { + return NanThrowTypeError( + "createSsl's third argument must be a boolean if provided"); } - key_cert_pair.private_key = ::node::Buffer::Data(args[1]); - if (!::node::Buffer::HasInstance(args[2])) { - return NanThrowTypeError("createSsl's third argument must be a Buffer"); + Handle<Array> pair_list = Local<Array>::Cast(args[1]); + uint32_t key_cert_pair_count = pair_list->Length(); + grpc_ssl_pem_key_cert_pair *key_cert_pairs = new grpc_ssl_pem_key_cert_pair[ + key_cert_pair_count]; + + Handle<String> key_key = NanNew("private_key"); + Handle<String> cert_key = NanNew("cert_chain"); + + for(uint32_t i = 0; i < key_cert_pair_count; i++) { + if (!pair_list->Get(i)->IsObject()) { + delete key_cert_pairs; + return NanThrowTypeError("Key/cert pairs must be objects"); + } + Handle<Object> pair_obj = pair_list->Get(i)->ToObject(); + if (!pair_obj->HasOwnProperty(key_key)) { + delete key_cert_pairs; + return NanThrowTypeError( + "Key/cert pairs must have a private_key and a cert_chain"); + } + if (!pair_obj->HasOwnProperty(cert_key)) { + delete key_cert_pairs; + return NanThrowTypeError( + "Key/cert pairs must have a private_key and a cert_chain"); + } + if (!::node::Buffer::HasInstance(pair_obj->Get(key_key))) { + delete key_cert_pairs; + return NanThrowTypeError("private_key must be a Buffer"); + } + if (!::node::Buffer::HasInstance(pair_obj->Get(cert_key))) { + delete key_cert_pairs; + return NanThrowTypeError("cert_chain must be a Buffer"); + } + key_cert_pairs[i].private_key = ::node::Buffer::Data( + pair_obj->Get(key_key)); + key_cert_pairs[i].cert_chain = ::node::Buffer::Data( + pair_obj->Get(cert_key)); } - key_cert_pair.cert_chain = ::node::Buffer::Data(args[2]); - // TODO Add a force_client_auth parameter and pass it as the last parameter - // here. grpc_server_credentials *creds = - grpc_ssl_server_credentials_create(root_certs, &key_cert_pair, 1, 0); + grpc_ssl_server_credentials_create(root_certs, + key_cert_pairs, + key_cert_pair_count, + force_client_auth); + delete key_cert_pairs; if (creds == NULL) { NanReturnNull(); } diff --git a/src/node/health_check/health.js b/src/node/health_check/health.js index 87e00197fe..84d7e0568e 100644 --- a/src/node/health_check/health.js +++ b/src/node/health_check/health.js @@ -45,17 +45,13 @@ function HealthImplementation(statusMap) { this.statusMap = _.clone(statusMap); } -HealthImplementation.prototype.setStatus = function(host, service, status) { - if (!this.statusMap[host]) { - this.statusMap[host] = {}; - } - this.statusMap[host][service] = status; +HealthImplementation.prototype.setStatus = function(service, status) { + this.statusMap[service] = status; }; HealthImplementation.prototype.check = function(call, callback){ - var host = call.request.host; var service = call.request.service; - var status = _.get(this.statusMap, [host, service], null); + var status = _.get(this.statusMap, service, null); if (status === null) { callback({code:grpc.status.NOT_FOUND}); } else { diff --git a/src/node/health_check/health.proto b/src/node/health_check/health.proto index d31df1e0a7..57f4aaa9c0 100644 --- a/src/node/health_check/health.proto +++ b/src/node/health_check/health.proto @@ -32,8 +32,7 @@ syntax = "proto3"; package grpc.health.v1alpha; message HealthCheckRequest { - string host = 1; - string service = 2; + string service = 1; } message HealthCheckResponse { @@ -47,4 +46,4 @@ message HealthCheckResponse { service Health { rpc Check(HealthCheckRequest) returns (HealthCheckResponse); -}
\ No newline at end of file +} diff --git a/src/node/interop/interop_server.js b/src/node/interop/interop_server.js index 1242a0f939..99155e9958 100644 --- a/src/node/interop/interop_server.js +++ b/src/node/interop/interop_server.js @@ -169,8 +169,8 @@ function getServer(port, tls) { var key_data = fs.readFileSync(key_path); var pem_data = fs.readFileSync(pem_path); server_creds = grpc.ServerCredentials.createSsl(null, - key_data, - pem_data); + [{private_key: key_data, + cert_chain: pem_data}]); } else { server_creds = grpc.ServerCredentials.createInsecure(); } diff --git a/src/node/test/health_test.js b/src/node/test/health_test.js index be4ef1d251..04959f5f55 100644 --- a/src/node/test/health_test.js +++ b/src/node/test/health_test.js @@ -41,13 +41,9 @@ var grpc = require('../'); describe('Health Checking', function() { var statusMap = { - '': { - '': 'SERVING', - 'grpc.test.TestService': 'NOT_SERVING', - }, - virtual_host: { - 'grpc.test.TestService': 'SERVING' - } + '': 'SERVING', + 'grpc.test.TestServiceNotServing': 'NOT_SERVING', + 'grpc.test.TestServiceServing': 'SERVING' }; var healthServer = new grpc.Server(); healthServer.addProtoService(health.service, @@ -71,15 +67,15 @@ describe('Health Checking', function() { }); }); it('should say that a disabled service is NOT_SERVING', function(done) { - healthClient.check({service: 'grpc.test.TestService'}, + healthClient.check({service: 'grpc.test.TestServiceNotServing'}, function(err, response) { assert.ifError(err); assert.strictEqual(response.status, 'NOT_SERVING'); done(); }); }); - it('should say that a service on another host is SERVING', function(done) { - healthClient.check({host: 'virtual_host', service: 'grpc.test.TestService'}, + it('should say that an enabled service is SERVING', function(done) { + healthClient.check({service: 'grpc.test.TestServiceServing'}, function(err, response) { assert.ifError(err); assert.strictEqual(response.status, 'SERVING'); @@ -93,12 +89,4 @@ describe('Health Checking', function() { done(); }); }); - it('should get NOT_FOUND if the host is not registered', function(done) { - healthClient.check({host: 'wrong_host', service: 'grpc.test.TestService'}, - function(err, response) { - assert(err); - assert.strictEqual(err.code, grpc.status.NOT_FOUND); - done(); - }); - }); }); diff --git a/src/node/test/server_test.js b/src/node/test/server_test.js index 20c9a07ffa..78bac8da29 100644 --- a/src/node/test/server_test.js +++ b/src/node/test/server_test.js @@ -70,7 +70,9 @@ describe('server', function() { var pem_path = path.join(__dirname, '../test/data/server1.pem'); var key_data = fs.readFileSync(key_path); var pem_data = fs.readFileSync(pem_path); - var creds = grpc.ServerCredentials.createSsl(null, key_data, pem_data); + var creds = grpc.ServerCredentials.createSsl(null, + [{private_key: key_data, + cert_chain: pem_data}]); assert.doesNotThrow(function() { port = server.addHttp2Port('0.0.0.0:0', creds); }); diff --git a/src/php/tests/generated_code/AbstractGeneratedCodeTest.php b/src/php/tests/generated_code/AbstractGeneratedCodeTest.php index 8b7e67f57c..287621d930 100644 --- a/src/php/tests/generated_code/AbstractGeneratedCodeTest.php +++ b/src/php/tests/generated_code/AbstractGeneratedCodeTest.php @@ -39,6 +39,14 @@ abstract class AbstractGeneratedCodeTest extends PHPUnit_Framework_TestCase { protected static $client; protected static $timeout; + public function testWaitForNotReady() { + $this->assertFalse(self::$client->waitForReady(1)); + } + + public function testWaitForReady() { + $this->assertTrue(self::$client->waitForReady(250000)); + } + public function testSimpleRequest() { $div_arg = new math\DivArgs(); $div_arg->setDividend(7); diff --git a/src/php/tests/generated_code/GeneratedCodeTest.php b/src/php/tests/generated_code/GeneratedCodeTest.php index 1e4742fc80..a1a2ce81db 100755 --- a/src/php/tests/generated_code/GeneratedCodeTest.php +++ b/src/php/tests/generated_code/GeneratedCodeTest.php @@ -35,7 +35,7 @@ require 'AbstractGeneratedCodeTest.php'; class GeneratedCodeTest extends AbstractGeneratedCodeTest { public static function setUpBeforeClass() { - self::$client = new math\MathClient(new Grpc\BaseStub( - getenv('GRPC_TEST_HOST'), [])); + self::$client = new math\MathClient( + getenv('GRPC_TEST_HOST'), []); } } diff --git a/src/php/tests/generated_code/GeneratedCodeWithCallbackTest.php b/src/php/tests/generated_code/GeneratedCodeWithCallbackTest.php index f8ec1e7da8..68f57d34ad 100644 --- a/src/php/tests/generated_code/GeneratedCodeWithCallbackTest.php +++ b/src/php/tests/generated_code/GeneratedCodeWithCallbackTest.php @@ -35,13 +35,13 @@ require 'AbstractGeneratedCodeTest.php'; class GeneratedCodeWithCallbackTest extends AbstractGeneratedCodeTest { public static function setUpBeforeClass() { - self::$client = new math\MathClient(new Grpc\BaseStub( + self::$client = new math\MathClient( getenv('GRPC_TEST_HOST'), ['update_metadata' => function($a_hash, $client = array()) { $a_copy = $a_hash; $a_copy['foo'] = ['bar']; return $a_copy; - }])); + }]); } } diff --git a/src/python/grpcio/grpc/_adapter/_c/types.h b/src/python/grpcio/grpc/_adapter/_c/types.h index f646465c63..f6ff957baa 100644 --- a/src/python/grpcio/grpc/_adapter/_c/types.h +++ b/src/python/grpcio/grpc/_adapter/_c/types.h @@ -146,6 +146,7 @@ typedef struct Server { PyObject_HEAD grpc_server *c_serv; CompletionQueue *cq; + int shutdown_called; } Server; Server *pygrpc_Server_new(PyTypeObject *type, PyObject *args, PyObject *kwargs); void pygrpc_Server_dealloc(Server *self); @@ -156,6 +157,7 @@ PyObject *pygrpc_Server_add_http2_port( PyObject *pygrpc_Server_start(Server *self, PyObject *ignored); PyObject *pygrpc_Server_shutdown( Server *self, PyObject *args, PyObject *kwargs); +PyObject *pygrpc_Server_cancel_all_calls(Server *self, PyObject *unused); extern PyTypeObject pygrpc_Server_type; /*=========*/ diff --git a/src/python/grpcio/grpc/_adapter/_c/types/server.c b/src/python/grpcio/grpc/_adapter/_c/types/server.c index 15c98f28eb..8feab8aab1 100644 --- a/src/python/grpcio/grpc/_adapter/_c/types/server.c +++ b/src/python/grpcio/grpc/_adapter/_c/types/server.c @@ -45,6 +45,8 @@ PyMethodDef pygrpc_Server_methods[] = { METH_KEYWORDS, ""}, {"start", (PyCFunction)pygrpc_Server_start, METH_NOARGS, ""}, {"shutdown", (PyCFunction)pygrpc_Server_shutdown, METH_KEYWORDS, ""}, + {"cancel_all_calls", (PyCFunction)pygrpc_Server_cancel_all_calls, + METH_NOARGS, ""}, {NULL} }; const char pygrpc_Server_doc[] = "See grpc._adapter._types.Server."; @@ -109,6 +111,7 @@ Server *pygrpc_Server_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) pygrpc_discard_channel_args(c_args); self->cq = cq; Py_INCREF(self->cq); + self->shutdown_called = 0; return self; } @@ -163,6 +166,7 @@ PyObject *pygrpc_Server_add_http2_port( PyObject *pygrpc_Server_start(Server *self, PyObject *ignored) { grpc_server_start(self->c_serv); + self->shutdown_called = 0; Py_RETURN_NONE; } @@ -176,5 +180,17 @@ PyObject *pygrpc_Server_shutdown( } tag = pygrpc_produce_server_shutdown_tag(user_tag); grpc_server_shutdown_and_notify(self->c_serv, self->cq->c_cq, tag); + self->shutdown_called = 1; + Py_RETURN_NONE; +} + +PyObject *pygrpc_Server_cancel_all_calls(Server *self, PyObject *unused) { + if (!self->shutdown_called) { + PyErr_SetString( + PyExc_RuntimeError, + "shutdown must have been called prior to calling cancel_all_calls!"); + return NULL; + } + grpc_server_cancel_all_calls(self->c_serv); Py_RETURN_NONE; } diff --git a/src/python/grpcio/grpc/_adapter/_low.py b/src/python/grpcio/grpc/_adapter/_low.py index 147086e725..3859ebb0e2 100644 --- a/src/python/grpcio/grpc/_adapter/_low.py +++ b/src/python/grpcio/grpc/_adapter/_low.py @@ -124,3 +124,6 @@ class Server(_types.Server): def request_call(self, completion_queue, tag): return self.server.request_call(completion_queue.completion_queue, tag) + + def cancel_all_calls(self): + return self.server.cancel_all_calls() diff --git a/src/python/grpcio/grpc/_links/invocation.py b/src/python/grpcio/grpc/_links/invocation.py index 0058ae91f8..a74c77ebcc 100644 --- a/src/python/grpcio/grpc/_links/invocation.py +++ b/src/python/grpcio/grpc/_links/invocation.py @@ -101,7 +101,7 @@ class _Kernel(object): else: ticket = links.Ticket( operation_id, rpc_state.sequence_number, None, None, None, None, 1, - None, None, None, None, None, None) + None, None, None, None, None, None, None) rpc_state.sequence_number += 1 self._relay.add_value(ticket) rpc_state.low_write = _LowWrite.OPEN @@ -118,7 +118,7 @@ class _Kernel(object): ticket = links.Ticket( operation_id, rpc_state.sequence_number, None, None, None, None, None, None, rpc_state.response_deserializer(event.bytes), None, None, None, - None) + None, None) rpc_state.sequence_number += 1 self._relay.add_value(ticket) @@ -129,7 +129,7 @@ class _Kernel(object): ticket = links.Ticket( operation_id, rpc_state.sequence_number, None, None, links.Ticket.Subscription.FULL, None, None, event.metadata, None, None, - None, None, None) + None, None, None, None) rpc_state.sequence_number += 1 self._relay.add_value(ticket) @@ -146,7 +146,7 @@ class _Kernel(object): ticket = links.Ticket( operation_id, rpc_state.sequence_number, None, None, None, None, None, None, None, event.metadata, event.status.code, event.status.details, - termination) + termination, None) rpc_state.sequence_number += 1 self._relay.add_value(ticket) diff --git a/src/python/grpcio/grpc/_links/service.py b/src/python/grpcio/grpc/_links/service.py index 5c636d61ab..de78e82cd6 100644 --- a/src/python/grpcio/grpc/_links/service.py +++ b/src/python/grpcio/grpc/_links/service.py @@ -131,7 +131,7 @@ class _Kernel(object): ticket = links.Ticket( call, 0, group, method, links.Ticket.Subscription.FULL, service_acceptance.deadline - time.time(), None, event.metadata, None, - None, None, None, None) + None, None, None, None, 'TODO: Service Context Object!') self._relay.add_value(ticket) def _on_read_event(self, event): @@ -157,7 +157,7 @@ class _Kernel(object): # rpc_state.read = _Read.AWAITING_ALLOWANCE ticket = links.Ticket( call, rpc_state.sequence_number, None, None, None, None, None, None, - payload, None, None, None, termination) + payload, None, None, None, termination, None) rpc_state.sequence_number += 1 self._relay.add_value(ticket) @@ -176,7 +176,7 @@ class _Kernel(object): else: ticket = links.Ticket( call, rpc_state.sequence_number, None, None, None, None, 1, None, - None, None, None, None, None) + None, None, None, None, None, None) rpc_state.sequence_number += 1 self._relay.add_value(ticket) rpc_state.low_write = _LowWrite.OPEN @@ -198,7 +198,7 @@ class _Kernel(object): termination = links.Ticket.Termination.TRANSMISSION_FAILURE ticket = links.Ticket( call, rpc_state.sequence_number, None, None, None, None, None, None, - None, None, None, None, termination) + None, None, None, None, termination, None) rpc_state.sequence_number += 1 self._relay.add_value(ticket) @@ -259,7 +259,7 @@ class _Kernel(object): termination = links.Ticket.Termination.COMPLETION ticket = links.Ticket( call, rpc_state.sequence_number, None, None, None, None, None, - None, payload, None, None, None, termination) + None, payload, None, None, None, termination, None) rpc_state.sequence_number += 1 self._relay.add_value(ticket) diff --git a/src/python/grpcio/grpc/framework/core/__init__.py b/src/python/grpcio/grpc/framework/core/__init__.py new file mode 100644 index 0000000000..7086519106 --- /dev/null +++ b/src/python/grpcio/grpc/framework/core/__init__.py @@ -0,0 +1,30 @@ +# 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. + + diff --git a/src/python/grpcio/grpc/framework/core/_constants.py b/src/python/grpcio/grpc/framework/core/_constants.py new file mode 100644 index 0000000000..d3be3a4c4a --- /dev/null +++ b/src/python/grpcio/grpc/framework/core/_constants.py @@ -0,0 +1,59 @@ +# 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. + +"""Private constants for the package.""" + +from grpc.framework.interfaces.base import base +from grpc.framework.interfaces.links import links + +TICKET_SUBSCRIPTION_FOR_BASE_SUBSCRIPTION_KIND = { + base.Subscription.Kind.NONE: links.Ticket.Subscription.NONE, + base.Subscription.Kind.TERMINATION_ONLY: + links.Ticket.Subscription.TERMINATION, + base.Subscription.Kind.FULL: links.Ticket.Subscription.FULL, + } + +# Mapping from abortive operation outcome to ticket termination to be +# sent to the other side of the operation, or None to indicate that no +# ticket should be sent to the other side in the event of such an +# outcome. +ABORTION_OUTCOME_TO_TICKET_TERMINATION = { + base.Outcome.CANCELLED: links.Ticket.Termination.CANCELLATION, + base.Outcome.EXPIRED: links.Ticket.Termination.EXPIRATION, + base.Outcome.LOCAL_SHUTDOWN: links.Ticket.Termination.SHUTDOWN, + base.Outcome.REMOTE_SHUTDOWN: None, + base.Outcome.RECEPTION_FAILURE: links.Ticket.Termination.RECEPTION_FAILURE, + base.Outcome.TRANSMISSION_FAILURE: None, + base.Outcome.LOCAL_FAILURE: links.Ticket.Termination.LOCAL_FAILURE, + base.Outcome.REMOTE_FAILURE: links.Ticket.Termination.REMOTE_FAILURE, +} + +INTERNAL_ERROR_LOG_MESSAGE = ':-( RPC Framework (Core) internal error! )-:' +TERMINATION_CALLBACK_EXCEPTION_LOG_MESSAGE = ( + 'Exception calling termination callback!') diff --git a/src/python/grpcio/grpc/framework/core/_context.py b/src/python/grpcio/grpc/framework/core/_context.py new file mode 100644 index 0000000000..24a12b612e --- /dev/null +++ b/src/python/grpcio/grpc/framework/core/_context.py @@ -0,0 +1,92 @@ +# 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. + +"""State and behavior for operation context.""" + +import time + +# _interfaces is referenced from specification in this module. +from grpc.framework.core import _interfaces # pylint: disable=unused-import +from grpc.framework.interfaces.base import base + + +class OperationContext(base.OperationContext): + """An implementation of interfaces.OperationContext.""" + + def __init__( + self, lock, termination_manager, transmission_manager, + expiration_manager): + """Constructor. + + Args: + lock: The operation-wide lock. + termination_manager: The _interfaces.TerminationManager for the operation. + transmission_manager: The _interfaces.TransmissionManager for the + operation. + expiration_manager: The _interfaces.ExpirationManager for the operation. + """ + self._lock = lock + self._termination_manager = termination_manager + self._transmission_manager = transmission_manager + self._expiration_manager = expiration_manager + + def _abort(self, outcome): + with self._lock: + if self._termination_manager.outcome is None: + self._termination_manager.abort(outcome) + self._transmission_manager.abort(outcome) + self._expiration_manager.terminate() + + def outcome(self): + """See base.OperationContext.outcome for specification.""" + with self._lock: + return self._termination_manager.outcome + + def add_termination_callback(self, callback): + """See base.OperationContext.add_termination_callback.""" + with self._lock: + if self._termination_manager.outcome is None: + self._termination_manager.add_callback(callback) + return None + else: + return self._termination_manager.outcome + + def time_remaining(self): + """See base.OperationContext.time_remaining for specification.""" + with self._lock: + deadline = self._expiration_manager.deadline() + return max(0.0, deadline - time.time()) + + def cancel(self): + """See base.OperationContext.cancel for specification.""" + self._abort(base.Outcome.CANCELLED) + + def fail(self, exception): + """See base.OperationContext.fail for specification.""" + self._abort(base.Outcome.LOCAL_FAILURE) diff --git a/src/python/grpcio/grpc/framework/core/_emission.py b/src/python/grpcio/grpc/framework/core/_emission.py new file mode 100644 index 0000000000..7c702ab2ce --- /dev/null +++ b/src/python/grpcio/grpc/framework/core/_emission.py @@ -0,0 +1,97 @@ +# 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. + +"""State and behavior for handling emitted values.""" + +from grpc.framework.core import _interfaces +from grpc.framework.interfaces.base import base + + +class EmissionManager(_interfaces.EmissionManager): + """An EmissionManager implementation.""" + + def __init__( + self, lock, termination_manager, transmission_manager, + expiration_manager): + """Constructor. + + Args: + lock: The operation-wide lock. + termination_manager: The _interfaces.TerminationManager for the operation. + transmission_manager: The _interfaces.TransmissionManager for the + operation. + expiration_manager: The _interfaces.ExpirationManager for the operation. + """ + self._lock = lock + self._termination_manager = termination_manager + self._transmission_manager = transmission_manager + self._expiration_manager = expiration_manager + self._ingestion_manager = None + + self._initial_metadata_seen = False + self._payload_seen = False + self._completion_seen = False + + def set_ingestion_manager(self, ingestion_manager): + """Sets the ingestion manager with which this manager will cooperate. + + Args: + ingestion_manager: The _interfaces.IngestionManager for the operation. + """ + self._ingestion_manager = ingestion_manager + + def advance( + self, initial_metadata=None, payload=None, completion=None, + allowance=None): + initial_metadata_present = initial_metadata is not None + payload_present = payload is not None + completion_present = completion is not None + allowance_present = allowance is not None + with self._lock: + if self._termination_manager.outcome is None: + if (initial_metadata_present and ( + self._initial_metadata_seen or self._payload_seen or + self._completion_seen) or + payload_present and self._completion_seen or + completion_present and self._completion_seen or + allowance_present and allowance <= 0): + self._termination_manager.abort(base.Outcome.LOCAL_FAILURE) + self._transmission_manager.abort(base.Outcome.LOCAL_FAILURE) + self._expiration_manager.terminate() + else: + self._initial_metadata_seen |= initial_metadata_present + self._payload_seen |= payload_present + self._completion_seen |= completion_present + if completion_present: + self._termination_manager.emission_complete() + self._ingestion_manager.local_emissions_done() + self._transmission_manager.advance( + initial_metadata, payload, completion, allowance) + if allowance_present: + self._ingestion_manager.add_local_allowance(allowance) diff --git a/src/python/grpcio/grpc/framework/core/_end.py b/src/python/grpcio/grpc/framework/core/_end.py new file mode 100644 index 0000000000..fb2c532df6 --- /dev/null +++ b/src/python/grpcio/grpc/framework/core/_end.py @@ -0,0 +1,251 @@ +# 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. + +"""Implementation of base.End.""" + +import abc +import enum +import threading +import uuid + +from grpc.framework.core import _operation +from grpc.framework.core import _utilities +from grpc.framework.foundation import callable_util +from grpc.framework.foundation import later +from grpc.framework.foundation import logging_pool +from grpc.framework.interfaces.base import base +from grpc.framework.interfaces.links import links +from grpc.framework.interfaces.links import utilities + +_IDLE_ACTION_EXCEPTION_LOG_MESSAGE = 'Exception calling idle action!' + + +class End(base.End, links.Link): + """A bridge between base.End and links.Link. + + Implementations of this interface translate arriving tickets into + calls on application objects implementing base interfaces and + translate calls from application objects implementing base interfaces + into tickets sent to a joined link. + """ + __metaclass__ = abc.ABCMeta + + +class _Cycle(object): + """State for a single start-stop End lifecycle.""" + + def __init__(self, pool): + self.pool = pool + self.grace = False + self.futures = [] + self.operations = {} + self.idle_actions = [] + + +def _abort(operations): + for operation in operations: + operation.abort(base.Outcome.LOCAL_SHUTDOWN) + + +def _cancel_futures(futures): + for future in futures: + futures.cancel() + + +def _future_shutdown(lock, cycle, event): + def in_future(): + with lock: + _abort(cycle.operations.values()) + _cancel_futures(cycle.futures) + pool = cycle.pool + cycle.pool.shutdown(wait=True) + return in_future + + +def _termination_action(lock, stats, operation_id, cycle): + """Constructs the termination action for a single operation. + + Args: + lock: A lock to hold during the termination action. + states: A mapping from base.Outcome values to integers to increment with + the outcome given to the termination action. + operation_id: The operation ID for the termination action. + cycle: A _Cycle value to be updated during the termination action. + + Returns: + A callable that takes an operation outcome as its sole parameter and that + should be used as the termination action for the operation associated + with the given operation ID. + """ + def termination_action(outcome): + with lock: + stats[outcome] += 1 + cycle.operations.pop(operation_id, None) + if not cycle.operations: + for action in cycle.idle_actions: + cycle.pool.submit(action) + cycle.idle_actions = [] + if cycle.grace: + _cancel_futures(cycle.futures) + return termination_action + + +class _End(End): + """An End implementation.""" + + def __init__(self, servicer_package): + """Constructor. + + Args: + servicer_package: A _ServicerPackage for servicing operations or None if + this end will not be used to service operations. + """ + self._lock = threading.Condition() + self._servicer_package = servicer_package + + self._stats = {outcome: 0 for outcome in base.Outcome} + + self._mate = None + + self._cycle = None + + def start(self): + """See base.End.start for specification.""" + with self._lock: + if self._cycle is not None: + raise ValueError('Tried to start a not-stopped End!') + else: + self._cycle = _Cycle(logging_pool.pool(1)) + + def stop(self, grace): + """See base.End.stop for specification.""" + with self._lock: + if self._cycle is None: + event = threading.Event() + event.set() + return event + elif not self._cycle.operations: + event = threading.Event() + self._cycle.pool.submit(event.set) + self._cycle.pool.shutdown(wait=False) + self._cycle = None + return event + else: + self._cycle.grace = True + event = threading.Event() + self._cycle.idle_actions.append(event.set) + if 0 < grace: + future = later.later( + grace, _future_shutdown(self._lock, self._cycle, event)) + self._cycle.futures.append(future) + else: + _abort(self._cycle.operations.values()) + return event + + def operate( + self, group, method, subscription, timeout, initial_metadata=None, + payload=None, completion=None): + """See base.End.operate for specification.""" + operation_id = uuid.uuid4() + with self._lock: + if self._cycle is None or self._cycle.grace: + raise ValueError('Can\'t operate on stopped or stopping End!') + termination_action = _termination_action( + self._lock, self._stats, operation_id, self._cycle) + operation = _operation.invocation_operate( + operation_id, group, method, subscription, timeout, initial_metadata, + payload, completion, self._mate.accept_ticket, termination_action, + self._cycle.pool) + self._cycle.operations[operation_id] = operation + return operation.context, operation.operator + + def operation_stats(self): + """See base.End.operation_stats for specification.""" + with self._lock: + return dict(self._stats) + + def add_idle_action(self, action): + """See base.End.add_idle_action for specification.""" + with self._lock: + if self._cycle is None: + raise ValueError('Can\'t add idle action to stopped End!') + action_with_exceptions_logged = callable_util.with_exceptions_logged( + action, _IDLE_ACTION_EXCEPTION_LOG_MESSAGE) + if self._cycle.operations: + self._cycle.idle_actions.append(action_with_exceptions_logged) + else: + self._cycle.pool.submit(action_with_exceptions_logged) + + def accept_ticket(self, ticket): + """See links.Link.accept_ticket for specification.""" + with self._lock: + if self._cycle is not None and not self._cycle.grace: + operation = self._cycle.operations.get(ticket.operation_id) + if operation is not None: + operation.handle_ticket(ticket) + elif self._servicer_package is not None: + termination_action = _termination_action( + self._lock, self._stats, ticket.operation_id, self._cycle) + operation = _operation.service_operate( + self._servicer_package, ticket, self._mate.accept_ticket, + termination_action, self._cycle.pool) + if operation is not None: + self._cycle.operations[ticket.operation_id] = operation + + def join_link(self, link): + """See links.Link.join_link for specification.""" + with self._lock: + self._mate = utilities.NULL_LINK if link is None else link + + +def serviceless_end_link(): + """Constructs an End usable only for invoking operations. + + Returns: + An End usable for translating operations into ticket exchange. + """ + return _End(None) + + +def serviceful_end_link(servicer, default_timeout, maximum_timeout): + """Constructs an End capable of servicing operations. + + Args: + servicer: An interfaces.Servicer for servicing operations. + default_timeout: A length of time in seconds to be used as the default + time alloted for a single operation. + maximum_timeout: A length of time in seconds to be used as the maximum + time alloted for a single operation. + + Returns: + An End capable of servicing the operations requested of it through ticket + exchange. + """ + return _End( + _utilities.ServicerPackage(servicer, default_timeout, maximum_timeout)) diff --git a/src/python/grpcio/grpc/framework/core/_expiration.py b/src/python/grpcio/grpc/framework/core/_expiration.py new file mode 100644 index 0000000000..d94bdf2d2b --- /dev/null +++ b/src/python/grpcio/grpc/framework/core/_expiration.py @@ -0,0 +1,152 @@ +# 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. + +"""State and behavior for operation expiration.""" + +import time + +from grpc.framework.core import _interfaces +from grpc.framework.foundation import later +from grpc.framework.interfaces.base import base + + +class _ExpirationManager(_interfaces.ExpirationManager): + """An implementation of _interfaces.ExpirationManager.""" + + def __init__( + self, commencement, timeout, maximum_timeout, lock, termination_manager, + transmission_manager): + """Constructor. + + Args: + commencement: The time in seconds since the epoch at which the operation + began. + timeout: A length of time in seconds to allow for the operation to run. + maximum_timeout: The maximum length of time in seconds to allow for the + operation to run despite what is requested via this object's + change_timout method. + lock: The operation-wide lock. + termination_manager: The _interfaces.TerminationManager for the operation. + transmission_manager: The _interfaces.TransmissionManager for the + operation. + """ + self._lock = lock + self._termination_manager = termination_manager + self._transmission_manager = transmission_manager + self._commencement = commencement + self._maximum_timeout = maximum_timeout + + self._timeout = timeout + self._deadline = commencement + timeout + self._index = None + self._future = None + + def _expire(self, index): + def expire(): + with self._lock: + if self._future is not None and index == self._index: + self._future = None + self._termination_manager.expire() + self._transmission_manager.abort(base.Outcome.EXPIRED) + return expire + + def start(self): + self._index = 0 + self._future = later.later(self._timeout, self._expire(0)) + + def change_timeout(self, timeout): + if self._future is not None and timeout != self._timeout: + self._future.cancel() + new_timeout = min(timeout, self._maximum_timeout) + new_index = self._index + 1 + self._timeout = new_timeout + self._deadline = self._commencement + new_timeout + self._index = new_index + delay = self._deadline - time.time() + self._future = later.later(delay, self._expire(new_index)) + if new_timeout != timeout: + self._transmission_manager.timeout(new_timeout) + + def deadline(self): + return self._deadline + + def terminate(self): + if self._future: + self._future.cancel() + self._future = None + self._deadline_index = None + + +def invocation_expiration_manager( + timeout, lock, termination_manager, transmission_manager): + """Creates an _interfaces.ExpirationManager appropriate for front-side use. + + Args: + timeout: A length of time in seconds to allow for the operation to run. + lock: The operation-wide lock. + termination_manager: The _interfaces.TerminationManager for the operation. + transmission_manager: The _interfaces.TransmissionManager for the + operation. + + Returns: + An _interfaces.ExpirationManager appropriate for invocation-side use. + """ + expiration_manager = _ExpirationManager( + time.time(), timeout, timeout, lock, termination_manager, + transmission_manager) + expiration_manager.start() + return expiration_manager + + +def service_expiration_manager( + timeout, default_timeout, maximum_timeout, lock, termination_manager, + transmission_manager): + """Creates an _interfaces.ExpirationManager appropriate for back-side use. + + Args: + timeout: A length of time in seconds to allow for the operation to run. May + be None in which case default_timeout will be used. + default_timeout: The default length of time in seconds to allow for the + operation to run if the front-side customer has not specified such a value + (or if the value they specified is not yet known). + maximum_timeout: The maximum length of time in seconds to allow for the + operation to run. + lock: The operation-wide lock. + termination_manager: The _interfaces.TerminationManager for the operation. + transmission_manager: The _interfaces.TransmissionManager for the + operation. + + Returns: + An _interfaces.ExpirationManager appropriate for service-side use. + """ + expiration_manager = _ExpirationManager( + time.time(), default_timeout if timeout is None else timeout, + maximum_timeout, lock, termination_manager, transmission_manager) + expiration_manager.start() + return expiration_manager diff --git a/src/python/grpcio/grpc/framework/core/_ingestion.py b/src/python/grpcio/grpc/framework/core/_ingestion.py new file mode 100644 index 0000000000..59f7f8adc8 --- /dev/null +++ b/src/python/grpcio/grpc/framework/core/_ingestion.py @@ -0,0 +1,410 @@ +# 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. + +"""State and behavior for ingestion during an operation.""" + +import abc +import collections + +from grpc.framework.core import _constants +from grpc.framework.core import _interfaces +from grpc.framework.foundation import abandonment +from grpc.framework.foundation import callable_util +from grpc.framework.interfaces.base import base + +_CREATE_SUBSCRIPTION_EXCEPTION_LOG_MESSAGE = 'Exception initializing ingestion!' +_INGESTION_EXCEPTION_LOG_MESSAGE = 'Exception during ingestion!' + + +class _SubscriptionCreation(collections.namedtuple( + '_SubscriptionCreation', ('subscription', 'remote_error', 'abandoned'))): + """A sum type for the outcome of ingestion initialization. + + Either subscription will be non-None, remote_error will be True, or abandoned + will be True. + + Attributes: + subscription: A base.Subscription describing the customer's interest in + operation values from the other side. + remote_error: A boolean indicating that the subscription could not be + created due to an error on the remote side of the operation. + abandoned: A boolean indicating that subscription creation was abandoned. + """ + + +class _SubscriptionCreator(object): + """Common specification of subscription-creating behavior.""" + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def create(self, group, method): + """Creates the base.Subscription of the local customer. + + Any exceptions raised by this method should be attributed to and treated as + defects in the customer code called by this method. + + Args: + group: The group identifier of the operation. + method: The method identifier of the operation. + + Returns: + A _SubscriptionCreation describing the result of subscription creation. + """ + raise NotImplementedError() + + +class _ServiceSubscriptionCreator(_SubscriptionCreator): + """A _SubscriptionCreator appropriate for service-side use.""" + + def __init__(self, servicer, operation_context, output_operator): + """Constructor. + + Args: + servicer: The base.Servicer that will service the operation. + operation_context: A base.OperationContext for the operation to be passed + to the customer. + output_operator: A base.Operator for the operation to be passed to the + customer and to be called by the customer to accept operation data + emitted by the customer. + """ + self._servicer = servicer + self._operation_context = operation_context + self._output_operator = output_operator + + def create(self, group, method): + try: + subscription = self._servicer.service( + group, method, self._operation_context, self._output_operator) + except base.NoSuchMethodError: + return _SubscriptionCreation(None, True, False) + except abandonment.Abandoned: + return _SubscriptionCreation(None, False, True) + else: + return _SubscriptionCreation(subscription, False, False) + + +def _wrap(behavior): + def wrapped(*args, **kwargs): + try: + behavior(*args, **kwargs) + except abandonment.Abandoned: + return False + else: + return True + return wrapped + + +class _IngestionManager(_interfaces.IngestionManager): + """An implementation of _interfaces.IngestionManager.""" + + def __init__( + self, lock, pool, subscription, subscription_creator, termination_manager, + transmission_manager, expiration_manager): + """Constructor. + + Args: + lock: The operation-wide lock. + pool: A thread pool in which to execute customer code. + subscription: A base.Subscription describing the customer's interest in + operation values from the other side. May be None if + subscription_creator is not None. + subscription_creator: A _SubscriptionCreator wrapping the portion of + customer code that when called returns the base.Subscription describing + the customer's interest in operation values from the other side. May be + None if subscription is not None. + termination_manager: The _interfaces.TerminationManager for the operation. + transmission_manager: The _interfaces.TransmissionManager for the + operation. + expiration_manager: The _interfaces.ExpirationManager for the operation. + """ + self._lock = lock + self._pool = pool + self._termination_manager = termination_manager + self._transmission_manager = transmission_manager + self._expiration_manager = expiration_manager + + if subscription is None: + self._subscription_creator = subscription_creator + self._wrapped_operator = None + elif subscription.kind is base.Subscription.Kind.FULL: + self._subscription_creator = None + self._wrapped_operator = _wrap(subscription.operator.advance) + else: + # TODO(nathaniel): Support other subscriptions. + raise ValueError('Unsupported subscription "%s"!' % subscription.kind) + self._pending_initial_metadata = None + self._pending_payloads = [] + self._pending_completion = None + self._local_allowance = 1 + # A nonnegative integer or None, with None indicating that the local + # customer is done emitting anyway so there's no need to bother it by + # informing it that the remote customer has granted it further permission to + # emit. + self._remote_allowance = 0 + self._processing = False + + def _abort_internal_only(self): + self._subscription_creator = None + self._wrapped_operator = None + self._pending_initial_metadata = None + self._pending_payloads = None + self._pending_completion = None + + def _abort_and_notify(self, outcome): + self._abort_internal_only() + self._termination_manager.abort(outcome) + self._transmission_manager.abort(outcome) + self._expiration_manager.terminate() + + def _operator_next(self): + """Computes the next step for full-subscription ingestion. + + Returns: + An initial_metadata, payload, completion, allowance, continue quintet + indicating what operation values (if any) are available to pass into + customer code and whether or not there is anything immediately + actionable to call customer code to do. + """ + if self._wrapped_operator is None: + return None, None, None, None, False + else: + initial_metadata, payload, completion, allowance, action = [None] * 5 + if self._pending_initial_metadata is not None: + initial_metadata = self._pending_initial_metadata + self._pending_initial_metadata = None + action = True + if self._pending_payloads and 0 < self._local_allowance: + payload = self._pending_payloads.pop(0) + self._local_allowance -= 1 + action = True + if not self._pending_payloads and self._pending_completion is not None: + completion = self._pending_completion + self._pending_completion = None + action = True + if self._remote_allowance is not None and 0 < self._remote_allowance: + allowance = self._remote_allowance + self._remote_allowance = 0 + action = True + return initial_metadata, payload, completion, allowance, bool(action) + + def _operator_process( + self, wrapped_operator, initial_metadata, payload, + completion, allowance): + while True: + advance_outcome = callable_util.call_logging_exceptions( + wrapped_operator, _INGESTION_EXCEPTION_LOG_MESSAGE, + initial_metadata=initial_metadata, payload=payload, + completion=completion, allowance=allowance) + if advance_outcome.exception is None: + if advance_outcome.return_value: + with self._lock: + if self._termination_manager.outcome is not None: + return + if completion is not None: + self._termination_manager.ingestion_complete() + initial_metadata, payload, completion, allowance, moar = ( + self._operator_next()) + if not moar: + self._processing = False + return + else: + with self._lock: + if self._termination_manager.outcome is None: + self._abort_and_notify(base.Outcome.LOCAL_FAILURE) + return + else: + with self._lock: + if self._termination_manager.outcome is None: + self._abort_and_notify(base.Outcome.LOCAL_FAILURE) + return + + def _operator_post_create(self, subscription): + wrapped_operator = _wrap(subscription.operator.advance) + with self._lock: + if self._termination_manager.outcome is not None: + return + self._wrapped_operator = wrapped_operator + self._subscription_creator = None + metadata, payload, completion, allowance, moar = self._operator_next() + if not moar: + self._processing = False + return + self._operator_process( + wrapped_operator, metadata, payload, completion, allowance) + + def _create(self, subscription_creator, group, name): + outcome = callable_util.call_logging_exceptions( + subscription_creator.create, _CREATE_SUBSCRIPTION_EXCEPTION_LOG_MESSAGE, + group, name) + if outcome.return_value is None: + with self._lock: + if self._termination_manager.outcome is None: + self._abort_and_notify(base.Outcome.LOCAL_FAILURE) + elif outcome.return_value.abandoned: + with self._lock: + if self._termination_manager.outcome is None: + self._abort_and_notify(base.Outcome.LOCAL_FAILURE) + elif outcome.return_value.remote_error: + with self._lock: + if self._termination_manager.outcome is None: + self._abort_and_notify(base.Outcome.REMOTE_FAILURE) + elif outcome.return_value.subscription.kind is base.Subscription.Kind.FULL: + self._operator_post_create(outcome.return_value.subscription) + else: + # TODO(nathaniel): Support other subscriptions. + raise ValueError( + 'Unsupported "%s"!' % outcome.return_value.subscription.kind) + + def _store_advance(self, initial_metadata, payload, completion, allowance): + if initial_metadata is not None: + self._pending_initial_metadata = initial_metadata + if payload is not None: + self._pending_payloads.append(payload) + if completion is not None: + self._pending_completion = completion + if allowance is not None and self._remote_allowance is not None: + self._remote_allowance += allowance + + def _operator_advance(self, initial_metadata, payload, completion, allowance): + if self._processing: + self._store_advance(initial_metadata, payload, completion, allowance) + else: + action = False + if initial_metadata is not None: + action = True + if payload is not None: + if 0 < self._local_allowance: + self._local_allowance -= 1 + action = True + else: + self._pending_payloads.append(payload) + payload = False + if completion is not None: + if self._pending_payloads: + self._pending_completion = completion + else: + action = True + if allowance is not None and self._remote_allowance is not None: + allowance += self._remote_allowance + self._remote_allowance = 0 + action = True + if action: + self._pool.submit( + callable_util.with_exceptions_logged( + self._operator_process, _constants.INTERNAL_ERROR_LOG_MESSAGE), + self._wrapped_operator, initial_metadata, payload, completion, + allowance) + + def set_group_and_method(self, group, method): + """See _interfaces.IngestionManager.set_group_and_method for spec.""" + if self._subscription_creator is not None and not self._processing: + self._pool.submit( + callable_util.with_exceptions_logged( + self._create, _constants.INTERNAL_ERROR_LOG_MESSAGE), + self._subscription_creator, group, method) + self._processing = True + + def add_local_allowance(self, allowance): + """See _interfaces.IngestionManager.add_local_allowance for spec.""" + if any((self._subscription_creator, self._wrapped_operator,)): + self._local_allowance += allowance + if not self._processing: + initial_metadata, payload, completion, allowance, moar = ( + self._operator_next()) + if moar: + self._pool.submit( + callable_util.with_exceptions_logged( + self._operator_process, + _constants.INTERNAL_ERROR_LOG_MESSAGE), + initial_metadata, payload, completion, allowance) + + def local_emissions_done(self): + self._remote_allowance = None + + def advance(self, initial_metadata, payload, completion, allowance): + """See _interfaces.IngestionManager.advance for specification.""" + if self._subscription_creator is not None: + self._store_advance(initial_metadata, payload, completion, allowance) + elif self._wrapped_operator is not None: + self._operator_advance(initial_metadata, payload, completion, allowance) + + +def invocation_ingestion_manager( + subscription, lock, pool, termination_manager, transmission_manager, + expiration_manager): + """Creates an IngestionManager appropriate for invocation-side use. + + Args: + subscription: A base.Subscription indicating the customer's interest in the + data and results from the service-side of the operation. + lock: The operation-wide lock. + pool: A thread pool in which to execute customer code. + termination_manager: The _interfaces.TerminationManager for the operation. + transmission_manager: The _interfaces.TransmissionManager for the + operation. + expiration_manager: The _interfaces.ExpirationManager for the operation. + + Returns: + An IngestionManager appropriate for invocation-side use. + """ + return _IngestionManager( + lock, pool, subscription, None, termination_manager, transmission_manager, + expiration_manager) + + +def service_ingestion_manager( + servicer, operation_context, output_operator, lock, pool, + termination_manager, transmission_manager, expiration_manager): + """Creates an IngestionManager appropriate for service-side use. + + The returned IngestionManager will require its set_group_and_name method to be + called before its advance method may be called. + + Args: + servicer: A base.Servicer for servicing the operation. + operation_context: A base.OperationContext for the operation to be passed to + the customer. + output_operator: A base.Operator for the operation to be passed to the + customer and to be called by the customer to accept operation data output + by the customer. + lock: The operation-wide lock. + pool: A thread pool in which to execute customer code. + termination_manager: The _interfaces.TerminationManager for the operation. + transmission_manager: The _interfaces.TransmissionManager for the + operation. + expiration_manager: The _interfaces.ExpirationManager for the operation. + + Returns: + An IngestionManager appropriate for service-side use. + """ + subscription_creator = _ServiceSubscriptionCreator( + servicer, operation_context, output_operator) + return _IngestionManager( + lock, pool, None, subscription_creator, termination_manager, + transmission_manager, expiration_manager) diff --git a/src/python/grpcio/grpc/framework/core/_interfaces.py b/src/python/grpcio/grpc/framework/core/_interfaces.py new file mode 100644 index 0000000000..a626b9f767 --- /dev/null +++ b/src/python/grpcio/grpc/framework/core/_interfaces.py @@ -0,0 +1,308 @@ +# 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. + +"""Package-internal interfaces.""" + +import abc + +from grpc.framework.interfaces.base import base + + +class TerminationManager(object): + """An object responsible for handling the termination of an operation. + + Attributes: + outcome: None if the operation is active or a base.Outcome value if it has + terminated. + """ + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def add_callback(self, callback): + """Registers a callback to be called on operation termination. + + If the operation has already terminated the callback will not be called. + + Args: + callback: A callable that will be passed an interfaces.Outcome value. + + Returns: + None if the operation has not yet terminated and the passed callback will + be called when it does, or a base.Outcome value describing the operation + termination if the operation has terminated and the callback will not be + called as a result of this method call. + """ + raise NotImplementedError() + + @abc.abstractmethod + def emission_complete(self): + """Indicates that emissions from customer code have completed.""" + raise NotImplementedError() + + @abc.abstractmethod + def transmission_complete(self): + """Indicates that transmissions to the remote end are complete. + + Returns: + True if the operation has terminated or False if the operation remains + ongoing. + """ + raise NotImplementedError() + + @abc.abstractmethod + def reception_complete(self): + """Indicates that reception from the other side is complete.""" + raise NotImplementedError() + + @abc.abstractmethod + def ingestion_complete(self): + """Indicates that customer code ingestion of received values is complete.""" + raise NotImplementedError() + + @abc.abstractmethod + def expire(self): + """Indicates that the operation must abort because it has taken too long.""" + raise NotImplementedError() + + @abc.abstractmethod + def abort(self, outcome): + """Indicates that the operation must abort for the indicated reason. + + Args: + outcome: An interfaces.Outcome indicating operation abortion. + """ + raise NotImplementedError() + + +class TransmissionManager(object): + """A manager responsible for transmitting to the other end of an operation.""" + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def kick_off( + self, group, method, timeout, initial_metadata, payload, completion, + allowance): + """Transmits the values associated with operation invocation.""" + raise NotImplementedError() + + @abc.abstractmethod + def advance(self, initial_metadata, payload, completion, allowance): + """Accepts values for transmission to the other end of the operation. + + Args: + initial_metadata: An initial metadata value to be transmitted to the other + side of the operation. May only ever be non-None once. + payload: A payload value. + completion: A base.Completion value. May only ever be non-None in the last + transmission to be made to the other side. + allowance: A positive integer communicating the number of additional + payloads allowed to be transmitted from the other side to this side of + the operation, or None if no additional allowance is being granted in + this call. + """ + raise NotImplementedError() + + @abc.abstractmethod + def timeout(self, timeout): + """Accepts for transmission to the other side a new timeout value. + + Args: + timeout: A positive float used as the new timeout value for the operation + to be transmitted to the other side. + """ + raise NotImplementedError() + + @abc.abstractmethod + def allowance(self, allowance): + """Indicates to this manager that the remote customer is allowing payloads. + + Args: + allowance: A positive integer indicating the number of additional payloads + the remote customer is allowing to be transmitted from this side of the + operation. + """ + raise NotImplementedError() + + @abc.abstractmethod + def remote_complete(self): + """Indicates to this manager that data from the remote side is complete.""" + raise NotImplementedError() + + @abc.abstractmethod + def abort(self, outcome): + """Indicates that the operation has aborted. + + Args: + outcome: An interfaces.Outcome for the operation. If None, indicates that + the operation abortion should not be communicated to the other side of + the operation. + """ + raise NotImplementedError() + + +class ExpirationManager(object): + """A manager responsible for aborting the operation if it runs out of time.""" + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def change_timeout(self, timeout): + """Changes the timeout allotted for the operation. + + Operation duration is always measure from the beginning of the operation; + calling this method changes the operation's allotted time to timeout total + seconds, not timeout seconds from the time of this method call. + + Args: + timeout: A length of time in seconds to allow for the operation. + """ + raise NotImplementedError() + + @abc.abstractmethod + def deadline(self): + """Returns the time until which the operation is allowed to run. + + Returns: + The time (seconds since the epoch) at which the operation will expire. + """ + raise NotImplementedError() + + @abc.abstractmethod + def terminate(self): + """Indicates to this manager that the operation has terminated.""" + raise NotImplementedError() + + +class EmissionManager(base.Operator): + """A manager of values emitted by customer code.""" + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def advance( + self, initial_metadata=None, payload=None, completion=None, + allowance=None): + """Accepts a value emitted by customer code. + + This method should only be called by customer code. + + Args: + initial_metadata: An initial metadata value emitted by the local customer + to be sent to the other side of the operation. + payload: A payload value emitted by the local customer to be sent to the + other side of the operation. + completion: A Completion value emitted by the local customer to be sent to + the other side of the operation. + allowance: A positive integer indicating an additional number of payloads + that the local customer is willing to accept from the other side of the + operation. + """ + raise NotImplementedError() + + +class IngestionManager(object): + """A manager responsible for executing customer code. + + This name of this manager comes from its responsibility to pass successive + values from the other side of the operation into the code of the local + customer. + """ + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def set_group_and_method(self, group, method): + """Communicates to this IngestionManager the operation group and method. + + Args: + group: The group identifier of the operation. + method: The method identifier of the operation. + """ + raise NotImplementedError() + + @abc.abstractmethod + def add_local_allowance(self, allowance): + """Communicates to this IngestionManager that more payloads may be ingested. + + Args: + allowance: A positive integer indicating an additional number of payloads + that the local customer is willing to ingest. + """ + raise NotImplementedError() + + @abc.abstractmethod + def local_emissions_done(self): + """Indicates to this manager that local emissions are done.""" + raise NotImplementedError() + + @abc.abstractmethod + def advance(self, initial_metadata, payload, completion, allowance): + """Advances the operation by passing values to the local customer.""" + raise NotImplementedError() + + +class ReceptionManager(object): + """A manager responsible for receiving tickets from the other end.""" + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def receive_ticket(self, ticket): + """Handle a ticket from the other side of the operation. + + Args: + ticket: An interfaces.BackToFrontTicket or interfaces.FrontToBackTicket + appropriate to this end of the operation and this object. + """ + raise NotImplementedError() + + +class Operation(object): + """An ongoing operation. + + Attributes: + context: A base.OperationContext object for the operation. + operator: A base.Operator object for the operation for use by the customer + of the operation. + """ + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def handle_ticket(self, ticket): + """Handle a ticket from the other side of the operation. + + Args: + ticket: A links.Ticket from the other side of the operation. + """ + raise NotImplementedError() + + @abc.abstractmethod + def abort(self, outcome): + """Aborts the operation. + + Args: + outcome: A base.Outcome value indicating operation abortion. + """ + raise NotImplementedError() diff --git a/src/python/grpcio/grpc/framework/core/_operation.py b/src/python/grpcio/grpc/framework/core/_operation.py new file mode 100644 index 0000000000..d20e40a53d --- /dev/null +++ b/src/python/grpcio/grpc/framework/core/_operation.py @@ -0,0 +1,192 @@ +# 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. + +"""Implementation of operations.""" + +import threading + +# _utilities is referenced from specification in this module. +from grpc.framework.core import _context +from grpc.framework.core import _emission +from grpc.framework.core import _expiration +from grpc.framework.core import _ingestion +from grpc.framework.core import _interfaces +from grpc.framework.core import _reception +from grpc.framework.core import _termination +from grpc.framework.core import _transmission +from grpc.framework.core import _utilities # pylint: disable=unused-import + + +class _EasyOperation(_interfaces.Operation): + """A trivial implementation of interfaces.Operation.""" + + def __init__( + self, lock, termination_manager, transmission_manager, expiration_manager, + context, operator, reception_manager): + """Constructor. + + Args: + lock: The operation-wide lock. + termination_manager: The _interfaces.TerminationManager for the operation. + transmission_manager: The _interfaces.TransmissionManager for the + operation. + expiration_manager: The _interfaces.ExpirationManager for the operation. + context: A base.OperationContext for use by the customer during the + operation. + operator: A base.Operator for use by the customer during the operation. + reception_manager: The _interfaces.ReceptionManager for the operation. + """ + self._lock = lock + self._termination_manager = termination_manager + self._transmission_manager = transmission_manager + self._expiration_manager = expiration_manager + self._reception_manager = reception_manager + + self.context = context + self.operator = operator + + def handle_ticket(self, ticket): + with self._lock: + self._reception_manager.receive_ticket(ticket) + + def abort(self, outcome): + with self._lock: + if self._termination_manager.outcome is None: + self._termination_manager.abort(outcome) + self._transmission_manager.abort(outcome) + self._expiration_manager.terminate() + + +def invocation_operate( + operation_id, group, method, subscription, timeout, initial_metadata, + payload, completion, ticket_sink, termination_action, pool): + """Constructs objects necessary for front-side operation management. + + Args: + operation_id: An object identifying the operation. + group: The group identifier of the operation. + method: The method identifier of the operation. + subscription: A base.Subscription describing the customer's interest in the + results of the operation. + timeout: A length of time in seconds to allow for the operation. + initial_metadata: An initial metadata value to be sent to the other side of + the operation. May be None if the initial metadata will be passed later or + if there will be no initial metadata passed at all. + payload: The first payload value to be transmitted to the other side. May be + None if there is no such value or if the customer chose not to pass it at + operation invocation. + completion: A base.Completion value indicating the end of values passed to + the other side of the operation. + ticket_sink: A callable that accepts links.Tickets and delivers them to the + other side of the operation. + termination_action: A callable that accepts the outcome of the operation as + a base.Outcome value to be called on operation completion. + pool: A thread pool with which to do the work of the operation. + + Returns: + An _interfaces.Operation for the operation. + """ + lock = threading.Lock() + with lock: + termination_manager = _termination.invocation_termination_manager( + termination_action, pool) + transmission_manager = _transmission.TransmissionManager( + operation_id, ticket_sink, lock, pool, termination_manager) + expiration_manager = _expiration.invocation_expiration_manager( + timeout, lock, termination_manager, transmission_manager) + operation_context = _context.OperationContext( + lock, termination_manager, transmission_manager, expiration_manager) + emission_manager = _emission.EmissionManager( + lock, termination_manager, transmission_manager, expiration_manager) + ingestion_manager = _ingestion.invocation_ingestion_manager( + subscription, lock, pool, termination_manager, transmission_manager, + expiration_manager) + reception_manager = _reception.ReceptionManager( + termination_manager, transmission_manager, expiration_manager, + ingestion_manager) + + termination_manager.set_expiration_manager(expiration_manager) + transmission_manager.set_expiration_manager(expiration_manager) + emission_manager.set_ingestion_manager(ingestion_manager) + + transmission_manager.kick_off( + group, method, timeout, initial_metadata, payload, completion, None) + + return _EasyOperation( + lock, termination_manager, transmission_manager, expiration_manager, + operation_context, emission_manager, reception_manager) + + +def service_operate( + servicer_package, ticket, ticket_sink, termination_action, pool): + """Constructs an Operation for service of an operation. + + Args: + servicer_package: A _utilities.ServicerPackage to be used servicing the + operation. + ticket: The first links.Ticket received for the operation. + ticket_sink: A callable that accepts links.Tickets and delivers them to the + other side of the operation. + termination_action: A callable that accepts the outcome of the operation as + a base.Outcome value to be called on operation completion. + pool: A thread pool with which to do the work of the operation. + + Returns: + An _interfaces.Operation for the operation. + """ + lock = threading.Lock() + with lock: + termination_manager = _termination.service_termination_manager( + termination_action, pool) + transmission_manager = _transmission.TransmissionManager( + ticket.operation_id, ticket_sink, lock, pool, termination_manager) + expiration_manager = _expiration.service_expiration_manager( + ticket.timeout, servicer_package.default_timeout, + servicer_package.maximum_timeout, lock, termination_manager, + transmission_manager) + operation_context = _context.OperationContext( + lock, termination_manager, transmission_manager, expiration_manager) + emission_manager = _emission.EmissionManager( + lock, termination_manager, transmission_manager, expiration_manager) + ingestion_manager = _ingestion.service_ingestion_manager( + servicer_package.servicer, operation_context, emission_manager, lock, + pool, termination_manager, transmission_manager, expiration_manager) + reception_manager = _reception.ReceptionManager( + termination_manager, transmission_manager, expiration_manager, + ingestion_manager) + + termination_manager.set_expiration_manager(expiration_manager) + transmission_manager.set_expiration_manager(expiration_manager) + emission_manager.set_ingestion_manager(ingestion_manager) + + reception_manager.receive_ticket(ticket) + + return _EasyOperation( + lock, termination_manager, transmission_manager, expiration_manager, + operation_context, emission_manager, reception_manager) diff --git a/src/python/grpcio/grpc/framework/core/_reception.py b/src/python/grpcio/grpc/framework/core/_reception.py new file mode 100644 index 0000000000..b64faf8146 --- /dev/null +++ b/src/python/grpcio/grpc/framework/core/_reception.py @@ -0,0 +1,137 @@ +# 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. + +"""State and behavior for ticket reception.""" + +from grpc.framework.core import _interfaces +from grpc.framework.interfaces.base import base +from grpc.framework.interfaces.base import utilities +from grpc.framework.interfaces.links import links + +_REMOTE_TICKET_TERMINATION_TO_LOCAL_OUTCOME = { + links.Ticket.Termination.CANCELLATION: base.Outcome.CANCELLED, + links.Ticket.Termination.EXPIRATION: base.Outcome.EXPIRED, + links.Ticket.Termination.SHUTDOWN: base.Outcome.REMOTE_SHUTDOWN, + links.Ticket.Termination.RECEPTION_FAILURE: base.Outcome.RECEPTION_FAILURE, + links.Ticket.Termination.TRANSMISSION_FAILURE: + base.Outcome.TRANSMISSION_FAILURE, + links.Ticket.Termination.LOCAL_FAILURE: base.Outcome.REMOTE_FAILURE, +} + + +class ReceptionManager(_interfaces.ReceptionManager): + """A ReceptionManager based around a _Receiver passed to it.""" + + def __init__( + self, termination_manager, transmission_manager, expiration_manager, + ingestion_manager): + """Constructor. + + Args: + termination_manager: The operation's _interfaces.TerminationManager. + transmission_manager: The operation's _interfaces.TransmissionManager. + expiration_manager: The operation's _interfaces.ExpirationManager. + ingestion_manager: The operation's _interfaces.IngestionManager. + """ + self._termination_manager = termination_manager + self._transmission_manager = transmission_manager + self._expiration_manager = expiration_manager + self._ingestion_manager = ingestion_manager + + self._lowest_unseen_sequence_number = 0 + self._out_of_sequence_tickets = {} + self._aborted = False + + def _abort(self, outcome): + self._aborted = True + self._termination_manager.abort(outcome) + self._transmission_manager.abort(outcome) + self._expiration_manager.terminate() + + def _sequence_failure(self, ticket): + """Determines a just-arrived ticket's sequential legitimacy. + + Args: + ticket: A just-arrived ticket. + + Returns: + True if the ticket is sequentially legitimate; False otherwise. + """ + if ticket.sequence_number < self._lowest_unseen_sequence_number: + return True + elif ticket.sequence_number in self._out_of_sequence_tickets: + return True + else: + return False + + def _process_one(self, ticket): + if ticket.sequence_number == 0: + self._ingestion_manager.set_group_and_method(ticket.group, ticket.method) + if ticket.timeout is not None: + self._expiration_manager.change_timeout(ticket.timeout) + if ticket.termination is None: + completion = None + else: + completion = utilities.completion( + ticket.terminal_metadata, ticket.code, ticket.message) + self._ingestion_manager.advance( + ticket.initial_metadata, ticket.payload, completion, ticket.allowance) + if ticket.allowance is not None: + self._transmission_manager.allowance(ticket.allowance) + + def _process(self, ticket): + """Process those tickets ready to be processed. + + Args: + ticket: A just-arrived ticket the sequence number of which matches this + _ReceptionManager's _lowest_unseen_sequence_number field. + """ + while True: + self._process_one(ticket) + next_ticket = self._out_of_sequence_tickets.pop( + ticket.sequence_number + 1, None) + if next_ticket is None: + self._lowest_unseen_sequence_number = ticket.sequence_number + 1 + return + else: + ticket = next_ticket + + def receive_ticket(self, ticket): + """See _interfaces.ReceptionManager.receive_ticket for specification.""" + if self._aborted: + return + elif self._sequence_failure(ticket): + self._abort(base.Outcome.RECEPTION_FAILURE) + elif ticket.termination not in (None, links.Ticket.Termination.COMPLETION): + outcome = _REMOTE_TICKET_TERMINATION_TO_LOCAL_OUTCOME[ticket.termination] + self._abort(outcome) + elif ticket.sequence_number == self._lowest_unseen_sequence_number: + self._process(ticket) + else: + self._out_of_sequence_tickets[ticket.sequence_number] = ticket diff --git a/src/python/grpcio/grpc/framework/core/_termination.py b/src/python/grpcio/grpc/framework/core/_termination.py new file mode 100644 index 0000000000..ad9f6123d8 --- /dev/null +++ b/src/python/grpcio/grpc/framework/core/_termination.py @@ -0,0 +1,212 @@ +# 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. + +"""State and behavior for operation termination.""" + +import abc + +from grpc.framework.core import _constants +from grpc.framework.core import _interfaces +from grpc.framework.foundation import callable_util +from grpc.framework.interfaces.base import base + + +def _invocation_completion_predicate( + unused_emission_complete, unused_transmission_complete, + unused_reception_complete, ingestion_complete): + return ingestion_complete + + +def _service_completion_predicate( + unused_emission_complete, transmission_complete, unused_reception_complete, + unused_ingestion_complete): + return transmission_complete + + +class TerminationManager(_interfaces.TerminationManager): + """A _interfaces.TransmissionManager on which another manager may be set.""" + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def set_expiration_manager(self, expiration_manager): + """Sets the expiration manager with which this manager will interact. + + Args: + expiration_manager: The _interfaces.ExpirationManager associated with the + current operation. + """ + raise NotImplementedError() + + +class _TerminationManager(TerminationManager): + """An implementation of TerminationManager.""" + + def __init__(self, predicate, action, pool): + """Constructor. + + Args: + predicate: One of _invocation_completion_predicate or + _service_completion_predicate to be used to determine when the operation + has completed. + action: A behavior to pass the operation outcome on operation termination. + pool: A thread pool. + """ + self._predicate = predicate + self._action = action + self._pool = pool + self._expiration_manager = None + + self.outcome = None + self._callbacks = [] + + self._emission_complete = False + self._transmission_complete = False + self._reception_complete = False + self._ingestion_complete = False + + def set_expiration_manager(self, expiration_manager): + self._expiration_manager = expiration_manager + + def _terminate_internal_only(self, outcome): + """Terminates the operation. + + Args: + outcome: A base.Outcome describing the outcome of the operation. + """ + self.outcome = outcome + callbacks = list(self._callbacks) + self._callbacks = None + + act = callable_util.with_exceptions_logged( + self._action, _constants.INTERNAL_ERROR_LOG_MESSAGE) + + if outcome is base.Outcome.LOCAL_FAILURE: + self._pool.submit(act, outcome) + else: + def call_callbacks_and_act(callbacks, outcome): + for callback in callbacks: + callback_outcome = callable_util.call_logging_exceptions( + callback, _constants.TERMINATION_CALLBACK_EXCEPTION_LOG_MESSAGE, + outcome) + if callback_outcome.exception is not None: + outcome = base.Outcome.LOCAL_FAILURE + break + act(outcome) + + self._pool.submit( + callable_util.with_exceptions_logged( + call_callbacks_and_act, _constants.INTERNAL_ERROR_LOG_MESSAGE), + callbacks, outcome) + + def _terminate_and_notify(self, outcome): + self._terminate_internal_only(outcome) + self._expiration_manager.terminate() + + def _perhaps_complete(self): + if self._predicate( + self._emission_complete, self._transmission_complete, + self._reception_complete, self._ingestion_complete): + self._terminate_and_notify(base.Outcome.COMPLETED) + return True + else: + return False + + def is_active(self): + """See _interfaces.TerminationManager.is_active for specification.""" + return self.outcome is None + + def add_callback(self, callback): + """See _interfaces.TerminationManager.add_callback for specification.""" + if self.outcome is None: + self._callbacks.append(callback) + return None + else: + return self.outcome + + def emission_complete(self): + """See superclass method for specification.""" + if self.outcome is None: + self._emission_complete = True + self._perhaps_complete() + + def transmission_complete(self): + """See superclass method for specification.""" + if self.outcome is None: + self._transmission_complete = True + return self._perhaps_complete() + else: + return False + + def reception_complete(self): + """See superclass method for specification.""" + if self.outcome is None: + self._reception_complete = True + self._perhaps_complete() + + def ingestion_complete(self): + """See superclass method for specification.""" + if self.outcome is None: + self._ingestion_complete = True + self._perhaps_complete() + + def expire(self): + """See _interfaces.TerminationManager.expire for specification.""" + self._terminate_internal_only(base.Outcome.EXPIRED) + + def abort(self, outcome): + """See _interfaces.TerminationManager.abort for specification.""" + self._terminate_and_notify(outcome) + + +def invocation_termination_manager(action, pool): + """Creates a TerminationManager appropriate for invocation-side use. + + Args: + action: An action to call on operation termination. + pool: A thread pool in which to execute the passed action and any + termination callbacks that are registered during the operation. + + Returns: + A TerminationManager appropriate for invocation-side use. + """ + return _TerminationManager(_invocation_completion_predicate, action, pool) + + +def service_termination_manager(action, pool): + """Creates a TerminationManager appropriate for service-side use. + + Args: + action: An action to call on operation termination. + pool: A thread pool in which to execute the passed action and any + termination callbacks that are registered during the operation. + + Returns: + A TerminationManager appropriate for service-side use. + """ + return _TerminationManager(_service_completion_predicate, action, pool) diff --git a/src/python/grpcio/grpc/framework/core/_transmission.py b/src/python/grpcio/grpc/framework/core/_transmission.py new file mode 100644 index 0000000000..03644f4d49 --- /dev/null +++ b/src/python/grpcio/grpc/framework/core/_transmission.py @@ -0,0 +1,294 @@ +# 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. + +"""State and behavior for ticket transmission during an operation.""" + +from grpc.framework.core import _constants +from grpc.framework.core import _interfaces +from grpc.framework.foundation import callable_util +from grpc.framework.interfaces.base import base +from grpc.framework.interfaces.links import links + +_TRANSMISSION_EXCEPTION_LOG_MESSAGE = 'Exception during transmission!' + + +def _explode_completion(completion): + if completion is None: + return None, None, None, None + else: + return ( + completion.terminal_metadata, completion.code, completion.message, + links.Ticket.Termination.COMPLETION) + + +class TransmissionManager(_interfaces.TransmissionManager): + """An _interfaces.TransmissionManager that sends links.Tickets.""" + + def __init__( + self, operation_id, ticket_sink, lock, pool, termination_manager): + """Constructor. + + Args: + operation_id: The operation's ID. + ticket_sink: A callable that accepts tickets and sends them to the other + side of the operation. + lock: The operation-servicing-wide lock object. + pool: A thread pool in which the work of transmitting tickets will be + performed. + termination_manager: The _interfaces.TerminationManager associated with + this operation. + """ + self._lock = lock + self._pool = pool + self._ticket_sink = ticket_sink + self._operation_id = operation_id + self._termination_manager = termination_manager + self._expiration_manager = None + + self._lowest_unused_sequence_number = 0 + self._remote_allowance = 1 + self._remote_complete = False + self._timeout = None + self._local_allowance = 0 + self._initial_metadata = None + self._payloads = [] + self._completion = None + self._aborted = False + self._abortion_outcome = None + self._transmitting = False + + def set_expiration_manager(self, expiration_manager): + """Sets the ExpirationManager with which this manager will cooperate.""" + self._expiration_manager = expiration_manager + + def _next_ticket(self): + """Creates the next ticket to be transmitted. + + Returns: + A links.Ticket to be sent to the other side of the operation or None if + there is nothing to be sent at this time. + """ + if self._aborted: + if self._abortion_outcome is None: + return None + else: + termination = _constants.ABORTION_OUTCOME_TO_TICKET_TERMINATION[ + self._abortion_outcome] + if termination is None: + return None + else: + self._abortion_outcome = None + return links.Ticket( + self._operation_id, self._lowest_unused_sequence_number, None, + None, None, None, None, None, None, None, None, None, + termination, None) + + action = False + # TODO(nathaniel): Support other subscriptions. + local_subscription = links.Ticket.Subscription.FULL + timeout = self._timeout + if timeout is not None: + self._timeout = None + action = True + if self._local_allowance <= 0: + allowance = None + else: + allowance = self._local_allowance + self._local_allowance = 0 + action = True + initial_metadata = self._initial_metadata + if initial_metadata is not None: + self._initial_metadata = None + action = True + if not self._payloads or self._remote_allowance <= 0: + payload = None + else: + payload = self._payloads.pop(0) + self._remote_allowance -= 1 + action = True + if self._completion is None or self._payloads: + terminal_metadata, code, message, termination = None, None, None, None + else: + terminal_metadata, code, message, termination = _explode_completion( + self._completion) + self._completion = None + action = True + + if action: + ticket = links.Ticket( + self._operation_id, self._lowest_unused_sequence_number, None, None, + local_subscription, timeout, allowance, initial_metadata, payload, + terminal_metadata, code, message, termination, None) + self._lowest_unused_sequence_number += 1 + return ticket + else: + return None + + def _transmit(self, ticket): + """Commences the transmission loop sending tickets. + + Args: + ticket: A links.Ticket to be sent to the other side of the operation. + """ + def transmit(ticket): + while True: + transmission_outcome = callable_util.call_logging_exceptions( + self._ticket_sink, _TRANSMISSION_EXCEPTION_LOG_MESSAGE, ticket) + if transmission_outcome.exception is None: + with self._lock: + if ticket.termination is links.Ticket.Termination.COMPLETION: + self._termination_manager.transmission_complete() + ticket = self._next_ticket() + if ticket is None: + self._transmitting = False + return + else: + with self._lock: + if self._termination_manager.outcome is None: + self._termination_manager.abort(base.Outcome.TRANSMISSION_FAILURE) + self._expiration_manager.terminate() + return + + self._pool.submit(callable_util.with_exceptions_logged( + transmit, _constants.INTERNAL_ERROR_LOG_MESSAGE), ticket) + self._transmitting = True + + def kick_off( + self, group, method, timeout, initial_metadata, payload, completion, + allowance): + """See _interfaces.TransmissionManager.kickoff for specification.""" + # TODO(nathaniel): Support other subscriptions. + subscription = links.Ticket.Subscription.FULL + terminal_metadata, code, message, termination = _explode_completion( + completion) + self._remote_allowance = 1 if payload is None else 0 + ticket = links.Ticket( + self._operation_id, 0, group, method, subscription, timeout, allowance, + initial_metadata, payload, terminal_metadata, code, message, + termination, None) + self._lowest_unused_sequence_number = 1 + self._transmit(ticket) + + def advance(self, initial_metadata, payload, completion, allowance): + """See _interfaces.TransmissionManager.advance for specification.""" + effective_initial_metadata = initial_metadata + effective_payload = payload + effective_completion = completion + if allowance is not None and not self._remote_complete: + effective_allowance = allowance + else: + effective_allowance = None + if self._transmitting: + if effective_initial_metadata is not None: + self._initial_metadata = effective_initial_metadata + if effective_payload is not None: + self._payloads.append(effective_payload) + if effective_completion is not None: + self._completion = effective_completion + if effective_allowance is not None: + self._local_allowance += effective_allowance + else: + if effective_payload is not None: + if 0 < self._remote_allowance: + ticket_payload = effective_payload + self._remote_allowance -= 1 + else: + self._payloads.append(effective_payload) + ticket_payload = None + else: + ticket_payload = None + if effective_completion is not None and not self._payloads: + ticket_completion = effective_completion + else: + self._completion = effective_completion + ticket_completion = None + if any( + (effective_initial_metadata, ticket_payload, ticket_completion, + effective_allowance)): + terminal_metadata, code, message, termination = _explode_completion( + completion) + ticket = links.Ticket( + self._operation_id, self._lowest_unused_sequence_number, None, None, + None, None, allowance, effective_initial_metadata, ticket_payload, + terminal_metadata, code, message, termination, None) + self._lowest_unused_sequence_number += 1 + self._transmit(ticket) + + def timeout(self, timeout): + """See _interfaces.TransmissionManager.timeout for specification.""" + if self._transmitting: + self._timeout = timeout + else: + ticket = links.Ticket( + self._operation_id, self._lowest_unused_sequence_number, None, None, + None, timeout, None, None, None, None, None, None, None, None) + self._lowest_unused_sequence_number += 1 + self._transmit(ticket) + + def allowance(self, allowance): + """See _interfaces.TransmissionManager.allowance for specification.""" + if self._transmitting or not self._payloads: + self._remote_allowance += allowance + else: + self._remote_allowance += allowance - 1 + payload = self._payloads.pop(0) + if self._payloads: + completion = None + else: + completion = self._completion + self._completion = None + terminal_metadata, code, message, termination = _explode_completion( + completion) + ticket = links.Ticket( + self._operation_id, self._lowest_unused_sequence_number, None, None, + None, None, None, None, payload, terminal_metadata, code, message, + termination, None) + self._lowest_unused_sequence_number += 1 + self._transmit(ticket) + + def remote_complete(self): + """See _interfaces.TransmissionManager.remote_complete for specification.""" + self._remote_complete = True + self._local_allowance = 0 + + def abort(self, outcome): + """See _interfaces.TransmissionManager.abort for specification.""" + if self._transmitting: + self._aborted, self._abortion_outcome = True, outcome + else: + self._aborted = True + if outcome is not None: + termination = _constants.ABORTION_OUTCOME_TO_TICKET_TERMINATION[ + outcome] + if termination is not None: + ticket = links.Ticket( + self._operation_id, self._lowest_unused_sequence_number, None, + None, None, None, None, None, None, None, None, None, + termination, None) + self._transmit(ticket) diff --git a/src/python/grpcio/grpc/framework/core/_utilities.py b/src/python/grpcio/grpc/framework/core/_utilities.py new file mode 100644 index 0000000000..5b0d798751 --- /dev/null +++ b/src/python/grpcio/grpc/framework/core/_utilities.py @@ -0,0 +1,46 @@ +# 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. + +"""Package-internal utilities.""" + +import collections + + +class ServicerPackage( + collections.namedtuple( + 'ServicerPackage', ('servicer', 'default_timeout', 'maximum_timeout'))): + """A trivial bundle class. + + Attributes: + servicer: A base.Servicer. + default_timeout: A float indicating the length of time in seconds to allow + for an operation invoked without a timeout. + maximum_timeout: A float indicating the maximum length of time in seconds to + allow for an operation. + """ diff --git a/src/python/grpcio/grpc/framework/core/implementations.py b/src/python/grpcio/grpc/framework/core/implementations.py new file mode 100644 index 0000000000..364a7faed4 --- /dev/null +++ b/src/python/grpcio/grpc/framework/core/implementations.py @@ -0,0 +1,62 @@ +# 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. + +"""Entry points into the ticket-exchange-based base layer implementation.""" + +# base and links are referenced from specification in this module. +from grpc.framework.core import _end +from grpc.framework.interfaces.base import base # pylint: disable=unused-import +from grpc.framework.interfaces.links import links # pylint: disable=unused-import + + +def invocation_end_link(): + """Creates a base.End-links.Link suitable for operation invocation. + + Returns: + An object that is both a base.End and a links.Link, that supports operation + invocation, and that translates operation invocation into ticket exchange. + """ + return _end.serviceless_end_link() + + +def service_end_link(servicer, default_timeout, maximum_timeout): + """Creates a base.End-links.Link suitable for operation service. + + Args: + servicer: A base.Servicer for servicing operations. + default_timeout: A length of time in seconds to be used as the default + time alloted for a single operation. + maximum_timeout: A length of time in seconds to be used as the maximum + time alloted for a single operation. + + Returns: + An object that is both a base.End and a links.Link and that services + operations that arrive at it through ticket exchange. + """ + return _end.serviceful_end_link(servicer, default_timeout, maximum_timeout) diff --git a/src/python/grpcio/grpc/framework/interfaces/base/base.py b/src/python/grpcio/grpc/framework/interfaces/base/base.py index 9d1651daac..76e0a5bdae 100644 --- a/src/python/grpcio/grpc/framework/interfaces/base/base.py +++ b/src/python/grpcio/grpc/framework/interfaces/base/base.py @@ -27,10 +27,20 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""The base interface of RPC Framework.""" +"""The base interface of RPC Framework. +Implementations of this interface support the conduct of "operations": +exchanges between two distinct ends of an arbitrary number of data payloads +and metadata such as a name for the operation, initial and terminal metadata +in each direction, and flow control. These operations may be used for transfers +of data, remote procedure calls, status indication, or anything else +applications choose. +""" + +# threading is referenced from specification in this module. import abc import enum +import threading # abandonment is referenced from specification in this module. from grpc.framework.foundation import abandonment # pylint: disable=unused-import @@ -208,19 +218,26 @@ class End(object): raise NotImplementedError() @abc.abstractmethod - def stop_gracefully(self): - """Gracefully stops this object's service of operations. + def stop(self, grace): + """Stops this object's service of operations. - Operations in progress will be allowed to complete, and this method blocks - until all of them have. - """ - raise NotImplementedError() + This object will refuse service of new operations as soon as this method is + called but operations under way at the time of the call may be given a + grace period during which they are allowed to finish. - @abc.abstractmethod - def stop_immediately(self): - """Immediately stops this object's service of operations. + Args: + grace: A duration of time in seconds to allow ongoing operations to + terminate before being forcefully terminated by the stopping of this + End. May be zero to terminate all ongoing operations and immediately + stop. - Operations in progress will not be allowed to complete. + Returns: + A threading.Event that will be set to indicate all operations having + terminated and this End having completely stopped. The returned event + may not be set until after the full grace period (if some ongoing + operation continues for the full length of the period) or it may be set + much sooner (if for example this End had no operations in progress at + the time its stop method was called). """ raise NotImplementedError() diff --git a/src/python/grpcio/grpc/framework/interfaces/face/__init__.py b/src/python/grpcio/grpc/framework/interfaces/face/__init__.py new file mode 100644 index 0000000000..7086519106 --- /dev/null +++ b/src/python/grpcio/grpc/framework/interfaces/face/__init__.py @@ -0,0 +1,30 @@ +# 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. + + diff --git a/src/python/grpcio/grpc/framework/interfaces/face/face.py b/src/python/grpcio/grpc/framework/interfaces/face/face.py new file mode 100644 index 0000000000..948e7505b6 --- /dev/null +++ b/src/python/grpcio/grpc/framework/interfaces/face/face.py @@ -0,0 +1,933 @@ +# 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. + +"""Interfaces defining the Face layer of RPC Framework.""" + +import abc +import collections +import enum + +# cardinality, style, abandonment, future, and stream are +# referenced from specification in this module. +from grpc.framework.common import cardinality # pylint: disable=unused-import +from grpc.framework.common import style # pylint: disable=unused-import +from grpc.framework.foundation import abandonment # pylint: disable=unused-import +from grpc.framework.foundation import future # pylint: disable=unused-import +from grpc.framework.foundation import stream # pylint: disable=unused-import + + +class NoSuchMethodError(Exception): + """Raised by customer code to indicate an unrecognized method. + + Attributes: + group: The group of the unrecognized method. + name: The name of the unrecognized method. + """ + + def __init__(self, group, method): + """Constructor. + + Args: + group: The group identifier of the unrecognized RPC name. + method: The method identifier of the unrecognized RPC name. + """ + super(NoSuchMethodError, self).__init__() + self.group = group + self.method = method + + def __repr__(self): + return 'face.NoSuchMethodError(%s, %s)' % (self.group, self.method,) + + +class Abortion( + collections.namedtuple( + 'Abortion', + ('kind', 'initial_metadata', 'terminal_metadata', 'code', 'details',))): + """A value describing RPC abortion. + + Attributes: + kind: A Kind value identifying how the RPC failed. + initial_metadata: The initial metadata from the other side of the RPC or + None if no initial metadata value was received. + terminal_metadata: The terminal metadata from the other side of the RPC or + None if no terminal metadata value was received. + code: The code value from the other side of the RPC or None if no code value + was received. + details: The details value from the other side of the RPC or None if no + details value was received. + """ + + @enum.unique + class Kind(enum.Enum): + """Types of RPC abortion.""" + + CANCELLED = 'cancelled' + EXPIRED = 'expired' + LOCAL_SHUTDOWN = 'local shutdown' + REMOTE_SHUTDOWN = 'remote shutdown' + NETWORK_FAILURE = 'network failure' + LOCAL_FAILURE = 'local failure' + REMOTE_FAILURE = 'remote failure' + + +class AbortionError(Exception): + """Common super type for exceptions indicating RPC abortion. + + initial_metadata: The initial metadata from the other side of the RPC or + None if no initial metadata value was received. + terminal_metadata: The terminal metadata from the other side of the RPC or + None if no terminal metadata value was received. + code: The code value from the other side of the RPC or None if no code value + was received. + details: The details value from the other side of the RPC or None if no + details value was received. + """ + __metaclass__ = abc.ABCMeta + + def __init__(self, initial_metadata, terminal_metadata, code, details): + super(AbortionError, self).__init__() + self.initial_metadata = initial_metadata + self.terminal_metadata = terminal_metadata + self.code = code + self.details = details + + +class CancellationError(AbortionError): + """Indicates that an RPC has been cancelled.""" + + +class ExpirationError(AbortionError): + """Indicates that an RPC has expired ("timed out").""" + + +class LocalShutdownError(AbortionError): + """Indicates that an RPC has terminated due to local shutdown of RPCs.""" + + +class RemoteShutdownError(AbortionError): + """Indicates that an RPC has terminated due to remote shutdown of RPCs.""" + + +class NetworkError(AbortionError): + """Indicates that some error occurred on the network.""" + + +class LocalError(AbortionError): + """Indicates that an RPC has terminated due to a local defect.""" + + +class RemoteError(AbortionError): + """Indicates that an RPC has terminated due to a remote defect.""" + + +class RpcContext(object): + """Provides RPC-related information and action.""" + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def is_active(self): + """Describes whether the RPC is active or has terminated.""" + raise NotImplementedError() + + @abc.abstractmethod + def time_remaining(self): + """Describes the length of allowed time remaining for the RPC. + + Returns: + A nonnegative float indicating the length of allowed time in seconds + remaining for the RPC to complete before it is considered to have timed + out. + """ + raise NotImplementedError() + + @abc.abstractmethod + def add_abortion_callback(self, abortion_callback): + """Registers a callback to be called if the RPC is aborted. + + Args: + abortion_callback: A callable to be called and passed an Abortion value + in the event of RPC abortion. + """ + raise NotImplementedError() + + @abc.abstractmethod + def cancel(self): + """Cancels the RPC. + + Idempotent and has no effect if the RPC has already terminated. + """ + raise NotImplementedError() + + +class Call(RpcContext): + """Invocation-side utility object for an RPC.""" + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def initial_metadata(self): + """Accesses the initial metadata from the service-side of the RPC. + + This method blocks until the value is available or is known not to have been + emitted from the service-side of the RPC. + + Returns: + The initial metadata object emitted by the service-side of the RPC, or + None if there was no such value. + """ + raise NotImplementedError() + + @abc.abstractmethod + def terminal_metadata(self): + """Accesses the terminal metadata from the service-side of the RPC. + + This method blocks until the value is available or is known not to have been + emitted from the service-side of the RPC. + + Returns: + The terminal metadata object emitted by the service-side of the RPC, or + None if there was no such value. + """ + raise NotImplementedError() + + @abc.abstractmethod + def code(self): + """Accesses the code emitted by the service-side of the RPC. + + This method blocks until the value is available or is known not to have been + emitted from the service-side of the RPC. + + Returns: + The code object emitted by the service-side of the RPC, or None if there + was no such value. + """ + raise NotImplementedError() + + @abc.abstractmethod + def details(self): + """Accesses the details value emitted by the service-side of the RPC. + + This method blocks until the value is available or is known not to have been + emitted from the service-side of the RPC. + + Returns: + The details value emitted by the service-side of the RPC, or None if there + was no such value. + """ + raise NotImplementedError() + + +class ServicerContext(RpcContext): + """A context object passed to method implementations.""" + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def invocation_metadata(self): + """Accesses the metadata from the invocation-side of the RPC. + + This method blocks until the value is available or is known not to have been + emitted from the invocation-side of the RPC. + + Returns: + The metadata object emitted by the invocation-side of the RPC, or None if + there was no such value. + """ + raise NotImplementedError() + + @abc.abstractmethod + def initial_metadata(self, initial_metadata): + """Accepts the service-side initial metadata value of the RPC. + + This method need not be called by method implementations if they have no + service-side initial metadata to transmit. + + Args: + initial_metadata: The service-side initial metadata value of the RPC to + be transmitted to the invocation side of the RPC. + """ + raise NotImplementedError() + + @abc.abstractmethod + def terminal_metadata(self, terminal_metadata): + """Accepts the service-side terminal metadata value of the RPC. + + This method need not be called by method implementations if they have no + service-side terminal metadata to transmit. + + Args: + terminal_metadata: The service-side terminal metadata value of the RPC to + be transmitted to the invocation side of the RPC. + """ + raise NotImplementedError() + + @abc.abstractmethod + def code(self, code): + """Accepts the service-side code of the RPC. + + This method need not be called by method implementations if they have no + code to transmit. + + Args: + code: The code of the RPC to be transmitted to the invocation side of the + RPC. + """ + raise NotImplementedError() + + @abc.abstractmethod + def details(self, details): + """Accepts the service-side details of the RPC. + + This method need not be called by method implementations if they have no + service-side details to transmit. + + Args: + details: The service-side details value of the RPC to be transmitted to + the invocation side of the RPC. + """ + raise NotImplementedError() + + +class ResponseReceiver(object): + """Invocation-side object used to accept the output of an RPC.""" + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def initial_metadata(self, initial_metadata): + """Receives the initial metadata from the service-side of the RPC. + + Args: + initial_metadata: The initial metadata object emitted from the + service-side of the RPC. + """ + raise NotImplementedError() + + @abc.abstractmethod + def response(self, response): + """Receives a response from the service-side of the RPC. + + Args: + response: A response object emitted from the service-side of the RPC. + """ + raise NotImplementedError() + + @abc.abstractmethod + def complete(self, terminal_metadata, code, details): + """Receives the completion values emitted from the service-side of the RPC. + + Args: + terminal_metadata: The terminal metadata object emitted from the + service-side of the RPC. + code: The code object emitted from the service-side of the RPC. + details: The details object emitted from the service-side of the RPC. + """ + raise NotImplementedError() + + +class UnaryUnaryMultiCallable(object): + """Affords invoking a unary-unary RPC in any call style.""" + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def __call__( + self, request, timeout, metadata=None, with_call=False): + """Synchronously invokes the underlying RPC. + + Args: + request: The request value for the RPC. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of + the RPC. + with_call: Whether or not to include return a Call for the RPC in addition + to the reponse. + + Returns: + The response value for the RPC, and a Call for the RPC if with_call was + set to True at invocation. + + Raises: + AbortionError: Indicating that the RPC was aborted. + """ + raise NotImplementedError() + + @abc.abstractmethod + def future(self, request, timeout, metadata=None): + """Asynchronously invokes the underlying RPC. + + Args: + request: The request value for the RPC. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of + the RPC. + + Returns: + An object that is both a Call for the RPC and a future.Future. In the + event of RPC completion, the return Future's result value will be the + response value of the RPC. In the event of RPC abortion, the returned + Future's exception value will be an AbortionError. + """ + raise NotImplementedError() + + @abc.abstractmethod + def event( + self, request, receiver, abortion_callback, timeout, + metadata=None): + """Asynchronously invokes the underlying RPC. + + Args: + request: The request value for the RPC. + receiver: A ResponseReceiver to be passed the response data of the RPC. + abortion_callback: A callback to be called and passed an Abortion value + in the event of RPC abortion. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of + the RPC. + + Returns: + A Call for the RPC. + """ + raise NotImplementedError() + + +class UnaryStreamMultiCallable(object): + """Affords invoking a unary-stream RPC in any call style.""" + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def __call__(self, request, timeout, metadata=None): + """Invokes the underlying RPC. + + Args: + request: The request value for the RPC. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of + the RPC. + + Returns: + An object that is both a Call for the RPC and an iterator of response + values. Drawing response values from the returned iterator may raise + AbortionError indicating abortion of the RPC. + """ + raise NotImplementedError() + + @abc.abstractmethod + def event( + self, request, receiver, abortion_callback, timeout, + metadata=None): + """Asynchronously invokes the underlying RPC. + + Args: + request: The request value for the RPC. + receiver: A ResponseReceiver to be passed the response data of the RPC. + abortion_callback: A callback to be called and passed an Abortion value + in the event of RPC abortion. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of + the RPC. + + Returns: + A Call object for the RPC. + """ + raise NotImplementedError() + + +class StreamUnaryMultiCallable(object): + """Affords invoking a stream-unary RPC in any call style.""" + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def __call__( + self, request_iterator, timeout, metadata=None, + with_call=False): + """Synchronously invokes the underlying RPC. + + Args: + request_iterator: An iterator that yields request values for the RPC. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of + the RPC. + with_call: Whether or not to include return a Call for the RPC in addition + to the reponse. + + Returns: + The response value for the RPC, and a Call for the RPC if with_call was + set to True at invocation. + + Raises: + AbortionError: Indicating that the RPC was aborted. + """ + raise NotImplementedError() + + @abc.abstractmethod + def future(self, request_iterator, timeout, metadata=None): + """Asynchronously invokes the underlying RPC. + + Args: + request_iterator: An iterator that yields request values for the RPC. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of + the RPC. + + Returns: + An object that is both a Call for the RPC and a future.Future. In the + event of RPC completion, the return Future's result value will be the + response value of the RPC. In the event of RPC abortion, the returned + Future's exception value will be an AbortionError. + """ + raise NotImplementedError() + + @abc.abstractmethod + def event( + self, receiver, abortion_callback, timeout, metadata=None): + """Asynchronously invokes the underlying RPC. + + Args: + receiver: A ResponseReceiver to be passed the response data of the RPC. + abortion_callback: A callback to be called and passed an Abortion value + in the event of RPC abortion. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of + the RPC. + + Returns: + A single object that is both a Call object for the RPC and a + stream.Consumer to which the request values of the RPC should be passed. + """ + raise NotImplementedError() + + +class StreamStreamMultiCallable(object): + """Affords invoking a stream-stream RPC in any call style.""" + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def __call__(self, request_iterator, timeout, metadata=None): + """Invokes the underlying RPC. + + Args: + request_iterator: An iterator that yields request values for the RPC. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of + the RPC. + + Returns: + An object that is both a Call for the RPC and an iterator of response + values. Drawing response values from the returned iterator may raise + AbortionError indicating abortion of the RPC. + """ + raise NotImplementedError() + + @abc.abstractmethod + def event( + self, receiver, abortion_callback, timeout, metadata=None): + """Asynchronously invokes the underlying RPC. + + Args: + receiver: A ResponseReceiver to be passed the response data of the RPC. + abortion_callback: A callback to be called and passed an Abortion value + in the event of RPC abortion. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of + the RPC. + + Returns: + A single object that is both a Call object for the RPC and a + stream.Consumer to which the request values of the RPC should be passed. + """ + raise NotImplementedError() + + +class MethodImplementation(object): + """A sum type that describes a method implementation. + + Attributes: + cardinality: A cardinality.Cardinality value. + style: A style.Service value. + unary_unary_inline: The implementation of the method as a callable value + that takes a request value and a ServicerContext object and returns a + response value. Only non-None if cardinality is + cardinality.Cardinality.UNARY_UNARY and style is style.Service.INLINE. + unary_stream_inline: The implementation of the method as a callable value + that takes a request value and a ServicerContext object and returns an + iterator of response values. Only non-None if cardinality is + cardinality.Cardinality.UNARY_STREAM and style is style.Service.INLINE. + stream_unary_inline: The implementation of the method as a callable value + that takes an iterator of request values and a ServicerContext object and + returns a response value. Only non-None if cardinality is + cardinality.Cardinality.STREAM_UNARY and style is style.Service.INLINE. + stream_stream_inline: The implementation of the method as a callable value + that takes an iterator of request values and a ServicerContext object and + returns an iterator of response values. Only non-None if cardinality is + cardinality.Cardinality.STREAM_STREAM and style is style.Service.INLINE. + unary_unary_event: The implementation of the method as a callable value that + takes a request value, a response callback to which to pass the response + value of the RPC, and a ServicerContext. Only non-None if cardinality is + cardinality.Cardinality.UNARY_UNARY and style is style.Service.EVENT. + unary_stream_event: The implementation of the method as a callable value + that takes a request value, a stream.Consumer to which to pass the + response values of the RPC, and a ServicerContext. Only non-None if + cardinality is cardinality.Cardinality.UNARY_STREAM and style is + style.Service.EVENT. + stream_unary_event: The implementation of the method as a callable value + that takes a response callback to which to pass the response value of the + RPC and a ServicerContext and returns a stream.Consumer to which the + request values of the RPC should be passed. Only non-None if cardinality + is cardinality.Cardinality.STREAM_UNARY and style is style.Service.EVENT. + stream_stream_event: The implementation of the method as a callable value + that takes a stream.Consumer to which to pass the response values of the + RPC and a ServicerContext and returns a stream.Consumer to which the + request values of the RPC should be passed. Only non-None if cardinality + is cardinality.Cardinality.STREAM_STREAM and style is + style.Service.EVENT. + """ + __metaclass__ = abc.ABCMeta + + +class MultiMethodImplementation(object): + """A general type able to service many methods.""" + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def service(self, group, method, response_consumer, context): + """Services an RPC. + + Args: + group: The group identifier of the RPC. + method: The method identifier of the RPC. + response_consumer: A stream.Consumer to be called to accept the response + values of the RPC. + context: a ServicerContext object. + + Returns: + A stream.Consumer with which to accept the request values of the RPC. The + consumer returned from this method may or may not be invoked to + completion: in the case of RPC abortion, RPC Framework will simply stop + passing values to this object. Implementations must not assume that this + object will be called to completion of the request stream or even called + at all. + + Raises: + abandonment.Abandoned: May or may not be raised when the RPC has been + aborted. + NoSuchMethodError: If this MultiMethod does not recognize the given group + and name for the RPC and is not able to service the RPC. + """ + raise NotImplementedError() + + +class GenericStub(object): + """Affords RPC invocation via generic methods.""" + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def blocking_unary_unary( + self, group, method, request, timeout, metadata=None, + with_call=False): + """Invokes a unary-request-unary-response method. + + This method blocks until either returning the response value of the RPC + (in the event of RPC completion) or raising an exception (in the event of + RPC abortion). + + Args: + group: The group identifier of the RPC. + method: The method identifier of the RPC. + request: The request value for the RPC. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of the RPC. + with_call: Whether or not to include return a Call for the RPC in addition + to the reponse. + + Returns: + The response value for the RPC, and a Call for the RPC if with_call was + set to True at invocation. + + Raises: + AbortionError: Indicating that the RPC was aborted. + """ + raise NotImplementedError() + + @abc.abstractmethod + def future_unary_unary( + self, group, method, request, timeout, metadata=None): + """Invokes a unary-request-unary-response method. + + Args: + group: The group identifier of the RPC. + method: The method identifier of the RPC. + request: The request value for the RPC. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of the RPC. + + Returns: + An object that is both a Call for the RPC and a future.Future. In the + event of RPC completion, the return Future's result value will be the + response value of the RPC. In the event of RPC abortion, the returned + Future's exception value will be an AbortionError. + """ + raise NotImplementedError() + + @abc.abstractmethod + def inline_unary_stream( + self, group, method, request, timeout, metadata=None): + """Invokes a unary-request-stream-response method. + + Args: + group: The group identifier of the RPC. + method: The method identifier of the RPC. + request: The request value for the RPC. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of the RPC. + + Returns: + An object that is both a Call for the RPC and an iterator of response + values. Drawing response values from the returned iterator may raise + AbortionError indicating abortion of the RPC. + """ + raise NotImplementedError() + + @abc.abstractmethod + def blocking_stream_unary( + self, group, method, request_iterator, timeout, metadata=None, + with_call=False): + """Invokes a stream-request-unary-response method. + + This method blocks until either returning the response value of the RPC + (in the event of RPC completion) or raising an exception (in the event of + RPC abortion). + + Args: + group: The group identifier of the RPC. + method: The method identifier of the RPC. + request_iterator: An iterator that yields request values for the RPC. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of the RPC. + with_call: Whether or not to include return a Call for the RPC in addition + to the reponse. + + Returns: + The response value for the RPC, and a Call for the RPC if with_call was + set to True at invocation. + + Raises: + AbortionError: Indicating that the RPC was aborted. + """ + raise NotImplementedError() + + @abc.abstractmethod + def future_stream_unary( + self, group, method, request_iterator, timeout, metadata=None): + """Invokes a stream-request-unary-response method. + + Args: + group: The group identifier of the RPC. + method: The method identifier of the RPC. + request_iterator: An iterator that yields request values for the RPC. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of the RPC. + + Returns: + An object that is both a Call for the RPC and a future.Future. In the + event of RPC completion, the return Future's result value will be the + response value of the RPC. In the event of RPC abortion, the returned + Future's exception value will be an AbortionError. + """ + raise NotImplementedError() + + @abc.abstractmethod + def inline_stream_stream( + self, group, method, request_iterator, timeout, metadata=None): + """Invokes a stream-request-stream-response method. + + Args: + group: The group identifier of the RPC. + method: The method identifier of the RPC. + request_iterator: An iterator that yields request values for the RPC. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of the RPC. + + Returns: + An object that is both a Call for the RPC and an iterator of response + values. Drawing response values from the returned iterator may raise + AbortionError indicating abortion of the RPC. + """ + raise NotImplementedError() + + @abc.abstractmethod + def event_unary_unary( + self, group, method, request, receiver, abortion_callback, timeout, + metadata=None): + """Event-driven invocation of a unary-request-unary-response method. + + Args: + group: The group identifier of the RPC. + method: The method identifier of the RPC. + request: The request value for the RPC. + receiver: A ResponseReceiver to be passed the response data of the RPC. + abortion_callback: A callback to be called and passed an Abortion value + in the event of RPC abortion. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of the RPC. + + Returns: + A Call for the RPC. + """ + raise NotImplementedError() + + @abc.abstractmethod + def event_unary_stream( + self, group, method, request, receiver, abortion_callback, timeout, + metadata=None): + """Event-driven invocation of a unary-request-stream-response method. + + Args: + group: The group identifier of the RPC. + method: The method identifier of the RPC. + request: The request value for the RPC. + receiver: A ResponseReceiver to be passed the response data of the RPC. + abortion_callback: A callback to be called and passed an Abortion value + in the event of RPC abortion. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of the RPC. + + Returns: + A Call for the RPC. + """ + raise NotImplementedError() + + @abc.abstractmethod + def event_stream_unary( + self, group, method, receiver, abortion_callback, timeout, + metadata=None): + """Event-driven invocation of a unary-request-unary-response method. + + Args: + group: The group identifier of the RPC. + method: The method identifier of the RPC. + receiver: A ResponseReceiver to be passed the response data of the RPC. + abortion_callback: A callback to be called and passed an Abortion value + in the event of RPC abortion. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of the RPC. + + Returns: + A pair of a Call object for the RPC and a stream.Consumer to which the + request values of the RPC should be passed. + """ + raise NotImplementedError() + + @abc.abstractmethod + def event_stream_stream( + self, group, method, receiver, abortion_callback, timeout, + metadata=None): + """Event-driven invocation of a unary-request-stream-response method. + + Args: + group: The group identifier of the RPC. + method: The method identifier of the RPC. + receiver: A ResponseReceiver to be passed the response data of the RPC. + abortion_callback: A callback to be called and passed an Abortion value + in the event of RPC abortion. + timeout: A duration of time in seconds to allow for the RPC. + metadata: A metadata value to be passed to the service-side of the RPC. + + Returns: + A pair of a Call object for the RPC and a stream.Consumer to which the + request values of the RPC should be passed. + """ + raise NotImplementedError() + + @abc.abstractmethod + def unary_unary(self, group, method): + """Creates a UnaryUnaryMultiCallable for a unary-unary method. + + Args: + group: The group identifier of the RPC. + method: The method identifier of the RPC. + + Returns: + A UnaryUnaryMultiCallable value for the named unary-unary method. + """ + raise NotImplementedError() + + @abc.abstractmethod + def unary_stream(self, group, method): + """Creates a UnaryStreamMultiCallable for a unary-stream method. + + Args: + group: The group identifier of the RPC. + method: The method identifier of the RPC. + + Returns: + A UnaryStreamMultiCallable value for the name unary-stream method. + """ + raise NotImplementedError() + + @abc.abstractmethod + def stream_unary(self, group, method): + """Creates a StreamUnaryMultiCallable for a stream-unary method. + + Args: + group: The group identifier of the RPC. + method: The method identifier of the RPC. + + Returns: + A StreamUnaryMultiCallable value for the named stream-unary method. + """ + raise NotImplementedError() + + @abc.abstractmethod + def stream_stream(self, group, method): + """Creates a StreamStreamMultiCallable for a stream-stream method. + + Args: + group: The group identifier of the RPC. + method: The method identifier of the RPC. + + Returns: + A StreamStreamMultiCallable value for the named stream-stream method. + """ + raise NotImplementedError() + + +class DynamicStub(object): + """Affords RPC invocation via attributes corresponding to afforded methods. + + Instances of this type may be scoped to a single group so that attribute + access is unambiguous. + + Instances of this type respond to attribute access as follows: if the + requested attribute is the name of a unary-unary method, the value of the + attribute will be a UnaryUnaryMultiCallable with which to invoke an RPC; if + the requested attribute is the name of a unary-stream method, the value of the + attribute will be a UnaryStreamMultiCallable with which to invoke an RPC; if + the requested attribute is the name of a stream-unary method, the value of the + attribute will be a StreamUnaryMultiCallable with which to invoke an RPC; and + if the requested attribute is the name of a stream-stream method, the value of + the attribute will be a StreamStreamMultiCallable with which to invoke an RPC. + """ + __metaclass__ = abc.ABCMeta diff --git a/src/python/grpcio/grpc/framework/interfaces/face/utilities.py b/src/python/grpcio/grpc/framework/interfaces/face/utilities.py new file mode 100644 index 0000000000..db2ec6ed87 --- /dev/null +++ b/src/python/grpcio/grpc/framework/interfaces/face/utilities.py @@ -0,0 +1,178 @@ +# 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. + +"""Utilities for RPC Framework's Face interface.""" + +import collections + +# stream is referenced from specification in this module. +from grpc.framework.common import cardinality +from grpc.framework.common import style +from grpc.framework.foundation import stream # pylint: disable=unused-import +from grpc.framework.interfaces.face import face + + +class _MethodImplementation( + face.MethodImplementation, + collections.namedtuple( + '_MethodImplementation', + ['cardinality', 'style', 'unary_unary_inline', 'unary_stream_inline', + 'stream_unary_inline', 'stream_stream_inline', 'unary_unary_event', + 'unary_stream_event', 'stream_unary_event', 'stream_stream_event',])): + pass + + +def unary_unary_inline(behavior): + """Creates an face.MethodImplementation for the given behavior. + + Args: + behavior: The implementation of a unary-unary RPC method as a callable value + that takes a request value and an face.ServicerContext object and + returns a response value. + + Returns: + An face.MethodImplementation derived from the given behavior. + """ + return _MethodImplementation( + cardinality.Cardinality.UNARY_UNARY, style.Service.INLINE, behavior, + None, None, None, None, None, None, None) + + +def unary_stream_inline(behavior): + """Creates an face.MethodImplementation for the given behavior. + + Args: + behavior: The implementation of a unary-stream RPC method as a callable + value that takes a request value and an face.ServicerContext object and + returns an iterator of response values. + + Returns: + An face.MethodImplementation derived from the given behavior. + """ + return _MethodImplementation( + cardinality.Cardinality.UNARY_STREAM, style.Service.INLINE, None, + behavior, None, None, None, None, None, None) + + +def stream_unary_inline(behavior): + """Creates an face.MethodImplementation for the given behavior. + + Args: + behavior: The implementation of a stream-unary RPC method as a callable + value that takes an iterator of request values and an + face.ServicerContext object and returns a response value. + + Returns: + An face.MethodImplementation derived from the given behavior. + """ + return _MethodImplementation( + cardinality.Cardinality.STREAM_UNARY, style.Service.INLINE, None, None, + behavior, None, None, None, None, None) + + +def stream_stream_inline(behavior): + """Creates an face.MethodImplementation for the given behavior. + + Args: + behavior: The implementation of a stream-stream RPC method as a callable + value that takes an iterator of request values and an + face.ServicerContext object and returns an iterator of response values. + + Returns: + An face.MethodImplementation derived from the given behavior. + """ + return _MethodImplementation( + cardinality.Cardinality.STREAM_STREAM, style.Service.INLINE, None, None, + None, behavior, None, None, None, None) + + +def unary_unary_event(behavior): + """Creates an face.MethodImplementation for the given behavior. + + Args: + behavior: The implementation of a unary-unary RPC method as a callable + value that takes a request value, a response callback to which to pass + the response value of the RPC, and an face.ServicerContext. + + Returns: + An face.MethodImplementation derived from the given behavior. + """ + return _MethodImplementation( + cardinality.Cardinality.UNARY_UNARY, style.Service.EVENT, None, None, + None, None, behavior, None, None, None) + + +def unary_stream_event(behavior): + """Creates an face.MethodImplementation for the given behavior. + + Args: + behavior: The implementation of a unary-stream RPC method as a callable + value that takes a request value, a stream.Consumer to which to pass the + the response values of the RPC, and an face.ServicerContext. + + Returns: + An face.MethodImplementation derived from the given behavior. + """ + return _MethodImplementation( + cardinality.Cardinality.UNARY_STREAM, style.Service.EVENT, None, None, + None, None, None, behavior, None, None) + + +def stream_unary_event(behavior): + """Creates an face.MethodImplementation for the given behavior. + + Args: + behavior: The implementation of a stream-unary RPC method as a callable + value that takes a response callback to which to pass the response value + of the RPC and an face.ServicerContext and returns a stream.Consumer to + which the request values of the RPC should be passed. + + Returns: + An face.MethodImplementation derived from the given behavior. + """ + return _MethodImplementation( + cardinality.Cardinality.STREAM_UNARY, style.Service.EVENT, None, None, + None, None, None, None, behavior, None) + + +def stream_stream_event(behavior): + """Creates an face.MethodImplementation for the given behavior. + + Args: + behavior: The implementation of a stream-stream RPC method as a callable + value that takes a stream.Consumer to which to pass the response values + of the RPC and an face.ServicerContext and returns a stream.Consumer to + which the request values of the RPC should be passed. + + Returns: + An face.MethodImplementation derived from the given behavior. + """ + return _MethodImplementation( + cardinality.Cardinality.STREAM_STREAM, style.Service.EVENT, None, None, + None, None, None, None, None, behavior) diff --git a/src/python/grpcio/grpc/framework/interfaces/links/links.py b/src/python/grpcio/grpc/framework/interfaces/links/links.py index 5ebbac8a6f..b98a30a399 100644 --- a/src/python/grpcio/grpc/framework/interfaces/links/links.py +++ b/src/python/grpcio/grpc/framework/interfaces/links/links.py @@ -34,12 +34,30 @@ import collections import enum +class Transport(collections.namedtuple('Transport', ('kind', 'value',))): + """A sum type for handles to an underlying transport system. + + Attributes: + kind: A Kind value identifying the kind of value being passed to or from + the underlying transport. + value: The value being passed through RPC Framework between the high-level + application and the underlying transport. + """ + + @enum.unique + class Kind(enum.Enum): + CALL_OPTION = 'call option' + SERVICER_CONTEXT = 'servicer context' + INVOCATION_CONTEXT = 'invocation context' + + class Ticket( collections.namedtuple( 'Ticket', - ['operation_id', 'sequence_number', 'group', 'method', 'subscription', + ('operation_id', 'sequence_number', 'group', 'method', 'subscription', 'timeout', 'allowance', 'initial_metadata', 'payload', - 'terminal_metadata', 'code', 'message', 'termination'])): + 'terminal_metadata', 'code', 'message', 'termination', + 'transport',))): """A sum type for all values sent from a front to a back. Attributes: @@ -81,6 +99,8 @@ class Ticket( termination: A Termination value describing the end of the operation, or None if the operation has not yet terminated. If set, no further tickets may be sent in the same direction. + transport: A Transport value or None, with further semantics being a matter + between high-level application and underlying transport. """ @enum.unique @@ -98,7 +118,7 @@ class Ticket( COMPLETION = 'completion' CANCELLATION = 'cancellation' EXPIRATION = 'expiration' - LOCAL_SHUTDOWN = 'local shutdown' + SHUTDOWN = 'shutdown' RECEPTION_FAILURE = 'reception failure' TRANSMISSION_FAILURE = 'transmission failure' LOCAL_FAILURE = 'local failure' diff --git a/src/python/grpcio_test/grpc_test/_adapter/_low_test.py b/src/python/grpcio_test/grpc_test/_adapter/_low_test.py index 44fe760fbc..70149127da 100644 --- a/src/python/grpcio_test/grpc_test/_adapter/_low_test.py +++ b/src/python/grpcio_test/grpc_test/_adapter/_low_test.py @@ -52,7 +52,6 @@ def wait_for_events(completion_queues, deadline): def set_ith_result(i, completion_queue): result = completion_queue.next(deadline) with lock: - print i, completion_queue, result, time.time() - deadline results[i] = result for i, completion_queue in enumerate(completion_queues): thread = threading.Thread(target=set_ith_result, @@ -80,10 +79,12 @@ class InsecureServerInsecureClient(unittest.TestCase): del self.client_channel self.client_completion_queue.shutdown() - while self.client_completion_queue.next().type != _types.EventType.QUEUE_SHUTDOWN: + while (self.client_completion_queue.next().type != + _types.EventType.QUEUE_SHUTDOWN): pass self.server_completion_queue.shutdown() - while self.server_completion_queue.next().type != _types.EventType.QUEUE_SHUTDOWN: + while (self.server_completion_queue.next().type != + _types.EventType.QUEUE_SHUTDOWN): pass del self.client_completion_queue @@ -91,58 +92,68 @@ class InsecureServerInsecureClient(unittest.TestCase): del self.server def testEcho(self): - DEADLINE = time.time()+5 - DEADLINE_TOLERANCE = 0.25 - CLIENT_METADATA_ASCII_KEY = 'key' - CLIENT_METADATA_ASCII_VALUE = 'val' - CLIENT_METADATA_BIN_KEY = 'key-bin' - CLIENT_METADATA_BIN_VALUE = b'\0'*1000 - SERVER_INITIAL_METADATA_KEY = 'init_me_me_me' - SERVER_INITIAL_METADATA_VALUE = 'whodawha?' - SERVER_TRAILING_METADATA_KEY = 'california_is_in_a_drought' - SERVER_TRAILING_METADATA_VALUE = 'zomg it is' - SERVER_STATUS_CODE = _types.StatusCode.OK - SERVER_STATUS_DETAILS = 'our work is never over' - REQUEST = 'in death a member of project mayhem has a name' - RESPONSE = 'his name is robert paulson' - METHOD = 'twinkies' - HOST = 'hostess' + deadline = time.time() + 5 + event_time_tolerance = 2 + deadline_tolerance = 0.25 + client_metadata_ascii_key = 'key' + client_metadata_ascii_value = 'val' + client_metadata_bin_key = 'key-bin' + client_metadata_bin_value = b'\0'*1000 + server_initial_metadata_key = 'init_me_me_me' + server_initial_metadata_value = 'whodawha?' + server_trailing_metadata_key = 'california_is_in_a_drought' + server_trailing_metadata_value = 'zomg it is' + server_status_code = _types.StatusCode.OK + server_status_details = 'our work is never over' + request = 'blarghaflargh' + response = 'his name is robert paulson' + method = 'twinkies' + host = 'hostess' server_request_tag = object() - request_call_result = self.server.request_call(self.server_completion_queue, server_request_tag) + request_call_result = self.server.request_call(self.server_completion_queue, + server_request_tag) - self.assertEquals(_types.CallError.OK, request_call_result) + self.assertEqual(_types.CallError.OK, request_call_result) client_call_tag = object() - client_call = self.client_channel.create_call(self.client_completion_queue, METHOD, HOST, DEADLINE) - client_initial_metadata = [(CLIENT_METADATA_ASCII_KEY, CLIENT_METADATA_ASCII_VALUE), (CLIENT_METADATA_BIN_KEY, CLIENT_METADATA_BIN_VALUE)] + client_call = self.client_channel.create_call( + self.client_completion_queue, method, host, deadline) + client_initial_metadata = [ + (client_metadata_ascii_key, client_metadata_ascii_value), + (client_metadata_bin_key, client_metadata_bin_value) + ] client_start_batch_result = client_call.start_batch([ _types.OpArgs.send_initial_metadata(client_initial_metadata), - _types.OpArgs.send_message(REQUEST, 0), + _types.OpArgs.send_message(request, 0), _types.OpArgs.send_close_from_client(), _types.OpArgs.recv_initial_metadata(), _types.OpArgs.recv_message(), _types.OpArgs.recv_status_on_client() ], client_call_tag) - self.assertEquals(_types.CallError.OK, client_start_batch_result) + self.assertEqual(_types.CallError.OK, client_start_batch_result) - client_no_event, request_event, = wait_for_events([self.client_completion_queue, self.server_completion_queue], time.time() + 2) - self.assertEquals(client_no_event, None) - self.assertEquals(_types.EventType.OP_COMPLETE, request_event.type) + client_no_event, request_event, = wait_for_events( + [self.client_completion_queue, self.server_completion_queue], + time.time() + event_time_tolerance) + self.assertEqual(client_no_event, None) + self.assertEqual(_types.EventType.OP_COMPLETE, request_event.type) self.assertIsInstance(request_event.call, _low.Call) self.assertIs(server_request_tag, request_event.tag) - self.assertEquals(1, len(request_event.results)) + self.assertEqual(1, len(request_event.results)) received_initial_metadata = dict(request_event.results[0].initial_metadata) # Check that our metadata were transmitted - self.assertEquals( + self.assertEqual( dict(client_initial_metadata), - dict((x, received_initial_metadata[x]) for x in zip(*client_initial_metadata)[0])) + dict((x, received_initial_metadata[x]) + for x in zip(*client_initial_metadata)[0])) # Check that Python's user agent string is a part of the full user agent # string self.assertIn('Python-gRPC-{}'.format(_grpcio_metadata.__version__), received_initial_metadata['user-agent']) - self.assertEquals(METHOD, request_event.call_details.method) - self.assertEquals(HOST, request_event.call_details.host) - self.assertLess(abs(DEADLINE - request_event.call_details.deadline), DEADLINE_TOLERANCE) + self.assertEqual(method, request_event.call_details.method) + self.assertEqual(host, request_event.call_details.host) + self.assertLess(abs(deadline - request_event.call_details.deadline), + deadline_tolerance) # Check that the channel is connected, and that both it and the call have # the proper target and peer; do this after the first flurry of messages to @@ -155,33 +166,43 @@ class InsecureServerInsecureClient(unittest.TestCase): server_call_tag = object() server_call = request_event.call - server_initial_metadata = [(SERVER_INITIAL_METADATA_KEY, SERVER_INITIAL_METADATA_VALUE)] - server_trailing_metadata = [(SERVER_TRAILING_METADATA_KEY, SERVER_TRAILING_METADATA_VALUE)] + server_initial_metadata = [ + (server_initial_metadata_key, server_initial_metadata_value) + ] + server_trailing_metadata = [ + (server_trailing_metadata_key, server_trailing_metadata_value) + ] server_start_batch_result = server_call.start_batch([ _types.OpArgs.send_initial_metadata(server_initial_metadata), _types.OpArgs.recv_message(), - _types.OpArgs.send_message(RESPONSE, 0), + _types.OpArgs.send_message(response, 0), _types.OpArgs.recv_close_on_server(), - _types.OpArgs.send_status_from_server(server_trailing_metadata, SERVER_STATUS_CODE, SERVER_STATUS_DETAILS) + _types.OpArgs.send_status_from_server( + server_trailing_metadata, server_status_code, server_status_details) ], server_call_tag) - self.assertEquals(_types.CallError.OK, server_start_batch_result) + self.assertEqual(_types.CallError.OK, server_start_batch_result) - client_event, server_event, = wait_for_events([self.client_completion_queue, self.server_completion_queue], time.time() + 1) + client_event, server_event, = wait_for_events( + [self.client_completion_queue, self.server_completion_queue], + time.time() + event_time_tolerance) - self.assertEquals(6, len(client_event.results)) + self.assertEqual(6, len(client_event.results)) found_client_op_types = set() for client_result in client_event.results: - self.assertNotIn(client_result.type, found_client_op_types) # we expect each op type to be unique + # we expect each op type to be unique + self.assertNotIn(client_result.type, found_client_op_types) found_client_op_types.add(client_result.type) if client_result.type == _types.OpType.RECV_INITIAL_METADATA: - self.assertEquals(dict(server_initial_metadata), dict(client_result.initial_metadata)) + self.assertEqual(dict(server_initial_metadata), + dict(client_result.initial_metadata)) elif client_result.type == _types.OpType.RECV_MESSAGE: - self.assertEquals(RESPONSE, client_result.message) + self.assertEqual(response, client_result.message) elif client_result.type == _types.OpType.RECV_STATUS_ON_CLIENT: - self.assertEquals(dict(server_trailing_metadata), dict(client_result.trailing_metadata)) - self.assertEquals(SERVER_STATUS_DETAILS, client_result.status.details) - self.assertEquals(SERVER_STATUS_CODE, client_result.status.code) - self.assertEquals(set([ + self.assertEqual(dict(server_trailing_metadata), + dict(client_result.trailing_metadata)) + self.assertEqual(server_status_details, client_result.status.details) + self.assertEqual(server_status_code, client_result.status.code) + self.assertEqual(set([ _types.OpType.SEND_INITIAL_METADATA, _types.OpType.SEND_MESSAGE, _types.OpType.SEND_CLOSE_FROM_CLIENT, @@ -190,16 +211,16 @@ class InsecureServerInsecureClient(unittest.TestCase): _types.OpType.RECV_STATUS_ON_CLIENT ]), found_client_op_types) - self.assertEquals(5, len(server_event.results)) + self.assertEqual(5, len(server_event.results)) found_server_op_types = set() for server_result in server_event.results: self.assertNotIn(client_result.type, found_server_op_types) found_server_op_types.add(server_result.type) if server_result.type == _types.OpType.RECV_MESSAGE: - self.assertEquals(REQUEST, server_result.message) + self.assertEqual(request, server_result.message) elif server_result.type == _types.OpType.RECV_CLOSE_ON_SERVER: self.assertFalse(server_result.cancelled) - self.assertEquals(set([ + self.assertEqual(set([ _types.OpType.SEND_INITIAL_METADATA, _types.OpType.RECV_MESSAGE, _types.OpType.SEND_MESSAGE, @@ -211,5 +232,81 @@ class InsecureServerInsecureClient(unittest.TestCase): del server_call +class HangingServerShutdown(unittest.TestCase): + + def setUp(self): + self.server_completion_queue = _low.CompletionQueue() + self.server = _low.Server(self.server_completion_queue, []) + self.port = self.server.add_http2_port('[::]:0') + self.client_completion_queue = _low.CompletionQueue() + self.client_channel = _low.Channel('localhost:%d'%self.port, []) + + self.server.start() + + def tearDown(self): + self.server.shutdown() + del self.client_channel + + self.client_completion_queue.shutdown() + self.server_completion_queue.shutdown() + while True: + client_event, server_event = wait_for_events( + [self.client_completion_queue, self.server_completion_queue], + float("+inf")) + if (client_event.type == _types.EventType.QUEUE_SHUTDOWN and + server_event.type == _types.EventType.QUEUE_SHUTDOWN): + break + + del self.client_completion_queue + del self.server_completion_queue + del self.server + + def testHangingServerCall(self): + deadline = time.time() + 5 + deadline_tolerance = 0.25 + event_time_tolerance = 2 + cancel_all_calls_time_tolerance = 0.5 + request = 'blarghaflargh' + method = 'twinkies' + host = 'hostess' + server_request_tag = object() + request_call_result = self.server.request_call(self.server_completion_queue, + server_request_tag) + + client_call_tag = object() + client_call = self.client_channel.create_call(self.client_completion_queue, + method, host, deadline) + client_start_batch_result = client_call.start_batch([ + _types.OpArgs.send_initial_metadata([]), + _types.OpArgs.send_message(request, 0), + _types.OpArgs.send_close_from_client(), + _types.OpArgs.recv_initial_metadata(), + _types.OpArgs.recv_message(), + _types.OpArgs.recv_status_on_client() + ], client_call_tag) + + client_no_event, request_event, = wait_for_events( + [self.client_completion_queue, self.server_completion_queue], + time.time() + event_time_tolerance) + + # Now try to shutdown the server and expect that we see server shutdown + # almost immediately after calling cancel_all_calls. + with self.assertRaises(RuntimeError): + self.server.cancel_all_calls() + shutdown_tag = object() + self.server.shutdown(shutdown_tag) + pre_cancel_timestamp = time.time() + self.server.cancel_all_calls() + finish_shutdown_timestamp = None + client_call_event, server_shutdown_event = wait_for_events( + [self.client_completion_queue, self.server_completion_queue], + time.time() + event_time_tolerance) + self.assertIs(shutdown_tag, server_shutdown_event.tag) + self.assertGreater(pre_cancel_timestamp + cancel_all_calls_time_tolerance, + time.time()) + + del client_call + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/python/grpcio_test/grpc_test/_core_over_links_base_interface_test.py b/src/python/grpcio_test/grpc_test/_core_over_links_base_interface_test.py new file mode 100644 index 0000000000..72b1ae5642 --- /dev/null +++ b/src/python/grpcio_test/grpc_test/_core_over_links_base_interface_test.py @@ -0,0 +1,165 @@ +# 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. + +"""Tests the RPC Framework Core's implementation of the Base interface.""" + +import collections +import logging +import random +import time +import unittest + +from grpc._adapter import _intermediary_low +from grpc._links import invocation +from grpc._links import service +from grpc.framework.core import implementations +from grpc.framework.interfaces.base import utilities +from grpc_test import test_common as grpc_test_common +from grpc_test.framework.common import test_constants +from grpc_test.framework.interfaces.base import test_cases +from grpc_test.framework.interfaces.base import test_interfaces + +_INVOCATION_INITIAL_METADATA = ((b'0', b'abc'), (b'1', b'def'), (b'2', b'ghi'),) +_SERVICE_INITIAL_METADATA = ((b'3', b'jkl'), (b'4', b'mno'), (b'5', b'pqr'),) +_SERVICE_TERMINAL_METADATA = ((b'6', b'stu'), (b'7', b'vwx'), (b'8', b'yza'),) +_CODE = _intermediary_low.Code.OK +_MESSAGE = b'test message' + + +class _SerializationBehaviors( + collections.namedtuple( + '_SerializationBehaviors', + ('request_serializers', 'request_deserializers', 'response_serializers', + 'response_deserializers',))): + pass + + +class _Links( + collections.namedtuple( + '_Links', + ('invocation_end_link', 'invocation_grpc_link', 'service_grpc_link', + 'service_end_link'))): + pass + + +def _serialization_behaviors_from_serializations(serializations): + request_serializers = {} + request_deserializers = {} + response_serializers = {} + response_deserializers = {} + for (group, method), serialization in serializations.iteritems(): + request_serializers[group, method] = serialization.serialize_request + request_deserializers[group, method] = serialization.deserialize_request + response_serializers[group, method] = serialization.serialize_response + response_deserializers[group, method] = serialization.deserialize_response + return _SerializationBehaviors( + request_serializers, request_deserializers, response_serializers, + response_deserializers) + + +class _Implementation(test_interfaces.Implementation): + + def instantiate(self, serializations, servicer): + serialization_behaviors = _serialization_behaviors_from_serializations( + serializations) + invocation_end_link = implementations.invocation_end_link() + service_end_link = implementations.service_end_link( + servicer, test_constants.DEFAULT_TIMEOUT, + test_constants.MAXIMUM_TIMEOUT) + service_grpc_link = service.service_link( + serialization_behaviors.request_deserializers, + serialization_behaviors.response_serializers) + port = service_grpc_link.add_port(0, None) + channel = _intermediary_low.Channel('localhost:%d' % port, None) + invocation_grpc_link = invocation.invocation_link( + channel, b'localhost', + serialization_behaviors.request_serializers, + serialization_behaviors.response_deserializers) + + invocation_end_link.join_link(invocation_grpc_link) + invocation_grpc_link.join_link(invocation_end_link) + service_end_link.join_link(service_grpc_link) + service_grpc_link.join_link(service_end_link) + invocation_grpc_link.start() + service_grpc_link.start() + return invocation_end_link, service_end_link, ( + invocation_grpc_link, service_grpc_link) + + def destantiate(self, memo): + invocation_grpc_link, service_grpc_link = memo + invocation_grpc_link.stop() + service_grpc_link.stop_gracefully() + + def invocation_initial_metadata(self): + return _INVOCATION_INITIAL_METADATA + + def service_initial_metadata(self): + return _SERVICE_INITIAL_METADATA + + def invocation_completion(self): + return utilities.completion(None, None, None) + + def service_completion(self): + return utilities.completion(_SERVICE_TERMINAL_METADATA, _CODE, _MESSAGE) + + def metadata_transmitted(self, original_metadata, transmitted_metadata): + return original_metadata is None or grpc_test_common.metadata_transmitted( + original_metadata, transmitted_metadata) + + def completion_transmitted(self, original_completion, transmitted_completion): + if (original_completion.terminal_metadata is not None and + not grpc_test_common.metadata_transmitted( + original_completion.terminal_metadata, + transmitted_completion.terminal_metadata)): + return False + elif original_completion.code is not transmitted_completion.code: + return False + elif original_completion.message != transmitted_completion.message: + return False + else: + return True + + +def setUpModule(): + logging.warn('setUpModule!') + + +def tearDownModule(): + logging.warn('tearDownModule!') + + +def load_tests(loader, tests, pattern): + return unittest.TestSuite( + tests=tuple( + loader.loadTestsFromTestCase(test_case_class) + for test_case_class in test_cases.test_cases(_Implementation()))) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/src/python/grpcio_test/grpc_test/_links/_lonely_invocation_link_test.py b/src/python/grpcio_test/grpc_test/_links/_lonely_invocation_link_test.py index abe240e07a..373a2b2a1f 100644 --- a/src/python/grpcio_test/grpc_test/_links/_lonely_invocation_link_test.py +++ b/src/python/grpcio_test/grpc_test/_links/_lonely_invocation_link_test.py @@ -66,7 +66,7 @@ class LonelyInvocationLinkTest(unittest.TestCase): ticket = links.Ticket( test_operation_id, 0, test_group, test_method, links.Ticket.Subscription.FULL, test_constants.SHORT_TIMEOUT, 1, None, - None, None, None, None, termination) + None, None, None, None, termination, None) invocation_link.accept_ticket(ticket) invocation_link_mate.block_until_tickets_satisfy(test_cases.terminated) diff --git a/src/python/grpcio_test/grpc_test/_links/_transmission_test.py b/src/python/grpcio_test/grpc_test/_links/_transmission_test.py index 9cdc9620f0..02ddd512c2 100644 --- a/src/python/grpcio_test/grpc_test/_links/_transmission_test.py +++ b/src/python/grpcio_test/grpc_test/_links/_transmission_test.py @@ -128,14 +128,14 @@ class RoundTripTest(unittest.TestCase): invocation_ticket = links.Ticket( test_operation_id, 0, test_group, test_method, links.Ticket.Subscription.FULL, test_constants.LONG_TIMEOUT, None, None, - None, None, None, None, links.Ticket.Termination.COMPLETION) + None, None, None, None, links.Ticket.Termination.COMPLETION, None) invocation_link.accept_ticket(invocation_ticket) service_mate.block_until_tickets_satisfy(test_cases.terminated) service_ticket = links.Ticket( service_mate.tickets()[-1].operation_id, 0, None, None, None, None, None, None, None, None, test_code, test_message, - links.Ticket.Termination.COMPLETION) + links.Ticket.Termination.COMPLETION, None) service_link.accept_ticket(service_ticket) invocation_mate.block_until_tickets_satisfy(test_cases.terminated) @@ -174,33 +174,34 @@ class RoundTripTest(unittest.TestCase): invocation_ticket = links.Ticket( test_operation_id, 0, test_group, test_method, links.Ticket.Subscription.FULL, test_constants.LONG_TIMEOUT, None, None, - None, None, None, None, None) + None, None, None, None, None, None) invocation_link.accept_ticket(invocation_ticket) requests = scenario.requests() for request_index, request in enumerate(requests): request_ticket = links.Ticket( test_operation_id, 1 + request_index, None, None, None, None, 1, None, - request, None, None, None, None) + request, None, None, None, None, None) invocation_link.accept_ticket(request_ticket) service_mate.block_until_tickets_satisfy( test_cases.at_least_n_payloads_received_predicate(1 + request_index)) response_ticket = links.Ticket( service_mate.tickets()[0].operation_id, request_index, None, None, None, None, 1, None, scenario.response_for_request(request), None, - None, None, None) + None, None, None, None) service_link.accept_ticket(response_ticket) invocation_mate.block_until_tickets_satisfy( test_cases.at_least_n_payloads_received_predicate(1 + request_index)) request_count = len(requests) invocation_completion_ticket = links.Ticket( test_operation_id, request_count + 1, None, None, None, None, None, - None, None, None, None, None, links.Ticket.Termination.COMPLETION) + None, None, None, None, None, links.Ticket.Termination.COMPLETION, + None) invocation_link.accept_ticket(invocation_completion_ticket) service_mate.block_until_tickets_satisfy(test_cases.terminated) service_completion_ticket = links.Ticket( service_mate.tickets()[0].operation_id, request_count, None, None, None, None, None, None, None, None, test_code, test_message, - links.Ticket.Termination.COMPLETION) + links.Ticket.Termination.COMPLETION, None) service_link.accept_ticket(service_completion_ticket) invocation_mate.block_until_tickets_satisfy(test_cases.terminated) diff --git a/src/python/grpcio_test/grpc_test/framework/core/__init__.py b/src/python/grpcio_test/grpc_test/framework/core/__init__.py new file mode 100644 index 0000000000..7086519106 --- /dev/null +++ b/src/python/grpcio_test/grpc_test/framework/core/__init__.py @@ -0,0 +1,30 @@ +# 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. + + diff --git a/src/python/grpcio_test/grpc_test/framework/core/_base_interface_test.py b/src/python/grpcio_test/grpc_test/framework/core/_base_interface_test.py new file mode 100644 index 0000000000..8d72f131d5 --- /dev/null +++ b/src/python/grpcio_test/grpc_test/framework/core/_base_interface_test.py @@ -0,0 +1,96 @@ +# 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. + +"""Tests the RPC Framework Core's implementation of the Base interface.""" + +import logging +import random +import time +import unittest + +from grpc.framework.core import implementations +from grpc.framework.interfaces.base import utilities +from grpc_test.framework.common import test_constants +from grpc_test.framework.interfaces.base import test_cases +from grpc_test.framework.interfaces.base import test_interfaces + + +class _Implementation(test_interfaces.Implementation): + + def __init__(self): + self._invocation_initial_metadata = object() + self._service_initial_metadata = object() + self._invocation_terminal_metadata = object() + self._service_terminal_metadata = object() + + def instantiate(self, serializations, servicer): + invocation = implementations.invocation_end_link() + service = implementations.service_end_link( + servicer, test_constants.DEFAULT_TIMEOUT, + test_constants.MAXIMUM_TIMEOUT) + invocation.join_link(service) + service.join_link(invocation) + return invocation, service, None + + def destantiate(self, memo): + pass + + def invocation_initial_metadata(self): + return self._invocation_initial_metadata + + def service_initial_metadata(self): + return self._service_initial_metadata + + def invocation_completion(self): + return utilities.completion(self._invocation_terminal_metadata, None, None) + + def service_completion(self): + return utilities.completion(self._service_terminal_metadata, None, None) + + def metadata_transmitted(self, original_metadata, transmitted_metadata): + return transmitted_metadata is original_metadata + + def completion_transmitted(self, original_completion, transmitted_completion): + return ( + (original_completion.terminal_metadata is + transmitted_completion.terminal_metadata) and + original_completion.code is transmitted_completion.code and + original_completion.message is transmitted_completion.message + ) + + +def load_tests(loader, tests, pattern): + return unittest.TestSuite( + tests=tuple( + loader.loadTestsFromTestCase(test_case_class) + for test_case_class in test_cases.test_cases(_Implementation()))) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/src/python/grpcio_test/grpc_test/framework/interfaces/base/test_cases.py b/src/python/grpcio_test/grpc_test/framework/interfaces/base/test_cases.py index dd332fe5dd..5c8b176da4 100644 --- a/src/python/grpcio_test/grpc_test/framework/interfaces/base/test_cases.py +++ b/src/python/grpcio_test/grpc_test/framework/interfaces/base/test_cases.py @@ -211,8 +211,10 @@ class _OperationTest(unittest.TestCase): elif instruction.kind is _control.Instruction.Kind.CONCLUDE: break - invocation_end.stop_gracefully() - service_end.stop_gracefully() + invocation_stop_event = invocation_end.stop(0) + service_stop_event = service_end.stop(0) + invocation_stop_event.wait() + service_stop_event.wait() invocation_stats = invocation_end.operation_stats() service_stats = service_end.operation_stats() diff --git a/src/python/grpcio_test/grpc_test/framework/interfaces/links/test_cases.py b/src/python/grpcio_test/grpc_test/framework/interfaces/links/test_cases.py index 1e575d1a9e..ecf49d9cdb 100644 --- a/src/python/grpcio_test/grpc_test/framework/interfaces/links/test_cases.py +++ b/src/python/grpcio_test/grpc_test/framework/interfaces/links/test_cases.py @@ -300,7 +300,7 @@ class TransmissionTest(object): invocation_operation_id, 0, _TRANSMISSION_GROUP, _TRANSMISSION_METHOD, links.Ticket.Subscription.FULL, timeout, 0, invocation_initial_metadata, invocation_payload, invocation_terminal_metadata, invocation_code, - invocation_message, links.Ticket.Termination.COMPLETION) + invocation_message, links.Ticket.Termination.COMPLETION, None) self._invocation_link.accept_ticket(original_invocation_ticket) self._service_mate.block_until_tickets_satisfy( @@ -317,7 +317,7 @@ class TransmissionTest(object): service_operation_id, 0, None, None, links.Ticket.Subscription.FULL, timeout, 0, service_initial_metadata, service_payload, service_terminal_metadata, service_code, service_message, - links.Ticket.Termination.COMPLETION) + links.Ticket.Termination.COMPLETION, None) self._service_link.accept_ticket(original_service_ticket) self._invocation_mate.block_until_tickets_satisfy(terminated) self._assert_is_valid_service_sequence( diff --git a/src/python/grpcio_test/grpc_test/framework/interfaces/links/test_utilities.py b/src/python/grpcio_test/grpc_test/framework/interfaces/links/test_utilities.py index 6c2e3346aa..39c7f2fc63 100644 --- a/src/python/grpcio_test/grpc_test/framework/interfaces/links/test_utilities.py +++ b/src/python/grpcio_test/grpc_test/framework/interfaces/links/test_utilities.py @@ -29,9 +29,42 @@ """State and behavior appropriate for use in tests.""" +import logging import threading +import time from grpc.framework.interfaces.links import links +from grpc.framework.interfaces.links import utilities + +# A more-or-less arbitrary limit on the length of raw data values to be logged. +_UNCOMFORTABLY_LONG = 48 + + +def _safe_for_log_ticket(ticket): + """Creates a safe-for-printing-to-the-log ticket for a given ticket. + + Args: + ticket: Any links.Ticket. + + Returns: + A links.Ticket that is as much as can be equal to the given ticket but + possibly features values like the string "<payload of length 972321>" in + place of the actual values of the given ticket. + """ + if isinstance(ticket.payload, (basestring,)): + payload_length = len(ticket.payload) + else: + payload_length = -1 + if payload_length < _UNCOMFORTABLY_LONG: + return ticket + else: + return links.Ticket( + ticket.operation_id, ticket.sequence_number, + ticket.group, ticket.method, ticket.subscription, ticket.timeout, + ticket.allowance, ticket.initial_metadata, + '<payload of length {}>'.format(payload_length), + ticket.terminal_metadata, ticket.code, ticket.message, + ticket.termination, None) class RecordingLink(links.Link): @@ -64,3 +97,71 @@ class RecordingLink(links.Link): """Returns a copy of the list of all tickets received by this Link.""" with self._condition: return tuple(self._tickets) + + +class _Pipe(object): + """A conduit that logs all tickets passed through it.""" + + def __init__(self, name): + self._lock = threading.Lock() + self._name = name + self._left_mate = utilities.NULL_LINK + self._right_mate = utilities.NULL_LINK + + def accept_left_to_right_ticket(self, ticket): + with self._lock: + logging.warning( + '%s: moving left to right through %s: %s', time.time(), self._name, + _safe_for_log_ticket(ticket)) + try: + self._right_mate.accept_ticket(ticket) + except Exception as e: # pylint: disable=broad-except + logging.exception(e) + + def accept_right_to_left_ticket(self, ticket): + with self._lock: + logging.warning( + '%s: moving right to left through %s: %s', time.time(), self._name, + _safe_for_log_ticket(ticket)) + try: + self._left_mate.accept_ticket(ticket) + except Exception as e: # pylint: disable=broad-except + logging.exception(e) + + def join_left_mate(self, left_mate): + with self._lock: + self._left_mate = utilities.NULL_LINK if left_mate is None else left_mate + + def join_right_mate(self, right_mate): + with self._lock: + self._right_mate = ( + utilities.NULL_LINK if right_mate is None else right_mate) + + +class _Facade(links.Link): + + def __init__(self, accept, join): + self._accept = accept + self._join = join + + def accept_ticket(self, ticket): + self._accept(ticket) + + def join_link(self, link): + self._join(link) + + +def logging_links(name): + """Creates a conduit that logs all tickets passed through it. + + Args: + name: A name to use for the conduit to identify itself in logging output. + + Returns: + Two links.Links, the first of which is the "left" side of the conduit + and the second of which is the "right" side of the conduit. + """ + pipe = _Pipe(name) + left_facade = _Facade(pipe.accept_left_to_right_ticket, pipe.join_left_mate) + right_facade = _Facade(pipe.accept_right_to_left_ticket, pipe.join_right_mate) + return left_facade, right_facade diff --git a/src/ruby/ext/grpc/rb_call.c b/src/ruby/ext/grpc/rb_call.c index 36c6818a7e..6b5beb6f5d 100644 --- a/src/ruby/ext/grpc/rb_call.c +++ b/src/ruby/ext/grpc/rb_call.c @@ -82,6 +82,10 @@ static ID id_metadata; * received by the call and subsequently saved on it. */ static ID id_status; +/* id_write_flag is name of the attribute used to access the write_flag + * saved on the call. */ +static ID id_write_flag; + /* sym_* are the symbol for attributes of grpc_rb_sBatchResult. */ static VALUE sym_send_message; static VALUE sym_send_metadata; @@ -240,6 +244,30 @@ static VALUE grpc_rb_call_set_metadata(VALUE self, VALUE metadata) { return rb_ivar_set(self, id_metadata, metadata); } +/* + call-seq: + write_flag = call.write_flag + + Gets the write_flag value saved the call. */ +static VALUE grpc_rb_call_get_write_flag(VALUE self) { + return rb_ivar_get(self, id_write_flag); +} + +/* + call-seq: + call.write_flag = write_flag + + Saves the write_flag on the call. */ +static VALUE grpc_rb_call_set_write_flag(VALUE self, VALUE write_flag) { + if (!NIL_P(write_flag) && TYPE(write_flag) != T_FIXNUM) { + rb_raise(rb_eTypeError, "bad write_flag: got:<%s> want: <Fixnum>", + rb_obj_classname(write_flag)); + return Qnil; + } + + return rb_ivar_set(self, id_write_flag, write_flag); +} + /* grpc_rb_md_ary_fill_hash_cb is the hash iteration callback used to fill grpc_metadata_array. @@ -437,17 +465,19 @@ typedef struct run_batch_stack { grpc_status_code recv_status; char *recv_status_details; size_t recv_status_details_capacity; + uint write_flag; } run_batch_stack; /* grpc_run_batch_stack_init ensures the run_batch_stack is properly * initialized */ -static void grpc_run_batch_stack_init(run_batch_stack *st) { +static void grpc_run_batch_stack_init(run_batch_stack *st, uint write_flag) { MEMZERO(st, run_batch_stack, 1); grpc_metadata_array_init(&st->send_metadata); grpc_metadata_array_init(&st->send_trailing_metadata); grpc_metadata_array_init(&st->recv_metadata); grpc_metadata_array_init(&st->recv_trailing_metadata); st->op_num = 0; + st->write_flag = write_flag; } /* grpc_run_batch_stack_cleanup ensures the run_batch_stack is properly @@ -477,6 +507,7 @@ static void grpc_run_batch_stack_fill_ops(run_batch_stack *st, VALUE ops_hash) { for (i = 0; i < (size_t)RARRAY_LEN(ops_ary); i++) { this_op = rb_ary_entry(ops_ary, i); this_value = rb_hash_aref(ops_hash, this_op); + st->ops[st->op_num].flags = 0; switch (NUM2INT(this_op)) { case GRPC_OP_SEND_INITIAL_METADATA: /* N.B. later there is no need to explicitly delete the metadata keys @@ -490,6 +521,7 @@ static void grpc_run_batch_stack_fill_ops(run_batch_stack *st, VALUE ops_hash) { case GRPC_OP_SEND_MESSAGE: st->ops[st->op_num].data.send_message = grpc_rb_s_to_byte_buffer( RSTRING_PTR(this_value), RSTRING_LEN(this_value)); + st->ops[st->op_num].flags = st->write_flag; break; case GRPC_OP_SEND_CLOSE_FROM_CLIENT: break; @@ -525,7 +557,6 @@ static void grpc_run_batch_stack_fill_ops(run_batch_stack *st, VALUE ops_hash) { NUM2INT(this_op)); }; st->ops[st->op_num].op = (grpc_op_type)NUM2INT(this_op); - st->ops[st->op_num].flags = 0; st->ops[st->op_num].reserved = NULL; st->op_num++; } @@ -604,6 +635,8 @@ static VALUE grpc_rb_call_run_batch(VALUE self, VALUE cqueue, VALUE tag, grpc_event ev; grpc_call_error err; VALUE result = Qnil; + VALUE rb_write_flag = rb_ivar_get(self, id_write_flag); + uint write_flag = 0; TypedData_Get_Struct(self, grpc_call, &grpc_call_data_type, call); /* Validate the ops args, adding them to a ruby array */ @@ -611,7 +644,10 @@ static VALUE grpc_rb_call_run_batch(VALUE self, VALUE cqueue, VALUE tag, rb_raise(rb_eTypeError, "call#run_batch: ops hash should be a hash"); return Qnil; } - grpc_run_batch_stack_init(&st); + if (rb_write_flag != Qnil) { + write_flag = NUM2UINT(rb_write_flag); + } + grpc_run_batch_stack_init(&st, write_flag); grpc_run_batch_stack_fill_ops(&st, ops_hash); /* call grpc_call_start_batch, then wait for it to complete using @@ -638,6 +674,16 @@ static VALUE grpc_rb_call_run_batch(VALUE self, VALUE cqueue, VALUE tag, return result; } +static void Init_grpc_write_flags() { + /* Constants representing the write flags in grpc.h */ + VALUE grpc_rb_mWriteFlags = + rb_define_module_under(grpc_rb_mGrpcCore, "WriteFlags"); + rb_define_const(grpc_rb_mWriteFlags, "BUFFER_HINT", + UINT2NUM(GRPC_WRITE_BUFFER_HINT)); + rb_define_const(grpc_rb_mWriteFlags, "NO_COMPRESS", + UINT2NUM(GRPC_WRITE_NO_COMPRESS)); +} + static void Init_grpc_error_codes() { /* Constants representing the error codes of grpc_call_error in grpc.h */ VALUE grpc_rb_mRpcErrors = @@ -735,10 +781,14 @@ void Init_grpc_call() { rb_define_method(grpc_rb_cCall, "status=", grpc_rb_call_set_status, 1); rb_define_method(grpc_rb_cCall, "metadata", grpc_rb_call_get_metadata, 0); rb_define_method(grpc_rb_cCall, "metadata=", grpc_rb_call_set_metadata, 1); + rb_define_method(grpc_rb_cCall, "write_flag", grpc_rb_call_get_write_flag, 0); + rb_define_method(grpc_rb_cCall, "write_flag=", grpc_rb_call_set_write_flag, + 1); /* Ids used to support call attributes */ id_metadata = rb_intern("metadata"); id_status = rb_intern("status"); + id_write_flag = rb_intern("write_flag"); /* Ids used by the c wrapping internals. */ id_cq = rb_intern("__cq"); @@ -766,6 +816,7 @@ void Init_grpc_call() { Init_grpc_error_codes(); Init_grpc_op_codes(); + Init_grpc_write_flags(); } /* Gets the call from the ruby object */ diff --git a/src/ruby/lib/grpc/generic/active_call.rb b/src/ruby/lib/grpc/generic/active_call.rb index 17da401c6b..d9cb924735 100644 --- a/src/ruby/lib/grpc/generic/active_call.rb +++ b/src/ruby/lib/grpc/generic/active_call.rb @@ -59,7 +59,7 @@ module GRPC include Core::CallOps extend Forwardable attr_reader(:deadline) - def_delegators :@call, :cancel, :metadata + def_delegators :@call, :cancel, :metadata, :write_flag, :write_flag= # client_invoke begins a client invocation. # @@ -484,6 +484,7 @@ module GRPC # Operation limits access to an ActiveCall's methods for use as # a Operation on the client. Operation = view_class(:cancel, :cancelled, :deadline, :execute, - :metadata, :status, :start_call, :wait) + :metadata, :status, :start_call, :wait, :write_flag, + :write_flag=) end end diff --git a/src/ruby/spec/call_spec.rb b/src/ruby/spec/call_spec.rb index 3c5d33ffcd..dd3c45f754 100644 --- a/src/ruby/spec/call_spec.rb +++ b/src/ruby/spec/call_spec.rb @@ -31,6 +31,14 @@ require 'grpc' include GRPC::Core::StatusCodes +describe GRPC::Core::WriteFlags do + it 'should define the known write flag values' do + m = GRPC::Core::WriteFlags + expect(m.const_get(:BUFFER_HINT)).to_not be_nil + expect(m.const_get(:NO_COMPRESS)).to_not be_nil + end +end + describe GRPC::Core::RpcErrors do before(:each) do @known_types = { diff --git a/src/ruby/spec/generic/active_call_spec.rb b/src/ruby/spec/generic/active_call_spec.rb index 26208b714a..fcd7bd082f 100644 --- a/src/ruby/spec/generic/active_call_spec.rb +++ b/src/ruby/spec/generic/active_call_spec.rb @@ -35,6 +35,7 @@ describe GRPC::ActiveCall do ActiveCall = GRPC::ActiveCall Call = GRPC::Core::Call CallOps = GRPC::Core::CallOps + WriteFlags = GRPC::Core::WriteFlags before(:each) do @pass_through = proc { |x| x } @@ -129,6 +130,31 @@ describe GRPC::ActiveCall do @pass_through, deadline) expect(server_call.remote_read).to eq('marshalled:' + msg) end + + TEST_WRITE_FLAGS = [WriteFlags::BUFFER_HINT, WriteFlags::NO_COMPRESS] + TEST_WRITE_FLAGS.each do |f| + it "successfully makes calls with write_flag set to #{f}" do + call = make_test_call + ActiveCall.client_invoke(call, @client_queue) + marshal = proc { |x| 'marshalled:' + x } + client_call = ActiveCall.new(call, @client_queue, marshal, + @pass_through, deadline) + msg = 'message is a string' + client_call.write_flag = f + client_call.remote_send(msg) + + # confirm that the message was marshalled + recvd_rpc = @server.request_call(@server_queue, @server_tag, deadline) + recvd_call = recvd_rpc.call + server_ops = { + CallOps::SEND_INITIAL_METADATA => nil + } + recvd_call.run_batch(@server_queue, @server_tag, deadline, server_ops) + server_call = ActiveCall.new(recvd_call, @server_queue, @pass_through, + @pass_through, deadline) + expect(server_call.remote_read).to eq('marshalled:' + msg) + end + end end describe '#client_invoke' do @@ -261,7 +287,7 @@ describe GRPC::ActiveCall do client_call.writes_done(false) server_call = expect_server_to_receive(msg) e = client_call.each_remote_read - n = 3 # arbitrary value > 1 + n = 3 # arbitrary value > 1 n.times do server_call.remote_send(reply) expect(e.next).to eq(reply) |