diff --git a/indra/llcommon/llcond.cpp b/indra/llcommon/llcond.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d5362a48fc374b2e2fcf14c520203b9e7b07ba4f
--- /dev/null
+++ b/indra/llcommon/llcond.cpp
@@ -0,0 +1,111 @@
+/**
+ * @file   llcond.cpp
+ * @author Nat Goodspeed
+ * @date   2019-07-17
+ * @brief  Implementation for llcond.
+ * 
+ * $LicenseInfo:firstyear=2019&license=viewerlgpl$
+ * Copyright (c) 2019, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "llcond.h"
+// STL headers
+// std headers
+// external library headers
+// other Linden headers
+
+namespace // anonymous
+{
+
+// See comments in LLCond::convert(const LLDate&) below
+std::time_t compute_lldate_epoch()
+{
+    LLDate lldate_epoch;
+    std::tm tm;
+    // It should be noted that calling LLDate::split() to write directly
+    // into a std::tm struct depends on S32 being a typedef for int in
+    // stdtypes.h: split() takes S32*, whereas tm fields are documented to
+    // be int. If you get compile errors here, somebody changed the
+    // definition of S32. You'll have to declare six S32 variables,
+    // split() into them, then assign them into the relevant tm fields.
+    if (! lldate_epoch.split(&tm.tm_year, &tm.tm_mon, &tm.tm_mday,
+                             &tm.tm_hour, &tm.tm_min, &tm.tm_sec))
+    {
+        // Theoretically split() could return false. In that case, we
+        // don't have valid data, so we can't compute offset, so skip the
+        // rest of this.
+        return 0;
+    }
+
+    tm.tm_isdst = 0;
+    std::time_t lldate_epoch_time = std::mktime(&tm);
+    if (lldate_epoch_time == -1)
+    {
+        // Theoretically mktime() could return -1, meaning that the contents
+        // of the passed std::tm cannot be represented as a time_t. (Worrisome
+        // if LLDate's epoch happened to be exactly 1 tick before
+        // std::time_t's epoch...)
+        // In the error case, assume offset 0.
+        return 0;
+    }
+
+    // But if we got this far, lldate_epoch_time is the time_t we want.
+    return lldate_epoch_time;
+}
+
+} // anonymous namespace
+
+// convert LLDate to a chrono::time_point
+std::chrono::system_clock::time_point LLCond::convert(const LLDate& lldate)
+{
+    // std::chrono::system_clock's epoch MAY be the Unix epoch, namely
+    // midnight UTC on 1970-01-01, in fact it probably is. But until C++20,
+    // system_clock does not guarantee that. Unfortunately time_t doesn't
+    // specify its epoch either, other than to note that it "almost always" is
+    // the Unix epoch (https://en.cppreference.com/w/cpp/chrono/c/time_t).
+    // LLDate, being based on apr_time_t, does guarantee 1970-01-01T00:00 UTC.
+    // http://apr.apache.org/docs/apr/1.5/group__apr__time.html#gadb4bde16055748190eae190c55aa02bb
+
+    // The easy, efficient conversion would be
+    // std::chrono::system_clock::from_time_t(std::time_t(LLDate::secondsSinceEpoch())).
+    // But that assumes that both time_t and system_clock have the same epoch
+    // as LLDate -- an assumption that will work until it unexpectedly doesn't.
+
+    // It would be more formally correct to break out the year, month, day,
+    // hour, minute, second (UTC) using LLDate::split() and recombine them
+    // into std::time_t using std::mktime(). However, both split() and
+    // mktime() have integer second granularity, whereas callers of
+    // wait_until() are very likely to be interested in sub-second precision.
+    // In that sense std::chrono::system_clock::from_time_t() is still
+    // preferred.
+
+    // So use the split() / mktime() mechanism to determine the numeric value
+    // of the LLDate / apr_time_t epoch as expressed in time_t. (We assume
+    // that the epoch offset can be expressed as integer seconds, per split()
+    // and mktime(), which seems plausible.)
+
+    // n.b. A function-static variable is initialized only once in a
+    // thread-safe way.
+    static std::time_t lldate_epoch_time = compute_lldate_epoch();
+
+    // LLDate::secondsSinceEpoch() gets us, of course, how long it has
+    // been since lldate_epoch_time. So adding lldate_epoch_time should
+    // give us the correct time_t representation of a given LLDate even if
+    // time_t's epoch differs from LLDate's.
+    // We don't have to worry about the relative epochs of time_t and
+    // system_clock because from_time_t() takes care of that!
+    return std::chrono::system_clock::from_time_t(lldate_epoch_time +
+                                                  lldate.secondsSinceEpoch());
+}
+
+// convert F32Milliseconds to a chrono::duration
+std::chrono::milliseconds LLCond::convert(F32Milliseconds)
+{
+    // extract the F32 milliseconds from F32Milliseconds, construct
+    // std::chrono::milliseconds from that value
+    return std::chrono::milliseconds(timeout_duration.value());
+}
diff --git a/indra/llcommon/llcond.h b/indra/llcommon/llcond.h
index adfeb27f82662927005dde84f54c6b8881250824..5ed9f101233759ff3735e904b07322bed5e56878 100644
--- a/indra/llcommon/llcond.h
+++ b/indra/llcommon/llcond.h
@@ -14,6 +14,8 @@
 #if ! defined(LL_LLCOND_H)
 #define LL_LLCOND_H
 
+#include "llunits.h"
+#include "lldate.h"
 #include <boost/fiber/condition_variable.hpp>
 #include <mutex>
 #include <chrono>
@@ -37,9 +39,12 @@
 template <typename DATA>
 class LLCond
 {
+public:
+    typedef value_type DATA;
+
 private:
     // This is the DATA controlled by the condition_variable.
-    DATA mData;
+    value_type mData;
     // condition_variable must be used in conjunction with a mutex. Use
     // boost::fibers::mutex instead of std::mutex because the latter blocks
     // the entire calling thread, whereas the former blocks only the current
@@ -52,7 +57,7 @@ class LLCond
 public:
     /// LLCond can be explicitly initialized with a specific value for mData if
     /// desired.
-    LLCond(DATA&& init=DATA()):
+    LLCond(value_type&& init=value_type()):
         mData(init)
     {}
 
@@ -63,7 +68,7 @@ class LLCond
     /// get() returns a const reference to the stored DATA. The only way to
     /// get a non-const reference -- to modify the stored DATA -- is via
     /// update_one() or update_all().
-    const DATA& get() const { return mData; }
+    const value_type& get() const { return mData; }
 
     /**
      * Pass update_one() an invocable accepting non-const (DATA&). The
@@ -122,7 +127,7 @@ class LLCond
         // But what if they instead pass a predicate accepting non-const
         // (DATA&)? Such a predicate could modify mData, which would be Bad.
         // Forbid that.
-        while (! pred(const_cast<const DATA&>(mData)))
+        while (! pred(const_cast<const value_type&>(mData)))
         {
             mCond.wait(lk);
         }
@@ -143,19 +148,29 @@ class LLCond
     {
         std::unique_lock<boost::fibers::mutex> lk(mMutex);
         // see wait() for comments about this const_cast
-        while (! pred(const_cast<const DATA&>(mData)))
+        while (! pred(const_cast<const value_type&>(mData)))
         {
             if (boost::fibers::cv_status::timeout == mCond.wait_until(lk, timeout_time))
             {
                 // It's possible that wait_until() timed out AND the predicate
                 // became true more or less simultaneously. Even though
                 // wait_until() timed out, check the predicate one more time.
-                return pred(const_cast<const DATA&>(mData));
+                return pred(const_cast<const value_type&>(mData));
             }
         }
         return true;
     }
 
+    /**
+     * This wait_until() overload accepts LLDate as the time_point. Its
+     * semantics are the same as the generic wait_until() method.
+     */
+    template <typename Pred>
+    bool wait_until(const LLDate& timeout_time, Pred pred)
+    {
+        return wait_until(convert(timeout_time), pred);
+    }
+
     /**
      * Pass wait_for() a chrono::duration, indicating how long we're willing
      * to wait, and a predicate accepting (const DATA&), returning bool. The
@@ -178,6 +193,24 @@ class LLCond
         // stick to it.
         return wait_until(std::chrono::steady_clock::now() + timeout_duration, pred);
     }
+
+    /**
+     * This wait_for() overload accepts F32Milliseconds as the duration. Any
+     * duration unit defined in llunits.h is implicitly convertible to
+     * F32Milliseconds. The semantics of this method are the same as the
+     * generic wait_for() method.
+     */
+    template <typename Pred>
+    bool wait_for(F32Milliseconds timeout_duration, Pred pred)
+    {
+        return wait_for(convert(timeout_duration), pred);
+    }
+
+protected:
+    // convert LLDate to a chrono::time_point
+    std::chrono::system_clock::time_point convert(const LLDate&);
+    // convert F32Milliseconds to a chrono::duration
+    std::chrono::milliseconds convert(F32Milliseconds);
 };
 
 template <typename DATA>
