BIND 10 #2831: define and implement MemorySegmentMapped

BIND 10 Development do-not-reply at isc.org
Wed Apr 10 21:41:38 UTC 2013


#2831: define and implement MemorySegmentMapped
-------------------------------------+-------------------------------------
            Reporter:  jinmei        |                        Owner:
                Type:  task          |  jinmei
            Priority:  medium        |                       Status:
           Component:  data source   |  reviewing
            Keywords:                |                    Milestone:
           Sensitive:  0             |  Sprint-20130423
         Sub-Project:  DNS           |                   Resolution:
Estimated Difficulty:  5             |                 CVSS Scoring:
         Total Hours:  0             |              Defect Severity:  N/A
                                     |  Feature Depending on Ticket:
                                     |  shared memory data source
                                     |          Add Hours to Ticket:  0
                                     |                    Internal?:  0
-------------------------------------+-------------------------------------

Comment (by jinmei):

 Replying to [comment:13 muks]:

 > Apologies for the delayed review. Many factors took away time.

 No problem (this hasn't been a critically blocking task), thanks for
 the careful read and detailed review.

 Before answering the comments: I realized Boost managed_mapped_file
 could cause various build time issues: depending on the combination of
 Boost version, type of compiler (and maybe its version), and the
 system, it may be `-Werror` unfriendly or in the worst case (Solaris +
 !SunStudio, as usual) doesn't seem to compile at all.  So far, the
 only critical failure seemed to be the case for sunstudio, but I had
 to give up making it compile after trying several things.  Since the
 shared memory support is a kind of optional feature for limited large
 scale environments, my suggest at this moment is to introduce a
 ./configure knob so we can disable building it completely.

 I updated confiugre.ac and our m4 macro for boost to make it possible,
 with other portability tricks such as auto-detecting and suppressing
 `-Werror` regression.

 These changes are independent from other changes of the branch, so if
 it's more convenient for you, you can review it separately.  The
 changes are currently committed at the end of the branch, from e01ee17
 to ba0d613 (inclusive).

 > * Let's look at the following API doc comment of `setNamedAddress()`:
 [...]
 > It seems like this restriction can be enforced in the API itself to
 > prevent programming mistakes, by having a single static method that
 > allocates a memory buffer with an associated name. Deallocating can also
 > be done similarly by passing the name string without requiring the
 > (perhaps relocated) addresses.

 Unfortunately, this wouldn't always be the case in one of intended use
 cases (for our scheduled shmem data source work - see #2836).  I've
 added a note on this in the API doc.

 > * The API doc comment of `allocate()`:
 > {{{
 >     /// Depending on the implementation details, it may have to grow the
 >     /// internal memory segment (again, in an implementation dependent
 way)
 >     /// to allocate the required size of memory.  In that case the
 >     /// implementation must grow the internal segment sufficiently so
 the
 >     /// next call to allocate() for the same size will succeed, and
 throw
 >     /// a \c MemorySegmentGrown exception (not really allocating the
 memory
 >     /// yet).
 >     ///
 >     /// An application that uses this memory segment abstraction to
 allocate
 >     /// memory should expect this exception, and should normally catch
 it
 >     /// at an appropriate layer (which may be immediately after a call
 to
 >     /// \c allocate() or a bit higher layer).  It should interpret the
 >     /// exception as any raw address that belongs to the segment may
 have
 >     /// been remapped and must be re-fetched via an already established
 >     /// named address using the \c getNamedAddress() method.
 >     ///
 > }}}
 >
 > Because `MemorySegmentGrown` is thrown and no memory is allocated
 > according to the API doc, should the caller call `allocate()` again to
 > allocate the memory?

 Yes.

 > I am trying to follow the design behind why `MemorySegmentGrown` is an
 > exception, and not a value returned as the method's return value, or in
 > a method argument. It is not an exceptional situation that should be
 > allowed to unwind the stack all the way upwards, even if by mistake.
 > `allocate()` can return `NULL` and a `bool` in an argument this case,
 > which the caller can use and pass on even to its caller.

 Fair question.  I tried to clarify that in doc.  The specific
 intended usage is, again, #2836.  I admit the interface and usage
 is quite tricky, though.  If there's a better way to handle
 intermediate segment extension in the context of #2836, I'm happy to
 adopt it instead of the currently implemented approach.

 > It also seems inconsistent that `setNamedAddress()` doesn't throw if it
 > allocates memory, which may also cause addresses previously returned by
 > `allocate()` to be relocated (similar to when `allocate()` throws
 > `MemorySegmentGrown`).

 Ah, good catch.  I was actually not aware of the inconsistency, but
 the difference was intentional: `setNamedAddress()` is generally
 expected to be independently, not as part of a set of allocations.
 I clarified it in the doc (10d88f0).

 > * The `setNamedAddress()` API doc does not describe what happens if
 >   `NULL` is passed in the `addr` argument. If `NULL` will clear the
 >   association, then `clearNamedAddress()` would be redundant (we should
 >   not have to support two different ways of clearing the
 >   association). If indeed `NULL` can be associated with a name, it
 >   should be documented (with what a sample use-case may be for storing
 >   `NULL`).

 Hmm, I thought it was at least clear NULL is allowed, but I tried to
 explain more details about what it would mean.

 > * In the API doc of `MemorySegmentMapped()`, is it assumed that if
 >   `create` argument is true, then any existing file will be overwritten
 >   (truncated)?  If this is not the case, can the `create` argument be
 >   removed, such that this read+write version of `MemorySegmentMapped()`
 >   auto-creates the file if it doesn't exist, or opens it if it does
 >   exist?

 Not a direct answer to these questions, but I realized we'd also need
 a "create only" mode for our later work, so I revised the interface
 quite substantially.  Please check the new `OpenMode` enum, revised
 constructor, and their documentation.

 > * Is there a reason why you use 0 instead of `NULL` in a lot of places,
 >   including unittests? (They are equivalent, but `NULL` is a gentle
 >   reminder that it's a pointer).
 > {{{
 > void*
 > MemorySegmentLocal::getNamedAddress(const char* name) {
 >     std::map<std::string, void*>::iterator found =
 named_addrs_.find(name);
 >     if (found != named_addrs_.end()) {
 >         return (found->second);
 >     }
 >     return (0);
 > }
 > }}}

 Please see http://bind10.isc.org/ticket/2833#comment:16 (search for
 "NULL") and the followup discussion.  I've reverted to NULL for this
 branch, too.

 > * I think this public variable's definition should go into a `.cc` file:
 > {{{
 >     /// \brief The default value of the mapped file size when newly
 created.
 >     ///
 >     /// Its value, 32KB, is an arbitrary choice, but considered to be
 >     /// sufficiently but not too large.
 >     static const size_t INITIAL_SIZE = 32768;
 > }}}

 Good catch, done.

 > * Can the following comment be removed from
 > `MemorySegmentMapped::setNamedAddress()`? It seems weird reading it by
 > itself (it is as if by default the base `MemorySegment` interface
 > doesn't expect `true` to be returned):
 > {{{
 >     /// This version can actually return true.
 >     ///
 > }}}

 I can do it, but I thought it's useful to add note specific
 differences of specific implementations (of course the caller
 shouldn't assume the derived-class specific behavior when it uses the
 abstract interface).  I updated the documentation in that sense
 at b8d905c.  But if you think it's still rather confusing, I'm okay
 with removing it.

 > * `getNamedAddress()` has different behavior in the different
 > implementations with `name=NULL`.  If we don't want to support
 > `name=NULL` , it should be documented in the
 > `MemorySegment::getNamedAddress()` base class interface.
 [...]
 > * Same in `clearNamedAddress()` too.

 Okay.  In general, I'd prefer explicitly rejecting invalid (or
 unexpected) parameters, so I revised the implementation and
 documentation that way.

 And also for `setNamedAddress`.

 > * The exception message is incorrect:
 > {{{
 > void
 > MemorySegmentMapped::deallocate(void* ptr, size_t) {
 >     if (impl_->read_only_) {
 >         isc_throw(InvalidOperation, "allocate attempt on read-only
 segment");
 >     }
 >
 > }}}

 Good catch, corrected.

 > * What are the conditions under which this assertion is triggered? i.e.,
 >   how did you arrive at this `INITIAL_SIZE` in the condition
 >   expression?
 > {{{
 >     // It appears an assertion failure is triggered within Boost if the
 size
 >     // is too small.  To work this around we'll make it no-op if the
 size is
 >     // already reasonably small.
 >     if (getSize() < INITIAL_SIZE) {
 >         return;
 >     }
 > }}}

 I found it through our unit tests (if I disabled the above 'if', one
 of the unit tests triggered the assertion failure).  I tried to
 clarify the issue in more detail as a code comment.

 > * Can something like `MappedFile` be used as the type name instead of
 >   `BaseSegment`? The method calls on `base_sgmt_` seem to match a file
 >   naming.

 I don't understand the second sentence...anyway, I'm okay with
 renaming it, but boost::basic_managed_mapped_file actually comes from
 the "Managed Memory Segments" abstraction
 (see
 http://www.boost.org/doc/libs/1_53_0/doc/html/interprocess/managed_memory_segments.html),
 rather than file abstraction, so I prefer using the term "memory"
 and/or "segment".

 If `BaseSegment` sounds confusing to you, I propose, e.g.,
 `BoostSegment` and `boost_sgmt_`.  At the moment I didn't change the
 code.

 > * A general observation about API doc comments, which applies to the
 >   rest of BIND 10 API doc too: differently written comments seem to be
 >   used to say the same thing about exceptions:
 >
 > {{{
 >     /// This version never throws.
 > }}}
 >
 > and
 >
 > {{{
 >     /// \throw None
 > }}}
 >
 > Documentation about exceptions that can be thrown:
 [...]
 > Shall we use `\throw` in all cases to be consistent? It would be easier
 > when we get around to publishing such API documentation for the BIND 10
 > libraries.

 As a general matter I tend to agree (at least it's better to be
 consistent, and use of `\throw` would be the best choice), but
 regarding this one:
 {{{
     /// This version never throws.
 }}}
 it's rather implementation note than part of API spec (Basically, as
 API the base class description should be referenced).

 For this branch, I've added some more `\throw`s where they seem
 appropriate.  I also changed some of `InvalidOperation` cases to
 `MemorySegmentError` as it should be basically generic enough at the
 base class level.

 > * This is not very important, but I wanted to suggest using XOR instead
 >   of addition in the checksum computation. I checked about this to make
 >   sure before suggesting it, and it seems that for 2-bit errors,
 >   addition has a slightly better probability of generating a different
 >   checksum. So addition is good in this case.

 This checksum is basically quite poor anyway in terms of integrity
 check, so the difference in that sense is subtle anyway.  Nevertheless
 I'm okay to switch to any other algorithm as long as it's lightweight,
 but as it seems there's no reason to change it for now, I've kept it.

 > * A test should be added to verify that previous name/address
 >   associations continue to exist and are still usable after
 >   `shrinkToFit()` is called, and previously allocated data exists and is
 >   the same. For these names and data, I suggest using random sizes and
 >   random data, a list of which you can save in `vector`s for matching
 >   after the shrinking is performed.

 Okay, added these cases to the `namedAddress` test.

 > * For the above case, we should also test the case where the mapping
 >   changes (somehow force it to happen). In this case, we test that
 >   `getNamedAddress()` returns the updated address and the data at that
 >   address is consistent with what we expect.

 Also added it to the `namedAddress` test.

 > * Is it not possible to perform `shrink_to_fit()` when the `BaseSegment`
 >   object is still alive (and the mapped file is open)? The documentation
 >   doesn't seem to say anything for or against it. Same question applies
 >   to `grow()` used in `growSegment()` too.

 Do you mean whether the underlying Boost library allows that (and
 whether we really need reset `BaseSegment` before grow/shrink)?  If
 so, I thought Boost doesn't allow it, but, hmm, according to
 http://www.boost.org/doc/libs/1_53_0/doc/html/boost/interprocess/basic_managed_mapped_file.html
 it may not so be clear.  It could depend on what reading/writing
 means.  But from an additional quick experiment, if we keep
 `BaseSegment` and perform grow(), a subsequently call to allocate()
 caused crash, so I guess we cannot really assume it works that way.
 I suspect that's the same for shrink.

 ...but are we on the same page, or are you asking something different?

 > * Is it necessary to call `flush()` in `growSegment()`? If so, isn't it
 >   necessary to do this in `shrinkToFit()` also?

 Ah, no, it shouldn't be necessary any more because the destructor
 calls flush().  I guess grow had it before I extended the destructor.
 Now removed.

 > * I don't see any fork tests for interprocess testing. Please add these,
 >   as all of `MemorySegmentMapped`'s methods and the allocated data ought
 >   to be tested to function properly from independent process address
 >   spaces with a shared map file. This is our use-case.

 Okay, added.

 > * We should also have tests for write behavior.. i.e., are writes to
 >   memory reflected in the maps from other reader processes before the
 >   writer destroys its segment? Are they when the writer destroys its
 >   segment?

 These don't work, or more accurately, we cannot guarantee that.  Our
 readers open the mapped memory in read-only mode, which internally
 means they use it as a private memory space.  In this case it's not
 even guaranteed that modifications to the original file is ever
 reflected to reader's memory (different OSes showed different
 behaviors in my previous experiments).  We basically don't allow
 mixture of writer and readers (and that's the responsibility of the
 implementor and operator).

 > * In `checkSegmentNamedAddress()`, why the `memcpy()` instead of just
 >   `static_cast`ing it to an `uint32_t*` and assigning it? Similarly in
 >   `MemorySegmentMappedTest.namedAddress` too.

 hmm, I don't remember why I used memcpy, but at least I guess I wanted
 to avoid the patch you showed in that it repeats the hardcoded
 constants.  Still, however, it doesn't mean it should memcpy.  I
 revised the code using static_cast.

 > * I don't understand this assertion in `growSegment()`:
 > {{{
 >         const size_t new_size = prev_size * 2;
 >       assert(new_size != 0); // assume grow fails before size overflow
 > }}}

 I've revised a comment to clarify the intent at commit da33bc4.  Does
 that answer your question?

 > * Is this guaranteed by Boost?
 > {{{
 >     // Another shrink shouldn't cause disruption, and the size shouldn't
 change
 >     segment_->shrinkToFit();
 >     EXPECT_EQ(shrinked_size, segment_->getSize());
 > }}}

 Boost doesn't guarantee it, but our wrapper (basically) does.  See the
 discussion of "perform `shrink_to_fit()` with `BaseSegment` still
 alive" above.

 > * If `initial_size` is passed as 0 to the constructor, Boost (or
 >   something else under it) throws a library error. Also 0 passed to
 >   `allocate()` returns a non-NULL pointer. Not sure if we should bother
 >   to add lines to the API doc about these extreme cases, whether they
 >   are allowed or not, or assertions.

 As for the initial_size, Boost rejects it if it's too small because it
 internally needs to allocate some management data structures.  But its
 size is not public information (or even the existence of such
 restriction is not publicly described), so we cannot predict the
 exact threshold.  it should be safe to assume 0 will be always
 rejected, so I added a test case for it and added notes about it in
 the documentation.

 Regarding allocate() and deallocate(), I think it's okay to say
 nothing.  After all, the user shouldn't use the return value anyway,
 and as long as deallocate() works for any pointer value returned by
 allocate(), it's not really special.

 > * Though it is an advisory lock, it may make sense to additionally make
 >   use of `fcntl()` read and write locks to check for mutual exclusion in
 >   `MemorySegmentMapped`. This will make visible any programming mistakes
 >   quickly.

 Do you mean acquiring a lock when opening a segment in read-write
 mode (so there should be no more than one writer at the same time)?

 > * I have pushed a bunch of commits to the branch. Please also check
 >   them.

 These generally look good, thanks.

-- 
Ticket URL: <http://bind10.isc.org/ticket/2831#comment:14>
BIND 10 Development <http://bind10.isc.org>
BIND 10 Development


More information about the bind10-tickets mailing list