Hi Nadav,

Thanks for the review comments. I will clean up the patch and re-submit
tonight.

I found this issue while building OSv images for Nodejs v7.0.0 and 7.2.0.
The GC in Nodejs 7.x does a semaphore wait-for operation that eventually
triggers this issue (/node-7.0.0/deps/v8/src/heap/mark-compact.cc) causing
a nodejs application to die if/when a GC starts:

bool MarkCompactCollector::Sweeper::IsSweepingCompleted() {
  while (pending_sweeper_tasks_semaphore_.WaitFor(
      base::TimeDelta::FromSeconds(0))) {
    num_sweeping_tasks_.Increment(-1);
  }
  return num_sweeping_tasks_.Value() == 0;
}

More replies inline [RG]

On Wednesday, December 7, 2016, Nadav Har'El <[email protected]> wrote:

> Thanks, good catch (I have egg on my face for inserting this oversight in
> the first place...).
> I'm curious, how did you notice this bug? What kind of visible problem did
> it fix? Did it break an actual application?
>
> I have some comments and requests below.
>
> Thanks!
> Nadav.
>
>
> --
> Nadav Har'El
> [email protected]
>
> On Wed, Dec 7, 2016 at 8:51 AM, 'rean' via OSv Development <
> [email protected]> wrote:
>
>> Signed-off-by: rean <[email protected]>
>> ---
>>  core/semaphore.cc           | 23 +++++++++++++++++++++
>>  modules/tests/Makefile      |  2 +-
>>  tests/tst-sem-timed-wait.cc | 50 ++++++++++++++++++++++++++++++
>> +++++++++++++++
>>  3 files changed, 74 insertions(+), 1 deletion(-)
>>  create mode 100644 tests/tst-sem-timed-wait.cc
>>
>> diff --git a/core/semaphore.cc b/core/semaphore.cc
>> index 73732b8..6081a87 100644
>> --- a/core/semaphore.cc
>> +++ b/core/semaphore.cc
>> @@ -46,6 +46,29 @@ bool semaphore::wait(unsigned units, sched::timer* tmr)
>>      sched::thread::wait_until(_mtx,
>>              [&] { return (tmr && tmr->expired()) || !wr.owner; });
>>
>> +    // If wr.owner is not nullptr then there was a timeout (post() did
>> not
>> +    // wake us). In that case, just remove the wait_record (local
>> variable)
>> +    // that we just pushed onto _waiters (via push_back)
>>
>
> An important comment here is that we're holding the lock at this point, so
> can work on the wait list without race with post().
>
> Strangely, I did already write that comment (below)! But there was no code
> after it! I guess I was distracted while submitting that patch :-(
> So your change should probably go after that existing comment.
>
>
>> +    if (wr.owner) {
>> +       auto i = _waiters.begin();
>> +       // _val can be 0 right now, so we can't use the same condition
>> that
>> +       // post_unlocked uses "_val > 0 && i != _waiters.end()"
>>
>
> I don't think this comment is relevant - clearly post_unlocked() does
> something different
> (it needs to loop over all waiters it can wake) while all we need to do
> here is to search
> for a specific record, so we just use an ordinary loop over the waiters
> until we find the
> one we need.
>
[RG] Sure. This comment isn't useful, I'll remove it

>
>
+       while (i != _waiters.end()) {
>> +          auto wait_rec = i++;
>> +          if (wait_rec->owner == sched::thread::current()) {
>>
>
> Well, I guess you can indeed compare the owner like you did, and nothing
> will be wrong,
> but I think a more dire test would be to simply search for wr itself!
> I.e., something like
>
>     if (wait_rec == wr)
> or maybe
>     if (&*wait_rec == wr)
>


> But even more importantly, since we already *have* wr, do we really need
> to search for
> it in list just to remove it from the list?
> I don't remember the details right now (please look into it), but I seem
> to remember that
> the boost-intrusive list we use is a doubly linked list, which means that
> given an item
> that we know must be in the list (wr), we can remove it from the list
> immediately, without
> firs finding it inside the list.
>
> If you can do that, most of the code in your patch becomes redundant, and
> all we need to add is one line of code, erasing wr from the list if
> wr.owner :-)
>

[RG] I will give the direct erase route a try. That would be much simpler!
If it doesn't work I'll leave the search but add a comment that the direct
erase didn't work.


> +             // We found our wait record so remove it from the list.
>> +             // There is no need to wake ourselves or set wr.owner =
>> nullptr
>> +             // we want to remember that we timed out (if post happened
>> then
>> +             // wr.owner would have been set to nullptr)
>> +             if (wait_rec->units <= _val) {
>> +                _val -= wait_rec->units;
>> +             }
>>
>
> I don't think that this if() can ever succeed: consider that every time we
> added units, in post(), we checked all the waiters - including this one, if
> their units <= val. So I don't see how we can wake up without a post() and
> discover val is big enough.
> If I'm correct here, this entire if() - and three of the comment lines
> above it - should be removed?
>
> Note that even if you can explain why this if() case *can* happen, you
> will need to return "true" if you succeeded to take the units, since that
> is what the return boolean means. One way to cause the code the code below
> (which has return !wr.owner) to return "true" is to set wr.owner = nullptr
> here (and of course, if you do that the comment above becomes wrong).
>
> [RG] I wasn't sure whether this if() could succeed either. Since it seems
unlikely I'll remove it.

> +             // Remove our wait_record
>
This comment is redundant (a few lines above, you explained that you are
> removing our wait record from the list)
>
> [RG] I'll remove this comment

> +             _waiters.erase(wait_rec);
>> +          }
>> +       }
>>
> +    }
>> +
>>      // if wr.owner, it's a timeout - post() didn't wake us and didn't
>> decrease
>>      // the semaphore's value for us. Note we are holding the mutex, so
>> there
>>      // can be no race with post().
>> diff --git a/modules/tests/Makefile b/modules/tests/Makefile
>> index fb8d001..6e3aea7 100644
>> --- a/modules/tests/Makefile
>> +++ b/modules/tests/Makefile
>> @@ -84,7 +84,7 @@ tests := tst-pthread.so misc-ramdisk.so tst-vblk.so
>> tst-bsd-evh.so \
>>         tst-namespace.so tst-without-namespace.so payload-env.so \
>>         payload-merge-env.so misc-execve.so misc-execve-payload.so
>> misc-mutex2.so \
>>         tst-pthread-setcancelstate.so tst-syscall.so tst-pin.so
>> tst-run.so \
>> -       tst-ifaddrs.so tst-pthread-affinity-inherit.so
>> +       tst-ifaddrs.so tst-pthread-affinity-inherit.so
>> tst-sem-timed-wait.so
>>
>>  #      libstatic-thread-variable.so tst-static-thread-variable.so \
>>
>> diff --git a/tests/tst-sem-timed-wait.cc b/tests/tst-sem-timed-wait.cc
>> new file mode 100644
>> index 0000000..f401ed0
>> --- /dev/null
>> +++ b/tests/tst-sem-timed-wait.cc
>> @@ -0,0 +1,50 @@
>> +#include <osv/semaphore.hh>
>> +#include <stdio.h>
>> +#include <stdbool.h>
>> +#include <unistd.h>
>> +#include <errno.h>
>> +
>> +unsigned int tests_total = 0, tests_failed = 0;
>> +
>> +void report(const char* name, bool passed)
>> +{
>> +   static const char* status[] = {"FAIL", "PASS"};
>> +   printf("%s: %s\n", status[passed], name);
>> +   tests_total += 1;
>> +   tests_failed += !passed;
>> +}
>> +
>> +int main(void)
>> +{
>> +   printf("Starting sem_timed_wait test\n");
>> +
>> +   // Basic flow for test
>> +   // 1) Create a semaphore (initialized to 0)
>> +   // 2) Do a timed-wait on it
>> +   // 3) We're never signaled/woken so our stack-allocated wait_record
>> remains
>> +   // on the semaphore's waiters list
>> +   //
>> +   // In the failure case the end result is a stacktrace that looks like:
>> +   // Assertion failed: !hook.is_linked()
>> +   //(/usr/include/boost/intrusive/detail/generic_hook.hpp:
>> destructor_impl: 47)
>> +
>> +   //[backtrace]
>> +   //0x0000000000225a48 <__assert_fail+24>
>> +   //0x00000000003c40e9 <???+3948777>
>> +   //0x00000000003c4242 <semaphore::wait(unsigned int, sched::timer*)+98>
>> +   //0x0000100000c01057 <???+12587095>
>> +
>> +   semaphore sem(0);
>> +   timespec ts;
>> +   ts.tv_sec = 0;
>> +   ts.tv_nsec = 0;
>>
>
> You didn't need to create this "timespec" variable... You can just use
> use std::chrono::seconds(0) below and that's it - you don't even need to
> add 0 nanoseconds to it, as it obviously adds nothing.
>
[RG] I will give that a tr