@@ -186,26 +219,32 @@ class LLScalarCond: public LLCond<DATA>
     using super = LLCond<DATA>;
 
 public:
+    using super::value_type;
+    using super::get;
+    using super::wait;
+    using super::wait_until;
+    using super::wait_for;
+
     /// LLScalarCond can be explicitly initialized with a specific value for
     /// mData if desired.
-    LLCond(DATA&& init=DATA()):
+    LLCond(value_type&& init=value_type()):
         super(init)
     {}
 
     /// Pass set_one() a new value to which to update mData. set_one() will
     /// lock the mutex, update mData and then call notify_one() on the
     /// condition_variable.
-    void set_one(DATA&& value)
+    void set_one(value_type&& value)
     {
-        super::update_one([](DATA& data){ data = value; });
+        super::update_one([](value_type& data){ data = value; });
     }
 
     /// Pass set_all() a new value to which to update mData. set_all() will
     /// lock the mutex, update mData and then call notify_all() on the
     /// condition_variable.
-    void set_all(DATA&& value)
+    void set_all(value_type&& value)
     {
-        super::update_all([](DATA& data){ data = value; });
+        super::update_all([](value_type& data){ data = value; });
     }
 
     /**
@@ -213,9 +252,9 @@ class LLScalarCond: public LLCond<DATA>
      * mutex and, until the stored DATA equals that value, calls wait() on the
      * condition_variable.
      */
