From 53aeea4d82801f5d624a4f6a62090195d3a24f2f Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 16 Oct 2019 09:15:47 -0400 Subject: DRTVWR-476: Add LLEventLogProxy, LLEventLogProxyFor. LLEventLogProxy can be introduced to serve as a logging proxy for an existing LLEventPump subclass instance. Access through the LLEventLogProxy will be logged; access directly to the underlying LLEventPump will not. LLEventLogProxyFor functions as a drop-in replacement for the original LLEventPumpSubclass instance. It internally instantiates LLEventPumpSubclass and serves as a proxy for that instance. Add unit tests for LLEventMailDrop and LLEventLogProxyFor, both "plain" (events only) and via lleventcoro.h synchronization. --- indra/llcommon/lleventfilter.cpp | 59 ++++++++++++++++++ indra/llcommon/lleventfilter.h | 95 +++++++++++++++++++++++++++++ indra/llcommon/tests/lleventcoro_test.cpp | 78 +++++++++++++++++++++++ indra/llcommon/tests/lleventfilter_test.cpp | 75 +++++++++++++++++++++++ 4 files changed, 307 insertions(+) diff --git a/indra/llcommon/lleventfilter.cpp b/indra/llcommon/lleventfilter.cpp index 9fb18dc67d..06b3cb769e 100644 --- a/indra/llcommon/lleventfilter.cpp +++ b/indra/llcommon/lleventfilter.cpp @@ -37,6 +37,7 @@ // other Linden headers #include "llerror.h" // LL_ERRS #include "llsdutil.h" // llsd_matches() +#include "stringize.h" /***************************************************************************** * LLEventFilter @@ -409,3 +410,61 @@ void LLEventBatchThrottle::setSize(std::size_t size) flush(); } } + +/***************************************************************************** +* LLEventLogProxy +*****************************************************************************/ +LLEventLogProxy::LLEventLogProxy(LLEventPump& source, const std::string& name, bool tweak): + // note: we are NOT using the constructor that implicitly connects! + LLEventFilter(name, tweak), + // instead we simply capture a reference to the subject LLEventPump + mPump(source) +{ +} + +bool LLEventLogProxy::post(const LLSD& event) /* override */ +{ + auto counter = mCounter++; + auto eventplus = event; + if (eventplus.type() == LLSD::TypeMap) + { + eventplus["_cnt"] = counter; + } + std::string hdr{STRINGIZE(getName() << ": post " << counter)}; + LL_INFOS("LogProxy") << hdr << ": " << event << LL_ENDL; + bool result = mPump.post(eventplus); + LL_INFOS("LogProxy") << hdr << " => " << result << LL_ENDL; + return result; +} + +LLBoundListener LLEventLogProxy::listen_impl(const std::string& name, + const LLEventListener& target, + const NameList& after, + const NameList& before) +{ + LL_DEBUGS("LogProxy") << "LLEventLogProxy('" << getName() << "').listen('" + << name << "')" << LL_ENDL; + return mPump.listen(name, + [this, name, target](const LLSD& event)->bool + { return listener(name, target, event); }, + after, + before); +} + +bool LLEventLogProxy::listener(const std::string& name, + const LLEventListener& target, + const LLSD& event) const +{ + auto eventminus = event; + std::string counter{"**"}; + if (eventminus.has("_cnt")) + { + counter = stringize(eventminus["_cnt"].asInteger()); + eventminus.erase("_cnt"); + } + std::string hdr{STRINGIZE(getName() << " to " << name << " " << counter)}; + LL_INFOS("LogProxy") << hdr << ": " << eventminus << LL_ENDL; + bool result = target(eventminus); + LL_INFOS("LogProxy") << hdr << " => " << result << LL_ENDL; + return result; +} diff --git a/indra/llcommon/lleventfilter.h b/indra/llcommon/lleventfilter.h index 8e7c075581..79319353a7 100644 --- a/indra/llcommon/lleventfilter.h +++ b/indra/llcommon/lleventfilter.h @@ -427,4 +427,99 @@ private: const bool mConsume; }; +/***************************************************************************** +* LLEventLogProxy +*****************************************************************************/ +/** + * LLEventLogProxy is a little different than the other LLEventFilter + * subclasses declared in this header file, in that it completely wraps the + * passed LLEventPump (both input and output) instead of simply processing its + * output. Of course, if someone directly posts to the wrapped LLEventPump by + * looking up its string name in LLEventPumps, LLEventLogProxy can't intercept + * that post() call. But as long as consuming code is willing to access the + * LLEventLogProxy instance instead of the wrapped LLEventPump, all event data + * both post()ed and received is logged. + * + * The proxy role means that LLEventLogProxy intercepts more of LLEventPump's + * API than a typical LLEventFilter subclass. + */ +class LLEventLogProxy: public LLEventFilter +{ + typedef LLEventFilter super; +public: + /** + * Construct LLEventLogProxy, wrapping the specified LLEventPump. + * Unlike a typical LLEventFilter subclass, the name parameter is @emph + * not optional because typically you want LLEventLogProxy to completely + * replace the wrapped LLEventPump. So you give the subject LLEventPump + * some other name and give the LLEventLogProxy the name that would have + * been used for the subject LLEventPump. + */ + LLEventLogProxy(LLEventPump& source, const std::string& name, bool tweak=false); + + /// register a new listener + LLBoundListener listen_impl(const std::string& name, const LLEventListener& target, + const NameList& after, const NameList& before); + + /// Post an event to all listeners + virtual bool post(const LLSD& event) /* override */; + +private: + /// This method intercepts each call to any target listener. We pass it + /// the listener name and the caller's intended target listener plus the + /// posted LLSD event. + bool listener(const std::string& name, + const LLEventListener& target, + const LLSD& event) const; + + LLEventPump& mPump; + LLSD::Integer mCounter{0}; +}; + +/** + * LLEventPumpHolder is a helper for LLEventLogProxyFor. It simply + * stores an instance of T, presumably a subclass of LLEventPump. We derive + * LLEventLogProxyFor from LLEventPumpHolder, ensuring that + * LLEventPumpHolder's contained mWrappedPump is fully constructed before + * passing it to LLEventLogProxyFor's LLEventLogProxy base class constructor. + * But since LLEventPumpHolder presents none of the LLEventPump API, + * LLEventLogProxyFor inherits its methods unambiguously from + * LLEventLogProxy. + */ +template +class LLEventPumpHolder +{ +protected: + LLEventPumpHolder(const std::string& name, bool tweak=false): + mWrappedPump(name, tweak) + {} + T mWrappedPump; +}; + +/** + * LLEventLogProxyFor is a wrapper around any of the LLEventPump subclasses. + * Instantiating an LLEventLogProxy instantiates an internal T. Otherwise + * it behaves like LLEventLogProxy. + */ +template +class LLEventLogProxyFor: private LLEventPumpHolder, public LLEventLogProxy +{ + // We derive privately from LLEventPumpHolder because it's an + // implementation detail of LLEventLogProxyFor. The only reason it's a + // base class at all is to guarantee that it's constructed first so we can + // pass it to our LLEventLogProxy base class constructor. + typedef LLEventPumpHolder holder; + typedef LLEventLogProxy super; + +public: + LLEventLogProxyFor(const std::string& name, bool tweak=false): + // our wrapped LLEventPump subclass instance gets a name suffix + // because that's not the LLEventPump we want consumers to obtain when + // they ask LLEventPumps for this name + holder(name + "-", tweak), + // it's our LLEventLogProxy that gets the passed name + super(holder::mWrappedPump, name, tweak) + {} +}; + #endif /* ! defined(LL_LLEVENTFILTER_H) */ diff --git a/indra/llcommon/tests/lleventcoro_test.cpp b/indra/llcommon/tests/lleventcoro_test.cpp index 4e774b27d9..c13920eefd 100644 --- a/indra/llcommon/tests/lleventcoro_test.cpp +++ b/indra/llcommon/tests/lleventcoro_test.cpp @@ -37,12 +37,14 @@ #include #include +#include #include "../test/lltut.h" #include "llsd.h" #include "llsdutil.h" #include "llevents.h" #include "llcoros.h" +#include "lleventfilter.h" #include "lleventcoro.h" #include "../test/debug.h" #include "../test/sync.h" @@ -255,4 +257,80 @@ namespace tut LLCoros::instance().launch("test<5>", [this](){ coroPumpPost(); }); ensure_equals(result.asInteger(), 18); } + + template + void test() + { + PUMP pump(typeid(PUMP).name()); + bool running{false}; + LLSD data{LLSD::emptyArray()}; + // start things off by posting once before even starting the listener + // coro + LL_DEBUGS() << "test() posting first" << LL_ENDL; + LLSD first{LLSDMap("desc", "first")("value", 0)}; + bool consumed = pump.post(first); + ensure("should not have consumed first", ! consumed); + // now launch the coro + LL_DEBUGS() << "test() launching listener coro" << LL_ENDL; + running = true; + LLCoros::instance().launch( + "listener", + [&pump, &running, &data](){ + // important for this test that we consume posted values + LLCoros::instance().set_consuming(true); + // should immediately retrieve 'first' without waiting + LL_DEBUGS() << "listener coro waiting for first" << LL_ENDL; + data.append(llcoro::suspendUntilEventOnWithTimeout(pump, 0.1, LLSD())); + // Don't use ensure() from within the coro -- ensure() failure + // throws tut::fail, which won't propagate out to the main + // test driver, which will result in an odd failure. + // Wait for 'second' because it's not already pending. + LL_DEBUGS() << "listener coro waiting for second" << LL_ENDL; + data.append(llcoro::suspendUntilEventOnWithTimeout(pump, 0.1, LLSD())); + // and wait for 'third', which should involve no further waiting + LL_DEBUGS() << "listener coro waiting for third" << LL_ENDL; + data.append(llcoro::suspendUntilEventOnWithTimeout(pump, 0.1, LLSD())); + LL_DEBUGS() << "listener coro done" << LL_ENDL; + running = false; + }); + // back from coro at the point where it's waiting for 'second' + LL_DEBUGS() << "test() posting second" << LL_ENDL; + LLSD second{llsd::map("desc", "second", "value", 1)}; + consumed = pump.post(second); + ensure("should have consumed second", consumed); + // This is a key point: even though we've post()ed the value for which + // the coroutine is waiting, it's actually still suspended until we + // pause for some other reason. The coroutine will only pick up one + // value at a time from our 'pump'. It's important to exercise the + // case when we post() two values before it picks up either. + LL_DEBUGS() << "test() posting third" << LL_ENDL; + LLSD third{llsd::map("desc", "third", "value", 2)}; + consumed = pump.post(third); + ensure("should NOT yet have consumed third", ! consumed); + // now just wait for coro to finish -- which it eventually will, given + // that all its suspend calls have short timeouts. + while (running) + { + LL_DEBUGS() << "test() waiting for coro done" << LL_ENDL; + llcoro::suspendUntilTimeout(0.1); + } + // okay, verify expected results + ensure_equals("should have received three values", data, + llsd::array(first, second, third)); + LL_DEBUGS() << "test() done" << LL_ENDL; + } + + template<> template<> + void object::test<6>() + { + set_test_name("LLEventMailDrop"); + tut::test(); + } + + template<> template<> + void object::test<7>() + { + set_test_name("LLEventLogProxyFor"); + tut::test< LLEventLogProxyFor >(); + } } diff --git a/indra/llcommon/tests/lleventfilter_test.cpp b/indra/llcommon/tests/lleventfilter_test.cpp index 1875013794..fa2cb03e95 100644 --- a/indra/llcommon/tests/lleventfilter_test.cpp +++ b/indra/llcommon/tests/lleventfilter_test.cpp @@ -36,9 +36,12 @@ // other Linden headers #include "../test/lltut.h" #include "stringize.h" +#include "llsdutil.h" #include "listener.h" #include "tests/wrapllerrs.h" +#include + /***************************************************************************** * Test classes *****************************************************************************/ @@ -401,6 +404,78 @@ namespace tut throttle.post(";17"); ensure_equals("17", cat.result, "136;12;17"); // "17" delivered } + + template + void test() + { + PUMP pump(typeid(PUMP).name()); + LLSD data{LLSD::emptyArray()}; + bool consumed{true}; + // listener that appends to 'data' + // but that also returns the current value of 'consumed' + // Instantiate this separately because we're going to listen() + // multiple times with the same lambda: LLEventMailDrop only replays + // queued events on a new listen() call. + auto lambda = + [&data, &consumed](const LLSD& event)->bool + { + data.append(event); + return consumed; + }; + { + LLTempBoundListener conn = pump.listen("lambda", lambda); + pump.post("first"); + } + // first post() should certainly be received by listener + ensure_equals("first", data, llsd::array("first")); + // the question is, since consumed was true, did it queue the value? + data = LLSD::emptyArray(); + { + // if it queued the value, it would be delivered on subsequent + // listen() call + LLTempBoundListener conn = pump.listen("lambda", lambda); + } + ensure_equals("empty1", data, LLSD::emptyArray()); + data = LLSD::emptyArray(); + // now let's NOT consume the posted data + consumed = false; + { + LLTempBoundListener conn = pump.listen("lambda", lambda); + pump.post("second"); + pump.post("third"); + } + // the two events still arrive + ensure_equals("second,third1", data, llsd::array("second", "third")); + data = LLSD::emptyArray(); + { + // when we reconnect, these should be delivered again + // but this time they should be consumed + consumed = true; + LLTempBoundListener conn = pump.listen("lambda", lambda); + } + // unconsumed events were delivered again + ensure_equals("second,third2", data, llsd::array("second", "third")); + data = LLSD::emptyArray(); + { + // when we reconnect this time, no more unconsumed events + LLTempBoundListener conn = pump.listen("lambda", lambda); + } + ensure_equals("empty2", data, LLSD::emptyArray()); + } + + template<> template<> + void filter_object::test<6>() + { + set_test_name("LLEventMailDrop"); + tut::test(); + } + + template<> template<> + void filter_object::test<7>() + { + set_test_name("LLEventLogProxyFor"); + tut::test< LLEventLogProxyFor >(); + } } // namespace tut /***************************************************************************** -- cgit v1.2.3