Bug 253 - ARP does not retry upon loss
ARP does not retry upon loss
Status: RESOLVED FIXED
Product: ns-3
Classification: Unclassified
Component: internet
ns-3.1
All All
: P1 critical
Assigned To: Tom Henderson
:
Depends on:
Blocks:
  Show dependency treegraph
 
Reported: 2008-07-16 08:54 UTC by Tom Henderson
Modified: 2008-08-01 11:10 UTC (History)
2 users (show)

See Also:


Attachments
patch against udp-echo.cc to reproduce (413 bytes, patch)
2008-07-16 08:55 UTC, Tom Henderson
Details | Diff
proposed patch (8.82 KB, patch)
2008-07-18 23:41 UTC, Tom Henderson
Details | Diff
revised workaround patch (8.85 KB, patch)
2008-07-23 13:27 UTC, Tom Henderson
Details | Diff
revised patch (12.19 KB, patch)
2008-07-28 09:58 UTC, Tom Henderson
Details | Diff
small fix based on Mathieu's comment (13.15 KB, patch)
2008-07-29 01:36 UTC, Tom Henderson
Details | Diff
latest iteration (11.67 KB, patch)
2008-07-30 09:26 UTC, Tom Henderson
Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Tom Henderson 2008-07-16 08:54:14 UTC
The ARP implementation is not robust to packet loss.
Comment 1 Tom Henderson 2008-07-16 08:55:27 UTC
Created attachment 202 [details]
patch against udp-echo.cc to reproduce

This patch, if applied to udp-echo.cc, reproduces the problem.
Comment 2 Tom Henderson 2008-07-17 02:09:08 UTC
If no one beats me to this, I will provide a patch fix by Friday.  

- add an integer attribute to ArpCache called "MaxRetries"
- add a m_retries member variable to ArpCache::Entry that counts up to m_maxRetries
- add some timer logic that is invoked when the cache entry enters WaitReply state.  The timer will fire every m_waitReplyTimeout interval.  If m_retries <= m_maxRetries, resend the arp request and increment m_retries and restart timer; otherwise, move to dead state
Comment 3 Tom Henderson 2008-07-18 23:41:20 UTC
Created attachment 204 [details]
proposed patch