-    void wait_equal(const DATA& value)
+    void wait_equal(const value_type& value)
     {
-        super::wait([&value](const DATA& data){ return (data == value); });
+        super::wait([&value](const value_type& data){ return (data == value); });
     }
 
     /**
@@ -228,10 +267,19 @@ class LLScalarCond: public LLCond<DATA>
      */
     template <typename Clock, typename Duration>
     bool wait_until_equal(const std::chrono::time_point<Clock, Duration>& timeout_time,
-                          const DATA& value)
+                          const value_type& value)
     {
         return super::wait_until(timeout_time,
-                                 [&value](const DATA& data){ return (data == value); });
+                                 [&value](const value_type& data){ return (data == value); });
+    }
+
+    /**
+     * This wait_until_equal() overload accepts LLDate as the time_point. Its
+     * semantics are the same as the generic wait_until_equal() method.
+     */
+    bool wait_until_equal(const LLDate& timeout_time, const value_type& value)
+    {
+        return wait_until_equal(super::convert(timeout_time), value);
     }
 
     /**
@@ -244,10 +292,21 @@ class LLScalarCond: public LLCond<DATA>
      */
     template <typename Rep, typename Period>
     bool wait_for_equal(const std::chrono::duration<Rep, Period>& timeout_duration,
-                        const DATA& value)
+                        const value_type& value)
     {
         return super::wait_for(timeout_duration,
-                               [&value](const DATA& data){ return (data == value); });
+                               [&value](const value_type& data){ return (data == value); });
+    }
+
+    /**
+     * This wait_for_equal() overload accepts F32Milliseconds as the duration.
+     * Any duration unit defined in llunits.h is implicitly convertible to
+     * F32Milliseconds. The semantics of this method are the same as the
+     * generic wait_for_equal() method.
+     */
+    bool wait_for_equal(F32Milliseconds timeout_duration, const value_type& value)
+    {
+        return wait_for_equal(super::convert(timeout_duration), value);
     }
 
     /**
@@ -255,9 +314,9 @@ class LLScalarCond: public LLCond<DATA>
      * locks the mutex and, until the stored DATA no longer equals that value,
      * calls wait() on the condition_variable.
      */
