| /** | |
| Licensed to the Apache Software Foundation (ASF) under one | |
| or more contributor license agreements. See the NOTICE file | |
| distributed with this work for additional information | |
| regarding copyright ownership. The ASF licenses this file | |
| to you under the Apache License, Version 2.0 (the | |
| "License"); you may not use this file except in compliance | |
| with the License. You may obtain a copy of the License at | |
| http://www.apache.org/licenses/LICENSE-2.0 | |
| Unless required by applicable law or agreed to in writing, software | |
| distributed under the License is distributed on an "AS IS" BASIS, | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| See the License for the specific language governing permissions and | |
| limitations under the License. | |
| */ | |
| /** | |
| * @file TransformationPlugin.cc | |
| */ | |
| #include "tscpp/api/TransformationPlugin.h" | |
| #include "ts/ts.h" | |
| #include <cstddef> | |
| #include <cinttypes> | |
| #include "utils_internal.h" | |
| #include "logging_internal.h" | |
| #include "tscpp/api/noncopyable.h" | |
| #include "tscpp/api/Continuation.h" | |
| #ifndef INT64_MAX | |
| #define INT64_MAX (9223372036854775807LL) | |
| #endif | |
| namespace atscppapi | |
| { | |
| namespace detail | |
| { | |
| class ResumeAfterPauseCont : public Continuation | |
| { | |
| public: | |
| ResumeAfterPauseCont() : Continuation() {} | |
| ResumeAfterPauseCont(Continuation::Mutex m) : Continuation(m) {} | |
| protected: | |
| int _run(TSEvent event, void *edata) override; | |
| }; | |
| } // end namespace detail | |
| /** | |
| * @private | |
| */ | |
| struct TransformationPluginState : noncopyable, public detail::ResumeAfterPauseCont { | |
| TSVConn vconn_; | |
| Transaction &transaction_; | |
| TransformationPlugin &transformation_plugin_; | |
| TransformationPlugin::Type type_; | |
| TSVIO output_vio_; // this gets initialized on an output(). | |
| TSHttpTxn txn_; | |
| TSIOBuffer output_buffer_; | |
| TSIOBufferReader output_buffer_reader_; | |
| int64_t bytes_written_; | |
| bool paused_; | |
| // We can only send a single WRITE_COMPLETE even though | |
| // we may receive an immediate event after we've sent a | |
| // write complete, so we'll keep track of whether or not we've | |
| // sent the input end our write complete. | |
| bool input_complete_dispatched_; | |
| std::string request_xform_output_; // in case of request xform, data produced is buffered here | |
| TransformationPluginState(atscppapi::Transaction &transaction, TransformationPlugin &transformation_plugin, | |
| TransformationPlugin::Type type, TSHttpTxn txn) | |
| : detail::ResumeAfterPauseCont(TSMutexCreate()), | |
| vconn_(nullptr), | |
| transaction_(transaction), | |
| transformation_plugin_(transformation_plugin), | |
| type_(type), | |
| output_vio_(nullptr), | |
| txn_(txn), | |
| output_buffer_(nullptr), | |
| output_buffer_reader_(nullptr), | |
| bytes_written_(0), | |
| paused_(false), | |
| input_complete_dispatched_(false) | |
| { | |
| output_buffer_ = TSIOBufferCreate(); | |
| output_buffer_reader_ = TSIOBufferReaderAlloc(output_buffer_); | |
| }; | |
| ~TransformationPluginState() override | |
| { | |
| if (output_buffer_reader_) { | |
| TSIOBufferReaderFree(output_buffer_reader_); | |
| output_buffer_reader_ = nullptr; | |
| } | |
| if (output_buffer_) { | |
| TSIOBufferDestroy(output_buffer_); | |
| output_buffer_ = nullptr; | |
| } | |
| } | |
| }; | |
| } // end namespace atscppapi | |
| using namespace atscppapi; | |
| namespace | |
| { | |
| using ResumeAfterPauseCont = atscppapi::detail::ResumeAfterPauseCont; | |
| void | |
| cleanupTransformation(TSCont contp) | |
| { | |
| LOG_DEBUG("Destroying transformation contp=%p", contp); | |
| TSContDataSet(contp, reinterpret_cast<void *>(0xDEADDEAD)); | |
| TSContDestroy(contp); | |
| } | |
| int | |
| handleTransformationPluginRead(TSCont contp, TransformationPluginState *state) | |
| { | |
| // Traffic Server naming is quite confusing, in this context the write_vio | |
| // is actually the vio we read from. | |
| TSVIO write_vio = TSVConnWriteVIOGet(contp); | |
| if (write_vio) { | |
| if (state->paused_) { | |
| LOG_DEBUG("Transformation contp=%p write_vio=%p, is paused", contp, write_vio); | |
| return 0; | |
| } | |
| int64_t to_read = TSVIONTodoGet(write_vio); | |
| LOG_DEBUG("Transformation contp=%p write_vio=%p, to_read=%" PRId64, contp, write_vio, to_read); | |
| if (to_read > 0) { | |
| /* | |
| * The amount of data left to read needs to be truncated by | |
| * the amount of data actually in the read buffer. | |
| **/ | |
| int64_t avail = TSIOBufferReaderAvail(TSVIOReaderGet(write_vio)); | |
| LOG_DEBUG("Transformation contp=%p write_vio=%p, to_read=%" PRId64 ", buffer reader avail=%" PRId64, contp, write_vio, | |
| to_read, avail); | |
| if (to_read > avail) { | |
| to_read = avail; | |
| LOG_DEBUG("Transformation contp=%p write_vio=%p, to read > avail, fixing to_read to be equal to avail. to_read=%" PRId64 | |
| ", buffer reader avail=%" PRId64, | |
| contp, write_vio, to_read, avail); | |
| } | |
| if (to_read > 0) { | |
| /* Create a buffer and a buffer reader */ | |
| TSIOBuffer input_buffer = TSIOBufferCreate(); | |
| TSIOBufferReader input_reader = TSIOBufferReaderAlloc(input_buffer); | |
| /* Copy the data from the read buffer to the input buffer. */ | |
| TSIOBufferCopy(input_buffer, TSVIOReaderGet(write_vio), to_read, 0); | |
| /* Tell the read buffer that we have read the data and are no | |
| longer interested in it. */ | |
| TSIOBufferReaderConsume(TSVIOReaderGet(write_vio), to_read); | |
| /* Modify the read VIO to reflect how much data we've completed. */ | |
| TSVIONDoneSet(write_vio, TSVIONDoneGet(write_vio) + to_read); | |
| std::string in_data = utils::internal::consumeFromTSIOBufferReader(input_reader); | |
| LOG_DEBUG("Transformation contp=%p write_vio=%p consumed %ld bytes from bufferreader", contp, write_vio, in_data.length()); | |
| /* Clean up the buffer and reader */ | |
| TSIOBufferReaderFree(input_reader); | |
| TSIOBufferDestroy(input_buffer); | |
| /* Now call the client to tell them about data */ | |
| if (in_data.length() > 0) { | |
| state->transformation_plugin_.consume(in_data); | |
| } | |
| } | |
| /* now that we've finished reading we will check if there is anything left to read. */ | |
| TSCont vio_cont = TSVIOContGet(write_vio); // for some reason this can occasionally be null | |
| if (TSVIONTodoGet(write_vio) > 0) { | |
| LOG_DEBUG("Transformation contp=%p write_vio=%p, vio_cont=%p still has bytes left to process, todo > 0.", contp, write_vio, | |
| vio_cont); | |
| if (to_read > 0) { | |
| TSVIOReenable(write_vio); | |
| /* Call back the read VIO continuation to let it know that we are ready for more data. */ | |
| if (vio_cont) { | |
| TSContCall(vio_cont, static_cast<TSEvent>(TS_EVENT_VCONN_WRITE_READY), write_vio); | |
| } | |
| } | |
| } else { | |
| LOG_DEBUG("Transformation contp=%p write_vio=%p, vio_cont=%p has no bytes left to process, will send WRITE_COMPLETE.", | |
| contp, write_vio, vio_cont); | |
| /* Call back the write VIO continuation to let it know that we have completed the write operation. */ | |
| if (!state->input_complete_dispatched_) { | |
| state->transformation_plugin_.handleInputComplete(); | |
| state->input_complete_dispatched_ = true; | |
| if (vio_cont && nullptr != TSVIOBufferGet(write_vio)) { | |
| TSContCall(vio_cont, static_cast<TSEvent>(TS_EVENT_VCONN_WRITE_COMPLETE), write_vio); | |
| } | |
| } | |
| } | |
| } else { | |
| TSCont vio_cont = TSVIOContGet(write_vio); // for some reason this can occasionally be null? | |
| LOG_DEBUG("Transformation contp=%p write_vio=%p, vio_cont=%p has no bytes left to process.", contp, write_vio, vio_cont); | |
| /* Call back the write VIO continuation to let it know that we have completed the write operation. */ | |
| if (!state->input_complete_dispatched_) { | |
| state->transformation_plugin_.handleInputComplete(); | |
| state->input_complete_dispatched_ = true; | |
| if (vio_cont && nullptr != TSVIOBufferGet(write_vio)) { | |
| TSContCall(vio_cont, static_cast<TSEvent>(TS_EVENT_VCONN_WRITE_COMPLETE), write_vio); | |
| } | |
| } | |
| } | |
| } else { | |
| LOG_ERROR("Transformation contp=%p write_vio=%p was nullptr!", contp, write_vio); | |
| } | |
| return 0; | |
| } | |
| int | |
| handleTransformationPluginEvents(TSCont contp, TSEvent event, void *edata) | |
| { | |
| TransformationPluginState *state = static_cast<TransformationPluginState *>(TSContDataGet(contp)); | |
| LOG_DEBUG("Transformation contp=%p event=%d edata=%p tshttptxn=%p", contp, event, edata, state->txn_); | |
| // The first thing you always do is check if the VConn is closed. | |
| int connection_closed = TSVConnClosedGet(state->vconn_); | |
| if (connection_closed) { | |
| LOG_DEBUG("Transformation contp=%p tshttptxn=%p is closed connection_closed=%d ", contp, state->txn_, connection_closed); | |
| // we'll do the cleanupTransformation in the TransformationPlugin destructor. | |
| return 0; | |
| } | |
| if (event == TS_EVENT_VCONN_WRITE_COMPLETE) { | |
| TSVConn output_vconn = TSTransformOutputVConnGet(state->vconn_); | |
| LOG_DEBUG("Transformation contp=%p tshttptxn=%p received WRITE_COMPLETE, shutting down outputvconn=%p ", contp, state->txn_, | |
| output_vconn); | |
| TSVConnShutdown(output_vconn, 0, 1); // The other end is done reading our output | |
| return 0; | |
| } else if (event == TS_EVENT_ERROR) { | |
| TSVIO write_vio; | |
| /* Get the write VIO for the write operation that was | |
| performed on ourself. This VIO contains the continuation of | |
| our parent transformation. */ | |
| write_vio = TSVConnWriteVIOGet(state->vconn_); | |
| TSCont vio_cont = TSVIOContGet(write_vio); | |
| LOG_ERROR("Transformation contp=%p tshttptxn=%p received EVENT_ERROR forwarding to write_vio=%p viocont=%p", contp, state->txn_, | |
| write_vio, vio_cont); | |
| if (vio_cont) { | |
| TSContCall(vio_cont, TS_EVENT_ERROR, write_vio); | |
| } | |
| return 0; | |
| } | |
| // All other events including WRITE_READY will just attempt to transform more data. | |
| return handleTransformationPluginRead(state->vconn_, state); | |
| } | |
| } /* anonymous namespace */ | |
| TransformationPlugin::TransformationPlugin(Transaction &transaction, TransformationPlugin::Type type) | |
| : TransactionPlugin(transaction), state_(nullptr) | |
| { | |
| state_ = | |
| std::make_unique<TransformationPluginState>(transaction, *this, type, static_cast<TSHttpTxn>(transaction.getAtsHandle())); | |
| state_->vconn_ = TSTransformCreate(handleTransformationPluginEvents, state_->txn_); | |
| TSContDataSet(state_->vconn_, static_cast<void *>(state_.get())); // edata in a TransformationHandler is NOT a TSHttpTxn. | |
| LOG_DEBUG("Creating TransformationPlugin=%p (vconn)contp=%p tshttptxn=%p transformation_type=%d", this, state_->vconn_, | |
| state_->txn_, type); | |
| TSHttpTxnHookAdd(state_->txn_, utils::internal::convertInternalTransformationTypeToTsHook(type), state_->vconn_); | |
| } | |
| TransformationPlugin::~TransformationPlugin() | |
| { | |
| LOG_DEBUG("Destroying TransformationPlugin=%p", this); | |
| cleanupTransformation(state_->vconn_); | |
| } | |
| void | |
| TransformationPlugin::pause() | |
| { | |
| if (state_->paused_) { | |
| LOG_ERROR("Can not pause transformation, already paused TransformationPlugin=%p (vconn)contp=%p tshttptxn=%p", this, | |
| state_->vconn_, state_->txn_); | |
| } else if (state_->input_complete_dispatched_) { | |
| LOG_ERROR("Can not pause transformation (transformation completed) TransformationPlugin=%p (vconn)contp=%p tshttptxn=%p", this, | |
| state_->vconn_, state_->txn_); | |
| } else { | |
| state_->paused_ = true; | |
| if (!static_cast<bool>(*static_cast<ResumeAfterPauseCont *>(state_.get()))) { | |
| *static_cast<ResumeAfterPauseCont *>(state_.get()) = | |
| ResumeAfterPauseCont(TSContMutexGet(reinterpret_cast<TSCont>(state_->txn_))); | |
| } | |
| } | |
| } | |
| bool | |
| TransformationPlugin::isPaused() const | |
| { | |
| return state_->paused_; | |
| } | |
| Continuation & | |
| TransformationPlugin::resumeCont() | |
| { | |
| TSReleaseAssert(state_->paused_); | |
| // The cast to a pointer to the intermediate base class ResumeAfterPauseCont is not strictly necessary. It is | |
| // possible that the transform plugin might want to defer work to other continuations in the future. This would | |
| // naturally result in TransactionPluginState having Continuation as an indirect base class multiple times, making | |
| // disambiguation necessary when converting. | |
| // | |
| return *static_cast<ResumeAfterPauseCont *>(state_.get()); | |
| } | |
| int | |
| ResumeAfterPauseCont::_run(TSEvent /* event ATS_UNUSED */, void * /* edata ATS_UNUSED */) | |
| { | |
| auto state = static_cast<TransformationPluginState *>(this); | |
| state->paused_ = false; | |
| handleTransformationPluginRead(state->vconn_, state); | |
| return TS_SUCCESS; | |
| } | |
| size_t | |
| TransformationPlugin::doProduce(std::string_view data) | |
| { | |
| LOG_DEBUG("TransformationPlugin=%p tshttptxn=%p producing output with length=%ld", this, state_->txn_, data.length()); | |
| int64_t write_length = static_cast<int64_t>(data.length()); | |
| if (!write_length) { | |
| return 0; | |
| } | |
| if (!state_->output_vio_) { | |
| TSVConn output_vconn = TSTransformOutputVConnGet(state_->vconn_); | |
| LOG_DEBUG("TransformationPlugin=%p tshttptxn=%p will issue a TSVConnWrite, output_vconn=%p.", this, state_->txn_, output_vconn); | |
| if (output_vconn) { | |
| // If you're confused about the following reference the traffic server transformation docs. | |
| // You always write INT64_MAX, this basically says you're not sure how much data you're going to write | |
| state_->output_vio_ = TSVConnWrite(output_vconn, state_->vconn_, state_->output_buffer_reader_, INT64_MAX); | |
| } else { | |
| LOG_ERROR("TransformationPlugin=%p tshttptxn=%p output_vconn=%p cannot issue TSVConnWrite due to null output vconn.", this, | |
| state_->txn_, output_vconn); | |
| return 0; | |
| } | |
| if (!state_->output_vio_) { | |
| LOG_ERROR("TransformationPlugin=%p tshttptxn=%p state_->output_vio=%p, TSVConnWrite failed.", this, state_->txn_, | |
| state_->output_vio_); | |
| return 0; | |
| } | |
| } | |
| // Finally we can copy this data into the output_buffer | |
| int64_t bytes_written = TSIOBufferWrite(state_->output_buffer_, data.data(), write_length); | |
| state_->bytes_written_ += bytes_written; // So we can set BytesDone on outputComplete(). | |
| LOG_DEBUG("TransformationPlugin=%p tshttptxn=%p write to TSIOBuffer %" PRId64 " bytes total bytes written %" PRId64, this, | |
| state_->txn_, bytes_written, state_->bytes_written_); | |
| // Sanity Checks | |
| if (bytes_written != write_length) { | |
| LOG_ERROR("TransformationPlugin=%p tshttptxn=%p bytes written < expected. bytes_written=%" PRId64 " write_length=%" PRId64, | |
| this, state_->txn_, bytes_written, write_length); | |
| } | |
| int connection_closed = TSVConnClosedGet(state_->vconn_); | |
| LOG_DEBUG("TransformationPlugin=%p tshttptxn=%p vconn=%p connection_closed=%d", this, state_->txn_, state_->vconn_, | |
| connection_closed); | |
| if (!connection_closed) { | |
| TSVIOReenable(state_->output_vio_); // Wake up the downstream vio | |
| } else { | |
| LOG_ERROR( | |
| "TransformationPlugin=%p tshttptxn=%p output_vio=%p connection_closed=%d : Couldn't reenable output vio (connection closed).", | |
| this, state_->txn_, state_->output_vio_, connection_closed); | |
| } | |
| return static_cast<size_t>(bytes_written); | |
| } | |
| size_t | |
| TransformationPlugin::produce(std::string_view data) | |
| { | |
| if (state_->type_ == REQUEST_TRANSFORMATION) { | |
| state_->request_xform_output_.append(data.data(), data.length()); | |
| return data.size(); | |
| } else if (state_->type_ == CLIENT_RESPONSE_SINK_TRANSFORMATION) { | |
| LOG_DEBUG("produce TransformationPlugin=%p tshttptxn=%p : This is a client response sink transform. Not producing any output", | |
| this, state_->txn_); | |
| return 0; | |
| } else if (state_->type_ == CLIENT_REQUEST_SINK_TRANSFORMATION) { | |
| LOG_DEBUG("produce TransformationPlugin=%p tshttptxn=%p : This is a client request sink transform. Not producing any output", | |
| this, state_->txn_); | |
| return 0; | |
| } else { | |
| return doProduce(data); | |
| } | |
| } | |
| size_t | |
| TransformationPlugin::setOutputComplete() | |
| { | |
| if (state_->type_ == CLIENT_RESPONSE_SINK_TRANSFORMATION || state_->type_ == CLIENT_REQUEST_SINK_TRANSFORMATION) { | |
| // There's no output stream for a sink transform, so we do nothing | |
| // | |
| // Warning: don't try to shutdown the VConn, since the default implementation (DummyVConnection) | |
| // has a stubbed out shutdown/close implementation | |
| return 0; | |
| } else if (state_->type_ == REQUEST_TRANSFORMATION) { | |
| doProduce(state_->request_xform_output_); | |
| } | |
| int connection_closed = TSVConnClosedGet(state_->vconn_); | |
| LOG_DEBUG("OutputComplete TransformationPlugin=%p tshttptxn=%p vconn=%p connection_closed=%d, total bytes written=%" PRId64, this, | |
| state_->txn_, state_->vconn_, connection_closed, state_->bytes_written_); | |
| if (!connection_closed && !state_->output_vio_) { | |
| LOG_DEBUG("TransformationPlugin=%p tshttptxn=%p output complete without writing any data, initiating write of 0 bytes.", this, | |
| state_->txn_); | |
| // We're done without ever outputting anything, to correctly | |
| // clean up we'll initiate a write then immediately set it to 0 bytes done. | |
| state_->output_vio_ = TSVConnWrite(TSTransformOutputVConnGet(state_->vconn_), state_->vconn_, state_->output_buffer_reader_, 0); | |
| if (state_->output_vio_) { | |
| TSVIONDoneSet(state_->output_vio_, 0); | |
| TSVIOReenable(state_->output_vio_); // Wake up the downstream vio | |
| } else { | |
| LOG_ERROR("TransformationPlugin=%p tshttptxn=%p unable to reenable output_vio=%p because VConnWrite failed.", this, | |
| state_->txn_, state_->output_vio_); | |
| } | |
| return 0; | |
| } | |
| if (!connection_closed) { | |
| // So there is a possible race condition here, if we wake up a dead | |
| // VIO it can cause a segfault, so we must check that the VCONN is not dead. | |
| int connection_closed = TSVConnClosedGet(state_->vconn_); | |
| if (!connection_closed) { | |
| TSVIONBytesSet(state_->output_vio_, state_->bytes_written_); | |
| TSVIOReenable(state_->output_vio_); // Wake up the downstream vio | |
| } else { | |
| LOG_ERROR("TransformationPlugin=%p tshttptxn=%p unable to reenable output_vio=%p connection was closed=%d.", this, | |
| state_->txn_, state_->output_vio_, connection_closed); | |
| } | |
| } else { | |
| LOG_ERROR("TransformationPlugin=%p tshttptxn=%p unable to reenable output_vio=%p connection was closed=%d.", this, state_->txn_, | |
| state_->output_vio_, connection_closed); | |
| } | |
| return state_->bytes_written_; | |
| } |