I tested this against the test case, and did some bounds checking on the maxRetries value.  It basically adds a timer to protect the ArpRequest, and a counter to control how many requests are sent before giving up.
Comment 4 Mathieu Lacage 2008-07-21 11:39:12 UTC
(In reply to comment #3)
> Created an attachment (id=204) [details]
> proposed patch
> 
> I tested this against the test case, and did some bounds checking on the
> maxRetries value.  It basically adds a timer to protect the ArpRequest, and a
> counter to control how many requests are sent before giving up.

The idea sounds ok modulo the expected question: how do real systems deal with that ? Why was it not enough for you to make the 'dead' timer value smaller ?

As to the patch, adding a Timer * instance _per entry_ seems horrendous from a memory perspective. I would much rather do one of:
1) do not use a Timer object and Simulator::Schedule without storing the resulting EventId. 
2) If you really must include the timer, do not use a "Timer *" But use a "Timer".
3) use a per-cache timer shared by all entries which scans through all entries to find waiting entries whenever it expires and uses the Entry::m_lastSeen field together with the  m_waitReplyTimeout value to guess how many retries were done for each entry and whether or not it should resend a request (i.e., if now - m_lastSeen / m_waitReplyTimeout is bigger than m_retries + 1, you should send a new request and increment m_retries and potentially re-schedule the wait reply timeout.

I really prefer 3 above.

Another comment is that your current patch as well as any of the proposed modifications make the following code uneeded in ArpL3Protocol::Lookup. It should be replaced at least by an ASSERT : if (IsWaitReply) {NS_ASSERT ()} or, better, be removed after some serious testing.

         else if (entry->IsWaitReply ())
            {
              NS_LOG_LOGIC ("node="<<m_node->GetId ()<<
                        ", wait reply for " << destination << " expired -- drop");
              entry->MarkDead ();
              Ptr<Packet> pending = entry->DequeuePending();
              while (pending != 0)
                {
                  m_dropTrace (pending);
                  pending = entry->DequeuePending();
                }
              m_dropTrace (packet);
            }


Comment 5 Tom Henderson 2008-07-21 12:16:04 UTC
(In reply to comment #4)
> (In reply to comment #3)
> > Created an attachment (id=204) [details] [details]
> > proposed patch
> > 
> > I tested this against the test case, and did some bounds checking on the
> > maxRetries value.  It basically adds a timer to protect the ArpRequest, and a
> > counter to control how many requests are sent before giving up.
> 
> The idea sounds ok modulo the expected question: how do real systems deal with
> that ? Why was it not enough for you to make the 'dead' timer value smaller ?

can you point me to the use of the dead timer?  I didn't see any evidence in the code of a timer or events being scheduled to handle this.  ArpL3Protocol::Lookup() seems to be data-driven only.

> 
> As to the patch, adding a Timer * instance _per entry_ seems horrendous from a
> memory perspective. I would much rather do one of:
> 1) do not use a Timer object and Simulator::Schedule without storing the
> resulting EventId. 
> 2) If you really must include the timer, do not use a "Timer *" But use a
> "Timer".

I considered this but the problem seemed to be:
- Timer is in src/simulator and is not a Ptr or Object; Timer access seems to need to be by reference
- the code that schedules this per-entry timer is outside of the cache entry itself
- there seemed to be other instances of exporting pointers to cache entries, so I followed along

If we go away from storing the timer in the entry, this problem may go away.

> 3) use a per-cache timer shared by all entries which scans through all entries
> to find waiting entries whenever it expires and uses the Entry::m_lastSeen
> field together with the  m_waitReplyTimeout value to guess how many retries
> were done for each entry and whether or not it should resend a request (i.e.,
> if now - m_lastSeen / m_waitReplyTimeout is bigger than m_retries + 1, you
> should send a new request and increment m_retries and potentially re-schedule
> the wait reply timeout.
> 
> I really prefer 3 above.

I would be fine with this, thanks.

> 
> Another comment is that your current patch as well as any of the proposed
> modifications make the following code uneeded in ArpL3Protocol::Lookup. It
> should be replaced at least by an ASSERT : if (IsWaitReply) {NS_ASSERT ()} or,
> better, be removed after some serious testing.
> 
>          else if (entry->IsWaitReply ())
>             {
>               NS_LOG_LOGIC ("node="<<m_node->GetId ()<<
>                         ", wait reply for " << destination << " expired --
> drop");
>               entry->MarkDead ();
>               Ptr<Packet> pending = entry->DequeuePending();
>               while (pending != 0)
>                 {
>                   m_dropTrace (pending);
>                   pending = entry->DequeuePending();
>                 }
>               m_dropTrace (packet);
>             }
> 

OK, will put an assert in for now.
Comment 6 Mathieu Lacage 2008-07-21 12:25:54 UTC
(In reply to comment #5)

> > The idea sounds ok modulo the expected question: how do real systems deal with
> > that ? Why was it not enough for you to make the 'dead' timer value smaller ?
> 
> can you point me to the use of the dead timer?  I didn't see any evidence in
> the code of a timer or events being scheduled to handle this. 
> ArpL3Protocol::Lookup() seems to be data-driven only.

Yes. I wrote that comment before reviewing your patch completely. Sorry for the noise.
>
> If we go away from storing the timer in the entry, this problem may go away.

Yes. So, let's ignore the issue.

Comment 7 Craig Dowell 2008-07-21 13:20:02 UTC
> > 3) use a per-cache timer 
[ ... ]
 
> > I really prefer 3 above.

> I would be fine with this, thanks.

When I initially thought about the problem I came to the conclusion that a per-cache timer would be best; but when I looked at Linux I found per-entry timers.

I believe the primary reason you would swallow this overhead is to avoid a "thundering herd" of retries which you could get if you scanned the entire cache on a single timer expiration; especially if we were to someday start aging the entries in the cache.

Since our default position is to do it like Linux, I thought multiple timers were were expensive but justifiable.
Comment 8 Mathieu Lacage 2008-07-21 13:29:32 UTC
(In reply to comment #7)
> > > 3) use a per-cache timer 
> [ ... ]
> 
> > > I really prefer 3 above.
> 
> > I would be fine with this, thanks.
> 
> When I initially thought about the problem I came to the conclusion that a
> per-cache timer would be best; but when I looked at Linux I found per-entry
> timers.

ok. that's a good data point.

> I believe the primary reason you would swallow this overhead is to avoid a
> "thundering herd" of retries which you could get if you scanned the entire
> cache on a single timer expiration; especially if we were to someday start
> aging the entries in the cache.

It should not be too hard to just fire an update for the first one and defer the next update to some later time.

Comment 9 Craig Dowell 2008-07-21 13:37:59 UTC
> It should not be too hard to just fire an update for the 
> first one and defer the next update to some later time.

Not hard -- we just need to consider possible broadcast storms and avoid them.  Linux folks do it with multiple timers -- we could do it with multiple timer expirations just as well.
Comment 10 Tom Henderson 2008-07-21 16:09:15 UTC
(In reply to comment #8)
> > I believe the primary reason you would swallow this overhead is to avoid a
> > "thundering herd" of retries which you could get if you scanned the entire
> > cache on a single timer expiration; especially if we were to someday start
> > aging the entries in the cache.
> 
> It should not be too hard to just fire an update for the first one and defer
> the next update to some later time.
> 

Along these links, a way to avoid the herd is to schedule transmissions at a small random interval later, such as:

while (iterating over cache entries)
{
  if (found entry that has timed out)
  {
    // Do not call SendArpRequest just yet-- schedule it for small time later
    Schedule (UniformVariable::GetSingleValue(0, Seconds(ArpTimerTickInterval/2)), &SendArpRequest);
  }
}

This type of idiom might be nice to establish to deal with Craig's other recently filed bug about too much determinism in our packet emissions. 
Comment 11 Tom Henderson 2008-07-23 13:27:01 UTC
Created attachment 206 [details]
revised workaround patch

small bugfix to previous patch:

void
ArpCache::Entry::IncrementRetries (void)
{
  NS_LOG_FUNCTION_NOARGS ();
  m_retries++;
+  UpdateSeen ();
}

I will next look at the per-cache timer implementation
Comment 12 Tom Henderson 2008-07-28 09:58:45 UTC
Created attachment 212 [details]
revised patch

This patch responds to tracker suggestions to implement a per-cache rather than per-entry timer, and to avoid use of ns3::Timer and use EventId directly.
Comment 13 Mathieu Lacage 2008-07-28 11:27:05 UTC
(In reply to comment #12)
> Created an attachment (id=212) [details]
> revised patch
> 
> This patch responds to tracker suggestions to implement a per-cache rather than
> per-entry timer, and to avoid use of ns3::Timer and use EventId directly. 

I would have expected that you need to check that the cache entry has been in the wait replay state for long enough to increment the counter. Otherwise you risk retransmitting arp requests for entries which have just been moved to the wait reply state.

Comment 14 Tom Henderson 2008-07-29 01:36:29 UTC
Created attachment 216 [details]
small fix based on Mathieu's comment

Mathieu, I agree with your comment; I think the below should avoid retransmitting entries that have just moved to waitreply state.  In general, entries will be first retransmitted after they have waited between 1 and 2 waitreply timeout intervals.

  for (CacheI i = m_arpCache.begin (); i != m_arpCache.end (); i++) 
     {
       entry = (*i).second;
-      if (entry != 0 && entry->IsWaitReply ())
+      if (entry != 0 && entry->IsWaitReply () && entry->IsExpired ())
Comment 15 Mathieu Lacage 2008-07-29 11:42:51 UTC
(In reply to comment #14)
> Created an attachment (id=216) [details]
> small fix based on Mathieu's comment
> 
> Mathieu, I agree with your comment; I think the below should avoid
> retransmitting entries that have just moved to waitreply state.  In general,
> entries will be first retransmitted after they have waited between 1 and 2
> waitreply timeout intervals.
> 
>   for (CacheI i = m_arpCache.begin (); i != m_arpCache.end (); i++) 
>      {
>        entry = (*i).second;
> -      if (entry != 0 && entry->IsWaitReply ())
> +      if (entry != 0 && entry->IsWaitReply () && entry->IsExpired ())

Yes, I had something similar in mind.

A small nit: you have a method ClearRetries and you don't use it from within the ArpCache::Entry code. It might make debugging easier to use it rather than access m_retries directly.

This has incorrect indentation:
+    /**
+     * This function is an event handler for the event that the
+     * ArpCache wants to check whether it must retry any Arp requests.
+     * If there are no Arp requests pending, this event is not scheduled.
+     */
+    void HandleWaitReplyTimeout (void);

Let's make this ASSERT a NS_FATAL_ERROR which asks for a bug report if it is ever hit and remove the code in the else block.

@@ -218,6 +220,7 @@ ArpL3Protocol::Lookup (Ptr<Packet> packe
             } 
           else if (entry->IsWaitReply ()) 
             {
+              NS_ASSERT (false); // Test for unreachable code, remove later

Why do you have ArpL3Protocol::GetDropTrace ?

ArpCache::SetDropTrace is not needed because ArpCache derives from Object and is accessible under the /NodeList/xx/$ArpL3Protocol/CacheList/xx/ so, you can make m_dropTrace a simple member of ArpCache which is never accessed by ArpL3Protocol.

Comment 16 Tom Henderson 2008-07-29 19:27:41 UTC
(In reply to comment #15)
> (In reply to comment #14)
> > Created an attachment (id=216) [details] [details]
> > small fix based on Mathieu's comment
> > 
> > Mathieu, I agree with your comment; I think the below should avoid
> > retransmitting entries that have just moved to waitreply state.  In general,
> > entries will be first retransmitted after they have waited between 1 and 2
> > waitreply timeout intervals.
> > 
> >   for (CacheI i = m_arpCache.begin (); i != m_arpCache.end (); i++) 
> >      {
> >        entry = (*i).second;
> > -      if (entry != 0 && entry->IsWaitReply ())
> > +      if (entry != 0 && entry->IsWaitReply () && entry->IsExpired ())
> 
> Yes, I had something similar in mind.
> 
> A small nit: you have a method ClearRetries and you don't use it from within
> the ArpCache::Entry code. It might make debugging easier to use it rather than
> access m_retries directly.

OK
> 
> This has incorrect indentation:
> +    /**
> +     * This function is an event handler for the event that the
> +     * ArpCache wants to check whether it must retry any Arp requests.
> +     * If there are no Arp requests pending, this event is not scheduled.
> +     */
> +    void HandleWaitReplyTimeout (void);
> 
> Let's make this ASSERT a NS_FATAL_ERROR which asks for a bug report if it is
> ever hit and remove the code in the else block.

OK

> 
> @@ -218,6 +220,7 @@ ArpL3Protocol::Lookup (Ptr<Packet> packe
>              } 
>            else if (entry->IsWaitReply ()) 
>              {
> +              NS_ASSERT (false); // Test for unreachable code, remove later
> 
> Why do you have ArpL3Protocol::GetDropTrace ?

Trying to reuse the ArpL3Protocol in ArpCache without introducing a dependency in ArpCache on ArpL3Protocol.  
> 
> ArpCache::SetDropTrace is not needed because ArpCache derives from Object and
> is accessible under the /NodeList/xx/$ArpL3Protocol/CacheList/xx/ so, you can
> make m_dropTrace a simple member of ArpCache which is never accessed by
> ArpL3Protocol.
> 

OK, I will add this separate drop trace instead.
Comment 17 Tom Henderson 2008-07-30 09:26:53 UTC
Created attachment 217 [details]
latest iteration

any further comments?
Comment 18 Tom Henderson 2008-08-01 11:10:16 UTC
changeset a18520551cdf