-    void wait_unequal(const DATA& value)
+    void wait_unequal(const value_type& value)
     {
-        super::wait([&value](const DATA& data){ return (data != value); });
+        super::wait([&value](const value_type& data){ return (data != value); });
     }
 
     /**
@@ -270,10 +329,19 @@ class LLScalarCond: public LLCond<DATA>
      */
     template <typename Clock, typename Duration>
     bool wait_until_unequal(const std::chrono::time_point<Clock, Duration>& timeout_time,
-                          const DATA& value)
+                            const value_type& value)
     {
         return super::wait_until(timeout_time,
-                                 [&value](const DATA& data){ return (data != value); });
+                                 [&value](const value_type& data){ return (data != value); });
+    }
+
+    /**
+     * This wait_until_unequal() overload accepts LLDate as the time_point.
+     * Its semantics are the same as the generic wait_until_unequal() method.
+     */
+    bool wait_until_unequal(const LLDate& timeout_time, const value_type& value)
+    {
+        return wait_until_unequal(super::convert(timeout_time), value);
     }
 
     /**
@@ -286,22 +354,48 @@ class LLScalarCond: public LLCond<DATA>
      */
     template <typename Rep, typename Period>
     bool wait_for_unequal(const std::chrono::duration<Rep, Period>& timeout_duration,
-                        const DATA& value)
+                          const value_type& value)
     {
         return super::wait_for(timeout_duration,
-                               [&value](const DATA& data){ return (data != value); });
+                               [&value](const value_type& data){ return (data != value); });
+    }
+
+    /**
+     * This wait_for_unequal() overload accepts F32Milliseconds as the duration.
+     * Any duration unit defined in llunits.h is implicitly convertible to
+     * F32Milliseconds. The semantics of this method are the same as the
+     * generic wait_for_unequal() method.
+     */
+    bool wait_for_unequal(F32Milliseconds timeout_duration, const value_type& value)
+    {
+        return wait_for_unequal(super::convert(timeout_duration), value);
     }
+
+protected:
+    using super::convert;
 };
 
 /// Using bool as LLScalarCond's DATA seems like a particularly useful case
 using LLBoolCond = LLScalarCond<bool>;
 
-// LLOneShotCond -- init false, set (and wait for) true? Or full suite?
+/// LLOneShotCond -- init false, set (and wait for) true
 class LLOneShotCond: public LLBoolCond
 {
     using super = LLBoolCond;
 
 public:
+    using super::value_type;
+    using super::get;
+    using super::wait;
+    using super::wait_until;
+    using super::wait_for;
+    using super::wait_equal;
+    using super::wait_until_equal;
+    using super::wait_for_equal;
+    using super::wait_unequal;
+    using super::wait_until_unequal;
+    using super::wait_for_unequal;
+
     /// The bool stored in LLOneShotCond is initially false
     LLOneShotCond(): super(false) {}
 
@@ -323,7 +417,7 @@ class LLOneShotCond: public LLBoolCond
      */
     void wait()
     {
-        super::wait_equal(true);
+        super::wait_unequal(false);
     }
 
     /**
@@ -336,7 +430,16 @@ class LLOneShotCond: public LLBoolCond
     template <typename Clock, typename Duration>
     bool wait_until(const std::chrono::time_point<Clock, Duration>& timeout_time)
     {
-        return super::wait_until_equal(timeout_time, true);
+        return super::wait_until_unequal(timeout_time, false);
+    }
+
+    /**
+     * This wait_until() overload accepts LLDate as the time_point.
+     * Its semantics are the same as the generic wait_until() method.
+     */
+    bool wait_until(const LLDate& timeout_time)
+    {
+        return wait_until(super::convert(timeout_time));
     }
 
     /**
@@ -349,7 +452,18 @@ class LLOneShotCond: public LLBoolCond
     template <typename Rep, typename Period>
     bool wait_for(const std::chrono::duration<Rep, Period>& timeout_duration)
     {
-        return super::wait_for_equal(timeout_duration, true);
+        return super::wait_for_unequal(timeout_duration, false);
+    }
+
+    /**
+     * This wait_for() overload accepts F32Milliseconds as the duration.
+     * Any duration unit defined in llunits.h is implicitly convertible to
+     * F32Milliseconds. The semantics of this method are the same as the
+     * generic wait_for() method.
+     */
+    bool wait_for(F32Milliseconds timeout_duration)
+    {
+        return wait_for(super::convert(timeout_duration));
     }
 };