| /** @file | |
| A brief file description | |
| @section license License | |
| Licensed to the Apache Software Foundation (ASF) under one | |
| or more contributor license agreements. See the NOTICE file | |
| distributed with this work for additional information | |
| regarding copyright ownership. The ASF licenses this file | |
| to you under the Apache License, Version 2.0 (the | |
| "License"); you may not use this file except in compliance | |
| with the License. You may obtain a copy of the License at | |
| http://www.apache.org/licenses/LICENSE-2.0 | |
| Unless required by applicable law or agreed to in writing, software | |
| distributed under the License is distributed on an "AS IS" BASIS, | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| See the License for the specific language governing permissions and | |
| limitations under the License. | |
| */ | |
| #include <typeinfo> | |
| #include <chrono> | |
| #include <tscore/TSSystemState.h> | |
| ////////////////////////////////////////////////////////////////////// | |
| // | |
| // The EThread Class | |
| // | |
| ///////////////////////////////////////////////////////////////////// | |
| #include "iocore/eventsystem/EThread.h" | |
| #include "iocore/eventsystem/EventProcessor.h" | |
| #include "iocore/eventsystem/Lock.h" | |
| #include "tscore/ink_hrtime.h" | |
| #include "tscore/ink_atomic.h" | |
| #if HAVE_EVENTFD | |
| #include <sys/eventfd.h> | |
| #endif | |
| struct AIOCallback; | |
| #define NO_HEARTBEAT -1 | |
| #define THREAD_MAX_HEARTBEAT_MSECONDS 60 | |
| // !! THIS MUST BE IN THE ENUM ORDER !! | |
| char const *const EThread::Metrics::Slice::STAT_NAME[] = { | |
| "proxy.process.eventloop.count", "proxy.process.eventloop.events", | |
| "proxy.process.eventloop.events.min", "proxy.process.eventloop.events.max", | |
| "proxy.process.eventloop.wait", "proxy.process.eventloop.time.min", | |
| "proxy.process.eventloop.time.max", "proxy.process.eventloop.drain.queue.max", | |
| "proxy.process.eventloop.io.wait.max", "proxy.process.eventloop.io.work.max", | |
| }; | |
| int thread_max_heartbeat_mseconds = THREAD_MAX_HEARTBEAT_MSECONDS; | |
| int loop_time_update_probability = 10; | |
| const ink_hrtime DELAY_FOR_RETRY = HRTIME_MSECONDS(10); | |
| // To define a class inherits from Thread: | |
| // 1) Define an independent thread_local static member | |
| // 2) Override Thread::set_specific() and assign that member and call Thread::set_specific() | |
| // 3) Define this_Xthread() which get thread specific data | |
| // 4) Clear thread specific data at destructor function. | |
| // | |
| // The below comments are copied from I_Thread.h | |
| // | |
| // Additionally, the EThread class (derived from Thread) maintains its | |
| // own independent data. All (and only) the threads created in the Event | |
| // Subsystem have this data. | |
| thread_local EThread *EThread::this_ethread_ptr; | |
| int EThread::default_wait_interval_ms = 10; | |
| void | |
| EThread::set_specific() | |
| { | |
| this_ethread_ptr = this; | |
| Thread::set_specific(); | |
| } | |
| void | |
| EThread::cons_common() | |
| { | |
| MUTEX_TAKE_LOCK(mutex, this); | |
| mutex->nthread_holding += THREAD_MUTEX_THREAD_HOLDING; | |
| memset(thread_private, 0, PER_THREAD_DATA); | |
| } | |
| EThread::EThread() | |
| { | |
| cons_common(); | |
| } | |
| EThread::EThread(ThreadType att, int anid) : id(anid), tt(att) | |
| { | |
| cons_common(); | |
| #if HAVE_EVENTFD | |
| evfd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); | |
| if (evfd < 0) { | |
| if (errno == EINVAL) { // flags invalid for kernel <= 2.6.26 | |
| evfd = eventfd(0, 0); | |
| if (evfd < 0) { | |
| Fatal("EThread::EThread: %d=eventfd(0,0),errno(%d)", evfd, errno); | |
| } | |
| } else { | |
| Fatal("EThread::EThread: %d=eventfd(0,EFD_NONBLOCK | EFD_CLOEXEC),errno(%d)", evfd, errno); | |
| } | |
| } | |
| #else | |
| ink_release_assert(pipe(evpipe) >= 0); | |
| fcntl(evpipe[0], F_SETFD, FD_CLOEXEC); | |
| fcntl(evpipe[0], F_SETFL, O_NONBLOCK); | |
| fcntl(evpipe[1], F_SETFD, FD_CLOEXEC); | |
| fcntl(evpipe[1], F_SETFL, O_NONBLOCK); | |
| #endif | |
| } | |
| EThread::EThread(ThreadType att, Event *e) : tt(att), start_event(e) | |
| { | |
| ink_assert(att == DEDICATED); | |
| cons_common(); | |
| } | |
| // Provide a destructor so that SDK functions which create and destroy | |
| // threads won't have to deal with EThread memory deallocation. | |
| EThread::~EThread() | |
| { | |
| ink_release_assert(mutex->thread_holding == this); | |
| if (this_ethread_ptr == this) { | |
| this_ethread_ptr = nullptr; | |
| } | |
| mutex->nthread_holding -= THREAD_MUTEX_THREAD_HOLDING; | |
| MUTEX_UNTAKE_LOCK(mutex, this); | |
| } | |
| bool | |
| EThread::is_event_type(EventType et) | |
| { | |
| return (event_types & (1 << static_cast<int>(et))) != 0; | |
| } | |
| void | |
| EThread::set_event_type(EventType et) | |
| { | |
| event_types |= (1 << static_cast<int>(et)); | |
| } | |
| ink_hrtime | |
| EThread::process_event(Event *e, int calling_code, ink_hrtime event_time) | |
| { | |
| ink_assert((!e->in_the_prot_queue && !e->in_the_priority_queue)); | |
| WEAK_MUTEX_TRY_LOCK(lock, e->mutex, this); | |
| if (!lock.is_locked()) { | |
| e->timeout_at = event_time + DELAY_FOR_RETRY; | |
| EventQueueExternal.enqueue_local(e); | |
| } else { | |
| if (e->cancelled) { | |
| MUTEX_RELEASE(lock); | |
| free_event(e); | |
| return event_time; | |
| } | |
| Continuation *c_temp = e->continuation; | |
| // Restore the client IP debugging flags | |
| set_cont_flags(e->continuation->control_flags); | |
| e->continuation->handleEvent(calling_code, e); | |
| if (loop_time_update_probability == 100) { | |
| event_time = ink_get_hrtime(); | |
| } else if (loop_time_update_probability > 0) { | |
| if (static_cast<int>(generator.random() % 100) < loop_time_update_probability) { | |
| event_time = ink_get_hrtime(); | |
| } | |
| } | |
| ink_assert(!e->in_the_priority_queue); | |
| ink_assert(c_temp == e->continuation); | |
| MUTEX_RELEASE(lock); | |
| if (e->period) { | |
| if (!e->in_the_prot_queue && !e->in_the_priority_queue) { | |
| if (e->period < 0) { | |
| e->timeout_at = e->period; | |
| } else { | |
| e->timeout_at = event_time + e->period; | |
| } | |
| EventQueueExternal.enqueue_local(e); | |
| } | |
| } else if (!e->in_the_prot_queue && !e->in_the_priority_queue) { | |
| free_event(e); | |
| } | |
| } | |
| return event_time; | |
| } | |
| ink_hrtime | |
| EThread::process_queue(Que(Event, link) * NegativeQueue, int *ev_count, int *nq_count, ink_hrtime event_time) | |
| { | |
| Event *e; | |
| // Move events from the external thread safe queues to the local queue. | |
| EventQueueExternal.dequeue_external(); | |
| // execute all the available external events that have | |
| // already been dequeued | |
| while ((e = EventQueueExternal.dequeue_local())) { | |
| ++(*ev_count); | |
| if (e->cancelled) { | |
| free_event(e); | |
| } else if (!e->timeout_at) { // IMMEDIATE | |
| ink_assert(e->period == 0); | |
| event_time = process_event(e, e->callback_event, event_time); | |
| } else if (e->timeout_at > 0) { // INTERVAL | |
| EventQueue.enqueue(e, event_time); | |
| } else { // NEGATIVE | |
| Event *p = nullptr; | |
| Event *a = NegativeQueue->head; | |
| while (a && a->timeout_at > e->timeout_at) { | |
| p = a; | |
| a = a->link.next; | |
| } | |
| if (!a) { | |
| NegativeQueue->enqueue(e); | |
| } else { | |
| NegativeQueue->insert(e, p); | |
| } | |
| } | |
| ++(*nq_count); | |
| } | |
| return event_time; | |
| } | |
| void | |
| EThread::execute_regular() | |
| { | |
| Event *e; | |
| Que(Event, link) NegativeQueue; | |
| ink_hrtime next_time; | |
| ink_hrtime delta; // time spent in the event loop | |
| ink_hrtime loop_start_time; // Time the loop started. | |
| ink_hrtime loop_finish_time; // Time at the end of the loop. | |
| loop_start_time = ink_get_hrtime(); | |
| // Track this so we can update on boundary crossing. | |
| auto prev_slice = this->metrics.prev_slice(metrics._slice.data() + (loop_start_time / HRTIME_SECOND) % Metrics::N_SLICES); | |
| int nq_count; | |
| int ev_count; | |
| // A statically initialized instance we can use as a prototype for initializing other instances. | |
| static const Metrics::Slice SLICE_INIT; | |
| Metrics::Slice *current_slice{nullptr}; | |
| // give priority to immediate events | |
| while (!TSSystemState::is_event_system_shut_down()) { | |
| nq_count = 0; // count # of elements put on negative queue. | |
| ev_count = 0; // # of events handled. | |
| current_slice = metrics._slice.data() + (loop_start_time / HRTIME_SECOND) % Metrics::N_SLICES; | |
| metrics.current_slice.store(current_slice, std::memory_order_release); | |
| if (current_slice != prev_slice) { | |
| // I have observed multi-second event loops in production, making this necessary. [amc] | |
| do { | |
| memcpy(prev_slice = this->metrics.next_slice(prev_slice), &SLICE_INIT, sizeof(SLICE_INIT)); | |
| } while (current_slice != prev_slice); | |
| current_slice->record_loop_start(loop_start_time); | |
| } | |
| ++(current_slice->_count); // loop started, bump count. | |
| ink_hrtime event_time = process_queue(&NegativeQueue, &ev_count, &nq_count, loop_start_time); | |
| bool done_one; | |
| do { | |
| done_one = false; | |
| // execute all the eligible internal events | |
| EventQueue.check_ready(event_time, this); | |
| while ((e = EventQueue.dequeue_ready(event_time))) { | |
| ink_assert(e); | |
| ink_assert(e->timeout_at > 0); | |
| if (e->cancelled) { | |
| free_event(e); | |
| } else { | |
| done_one = true; | |
| event_time = process_event(e, e->callback_event, event_time); | |
| } | |
| } | |
| } while (done_one); | |
| // execute any negative (poll) events | |
| if (NegativeQueue.head) { | |
| event_time = process_queue(&NegativeQueue, &ev_count, &nq_count, event_time); | |
| // execute poll events | |
| while ((e = NegativeQueue.dequeue())) { | |
| event_time = process_event(e, EVENT_POLL, event_time); | |
| } | |
| } | |
| next_time = EventQueue.earliest_timeout(); | |
| ink_hrtime sleep_time = next_time - event_time; | |
| if (sleep_time > 0) { | |
| if (EventQueueExternal.localQueue.empty()) { | |
| sleep_time = std::min(sleep_time, HRTIME_MSECONDS(thread_max_heartbeat_mseconds)); | |
| } else { | |
| // Because of a missed lock, Timed-Event and Negative-Event have been pushed into localQueue for retry in awhile. | |
| // Therefore, we have to set the limitation of sleep time in order to handle the next retry in time. | |
| sleep_time = std::min(sleep_time, DELAY_FOR_RETRY); | |
| } | |
| ++(current_slice->_wait); | |
| } else { | |
| sleep_time = 0; | |
| } | |
| // drained the queue by this point | |
| ink_hrtime post_drain = event_time; | |
| ink_hrtime drain_queue = post_drain - loop_start_time; | |
| // watchdog kick - pre-sleep | |
| // Relaxed store because this EThread is the only writer and the watchdog only needs a coherent timestamp. | |
| this->heartbeat_state.last_sleep.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); | |
| tail_cb->waitForActivity(sleep_time); | |
| // watchdog kick - post-wake | |
| // Relaxed store/fetch because the monitor thread is the single reader and per-field coherence is sufficient. | |
| this->heartbeat_state.last_wake.store(std::chrono::steady_clock::now(), std::memory_order_relaxed); | |
| this->heartbeat_state.seq.fetch_add(1, std::memory_order_relaxed); | |
| // loop cleanup | |
| loop_finish_time = ink_get_hrtime(); | |
| // @a delta can be negative due to time of day adjustments (which apparently happen quite frequently). I | |
| // tried using the monotonic clock to get around this but it was *very* stuttery (up to hundreds | |
| // of milliseconds), far too much to be actually used. | |
| delta = std::max<ink_hrtime>(0, loop_finish_time - loop_start_time); | |
| loop_start_time = loop_finish_time; | |
| metrics.decay(); | |
| metrics.record_loop_time(delta); | |
| current_slice->record_event_count(ev_count); | |
| current_slice->record_drain_queue(drain_queue); | |
| } | |
| } | |
| // | |
| // void EThread::execute() | |
| // | |
| // Execute loops forever on: | |
| // Find the earliest event. | |
| // Sleep until the event time or until an earlier event is inserted | |
| // When it's time for the event, try to get the appropriate continuation | |
| // lock. If successful, call the continuation, otherwise put the event back | |
| // into the queue. | |
| // | |
| void | |
| EThread::execute() | |
| { | |
| // Do the start event first. | |
| // coverity[lock] | |
| if (start_event) { | |
| { | |
| SCOPED_MUTEX_LOCK(lock, start_event->mutex, this); | |
| start_event->continuation->handleEvent(EVENT_IMMEDIATE, start_event); | |
| } | |
| free_event(start_event); | |
| start_event = nullptr; | |
| } | |
| switch (tt) { | |
| case REGULAR: { | |
| /* The Event Thread has two status: busy and sleep: | |
| * - Keep `EThread::lock` locked while Event Thread is busy, | |
| * - The `EThread::lock` is released while Event Thread is sleep. | |
| * When other threads try to acquire the `EThread::lock` of the target Event Thread: | |
| * - Acquired, indicating that the target Event Thread is sleep, | |
| * - Failed, indicating that the target Event Thread is busy. | |
| */ | |
| ink_mutex_acquire(&EventQueueExternal.lock); | |
| this->execute_regular(); | |
| ink_mutex_release(&EventQueueExternal.lock); | |
| break; | |
| } | |
| case DEDICATED: { | |
| break; | |
| } | |
| default: | |
| ink_assert(!"bad case value (execute)"); | |
| break; | |
| } /* End switch */ | |
| // coverity[missing_unlock] | |
| } | |
| EThread::Metrics::Slice & | |
| EThread::Metrics::Slice::operator+=(Slice const &that) | |
| { | |
| this->_events._max = std::max(this->_events._max, that._events._max); | |
| this->_events._min = std::min(this->_events._min, that._events._min); | |
| this->_events._total += that._events._total; | |
| this->_duration._min = std::min(this->_duration._min, that._duration._min); | |
| this->_duration._max = std::max(this->_duration._max, that._duration._max); | |
| this->_count += that._count; | |
| this->_wait += that._wait; | |
| this->_duration._max_drain_queue = std::max(this->_duration._max_drain_queue, that._duration._max_drain_queue); | |
| this->_duration._max_io_wait = std::max(this->_duration._max_io_wait, that._duration._max_io_wait); | |
| this->_duration._max_io_work = std::max(this->_duration._max_io_work, that._duration._max_io_work); | |
| return *this; | |
| } | |
| void | |
| EThread::Metrics::summarize(Metrics &global) | |
| { | |
| // Accumulate in local first so each sample only needs to be processed once, | |
| // not N_EVENT_TIMESCALES times. | |
| Slice sum; | |
| // To avoid race conditions, we back up one from the current metric block. It's close enough | |
| // and won't be updated during the time this method runs so it should be thread safe. | |
| // The acquire fence ensures we see all the preceeding updates to the previous slice. | |
| Slice *slice = this->prev_slice(current_slice.load(std::memory_order_acquire)); | |
| for (unsigned t = 0; t < N_TIMESCALES; ++t) { | |
| int count = SLICE_SAMPLE_COUNT[t]; | |
| if (t > 0) { | |
| count -= SLICE_SAMPLE_COUNT[t - 1]; | |
| } | |
| while (--count >= 0) { | |
| if (0 != slice->_duration._start) { | |
| sum += *slice; | |
| } | |
| slice = this->prev_slice(slice); | |
| } | |
| global._slice[t] += sum; // push out to return vector. | |
| } | |
| // Only summarize if there's no outstanding decay. | |
| if (0 == _decay_count) { | |
| global._loop_timing += _loop_timing; | |
| global._api_timing += _api_timing; | |
| } | |
| } | |
| void | |
| EThread::set_tail_handler(LoopTailHandler *handler) | |
| { | |
| ink_atomic_swap(&tail_cb, handler); | |
| } | |
| Event * | |
| EThread::schedule_imm(Continuation *cont, int callback_event, void *cookie) | |
| { | |
| Event *e = ::eventAllocator.alloc(); | |
| #ifdef ENABLE_EVENT_TRACKER | |
| e->set_location(); | |
| #endif | |
| e->callback_event = callback_event; | |
| e->cookie = cookie; | |
| return schedule(e->init(cont, 0, 0)); | |
| } | |
| Event * | |
| EThread::schedule_at(Continuation *cont, ink_hrtime t, int callback_event, void *cookie) | |
| { | |
| Event *e = ::eventAllocator.alloc(); | |
| #ifdef ENABLE_EVENT_TRACKER | |
| e->set_location(); | |
| #endif | |
| e->callback_event = callback_event; | |
| e->cookie = cookie; | |
| return schedule(e->init(cont, t, 0)); | |
| } | |
| Event * | |
| EThread::schedule_in(Continuation *cont, ink_hrtime t, int callback_event, void *cookie) | |
| { | |
| Event *e = ::eventAllocator.alloc(); | |
| #ifdef ENABLE_EVENT_TRACKER | |
| e->set_location(); | |
| #endif | |
| e->callback_event = callback_event; | |
| e->cookie = cookie; | |
| return schedule(e->init(cont, ink_get_hrtime() + t, 0)); | |
| } | |
| Event * | |
| EThread::schedule_every(Continuation *cont, ink_hrtime t, int callback_event, void *cookie) | |
| { | |
| Event *e = ::eventAllocator.alloc(); | |
| #ifdef ENABLE_EVENT_TRACKER | |
| e->set_location(); | |
| #endif | |
| e->callback_event = callback_event; | |
| e->cookie = cookie; | |
| if (t < 0) { | |
| return schedule(e->init(cont, t, t)); | |
| } else { | |
| return schedule(e->init(cont, ink_get_hrtime() + t, t)); | |
| } | |
| } | |
| Event * | |
| EThread::schedule(Event *e) | |
| { | |
| e->ethread = this; | |
| if (tt != REGULAR) { | |
| ink_assert(tt == DEDICATED); | |
| return eventProcessor.schedule(e, ET_CALL); | |
| } | |
| if (e->continuation->mutex) { | |
| e->mutex = e->continuation->mutex; | |
| } else { | |
| e->mutex = e->continuation->mutex = e->ethread->mutex; | |
| } | |
| ink_assert(e->mutex.get()); | |
| // Make sure client IP debugging works consistently | |
| // The continuation that gets scheduled later is not always the | |
| // client VC, it can be HttpCacheSM etc. so save the flags | |
| e->continuation->control_flags.set_flags(get_cont_flags().get_flags()); | |
| if (e->ethread == this_ethread()) { | |
| EventQueueExternal.enqueue_local(e); | |
| } else { | |
| EventQueueExternal.enqueue(e); | |
| } | |
| return e; | |
| } | |
| Event * | |
| EThread::schedule_imm_local(Continuation *cont, int callback_event, void *cookie) | |
| { | |
| Event *e = EVENT_ALLOC(eventAllocator, this); | |
| #ifdef ENABLE_EVENT_TRACKER | |
| e->set_location(); | |
| #endif | |
| e->callback_event = callback_event; | |
| e->cookie = cookie; | |
| return schedule_local(e->init(cont, 0, 0)); | |
| } | |
| Event * | |
| EThread::schedule_at_local(Continuation *cont, ink_hrtime t, int callback_event, void *cookie) | |
| { | |
| Event *e = EVENT_ALLOC(eventAllocator, this); | |
| #ifdef ENABLE_EVENT_TRACKER | |
| e->set_location(); | |
| #endif | |
| e->callback_event = callback_event; | |
| e->cookie = cookie; | |
| return schedule_local(e->init(cont, t, 0)); | |
| } | |
| Event * | |
| EThread::schedule_in_local(Continuation *cont, ink_hrtime t, int callback_event, void *cookie) | |
| { | |
| Event *e = EVENT_ALLOC(eventAllocator, this); | |
| #ifdef ENABLE_EVENT_TRACKER | |
| e->set_location(); | |
| #endif | |
| e->callback_event = callback_event; | |
| e->cookie = cookie; | |
| return schedule_local(e->init(cont, ink_get_hrtime() + t, 0)); | |
| } | |
| Event * | |
| EThread::schedule_every_local(Continuation *cont, ink_hrtime t, int callback_event, void *cookie) | |
| { | |
| Event *e = EVENT_ALLOC(eventAllocator, this); | |
| #ifdef ENABLE_EVENT_TRACKER | |
| e->set_location(); | |
| #endif | |
| e->callback_event = callback_event; | |
| e->cookie = cookie; | |
| if (t < 0) { | |
| return schedule_local(e->init(cont, t, t)); | |
| } else { | |
| return schedule_local(e->init(cont, ink_get_hrtime() + t, t)); | |
| } | |
| } | |
| Event * | |
| EThread::schedule_local(Event *e) | |
| { | |
| if (tt != REGULAR) { | |
| ink_assert(tt == DEDICATED); | |
| return eventProcessor.schedule(e, ET_CALL); | |
| } | |
| if (!e->mutex) { | |
| e->ethread = this; | |
| e->mutex = e->continuation->mutex; | |
| } else { | |
| ink_assert(e->ethread == this); | |
| } | |
| e->globally_allocated = false; | |
| // Make sure client IP debugging works consistently | |
| // The continuation that gets scheduled later is not always the | |
| // client VC, it can be HttpCacheSM etc. so save the flags | |
| e->continuation->control_flags.set_flags(get_cont_flags().get_flags()); | |
| // If you need to schedule an event from a different thread, use Ethread::schedule_imm/at/in/every functions | |
| ink_release_assert(this == this_ethread()); | |
| EventQueueExternal.enqueue_local(e); | |
| return e; | |
| } | |
| Event * | |
| EThread::schedule_spawn(Continuation *c, int ev, void *cookie) | |
| { | |
| ink_assert(this != this_ethread()); // really broken to call this from the same thread. | |
| if (start_event) { | |
| free_event(start_event); | |
| } | |
| start_event = EVENT_ALLOC(eventAllocator, this); | |
| start_event->ethread = this; | |
| start_event->mutex = this->mutex; | |
| start_event->init(c); | |
| start_event->callback_event = ev; | |
| start_event->cookie = cookie; | |
| return start_event; | |
| } |