Permalink
Browse files

Document the CoDel classifier.

  • Loading branch information...
1 parent a826e66 commit 65624330b265656ca2ade8108fe8850294144637 @jlouis committed Dec 23, 2012
Showing with 93 additions and 12 deletions.
  1. +93 −12 src/sv_codel.erl
View
@@ -11,21 +11,45 @@
%%% @end
-module(sv_codel).
+%% Public API
-export([init/2, enqueue/3, dequeue/2]).
%% Scrutiny
-export([qstate/1]).
--record(state,
- { queue = queue:new(),
- dropping = false,
- drop_next = 0,
- interval = 100, % ms
- target = 5, %ms
- first_above_time = 0,
- count = 0
+-type task() :: term().
+
+%% Internal state
+-record(state, {
+ %% The underlying queue to use. For now, since we are mainly in a test phase, we just use a standard
+ %% functional queue. But later the plan is to use a module here and then call the right kind of queue
+ %% functions for that module.
+ queue = queue:new(),
+
+ %% The `dropping' field tracks if the CoDel system is in a dropping state or not.
+ dropping = false,
+
+ %% If we are dropping, this value tracks the point in time where the next packet should
+ %% be dropped from the queue.
+ drop_next = 0,
+
+ %% First above time tracks when we first began seeing too much delay imposed by the queue.
+ %% This value may be 0 in which case it means we have not seen such a delay.
+ first_above_time = 0,
+
+ %% This variable tracks how many packets/jobs were recently dropped from the queue.
+ %% The value decays over time if no packets are dropped and is used to manipulate the control
+ %% law of the queue.
+ count = 0,
+
+ %% The `interval' and `target' are configurable parameters, described in @see init/2.
+ interval = 100, % ms
+ target = 5 %ms
}).
+%% @doc Look at the queue state as a proplist
+%% @end
+-spec qstate(#state{}) -> [{atom(), term()}].
qstate(#state {
queue = Q,
dropping = Drop,
@@ -43,15 +67,52 @@ qstate(#state {
{first_above_time, FAT},
{count, C}].
+%% @doc Initialize the CoDel state
+%% <p>The value `Target' defines the delay target in ms. If the queue has a sojourn-time through the queue
+%% which is above this value, then the queue begins to consider dropping packets.</p>
+%% <p>The value `Interval' is the window we have to be above `Target' before we consider that there may be
+%% problems. As such, it provides a hysteresis on the queue as well and small increases in latency does
+%% not affect the queue.</p>
+%% <p>Note that the interval makes sure we can use the queue as "good queue". If we get a sudden small
+%% spike in jobs, then the queue will make sure they get smoothed out and processed with no loss of jobs.
+%% But it also protects against "bad queue" where a standing queue won't dissipate due to consistent
+%% overload of the system</p>
+%% @end
+-spec init(pos_integer(), pos_integer()) -> #state{}.
+init(Target, Interval) when Target > Interval -> exit(misconfiguration);
init(Target, Interval) -> #state{ target = Target, interval = Interval }.
+%% @doc Enqueue a packet
+%% <p>Enqueue packet `Pkt' at time `TS' into the queue.</p>
+%% @end
+-spec enqueue(task(), term(), #state{}) -> #state{}.
enqueue(Pkt, TS, #state { queue = Q } = State) ->
State#state { queue = queue:in({Pkt, TS}, Q) }.
+%% @doc Dequeue a packet from the CoDel system
+%% Given a point in time, `Now' and a CoDel `State', extract the next task from it.
+%% @end
+-spec dequeue(Now, InState) ->
+ {empty, [Pkt], OutState} | {drop, [Pkt], OutState} | {Pkt, [Pkt], OutState}
+ when
+ Now :: term(),
+ Pkt :: task(),
+ InState :: #state{},
+ OutState :: #state{}.
+dequeue(Now, State) ->
+ dequeue_(Now, dodequeue(Now, State)).
+
+%% Internal functions
+%% ---------------------------------------------------------
+%% The control law defines the packet drop rate. Given a time T we drop the next packet at T+I, where
+%% I is the interval. Now, if we need to drop yet another packet, we drop it at I/math:sqrt(C) where C
+%% is the number of packets we have dropped so far in this round.
control_law(T, I, C) ->
T + I / math:sqrt(C).
+%% This is a helper function. It dequeues from the underlying queue and then analyzes the Sojourn
+%% time together with the next function, dodequeue_.
dodequeue(Now, #state { queue = Q } = State) ->
case queue:out(Q) of
{empty, NQ} ->
@@ -61,36 +122,52 @@ dodequeue(Now, #state { queue = Q } = State) ->
dodequeue_(Now, Pkt, Sojourn, State#state { queue = NQ })
end.
-
+
+%% Case split:
+%% The sojourn time through the queue is less than our target value. Thus, we should not drop, and
+%% we reset when we were first above.
dodequeue_(_Now, Pkt, Sojourn, #state { target = T } = State) when Sojourn < T ->
{nodrop, Pkt, State#state { first_above_time = 0 }};
+%% We are above target, but this is the first time we are above target. We set up the point in time when
+%% we went above the target to start tracking this.
dodequeue_(Now, Pkt, _Sojourn, #state { first_above_time = FAT, interval = I } = State) when FAT == 0 ->
{nodrop, Pkt, State#state { first_above_time = Now + I }};
+%% We have been above target for more than one interval. This is when we need to start dropping.
dodequeue_(Now, Pkt, _Sojourn, #state { first_above_time = FAT } = State) when Now >= FAT ->
{drop, Pkt, State};
+%% We are above target, but we have not yet been above target for a complete interval. Wait and see
+%% what happens, but don't begin dropping packets just yet.
dodequeue_(_Now, Pkt, _Sojourn, State) ->
{nodrop, Pkt, State}.
-dequeue(Now, State) ->
- dequeue_(Now, dodequeue(Now, State)).
-
+
+%% Dequeue worker. This drives the meat of the dequeue steps.
+%% Case split:
+%% We are in the dropping state, but are transitioning to not dropping.
dequeue_(Now, {nodrop, Pkt, #state { dropping = true } = State}) ->
dequeue_drop_next(Now, Pkt, State#state { dropping = false }, []);
+%% We are in the dropping state and are to continue dropping.
dequeue_(Now, {drop, Pkt, #state { dropping = true } = State}) ->
dequeue_drop_next(Now, Pkt, State, []);
+%% We are not in the dropping state, but should start dropping.
dequeue_(Now, {drop, Pkt, #state { dropping = false } = State}) ->
dequeue_start_drop(Now, Pkt, State);
+%% Default case for normal operation.
dequeue_(_Now, {nodrop, Pkt, #state { dropping = false } = State}) ->
{Pkt, [], State}.
+%% Consider dropping the next packet from the queue. This function drives a loop until the next timepoint
+%% where we should drop is in the future. The helper dequeue_drop_next_/3 carries out the book-keeping
dequeue_drop_next(Now, Pkt, #state { drop_next = DN, dropping = true } = State, Dropped)
when Now >= DN ->
dequeue_drop_next_(Now, dodequeue(Now, State), [Pkt | Dropped]);
dequeue_drop_next(_Now, Pkt, State, Dropped) ->
{Pkt, Dropped, State}.
+%% If the Sojourn time improves, we leave the dropping state.
dequeue_drop_next_(Now, {nodrop, Pkt, State}, Dropped) ->
dequeue_drop_next(Now, Pkt, State#state { dropping = false }, Dropped);
+%% We are still to drop packets, so update the count and the control law for the next loop round.
dequeue_drop_next_(
Now,
{drop, Pkt, #state { count = C, interval = I, drop_next = DN } = State},
@@ -101,6 +178,10 @@ dequeue_drop_next_(
State#state { count = C + 1, drop_next = control_law(DN, I, C + 1) },
Dropped).
+%% Function for setting up the dropping state. When we start dropping, we evaluate a bit on
+%% how long ago we last dropped. If we did this recently, we do not start off from the bottom of
+%% the control law, but rather pick a point a bit up the function. On the other hand, if it is a long time
+%% ago, we just pick the usual starting point of 1.
dequeue_start_drop(Now, Pkt, #state { drop_next = DN, interval = Interval, count = Count } = State)
when Now - DN < Interval, Count > 2 ->
{drop, [Pkt], State#state {

0 comments on commit 6562433

Please sign in to comment.