blob: c64e215ea99116a8f78d28b3632228ca844817f6 [file] [log] [blame]
/* Copyright 2016 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <algorithm>
#include "tensorflow/core/platform/cloud/curl_http_request.h"
#include "tensorflow/core/lib/core/errors.h"
#include "tensorflow/core/lib/gtl/map_util.h"
#include "tensorflow/core/lib/strings/scanner.h"
#include "tensorflow/core/lib/strings/str_util.h"
#include "tensorflow/core/platform/macros.h"
#include "tensorflow/core/platform/types.h"
#include "tensorflow/core/public/version.h"
#define CHECK_CURL_OK(expr) CHECK_EQ(expr, CURLE_OK)
namespace tensorflow {
namespace {
// Set to 1 to enable verbose debug output from curl.
constexpr uint64 kVerboseOutput = 0;
// Proxy to the real libcurl implementation.
class LibCurlProxy : public LibCurl {
public:
static LibCurlProxy* Load() {
static LibCurlProxy* libcurl = []() -> LibCurlProxy* {
curl_global_init(CURL_GLOBAL_ALL);
return new LibCurlProxy;
}();
return libcurl;
}
CURL* curl_easy_init() override { return ::curl_easy_init(); }
CURLcode curl_easy_setopt(CURL* curl, CURLoption option,
uint64 param) override {
return ::curl_easy_setopt(curl, option, param);
}
CURLcode curl_easy_setopt(CURL* curl, CURLoption option,
const char* param) override {
return ::curl_easy_setopt(curl, option, param);
}
CURLcode curl_easy_setopt(CURL* curl, CURLoption option,
void* param) override {
return ::curl_easy_setopt(curl, option, param);
}
CURLcode curl_easy_setopt(CURL* curl, CURLoption option,
size_t (*param)(void*, size_t, size_t,
FILE*)) override {
return ::curl_easy_setopt(curl, option, param);
}
CURLcode curl_easy_setopt(CURL* curl, CURLoption option,
size_t (*param)(const void*, size_t, size_t,
void*)) override {
return ::curl_easy_setopt(curl, option, param);
}
CURLcode curl_easy_setopt(CURL* curl, CURLoption option,
int (*param)(void* clientp, curl_off_t dltotal,
curl_off_t dlnow, curl_off_t ultotal,
curl_off_t ulnow)) override {
return ::curl_easy_setopt(curl, option, param);
}
CURLcode curl_easy_perform(CURL* curl) override {
return ::curl_easy_perform(curl);
}
CURLcode curl_easy_getinfo(CURL* curl, CURLINFO info,
uint64* value) override {
return ::curl_easy_getinfo(curl, info, value);
}
CURLcode curl_easy_getinfo(CURL* curl, CURLINFO info,
double* value) override {
return ::curl_easy_getinfo(curl, info, value);
}
void curl_easy_cleanup(CURL* curl) override {
return ::curl_easy_cleanup(curl);
}
char* curl_easy_escape(CURL* curl, const char* str, int length) override {
return ::curl_easy_escape(curl, str, length);
}
curl_slist* curl_slist_append(curl_slist* list, const char* str) override {
return ::curl_slist_append(list, str);
}
void curl_slist_free_all(curl_slist* list) override {
return ::curl_slist_free_all(list);
}
void curl_free(void* p) override { ::curl_free(p); }
};
} // namespace
CurlHttpRequest::CurlHttpRequest() : CurlHttpRequest(LibCurlProxy::Load()) {}
CurlHttpRequest::CurlHttpRequest(LibCurl* libcurl, Env* env)
: libcurl_(libcurl), env_(env) {
default_response_buffer_.reserve(CURL_MAX_WRITE_SIZE);
curl_ = libcurl_->curl_easy_init();
CHECK(curl_ != nullptr) << "Couldn't initialize a curl session.";
// NOTE: CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt is configured by
// default in //third_party:curl.BUILD and can be customized via an
// environment variable.
CHECK_CURL_OK(
libcurl_->curl_easy_setopt(curl_, CURLOPT_VERBOSE, kVerboseOutput));
CHECK_CURL_OK(libcurl_->curl_easy_setopt(
curl_, CURLOPT_USERAGENT,
strings::StrCat("TensorFlow/", TF_VERSION_STRING).c_str()));
// Do not use signals for timeouts - does not work in multi-threaded programs.
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_NOSIGNAL, 1L));
// TODO(b/74351157): Enable HTTP/2.
// Set up the progress meter.
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_NOPROGRESS, 0ULL));
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_XFERINFODATA, this));
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_XFERINFOFUNCTION,
&CurlHttpRequest::ProgressCallback));
// If response buffer is not set, libcurl will print results to stdout,
// so we always set it.
SetResultBuffer(&default_response_buffer_);
}
CurlHttpRequest::~CurlHttpRequest() {
if (curl_headers_) {
libcurl_->curl_slist_free_all(curl_headers_);
}
if (resolve_list_) {
libcurl_->curl_slist_free_all(resolve_list_);
}
if (put_body_) {
fclose(put_body_);
}
if (curl_) {
libcurl_->curl_easy_cleanup(curl_);
}
}
string CurlHttpRequest::EscapeString(const string& str) {
char* out_char_str = libcurl_->curl_easy_escape(curl_, str.c_str(), 0);
string out_str(out_char_str);
libcurl_->curl_free(out_char_str);
return out_str;
}
void CurlHttpRequest::SetUri(const string& uri) {
CheckNotSent();
is_uri_set_ = true;
uri_ = uri;
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_URL, uri.c_str()));
}
void CurlHttpRequest::SetRange(uint64 start, uint64 end) {
CheckNotSent();
CHECK_CURL_OK(libcurl_->curl_easy_setopt(
curl_, CURLOPT_RANGE, strings::StrCat(start, "-", end).c_str()));
}
void CurlHttpRequest::AddHeader(const string& name, const string& value) {
CheckNotSent();
curl_headers_ = libcurl_->curl_slist_append(
curl_headers_, strings::StrCat(name, ": ", value).c_str());
}
void CurlHttpRequest::AddResolveOverride(const string& hostname, int64 port,
const string& ip_addr) {
CheckNotSent();
// Resolve values are hostname:port:IP.add.ress
resolve_list_ = libcurl_->curl_slist_append(
resolve_list_,
strings::StrCat(hostname, ":", port, ":", ip_addr).c_str());
}
void CurlHttpRequest::AddAuthBearerHeader(const string& auth_token) {
CheckNotSent();
if (!auth_token.empty()) {
AddHeader("Authorization", strings::StrCat("Bearer ", auth_token));
}
}
void CurlHttpRequest::SetRequestStats(RequestStats* stats) {
CheckNotSent();
CHECK(stats_ == nullptr) << "SetRequestStats already called";
stats_ = stats;
}
void CurlHttpRequest::SetDeleteRequest() {
CheckNotSent();
CheckMethodNotSet();
is_method_set_ = true;
method_ = RequestMethod::kDelete;
CHECK_CURL_OK(
libcurl_->curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "DELETE"));
}
Status CurlHttpRequest::SetPutFromFile(const string& body_filepath,
size_t offset) {
CheckNotSent();
CheckMethodNotSet();
is_method_set_ = true;
method_ = RequestMethod::kPut;
if (put_body_) {
fclose(put_body_);
}
put_body_ = fopen(body_filepath.c_str(), "r");
if (!put_body_) {
return errors::InvalidArgument("Couldn't open the specified file: " +
body_filepath);
}
fseek(put_body_, 0, SEEK_END);
const auto size = ftell(put_body_) - offset;
fseek(put_body_, offset, SEEK_SET);
curl_headers_ = libcurl_->curl_slist_append(
curl_headers_, strings::StrCat("Content-Length: ", size).c_str());
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_PUT, 1));
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_READDATA,
reinterpret_cast<void*>(put_body_)));
// Using the default CURLOPT_READFUNCTION, which is doing an fread() on the
// FILE * userdata set with CURLOPT_READDATA.
return Status::OK();
}
void CurlHttpRequest::SetPutEmptyBody() {
CheckNotSent();
CheckMethodNotSet();
is_method_set_ = true;
method_ = RequestMethod::kPut;
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_PUT, 1));
AddHeader("Content-Length", "0");
AddHeader("Transfer-Encoding", "identity");
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_READDATA,
reinterpret_cast<void*>(this)));
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_READFUNCTION,
&CurlHttpRequest::ReadCallback));
}
void CurlHttpRequest::SetPostFromBuffer(const char* buffer, size_t size) {
CheckNotSent();
CheckMethodNotSet();
is_method_set_ = true;
method_ = RequestMethod::kPost;
curl_headers_ = libcurl_->curl_slist_append(
curl_headers_, strings::StrCat("Content-Length: ", size).c_str());
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_POST, 1));
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_READDATA,
reinterpret_cast<void*>(this)));
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_READFUNCTION,
&CurlHttpRequest::ReadCallback));
post_body_buffer_ = StringPiece(buffer, size);
}
void CurlHttpRequest::SetPostEmptyBody() {
CheckNotSent();
CheckMethodNotSet();
is_method_set_ = true;
method_ = RequestMethod::kPost;
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_POST, 1));
AddHeader("Content-Length", "0");
AddHeader("Transfer-Encoding", "identity");
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_READDATA,
reinterpret_cast<void*>(this)));
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_READFUNCTION,
&CurlHttpRequest::ReadCallback));
}
void CurlHttpRequest::SetResultBuffer(std::vector<char>* out_buffer) {
CheckNotSent();
CHECK(out_buffer != nullptr);
out_buffer->clear();
response_buffer_ = out_buffer;
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_WRITEDATA,
reinterpret_cast<void*>(this)));
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION,
&CurlHttpRequest::WriteCallback));
}
void CurlHttpRequest::SetResultBufferDirect(char* buffer, size_t size) {
CHECK(buffer != nullptr);
CheckNotSent();
direct_response_ = DirectResponseState{buffer, size, 0, 0};
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_WRITEDATA,
reinterpret_cast<void*>(this)));
CHECK_CURL_OK(libcurl_->curl_easy_setopt(
curl_, CURLOPT_WRITEFUNCTION, &CurlHttpRequest::WriteCallbackDirect));
}
bool CurlHttpRequest::IsDirectResponse() const {
return direct_response_.buffer_ != nullptr;
}
size_t CurlHttpRequest::WriteCallbackDirect(const void* ptr, size_t size,
size_t nmemb, void* userdata) {
CHECK(ptr != nullptr);
auto that = reinterpret_cast<CurlHttpRequest*>(userdata);
DirectResponseState* state = &that->direct_response_;
CHECK(state->buffer_ != nullptr);
CHECK(state->bytes_transferred_ <= state->buffer_size_);
size_t curl_bytes_received = size * nmemb;
size_t user_buffer_bytes_available =
state->buffer_size_ - state->bytes_transferred_;
size_t bytes_to_copy =
std::min<size_t>(curl_bytes_received, user_buffer_bytes_available);
memcpy(&state->buffer_[state->bytes_transferred_], ptr, bytes_to_copy);
state->bytes_transferred_ += bytes_to_copy;
state->bytes_received_ += curl_bytes_received;
// If we didn't have room to store the full response, returning less than
// curl_bytes_received here will abort the transfer and curl_easy_perform()
// will return CURLE_WRITE_ERROR. We will detect and handle this error there,
// and can use state->bytes_received_ as stored above for logging purposes.
return bytes_to_copy;
}
size_t CurlHttpRequest::GetResultBufferDirectBytesTransferred() {
CHECK(direct_response_.buffer_ != nullptr);
return direct_response_.bytes_transferred_;
}
void CurlHttpRequest::SetTimeouts(uint32 connection, uint32 inactivity,
uint32 total) {
CheckNotSent();
connect_timeout_secs_ = connection;
inactivity_timeout_secs_ = inactivity;
request_timeout_secs_ = total;
}
size_t CurlHttpRequest::WriteCallback(const void* ptr, size_t size,
size_t nmemb, void* this_object) {
CHECK(ptr);
auto that = reinterpret_cast<CurlHttpRequest*>(this_object);
CHECK(that->response_buffer_);
const size_t bytes_to_copy = size * nmemb;
that->response_buffer_->insert(
that->response_buffer_->end(), reinterpret_cast<const char*>(ptr),
reinterpret_cast<const char*>(ptr) + bytes_to_copy);
return bytes_to_copy;
}
size_t CurlHttpRequest::ReadCallback(void* ptr, size_t size, size_t nmemb,
FILE* this_object) {
CHECK(ptr);
auto that = reinterpret_cast<CurlHttpRequest*>(this_object);
CHECK(that->post_body_read_ <= that->post_body_buffer_.size());
const size_t bytes_to_copy = std::min(
size * nmemb, that->post_body_buffer_.size() - that->post_body_read_);
memcpy(ptr, that->post_body_buffer_.data() + that->post_body_read_,
bytes_to_copy);
that->post_body_read_ += bytes_to_copy;
return bytes_to_copy;
}
size_t CurlHttpRequest::HeaderCallback(const void* ptr, size_t size,
size_t nmemb, void* this_object) {
CHECK(ptr);
auto that = reinterpret_cast<CurlHttpRequest*>(this_object);
StringPiece header(reinterpret_cast<const char*>(ptr), size * nmemb);
StringPiece name, value;
// The supplied header has the form "<name>: <value>", parse it.
if (strings::Scanner(header)
.ScanEscapedUntil(':')
.StopCapture()
.OneLiteral(": ")
.GetResult(&value, &name)) {
string str_value(value);
absl::StripTrailingAsciiWhitespace(&str_value);
that->response_headers_[string(name)] = str_value;
}
return size * nmemb;
}
Status CurlHttpRequest::Send() {
CheckNotSent();
CHECK(is_uri_set_) << "URI has not been set.";
is_sent_ = true;
if (curl_headers_) {
CHECK_CURL_OK(
libcurl_->curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, curl_headers_));
}
if (resolve_list_) {
CHECK_CURL_OK(
libcurl_->curl_easy_setopt(curl_, CURLOPT_RESOLVE, resolve_list_));
}
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_HEADERDATA,
reinterpret_cast<void*>(this)));
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_HEADERFUNCTION,
&CurlHttpRequest::HeaderCallback));
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_TIMEOUT,
request_timeout_secs_));
CHECK_CURL_OK(libcurl_->curl_easy_setopt(curl_, CURLOPT_CONNECTTIMEOUT,
connect_timeout_secs_));
char error_buffer[CURL_ERROR_SIZE] = {0};
CHECK_CURL_OK(
libcurl_->curl_easy_setopt(curl_, CURLOPT_ERRORBUFFER, error_buffer));
if (stats_ != nullptr) {
stats_->RecordRequest(this, uri_, method_);
}
const CURLcode curl_result = libcurl_->curl_easy_perform(curl_);
TF_RETURN_IF_ERROR(CURLcodeToStatus(curl_result, error_buffer));
double written_size = 0;
CHECK_CURL_OK(libcurl_->curl_easy_getinfo(curl_, CURLINFO_SIZE_DOWNLOAD,
&written_size));
CHECK_CURL_OK(libcurl_->curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE,
&response_code_));
auto get_error_message = [this]() -> string {
string error_message = strings::StrCat(
"Error executing an HTTP request: HTTP response code ", response_code_);
StringPiece body = GetResponse();
if (!body.empty()) {
return strings::StrCat(
error_message, " with body '",
body.substr(0, std::min(body.size(), response_to_error_limit_)), "'");
}
return error_message;
};
Status result;
switch (response_code_) {
// The group of response codes indicating that the request achieved
// the expected goal.
case 200: // OK
case 201: // Created
case 204: // No Content
case 206: // Partial Content
result = Status::OK();
break;
case 416: // Requested Range Not Satisfiable
// The requested range had no overlap with the available range.
// This doesn't indicate an error, but we should produce an empty response
// body. (Not all servers do; GCS returns a short error message body.)
response_buffer_->clear();
if (IsDirectResponse()) {
direct_response_.bytes_transferred_ = 0;
}
result = Status::OK();
break;
// INVALID_ARGUMENT indicates a problem with how the request is constructed.
case 400: // Bad Request
case 411: // Length Required
result = errors::InvalidArgument(get_error_message());
break;
// PERMISSION_DENIED indicates an authentication or an authorization issue.
case 401: // Unauthorized
case 403: // Forbidden
result = errors::PermissionDenied(get_error_message());
break;
// NOT_FOUND indicates that the requested resource does not exist.
case 404: // Not found
case 410: // Gone
result = errors::NotFound(get_error_message());
break;
// FAILED_PRECONDITION indicates that the request failed because some
// of the underlying assumptions were not satisfied. The request
// shouldn't be retried unless the external context has changed.
case 302: // Found
case 303: // See Other
case 304: // Not Modified
case 307: // Temporary Redirect
case 412: // Precondition Failed
case 413: // Payload Too Large
result = errors::FailedPrecondition(get_error_message());
break;
// UNAVAILABLE indicates a problem that can go away if the request
// is just retried without any modification. 308 return codes are intended
// for write requests that can be retried. See the documentation and the
// official library:
// https://cloud.google.com/storage/docs/json_api/v1/how-tos/resumable-upload
// https://github.com/google/apitools/blob/master/apitools/base/py/transfer.py
case 308: // Resume Incomplete
case 409: // Conflict
case 429: // Too Many Requests
case 500: // Internal Server Error
case 502: // Bad Gateway
case 503: // Service Unavailable
default: // All other HTTP response codes also should be retried.
result = errors::Unavailable(get_error_message());
break;
}
if (!result.ok()) {
response_buffer_->clear();
}
if (stats_ != nullptr) {
stats_->RecordResponse(this, uri_, method_, result);
}
return result;
}
void CurlHttpRequest::CheckMethodNotSet() const {
CHECK(!is_method_set_) << "HTTP method has been already set.";
}
void CurlHttpRequest::CheckNotSent() const {
CHECK(!is_sent_) << "The request has already been sent.";
}
StringPiece CurlHttpRequest::GetResponse() const {
StringPiece response;
if (IsDirectResponse()) {
response = StringPiece(direct_response_.buffer_,
direct_response_.bytes_transferred_);
} else {
response = StringPiece(response_buffer_->data(), response_buffer_->size());
}
return response;
}
string CurlHttpRequest::GetResponseHeader(const string& name) const {
const auto& header = response_headers_.find(name);
return header != response_headers_.end() ? header->second : "";
}
uint64 CurlHttpRequest::GetResponseCode() const { return response_code_; }
// Cancels the transmission if no progress has been made for too long.
int CurlHttpRequest::ProgressCallback(void* this_object, curl_off_t dltotal,
curl_off_t dlnow, curl_off_t ultotal,
curl_off_t ulnow) {
auto that = reinterpret_cast<CurlHttpRequest*>(this_object);
const auto now = that->env_->NowSeconds();
const auto current_progress = dlnow + ulnow;
if (that->last_progress_timestamp_ == 0 ||
current_progress > that->last_progress_bytes_) {
// This is the first time the callback is called or some progress
// was made since the last tick.
that->last_progress_timestamp_ = now;
that->last_progress_bytes_ = current_progress;
return 0;
}
if (now - that->last_progress_timestamp_ > that->inactivity_timeout_secs_) {
double lookup_time = -1;
const auto lookup_time_status = that->libcurl_->curl_easy_getinfo(
that->curl_, CURLINFO_NAMELOOKUP_TIME, &lookup_time);
double connect_time = -1;
const auto connect_time_status = that->libcurl_->curl_easy_getinfo(
that->curl_, CURLINFO_CONNECT_TIME, &connect_time);
double pretransfer_time = -1;
const auto pretransfer_time_status = that->libcurl_->curl_easy_getinfo(
that->curl_, CURLINFO_PRETRANSFER_TIME, &pretransfer_time);
double starttransfer_time = -1;
const auto starttransfer_time_status = that->libcurl_->curl_easy_getinfo(
that->curl_, CURLINFO_PRETRANSFER_TIME, &starttransfer_time);
LOG(ERROR) << "The transmission of request " << this_object
<< " (URI: " << that->uri_ << ") has been stuck at "
<< current_progress << " of " << dltotal + ultotal
<< " bytes for " << now - that->last_progress_timestamp_
<< " seconds and will be aborted. CURL timing information: "
<< "lookup time: " << lookup_time << " ("
<< curl_easy_strerror(lookup_time_status)
<< "), connect time: " << connect_time << " ("
<< curl_easy_strerror(connect_time_status)
<< "), pre-transfer time: " << pretransfer_time << " ("
<< curl_easy_strerror(pretransfer_time_status)
<< "), start-transfer time: " << starttransfer_time << " ("
<< curl_easy_strerror(starttransfer_time_status) << ")";
return 1; // Will abort the request.
}
// No progress was made since the last call, but we should wait a bit longer.
return 0;
}
Status CurlHttpRequest::CURLcodeToStatus(CURLcode code,
const char* error_buffer) {
if (code == CURLE_OK) {
return Status::OK();
}
string error_message = strings::StrCat(
"Error executing an HTTP request: libcurl code ", code, " meaning '",
curl_easy_strerror(code), "', error details: ");
// Special-case response-too-large errors as FAILED_PRECONDITION.
if (code == CURLE_WRITE_ERROR && IsDirectResponse() &&
direct_response_.bytes_received_ > direct_response_.buffer_size_) {
string overflow_message = strings::StrCat(
"Received ", direct_response_.bytes_received_, " response bytes ",
"for a ", direct_response_.buffer_size_, "-byte buffer");
uint64 response_code = 0;
const CURLcode get_response_result = libcurl_->curl_easy_getinfo(
curl_, CURLINFO_RESPONSE_CODE, &response_code);
// Special-case 416 Range Not Satisfied responses; they sometimes have
// a response body (e.g. GCS sends one with an error message) but we
// pretend as though they don't, so actually ignore this error.
if (get_response_result == CURLE_OK && response_code == 416) {
return Status::OK();
}
return errors::FailedPrecondition(
strings::StrCat(error_message, overflow_message));
}
// Return Unavailable to retry by default. There may be other permanent
// failures that should be distinguished.
return errors::Unavailable(
strings::StrCat(error_message, *error_buffer ? error_buffer : "(none)"));
}
} // namespace tensorflow