> +
>> +   sched::timer tmr(*sched::thread::current());
>> +   osv::clock::wall::time_point time(std::chrono::seconds(ts.tv_sec) +
>> +                                      std::chrono::nanoseconds(ts.tv
>> _nsec));
>>
>
> Nitpick: I'm not sure you meant to use wall::time_point here, I am
> guessing you actually meant a wall::duration here - so you'll get a timeout
> in 0 seconds - not a timeout in the absolute date 0 (which means 1970). But
> what you've done here isn't wrong either - it just means you want a timeout
> in the long past (so it will timeout immediately). It's just strange :-)
> Anyway, feel free not to change.
>
>
[RG] I'll look at wall::time_point. I just wanted to have an immediate
timeout.

> +   tmr.set(time);
>> +   bool ret_val = sem.wait(1, &tmr);
>> +   report("sem_timedwait\0", ret_val == false);
>> +   printf("SUMMARY: %u tests / %u failures\n", tests_total,
>> tests_failed);
>> +   return tests_failed == 0 ? 0 : 1;
>> +}
>> --
>> 2.7.4
>>
>> thanks,
Rean

> --
>> You received this message because you are subscribed to the Google Groups
>> "OSv Development" group.
>> To unsubscribe from this group and stop receiving emails from it, send an
>> email to [email protected].
>> For more options, visit https://groups.google.com/d/optout.
>>
>
>

-- 
You received this message because you are subscribed to the Google Groups "OSv 
Development" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
For more options, visit https://groups.google.com/d/optout.

Reply via email to