]> git.ipfire.org Git - thirdparty/postfix.git/commitdiff
postfix-2.5-20071121
authorWietse Venema <wietse@porcupine.org>
Wed, 21 Nov 2007 05:00:00 +0000 (00:00 -0500)
committerViktor Dukhovni <viktor@dukhovni.org>
Tue, 5 Feb 2013 06:33:26 +0000 (06:33 +0000)
15 files changed:
postfix/HISTORY
postfix/RELEASE_NOTES
postfix/html/postconf.5.html
postfix/html/qmgr.8.html
postfix/man/man5/postconf.5
postfix/man/man8/qmgr.8
postfix/mantools/postlink
postfix/proto/postconf.proto
postfix/src/global/mail_params.h
postfix/src/global/mail_version.h
postfix/src/qmgr/Makefile.in
postfix/src/qmgr/qmgr.c
postfix/src/qmgr/qmgr.h
postfix/src/qmgr/qmgr_deliver.c
postfix/src/qmgr/qmgr_queue.c

index 59019d5b2aad9ab0c12cceec050ca05f55172296..9cb1b12b5b05e94dfd6409c03d75794f6bf7159e 100644 (file)
@@ -13813,3 +13813,29 @@ Apologies for any names omitted.
        Bugfix (introduced: 20071004) missing exception handling
        in smtp-sink per-command delay feature. Victor Duchovni.
        File: smtpstone/smtp-sink.c.
+
+2007117-20
+
+       Revised queue manager with separate mechanisms for
+       per-destination concurrency control and dead destination
+       detection.  The concurrency control supports non-integer
+       feedback for more gradual concurrency adjustments, and uses
+       hysteresis to avoid rapid oscillations.  A destination is
+       declared "dead" after a configurable number of pseudo-cohorts
+       (number of deliveries equal to a destination's concurrency)
+       reports connection or handshake failure. This work began
+       with a discussion that Wietse started with Patrik Rak and
+       Victor Duchovni late January 2004, and that Victor revived
+       late October 2007. To establish a baseline for further
+       improvement, Wietse implemented a few simple mechanisms.
+
+       Configuration parameters: qmgr_concurrency_feedback_debug,
+       qmgr_negative_concurrency_feedback_hysteresis,
+       qmgr_negative_concurrency_feedback_style,
+       qmgr_positive_concurrency_feedback_hysteresis,
+       qmgr_positive_concurrency_feedback_style, qmgr_sacrifice_cohorts.
+       See postconf(5) for detailed information. Right now, the
+       defaults are compatible with older Postfix versions.  After
+       further review the number of parameters will be consolidated
+       and the defaults will select the better algorithms.  Files:
+       qmgr/qmgr_queue.c, qmgr/qmgr_deliver.c.
index 62f58225d291cd73b2a03f6ee889a23da3523e97..c9dff4ce1f3861032a1a4f6096520ec01a6a73da 100644 (file)
@@ -17,7 +17,46 @@ Incompatibility with Postfix 2.3 and earlier
 If you upgrade from Postfix 2.3 or earlier, read RELEASE_NOTES-2.4
 before proceeding.
 
-Major changes with Postfix snapshot 20071110
+Major changes with Postfix snapshot 20071121
+============================================
+
+Revised queue manager with separate mechanisms for per-destination
+concurrency control and for dead destination detection.  The
+concurrency control supports non-integer feedback to allow for more
+gradual concurrency adjustments, and uses hysteresis to avoid rapid
+oscillations.  A destination is declared "dead" after a configurable
+number of pseudo-cohorts(*) reports connection or handshake failure.
+
+(*) A pseudo-cohort is a number of delivery requests equal to a
+    destination's delivery concurrency.
+
+The drawbacks of the old +/-1 feedback scheduler are a) overshoot
+due to exponential delivery concurrency growth with each pseudo-cohort(*)
+(5-10-20...); b) throttling down to zero concurrency after a single
+pseudo-cohort(*) failure. The second problem was especially an issue
+with low-concurrency channels where a single failure could be
+sufficient to mark a destination as "dead", and suspend further
+deliveries.
+
+The new code is a laboratory model with a multitude of configuration
+parameters, so that developers can experiment with different feedback
+functions and hysteresis values.  This is a baseline against which
+further improvements will be measured: a) is the additional improvement
+worth the additional complexity; b) is the design sound, i.e. free
+from arbitrary constants and other tweaks that optimize for a narrow
+range of application.
+
+New main.cf parameters: qmgr_concurrency_feedback_debug,
+qmgr_negative_feedback_hysteresis, qmgr_negative_feedback_method,
+qmgr_positive_feedback_hysteresis, qmgr_positive_feedback_method,
+qmgr_sacrifice_cohorts.  See postconf(5) for extensive descriptions.
+
+The default parameter settings are backwards compatible with older
+Postfix versions.  However, after a testing period, the number of
+parameters will be consolidated, and the default settings will be
+changed to take advantage of the "better" algorithm.
+
+Major changes with Postfix snapshot 20071111
 ============================================
 
 Header/body checks are now available in the SMTP client, after the
index 5cef8f2dad46257eb5a3d7cf77a02c66a5888a5f..2d9054f51b89af3c89a914be0938b7ce89f5ef56 100644 (file)
@@ -5915,6 +5915,18 @@ This feature is available in Postfix 2.0 and later.
 </p>
 
 
+</DD>
+
+<DT><b><a name="qmgr_concurrency_feedback_debug">qmgr_concurrency_feedback_debug</a>
+(default: no)</b></DT><DD>
+
+<p> Make the queue manager's feedback algorithm verbose for performance
+analysis purposes. </p>
+
+<p> This feature is temporarily available in Postfix 2.5; its final
+form is likely to change. </p>
+
+
 </DD>
 
 <DT><b><a name="qmgr_fudge_factor">qmgr_fudge_factor</a>
@@ -5966,6 +5978,136 @@ parameter is 1.
 </p>
 
 
+</DD>
+
+<DT><b><a name="qmgr_negative_concurrency_feedback_hysteresis">qmgr_negative_concurrency_feedback_hysteresis</a>
+(default: 1)</b></DT><DD>
+
+<p> The per-destination integer amount of negative concurrency
+feedback that must accumulate between negative adjustments of a
+destination's delivery concurrency.  The concurrency adjustment is
+equal in size to the negative hysteresis value, and is applied at
+the <b>beginning</b> of a cycle of (hysteresis / feedback) steps.
+At that same time, the destination's positive feedback hysteresis
+cycle is reset to its beginning.  </p>
+
+<p> This feature is temporarily available in Postfix 2.5; its final
+form is likely to change. The default setting is compatible with
+earlier Postfix versions. </p>
+
+
+</DD>
+
+<DT><b><a name="qmgr_negative_concurrency_feedback_style">qmgr_negative_concurrency_feedback_style</a>
+(default: fixed_1)</b></DT><DD>
+
+<p> The per-destination amount of negative delivery concurrency
+feedback, after a delivery completes with a connection or handshake
+failure.  </p>
+
+<dl>
+
+<dt> <b> inverse_concurrency </b> </dt> <dd> Variable feedback of
+1 / (delivery concurrency). With this setting, and with
+"<a href="postconf.5.html#qmgr_negative_concurrency_feedback_hysteresis">qmgr_negative_concurrency_feedback_hysteresis</a> = 1", the destination's
+delivery concurrency is decremented by 1 after each failed
+pseudo-cohort, and the destination is marked dead (further delivery
+suspended) after the failed pseudo-cohort count reaches
+$<a href="postconf.5.html#qmgr_sacrificial_cohorts">qmgr_sacrificial_cohorts</a>.  </dd>
+
+<dt> <b> inverse_sqrt_concurrency </b> </dt> <dd> Variable feedback
+of 1 / (square root of delivery concurrency). This is an intermediate
+form between the other two. It lacks sound justification, and is a
+candidate for removal. </dd>
+
+<dt> <b> fixed_1 </b> </dt> <dd> Constant feedback of 1. This setting
+is compatible with Postfix versions before 2.5, where a destination's
+delivery concurrency is throttled down to zero (and further delivery
+suspended) after a single failed pseudo-cohort.  </dd>
+
+</dl>
+
+<p> A pseudo-cohort is a number of deliveries equal to the destination's
+delivery concurrency. </p>
+
+<p> This feature is temporarily available in Postfix 2.5; its final
+form is likely to change. The default setting is compatible with
+earlier Postfix versions. </p>
+
+
+</DD>
+
+<DT><b><a name="qmgr_positive_concurrency_feedback_hysteresis">qmgr_positive_concurrency_feedback_hysteresis</a>
+(default: 1)</b></DT><DD>
+
+<p> The per-destination integer amount of positive concurrency
+feedback that must accumulate before positive adjustments of a
+destination's delivery concurrency.  The concurrency adjustment is
+equal in size to the positive hysteresis value, and is applied at
+the <b>end</b> of a cycle of (hysteresis / feedback) steps. At that
+same time, the destination's negative feedback hysteresis cycle is
+reset to its beginning. </p>
+
+<p> This feature is temporarily available in Postfix 2.5; its final
+form is likely to change. The default setting is compatible with
+earlier Postfix versions. </p>
+
+
+</DD>
+
+<DT><b><a name="qmgr_positive_concurrency_feedback_style">qmgr_positive_concurrency_feedback_style</a>
+(default: fixed_1)</b></DT><DD>
+
+<p> The per-destination amount of positive delivery concurrency
+feedback, after a delivery completes without connection or handshake
+failure.  </p>
+
+<dl>
+
+<dt> <b> inverse_concurrency </b> </dt> <dd> Variable feedback of
+1 / (delivery concurrency). With this setting, and with
+"<a href="postconf.5.html#qmgr_positive_concurrency_feedback_hysteresis">qmgr_positive_concurrency_feedback_hysteresis</a> = 1", the destination's
+delivery concurrency is incremented by 1 after each successful
+pseudo-cohort, until it reaches the per-destination maximal concurrency
+limit.  </dd>
+
+<dt> <b> inverse_sqrt_concurrency </b> </dt> <dd> Variable feedback
+of 1 / (square root of delivery concurrency). This is an intermediate
+form between the other two. It lacks sound justification, and is a
+candidate for removal. </dd>
+
+<dt> <b> fixed_1 </b> </dt> <dd> Constant feedback of 1. This setting
+is compatible with Postfix versions before 2.5, where a destination's
+delivery concurrency is doubled after each successful pseudo-cohort,
+until it reaches the per-destination maximal concurrency limit.
+</dd>
+
+</dl>
+
+<p> A pseudo-cohort is a number of deliveries equal to the destination's
+delivery concurrency. </p>
+
+<p> This feature is temporarily available in Postfix 2.5. The default
+setting is compatible with earlier Postfix versions. </p>
+
+
+</DD>
+
+<DT><b><a name="qmgr_sacrificial_cohorts">qmgr_sacrificial_cohorts</a>
+(default: 1)</b></DT><DD>
+
+<p> How many pseudo-cohorts must suffer connection or handshake
+failure before a specific destination is considered unavailable
+(and further delivery is suspended). A pseudo-cohort is a number
+of deliveries equal to a destination's concurrency. The pseudo-cohort
+failure count is reset each time a delivery completes without
+connection or handshake failure for that specific destination.  </p>
+
+<p> This feature is temporarily available in Postfix 2.5; its final
+form is likely to change. The default setting is compatible with
+earlier Postfix versions. </p>
+
+
 </DD>
 
 <DT><b><a name="qmqpd_authorized_clients">qmqpd_authorized_clients</a>
index ce4106af838e43975e8c353e5d6eeb62df143ab1..245cffdd42ef5ae0e6aab3987f89a3a33fd2def8 100644 (file)
@@ -262,6 +262,38 @@ QMGR(8)                                                                QMGR(8)
        <b><a href="postconf.5.html#default_destination_concurrency_limit">tion_concurrency_limit</a>)</b>
               Idem, for delivery via the named message <i>transport</i>.
 
+       <b><a href="postconf.5.html#qmgr_concurrency_feedback_debug">qmgr_concurrency_feedback_debug</a> (no)</b>
+              Make the queue manager's feedback algorithm verbose
+              for performance analysis purposes.
+
+       <b><a href="postconf.5.html#qmgr_negative_concurrency_feedback_hysteresis">qmgr_negative_concurrency_feedback_hysteresis</a> (1)</b>
+              The per-destination integer amount of negative con-
+              currency feedback that must accumulate between neg-
+              ative  adjustments of a destination's delivery con-
+              currency.
+
+       <b><a href="postconf.5.html#qmgr_negative_concurrency_feedback_style">qmgr_negative_concurrency_feedback_style</a> (fixed_1)</b>
+              The per-destination  amount  of  negative  delivery
+              concurrency  feedback,  after  a delivery completes
+              with a connection or handshake failure.
+
+       <b><a href="postconf.5.html#qmgr_positive_concurrency_feedback_hysteresis">qmgr_positive_concurrency_feedback_hysteresis</a> (1)</b>
+              The per-destination integer amount of positive con-
+              currency feedback that must accumulate before posi-
+              tive adjustments of a destination's  delivery  con-
+              currency.
+
+       <b><a href="postconf.5.html#qmgr_positive_concurrency_feedback_style">qmgr_positive_concurrency_feedback_style</a> (fixed_1)</b>
+              The  per-destination  amount  of  positive delivery
+              concurrency feedback, after  a  delivery  completes
+              without connection or handshake failure.
+
+       <b><a href="postconf.5.html#qmgr_sacrificial_cohorts">qmgr_sacrificial_cohorts</a> (1)</b>
+              How  many  pseudo-cohorts must suffer connection or
+              handshake failure before a specific destination  is
+              considered  unavailable  (and  further  delivery is
+              suspended).
+
 <b>RECIPIENT SCHEDULING CONTROLS</b>
        <b><a href="postconf.5.html#default_destination_recipient_limit">default_destination_recipient_limit</a> (50)</b>
               The default maximal number of recipients  per  mes-
@@ -305,21 +337,23 @@ QMGR(8)                                                                QMGR(8)
               Idem, for delivery via the named message <i>transport</i>.
 
 <b>OTHER RESOURCE AND RATE CONTROLS</b>
-       <b><a href="postconf.5.html#minimal_backoff_time">minimal_backoff_time</a> (version dependent)</b>
+       <b><a href="postconf.5.html#minimal_backoff_time">minimal_backoff_time</a> (300s)</b>
               The minimal time  between  attempts  to  deliver  a
-              deferred message.
+              deferred  message; prior to Postfix 2.4 the default
+              value was 1000s.
 
        <b><a href="postconf.5.html#maximal_backoff_time">maximal_backoff_time</a> (4000s)</b>
-              The  maximal  time  between  attempts  to deliver a
+              The maximal time  between  attempts  to  deliver  a
               deferred message.
 
        <b><a href="postconf.5.html#maximal_queue_lifetime">maximal_queue_lifetime</a> (5d)</b>
-              The maximal time a message is queued before  it  is
+              The  maximal  time a message is queued before it is
               sent back as undeliverable.
 
-       <b><a href="postconf.5.html#queue_run_delay">queue_run_delay</a> (version dependent)</b>
-              The  time between <a href="QSHAPE_README.html#deferred_queue">deferred queue</a> scans by the queue
-              manager.
+       <b><a href="postconf.5.html#queue_run_delay">queue_run_delay</a> (300s)</b>
+              The time between <a href="QSHAPE_README.html#deferred_queue">deferred queue</a> scans by the  queue
+              manager; prior to Postfix 2.4 the default value was
+              1000s.
 
        <b><a href="postconf.5.html#transport_retry_time">transport_retry_time</a> (60s)</b>
               The time between attempts by the Postfix queue man-
index d558846de57a5b28e1b7a48409c2d373eecd17e2..d39222ab129f0b0ed858fbbad4dc825d77d8309f 100644 (file)
@@ -3244,6 +3244,12 @@ clogging up the Postfix active queue. Specify 0 to disable.
 This feature is enabled with the helpful_warnings parameter.
 .PP
 This feature is available in Postfix 2.0 and later.
+.SH qmgr_concurrency_feedback_debug (default: no)
+Make the queue manager's feedback algorithm verbose for performance
+analysis purposes.
+.PP
+This feature is temporarily available in Postfix 2.5; its final
+form is likely to change.
 .SH qmgr_fudge_factor (default: 100)
 Obsolete feature: the percentage of delivery resources that a busy
 mail system will use up for delivery of a large mailing  list
@@ -3263,6 +3269,97 @@ takes priority over any other in-memory recipient limits (i.e.,
 the global qmgr_message_recipient_limit and the per transport
 _recipient_limit) if necessary. The minimum value allowed for this
 parameter is 1.
+.SH qmgr_negative_concurrency_feedback_hysteresis (default: 1)
+The per-destination integer amount of negative concurrency
+feedback that must accumulate between negative adjustments of a
+destination's delivery concurrency.  The concurrency adjustment is
+equal in size to the negative hysteresis value, and is applied at
+the \fBbeginning\fR of a cycle of (hysteresis / feedback) steps.
+At that same time, the destination's positive feedback hysteresis
+cycle is reset to its beginning.
+.PP
+This feature is temporarily available in Postfix 2.5; its final
+form is likely to change. The default setting is compatible with
+earlier Postfix versions.
+.SH qmgr_negative_concurrency_feedback_style (default: fixed_1)
+The per-destination amount of negative delivery concurrency
+feedback, after a delivery completes with a connection or handshake
+failure.
+.IP "\fB inverse_concurrency \fR"
+Variable feedback of
+1 / (delivery concurrency). With this setting, and with
+"qmgr_negative_concurrency_feedback_hysteresis = 1", the destination's
+delivery concurrency is decremented by 1 after each failed
+pseudo-cohort, and the destination is marked dead (further delivery
+suspended) after the failed pseudo-cohort count reaches
+$qmgr_sacrificial_cohorts.
+.IP "\fB inverse_sqrt_concurrency \fR"
+Variable feedback
+of 1 / (square root of delivery concurrency). This is an intermediate
+form between the other two. It lacks sound justification, and is a
+candidate for removal.
+.IP "\fB fixed_1 \fR"
+Constant feedback of 1. This setting
+is compatible with Postfix versions before 2.5, where a destination's
+delivery concurrency is throttled down to zero (and further delivery
+suspended) after a single failed pseudo-cohort.
+.PP
+A pseudo-cohort is a number of deliveries equal to the destination's
+delivery concurrency.
+.PP
+This feature is temporarily available in Postfix 2.5; its final
+form is likely to change. The default setting is compatible with
+earlier Postfix versions.
+.SH qmgr_positive_concurrency_feedback_hysteresis (default: 1)
+The per-destination integer amount of positive concurrency
+feedback that must accumulate before positive adjustments of a
+destination's delivery concurrency.  The concurrency adjustment is
+equal in size to the positive hysteresis value, and is applied at
+the \fBend\fR of a cycle of (hysteresis / feedback) steps. At that
+same time, the destination's negative feedback hysteresis cycle is
+reset to its beginning.
+.PP
+This feature is temporarily available in Postfix 2.5; its final
+form is likely to change. The default setting is compatible with
+earlier Postfix versions.
+.SH qmgr_positive_concurrency_feedback_style (default: fixed_1)
+The per-destination amount of positive delivery concurrency
+feedback, after a delivery completes without connection or handshake
+failure.
+.IP "\fB inverse_concurrency \fR"
+Variable feedback of
+1 / (delivery concurrency). With this setting, and with
+"qmgr_positive_concurrency_feedback_hysteresis = 1", the destination's
+delivery concurrency is incremented by 1 after each successful
+pseudo-cohort, until it reaches the per-destination maximal concurrency
+limit.
+.IP "\fB inverse_sqrt_concurrency \fR"
+Variable feedback
+of 1 / (square root of delivery concurrency). This is an intermediate
+form between the other two. It lacks sound justification, and is a
+candidate for removal.
+.IP "\fB fixed_1 \fR"
+Constant feedback of 1. This setting
+is compatible with Postfix versions before 2.5, where a destination's
+delivery concurrency is doubled after each successful pseudo-cohort,
+until it reaches the per-destination maximal concurrency limit.
+.PP
+A pseudo-cohort is a number of deliveries equal to the destination's
+delivery concurrency.
+.PP
+This feature is temporarily available in Postfix 2.5. The default
+setting is compatible with earlier Postfix versions.
+.SH qmgr_sacrificial_cohorts (default: 1)
+How many pseudo-cohorts must suffer connection or handshake
+failure before a specific destination is considered unavailable
+(and further delivery is suspended). A pseudo-cohort is a number
+of deliveries equal to a destination's concurrency. The pseudo-cohort
+failure count is reset each time a delivery completes without
+connection or handshake failure for that specific destination.
+.PP
+This feature is temporarily available in Postfix 2.5; its final
+form is likely to change. The default setting is compatible with
+earlier Postfix versions.
 .SH qmqpd_authorized_clients (default: empty)
 What clients are allowed to connect to the QMQP server port.
 .PP
index ca129d64da4008b3b60eaf564c37e64d7a928191..5e6f55dd42c75141c4ad5d9a3e9336014a3bb24b 100644 (file)
@@ -235,6 +235,29 @@ The default maximal number of parallel deliveries to the same
 destination.
 .IP "\fItransport\fB_destination_concurrency_limit ($default_destination_concurrency_limit)\fR"
 Idem, for delivery via the named message \fItransport\fR.
+.IP "\fBqmgr_concurrency_feedback_debug (no)\fR"
+Make the queue manager's feedback algorithm verbose for performance
+analysis purposes.
+.IP "\fBqmgr_negative_concurrency_feedback_hysteresis (1)\fR"
+The per-destination integer amount of negative concurrency
+feedback that must accumulate between negative adjustments of a
+destination's delivery concurrency.
+.IP "\fBqmgr_negative_concurrency_feedback_style (fixed_1)\fR"
+The per-destination amount of negative delivery concurrency
+feedback, after a delivery completes with a connection or handshake
+failure.
+.IP "\fBqmgr_positive_concurrency_feedback_hysteresis (1)\fR"
+The per-destination integer amount of positive concurrency
+feedback that must accumulate before positive adjustments of a
+destination's delivery concurrency.
+.IP "\fBqmgr_positive_concurrency_feedback_style (fixed_1)\fR"
+The per-destination amount of positive delivery concurrency
+feedback, after a delivery completes without connection or handshake
+failure.
+.IP "\fBqmgr_sacrificial_cohorts (1)\fR"
+How many pseudo-cohorts must suffer connection or handshake
+failure before a specific destination is considered unavailable
+(and further delivery is suspended).
 .SH "RECIPIENT SCHEDULING CONTROLS"
 .na
 .nf
@@ -274,15 +297,17 @@ Idem, for delivery via the named message \fItransport\fR.
 .nf
 .ad
 .fi
-.IP "\fBminimal_backoff_time (version dependent)\fR"
-The minimal time between attempts to deliver a deferred message.
+.IP "\fBminimal_backoff_time (300s)\fR"
+The minimal time between attempts to deliver a deferred message;
+prior to Postfix 2.4 the default value was 1000s.
 .IP "\fBmaximal_backoff_time (4000s)\fR"
 The maximal time between attempts to deliver a deferred message.
 .IP "\fBmaximal_queue_lifetime (5d)\fR"
 The maximal time a message is queued before it is sent back as
 undeliverable.
-.IP "\fBqueue_run_delay (version dependent)\fR"
-The time between deferred queue scans by the queue manager.
+.IP "\fBqueue_run_delay (300s)\fR"
+The time between deferred queue scans by the queue manager;
+prior to Postfix 2.4 the default value was 1000s.
 .IP "\fBtransport_retry_time (60s)\fR"
 The time between attempts by the Postfix queue manager to contact
 a malfunctioning message delivery transport.
index cf1586db442ffe5e4c0708b73d5211cd6d3d1c82..8f7b43c476dea8b07cd533cf01dafb92ccffbb38 100755 (executable)
@@ -334,6 +334,14 @@ while (<>) {
     s;\bqmgr_message_recip[-</bB>]*\n* *[<bB>]*ient_limit\b;<a href="postconf.5.html#qmgr_message_recipient_limit">$&</a>;g;
     s;\bqmgr_message_recip[-</bB>]*\n* *[<bB>]*ient_minimum\b;<a href="postconf.5.html#qmgr_message_recipient_minimum">$&</a>;g;
     s;\bqmqpd_authorized_clients\b;<a href="postconf.5.html#qmqpd_authorized_clients">$&</a>;g;
+
+    s;\bqmgr_negative_concurrency_feedback_hysteresis\b;<a href="postconf.5.html#qmgr_negative_concurrency_feedback_hysteresis">$&</a>;g;
+    s;\bqmgr_negative_concurrency_feedback_style\b;<a href="postconf.5.html#qmgr_negative_concurrency_feedback_style">$&</a>;g;
+    s;\bqmgr_positive_concurrency_feedback_hysteresis\b;<a href="postconf.5.html#qmgr_positive_concurrency_feedback_hysteresis">$&</a>;g;
+    s;\bqmgr_positive_concurrency_feedback_style\b;<a href="postconf.5.html#qmgr_positive_concurrency_feedback_style">$&</a>;g;
+    s;\bqmgr_sacrificial_cohorts\b;<a href="postconf.5.html#qmgr_sacrificial_cohorts">$&</a>;g;
+    s;\bqmgr_concurrency_feedback_debug\b;<a href="postconf.5.html#qmgr_concurrency_feedback_debug">$&</a>;g;
+
     s;\bqmqpd_error_delay\b;<a href="postconf.5.html#qmqpd_error_delay">$&</a>;g;
     s;\bqmqpd_timeout\b;<a href="postconf.5.html#qmqpd_timeout">$&</a>;g;
     s;\bqueue_directory\b;<a href="postconf.5.html#queue_directory">$&</a>;g;
index ce8fda2f1ced0d11318578aef758eb0f4ec3f9f8..42daf4e33fcf86ced302b993e83621c9f827b29e 100644 (file)
@@ -10674,3 +10674,121 @@ that change the delivery time or destination are not available.
 </p>
 
 <p> This feature is available in Postfix 2.5 and later. </p>
+
+%PARAM qmgr_concurrency_feedback_debug no
+
+<p> Make the queue manager's feedback algorithm verbose for performance
+analysis purposes. </p>
+
+<p> This feature is temporarily available in Postfix 2.5; its final
+form is likely to change. </p>
+
+%PARAM qmgr_sacrificial_cohorts 1
+
+<p> How many pseudo-cohorts must suffer connection or handshake
+failure before a specific destination is considered unavailable
+(and further delivery is suspended). A pseudo-cohort is a number
+of deliveries equal to a destination's concurrency. The pseudo-cohort
+failure count is reset each time a delivery completes without
+connection or handshake failure for that specific destination.  </p>
+
+<p> This feature is temporarily available in Postfix 2.5; its final
+form is likely to change. The default setting is compatible with
+earlier Postfix versions. </p>
+
+%PARAM qmgr_negative_concurrency_feedback_hysteresis 1
+
+<p> The per-destination integer amount of negative concurrency
+feedback that must accumulate between negative adjustments of a
+destination's delivery concurrency.  The concurrency adjustment is
+equal in size to the negative hysteresis value, and is applied at
+the <b>beginning</b> of a cycle of (hysteresis / feedback) steps.
+At that same time, the destination's positive feedback hysteresis
+cycle is reset to its beginning.  </p>
+
+<p> This feature is temporarily available in Postfix 2.5; its final
+form is likely to change. The default setting is compatible with
+earlier Postfix versions. </p>
+
+%PARAM qmgr_positive_concurrency_feedback_hysteresis 1
+
+<p> The per-destination integer amount of positive concurrency
+feedback that must accumulate before positive adjustments of a
+destination's delivery concurrency.  The concurrency adjustment is
+equal in size to the positive hysteresis value, and is applied at
+the <b>end</b> of a cycle of (hysteresis / feedback) steps. At that
+same time, the destination's negative feedback hysteresis cycle is
+reset to its beginning. </p>
+
+<p> This feature is temporarily available in Postfix 2.5; its final
+form is likely to change. The default setting is compatible with
+earlier Postfix versions. </p>
+
+%PARAM qmgr_negative_concurrency_feedback_style fixed_1
+
+<p> The per-destination amount of negative delivery concurrency
+feedback, after a delivery completes with a connection or handshake
+failure.  </p>
+
+<dl>
+
+<dt> <b> inverse_concurrency </b> </dt> <dd> Variable feedback of
+1 / (delivery concurrency). With this setting, and with
+"qmgr_negative_concurrency_feedback_hysteresis = 1", the destination's
+delivery concurrency is decremented by 1 after each failed
+pseudo-cohort, and the destination is marked dead (further delivery
+suspended) after the failed pseudo-cohort count reaches
+$qmgr_sacrificial_cohorts.  </dd>
+
+<dt> <b> inverse_sqrt_concurrency </b> </dt> <dd> Variable feedback
+of 1 / (square root of delivery concurrency). This is an intermediate
+form between the other two. It lacks sound justification, and is a
+candidate for removal. </dd>
+
+<dt> <b> fixed_1 </b> </dt> <dd> Constant feedback of 1. This setting
+is compatible with Postfix versions before 2.5, where a destination's
+delivery concurrency is throttled down to zero (and further delivery
+suspended) after a single failed pseudo-cohort.  </dd>
+
+</dl>
+
+<p> A pseudo-cohort is a number of deliveries equal to the destination's
+delivery concurrency. </p>
+
+<p> This feature is temporarily available in Postfix 2.5; its final
+form is likely to change. The default setting is compatible with
+earlier Postfix versions. </p>
+
+%PARAM qmgr_positive_concurrency_feedback_style fixed_1
+
+<p> The per-destination amount of positive delivery concurrency
+feedback, after a delivery completes without connection or handshake
+failure.  </p>
+
+<dl>
+
+<dt> <b> inverse_concurrency </b> </dt> <dd> Variable feedback of
+1 / (delivery concurrency). With this setting, and with
+"qmgr_positive_concurrency_feedback_hysteresis = 1", the destination's
+delivery concurrency is incremented by 1 after each successful
+pseudo-cohort, until it reaches the per-destination maximal concurrency
+limit.  </dd>
+
+<dt> <b> inverse_sqrt_concurrency </b> </dt> <dd> Variable feedback
+of 1 / (square root of delivery concurrency). This is an intermediate
+form between the other two. It lacks sound justification, and is a
+candidate for removal. </dd>
+
+<dt> <b> fixed_1 </b> </dt> <dd> Constant feedback of 1. This setting
+is compatible with Postfix versions before 2.5, where a destination's
+delivery concurrency is doubled after each successful pseudo-cohort,
+until it reaches the per-destination maximal concurrency limit.
+</dd>
+
+</dl>
+
+<p> A pseudo-cohort is a number of deliveries equal to the destination's
+delivery concurrency. </p>
+
+<p> This feature is temporarily available in Postfix 2.5. The default
+setting is compatible with earlier Postfix versions. </p>
index 7b91101070d89cc9f95fae4d7d8a9177455d7f30..35b4a555805d0ca1ad7ca7e11a4a720c0546728a 100644 (file)
@@ -2830,6 +2830,39 @@ extern char *var_smtp_body_chks;
 #define VAR_LMTP_BODY_CHKS     "lmtp_body_checks"
 #define DEF_LMTP_BODY_CHKS     ""
 
+ /*
+  * Scheduler concurrency feedback algorithms.
+  */
+#define VAR_QMGR_POS_FDBACK    "qmgr_positive_concurrency_feedback_style"
+#define DEF_QMGR_POS_FDBACK    QMGR_FDBACK_NAME_FIXED_1
+extern char *var_qmgr_pos_feedback;
+
+#define VAR_QMGR_NEG_FDBACK    "qmgr_negative_concurrency_feedback_style"
+#define DEF_QMGR_NEG_FDBACK    QMGR_FDBACK_NAME_FIXED_1
+extern char *var_qmgr_neg_feedback;
+
+#define QMGR_FDBACK_NAME_FIXED_1       "fixed_1"
+#define QMGR_FDBACK_NAME_INVERSE_1     "inverse_1"     /* deprecated */
+#define QMGR_FDBACK_NAME_INVERSE_WIN   "inverse_concurrency"
+#define QMGR_FDBACK_NAME_INV_SQRT      "inverse_sqrt"  /* deprecated */
+#define QMGR_FDBACK_NAME_INV_SQRT_WIN  "inverse_sqrt_concurrency"
+
+#define VAR_QMGR_POS_HYST      "qmgr_positive_concurrency_feedback_hysteresis"
+#define DEF_QMGR_POS_HYST      1
+extern int var_qmgr_pos_hysteresis;
+
+#define VAR_QMGR_NEG_HYST      "qmgr_negative_concurrency_feedback_hysteresis"
+#define DEF_QMGR_NEG_HYST      1
+extern int var_qmgr_neg_hysteresis;
+
+#define VAR_QMGR_SAC_COHORTS   "qmgr_sacrificial_cohorts"
+#define DEF_QMGR_SAC_COHORTS   1
+extern int var_qmgr_sac_cohorts;
+
+#define VAR_QMGR_FDBACK_DEBUG  "qmgr_concurrency_feedback_debug"
+#define DEF_QMGR_FDBACK_DEBUG  0
+extern bool var_qmgr_feedback_debug;
+
 /* LICENSE
 /* .ad
 /* .fi
index b4d627ed70d4fa7c6967c3c86ba0b44ff17b71d9..51f2f6868df6b5696f5ac3f4a512dfc5b3701e22 100644 (file)
@@ -20,7 +20,7 @@
   * Patches change both the patchlevel and the release date. Snapshots have no
   * patchlevel; they change the release date only.
   */
-#define MAIL_RELEASE_DATE      "20071111"
+#define MAIL_RELEASE_DATE      "20071121"
 #define MAIL_VERSION_NUMBER    "2.5"
 
 #ifdef SNAPSHOT
index d7db1a073e2e3b360ad41abce5d1d4b2a6b2f270..ea62bdecc3e276b545c3dcfe9dfc59b81772b63b 100644 (file)
@@ -14,7 +14,7 @@ CFLAGS        = $(DEBUG) $(OPT) $(DEFS)
 TESTPROG= 
 PROG   = qmgr
 INC_DIR        = ../../include
-LIBS   = ../../lib/libmaster.a ../../lib/libglobal.a ../../lib/libutil.a
+LIBS   = ../../lib/libmaster.a ../../lib/libglobal.a ../../lib/libutil.a -lm
 
 .c.o:; $(CC) $(CFLAGS) -c $*.c
 
@@ -290,6 +290,7 @@ qmgr_queue.o: ../../include/htable.h
 qmgr_queue.o: ../../include/mail_params.h
 qmgr_queue.o: ../../include/msg.h
 qmgr_queue.o: ../../include/mymalloc.h
+qmgr_queue.o: ../../include/name_code.h
 qmgr_queue.o: ../../include/recipient_list.h
 qmgr_queue.o: ../../include/scan_dir.h
 qmgr_queue.o: ../../include/sys_defs.h
index 4cc999671e27ffcdce6610e0ad84b4fea919862f..c932d1c2ae974c6607b327b265b3500301b671cd 100644 (file)
 /*     destination.
 /* .IP "\fItransport\fB_destination_concurrency_limit ($default_destination_concurrency_limit)\fR"
 /*     Idem, for delivery via the named message \fItransport\fR.
+/* .IP "\fBqmgr_concurrency_feedback_debug (no)\fR"
+/*     Make the queue manager's feedback algorithm verbose for performance
+/*     analysis purposes.
+/* .IP "\fBqmgr_negative_concurrency_feedback_hysteresis (1)\fR"
+/*     The per-destination integer amount of negative concurrency
+/*     feedback that must accumulate between negative adjustments of a
+/*     destination's delivery concurrency.
+/* .IP "\fBqmgr_negative_concurrency_feedback_style (fixed_1)\fR"
+/*     The per-destination amount of negative delivery concurrency
+/*     feedback, after a delivery completes with a connection or handshake
+/*     failure.
+/* .IP "\fBqmgr_positive_concurrency_feedback_hysteresis (1)\fR"
+/*     The per-destination integer amount of positive concurrency
+/*     feedback that must accumulate before positive adjustments of a
+/*     destination's delivery concurrency.
+/* .IP "\fBqmgr_positive_concurrency_feedback_style (fixed_1)\fR"
+/*     The per-destination amount of positive delivery concurrency
+/*     feedback, after a delivery completes without connection or handshake
+/*     failure.
+/* .IP "\fBqmgr_sacrificial_cohorts (1)\fR"
+/*     How many pseudo-cohorts must suffer connection or handshake
+/*     failure before a specific destination is considered unavailable
+/*     (and further delivery is suspended).
 /* RECIPIENT SCHEDULING CONTROLS
 /* .ad
 /* .fi
 /* OTHER RESOURCE AND RATE CONTROLS
 /* .ad
 /* .fi
-/* .IP "\fBminimal_backoff_time (version dependent)\fR"
-/*     The minimal time between attempts to deliver a deferred message.
+/* .IP "\fBminimal_backoff_time (300s)\fR"
+/*     The minimal time between attempts to deliver a deferred message;
+/*     prior to Postfix 2.4 the default value was 1000s.
 /* .IP "\fBmaximal_backoff_time (4000s)\fR"
 /*     The maximal time between attempts to deliver a deferred message.
 /* .IP "\fBmaximal_queue_lifetime (5d)\fR"
 /*     The maximal time a message is queued before it is sent back as
 /*     undeliverable.
-/* .IP "\fBqueue_run_delay (version dependent)\fR"
-/*     The time between deferred queue scans by the queue manager.
+/* .IP "\fBqueue_run_delay (300s)\fR"
+/*     The time between deferred queue scans by the queue manager;
+/*     prior to Postfix 2.4 the default value was 1000s.
 /* .IP "\fBtransport_retry_time (60s)\fR"
 /*     The time between attempts by the Postfix queue manager to contact
 /*     a malfunctioning message delivery transport.
@@ -390,6 +415,12 @@ int     var_local_rcpt_lim;
 int     var_proc_limit;
 bool    var_verp_bounce_off;
 int     var_qmgr_clog_warn_time;
+char   *var_qmgr_pos_feedback;
+char   *var_qmgr_neg_feedback;
+int     var_qmgr_pos_hysteresis;
+int     var_qmgr_neg_hysteresis;
+int     var_qmgr_sac_cohorts;
+int     var_qmgr_feedback_debug;
 
 static QMGR_SCAN *qmgr_scans[2];
 
@@ -614,6 +645,11 @@ static void qmgr_post_init(char *name, char **unused_argv)
     qmgr_scans[QMGR_SCAN_IDX_DEFERRED] = qmgr_scan_create(MAIL_QUEUE_DEFERRED);
     qmgr_scan_request(qmgr_scans[QMGR_SCAN_IDX_INCOMING], QMGR_SCAN_START);
     qmgr_deferred_run_event(0, (char *) 0);
+
+    /*
+     * Scheduler initialization.
+     */
+    qmgr_queue_feedback_init();
 }
 
 MAIL_VERSION_STAMP_DECLARE;
@@ -624,6 +660,8 @@ int     main(int argc, char **argv)
 {
     static CONFIG_STR_TABLE str_table[] = {
        VAR_DEFER_XPORTS, DEF_DEFER_XPORTS, &var_defer_xports, 0, 0,
+       VAR_QMGR_POS_FDBACK, DEF_QMGR_POS_FDBACK, &var_qmgr_pos_feedback, 1, 0,
+       VAR_QMGR_NEG_FDBACK, DEF_QMGR_NEG_FDBACK, &var_qmgr_neg_feedback, 1, 0,
        0,
     };
     static CONFIG_TIME_TABLE time_table[] = {
@@ -654,11 +692,15 @@ int     main(int argc, char **argv)
        VAR_LOCAL_RCPT_LIMIT, DEF_LOCAL_RCPT_LIMIT, &var_local_rcpt_lim, 0, 0,
        VAR_LOCAL_CON_LIMIT, DEF_LOCAL_CON_LIMIT, &var_local_con_lim, 0, 0,
        VAR_PROC_LIMIT, DEF_PROC_LIMIT, &var_proc_limit, 1, 0,
+       VAR_QMGR_POS_HYST, DEF_QMGR_POS_HYST, &var_qmgr_pos_hysteresis, 1, 0,
+       VAR_QMGR_NEG_HYST, DEF_QMGR_NEG_HYST, &var_qmgr_neg_hysteresis, 1, 0,
+       VAR_QMGR_SAC_COHORTS, DEF_QMGR_SAC_COHORTS, &var_qmgr_sac_cohorts, 1, 0,
        0,
     };
     static CONFIG_BOOL_TABLE bool_table[] = {
        VAR_ALLOW_MIN_USER, DEF_ALLOW_MIN_USER, &var_allow_min_user,
        VAR_VERP_BOUNCE_OFF, DEF_VERP_BOUNCE_OFF, &var_verp_bounce_off,
+       VAR_QMGR_FDBACK_DEBUG, DEF_QMGR_FDBACK_DEBUG, &var_qmgr_feedback_debug,
        0,
     };
 
index 6392e098f6c70e800e69aea249529c6730581f3d..7490e842e7718114bbb10833ee93b0d0812c9aec 100644 (file)
@@ -198,6 +198,9 @@ struct QMGR_QUEUE {
     int     todo_refcount;             /* queue entries (todo list) */
     int     busy_refcount;             /* queue entries (busy list) */
     int     window;                    /* slow open algorithm */
+    double  success;                   /* cumulative positive feedback */
+    double  failure;                   /* cumulative negative feedback */
+    double  fail_cohorts;              /* pseudo-cohort failure count */
     QMGR_TRANSPORT *transport;         /* transport linkage */
     QMGR_ENTRY_LIST todo;              /* todo queue entries */
     QMGR_ENTRY_LIST busy;              /* messages on the wire */
@@ -217,6 +220,7 @@ extern void qmgr_queue_done(QMGR_QUEUE *);
 extern void qmgr_queue_throttle(QMGR_QUEUE *, DSN *);
 extern void qmgr_queue_unthrottle(QMGR_QUEUE *);
 extern QMGR_QUEUE *qmgr_queue_find(QMGR_TRANSPORT *, const char *);
+extern void qmgr_queue_feedback_init(void);
 
 #define QMGR_QUEUE_THROTTLED(q) ((q)->window <= 0)
 
index 346562897ecac1ec9d069100865250779316208c..36c2a7b6b7a11e303b1d3782a7ee7185674fb3ce 100644 (file)
@@ -317,9 +317,11 @@ static void qmgr_deliver_update(int unused_event, char *context)
            if (VSTRING_LEN(dsb->reason) == 0)
                vstring_strcpy(dsb->reason, "unknown error");
            vstring_prepend(dsb->reason, SUSPENDED, sizeof(SUSPENDED) - 1);
-           qmgr_queue_throttle(queue, DSN_FROM_DSN_BUF(dsb));
-           if (queue->window == 0)
-               qmgr_defer_todo(queue, &dsb->dsn);
+           if (queue->window > 0) {
+               qmgr_queue_throttle(queue, DSN_FROM_DSN_BUF(dsb));
+               if (queue->window == 0)
+                   qmgr_defer_todo(queue, &dsb->dsn);
+           }
        }
     }
 
index 414de5836533c938947c40ec7dfa109c4454bbd2..4d86aea2f495f61915398e8fb08910649b146a5a 100644 (file)
@@ -50,8 +50,9 @@
 /*     transport. A null result means that the queue was not found.
 /*
 /*     qmgr_queue_throttle() handles a delivery error, and decrements the
-/*     concurrency limit for the destination. When the concurrency limit
-/*     for a destination becomes zero, qmgr_queue_throttle() starts a timer
+/*     concurrency limit for the destination, with a lower bound of 1.
+/*     When the cohort failure bound is reached, qmgr_queue_throttle()
+/*     sets the concurrency limit to zero and starts a timer
 /*     to re-enable delivery to the destination after a configurable delay.
 /*
 /*     qmgr_queue_unthrottle() undoes qmgr_queue_throttle()'s effects.
@@ -71,7 +72,7 @@
 /*     P.O. Box 704
 /*     Yorktown Heights, NY 10598, USA
 /*
-/*     Scheduler enhancements:
+/*     Pre-emptive scheduler enhancements:
 /*     Patrik Rak
 /*     Modra 6
 /*     155 00, Prague, Czech Republic
@@ -81,6 +82,7 @@
 
 #include <sys_defs.h>
 #include <time.h>
+#include <math.h>
 
 /* Utility library. */
 
 #include <mymalloc.h>
 #include <events.h>
 #include <htable.h>
+#include <name_code.h>
 
 /* Global library. */
 
 #include <mail_params.h>
 #include <recipient_list.h>
+#include <mail_proto.h>                        /* QMGR_LOG_WINDOW */
 
 /* Application-specific. */
 
 
 int     qmgr_queue_count;
 
+ /*
+  * Lookup tables for main.cf feedback method names.
+  */
+#define QMGR_FDBACK_CODE_BAD           0
+#define QMGR_FDBACK_CODE_FIXED_1       1
+#define QMGR_FDBACK_CODE_INVERSE_WIN   2
+#define QMGR_FDBACK_CODE_INVERSE_1     QMGR_FDBACK_CODE_INVERSE_WIN
+#define QMGR_FDBACK_CODE_INV_SQRT_WIN  3
+#define QMGR_FDBACK_CODE_INV_SQRT      QMGR_FDBACK_CODE_INV_SQRT_WIN
+
+NAME_CODE qmgr_feedback_map[] = {
+    QMGR_FDBACK_NAME_FIXED_1, QMGR_FDBACK_CODE_FIXED_1,
+    QMGR_FDBACK_NAME_INVERSE_WIN, QMGR_FDBACK_CODE_INVERSE_WIN,
+    QMGR_FDBACK_NAME_INVERSE_1, QMGR_FDBACK_CODE_INVERSE_1,
+    QMGR_FDBACK_NAME_INV_SQRT_WIN, QMGR_FDBACK_CODE_INV_SQRT_WIN,
+    QMGR_FDBACK_NAME_INV_SQRT, QMGR_FDBACK_CODE_INV_SQRT,
+    0, QMGR_FDBACK_CODE_BAD,
+};
+static int qmgr_pos_feedback_idx;
+static int qmgr_neg_feedback_idx;
+
+ /*
+  * Choosing the right feedback method at run-time.
+  */
+#define QMGR_FEEDBACK_VAL(idx, window) ( \
+       (idx) == QMGR_FDBACK_CODE_INVERSE_1 ? (1.0 / (window)) : \
+       (idx) == QMGR_FDBACK_CODE_FIXED_1 ? (1.0) : \
+       (1.0 / sqrt(window)) \
+    )
+
+#define QMGR_ERROR_OR_RETRY_QUEUE(queue) \
+       (strcmp(queue->transport->name, MAIL_SERVICE_RETRY) == 0 \
+           || strcmp(queue->transport->name, MAIL_SERVICE_ERROR) == 0)
+
+#define QMGR_LOG_FEEDBACK(feedback) \
+       if (var_qmgr_feedback_debug && !QMGR_ERROR_OR_RETRY_QUEUE(queue)) \
+           msg_info("%s: feedback %g", myname, feedback);
+
+#define QMGR_LOG_WINDOW(queue) \
+       if (var_qmgr_feedback_debug && !QMGR_ERROR_OR_RETRY_QUEUE(queue)) \
+           msg_info("%s: queue %s: limit %d window %d success %g failure %g fail_cohorts %g", \
+                   myname, queue->name, queue->transport->dest_concurrency_limit, \
+                   queue->window, queue->success, queue->failure, queue->fail_cohorts);
+
+/* qmgr_queue_feedback_init - initialize feedback selection */
+
+void    qmgr_queue_feedback_init(void)
+{
+
+    /*
+     * Positive and negative feedback method indices.
+     */
+    qmgr_pos_feedback_idx = name_code(qmgr_feedback_map, NAME_CODE_FLAG_NONE,
+                                     var_qmgr_pos_feedback);
+    if (qmgr_pos_feedback_idx == QMGR_FDBACK_CODE_BAD)
+       msg_fatal("%s: bad feedback method: %s",
+                 VAR_QMGR_POS_FDBACK, var_qmgr_pos_feedback);
+    if (var_qmgr_feedback_debug)
+       msg_info("positive feedback method %d, value at %d: %g",
+                qmgr_pos_feedback_idx, var_init_dest_concurrency,
+                QMGR_FEEDBACK_VAL(qmgr_pos_feedback_idx,
+                                  var_init_dest_concurrency));
+
+    qmgr_neg_feedback_idx = name_code(qmgr_feedback_map, NAME_CODE_FLAG_NONE,
+                                     var_qmgr_neg_feedback);
+    if (qmgr_neg_feedback_idx == QMGR_FDBACK_CODE_BAD)
+       msg_fatal("%s: bad feedback method: %s",
+                 VAR_QMGR_NEG_FDBACK, var_qmgr_neg_feedback);
+    if (var_qmgr_feedback_debug)
+       msg_info("negative feedback method %d, value at %d: %g",
+                qmgr_neg_feedback_idx, var_init_dest_concurrency,
+                QMGR_FEEDBACK_VAL(qmgr_neg_feedback_idx,
+                                  var_init_dest_concurrency));
+}
+
 /* qmgr_queue_unthrottle_wrapper - in case (char *) != (struct *) */
 
 static void qmgr_queue_unthrottle_wrapper(int unused_event, char *context)
@@ -122,10 +201,21 @@ void    qmgr_queue_unthrottle(QMGR_QUEUE *queue)
 {
     const char *myname = "qmgr_queue_unthrottle";
     QMGR_TRANSPORT *transport = queue->transport;
+    double  feedback;
+    double  multiplier;
 
     if (msg_verbose)
        msg_info("%s: queue %s", myname, queue->name);
 
+    /*
+     * Don't restart the negative feedback hysteresis cycle with every
+     * positive feedback. Restart it only when we make a positive concurrency
+     * adjustment (i.e. at the end of a positive feedback hysteresis cycle).
+     * Otherwise negative feedback would be too aggressive: negative feedback
+     * takes effect immediately at the start of its hysteresis cycle.
+     */
+    queue->fail_cohorts = 0;
+
     /*
      * Special case when this site was dead.
      */
@@ -135,7 +225,13 @@ void    qmgr_queue_unthrottle(QMGR_QUEUE *queue)
            msg_panic("%s: queue %s: window 0 status 0", myname, queue->name);
        dsn_free(queue->dsn);
        queue->dsn = 0;
-       queue->window = transport->init_dest_concurrency;
+       /* Back from the almost grave, best concurrency is anyone's guess. */
+       if (queue->busy_refcount > 0)
+           queue->window = queue->busy_refcount;
+       else
+           queue->window = transport->init_dest_concurrency;
+       queue->success = queue->failure = 0;
+       QMGR_LOG_WINDOW(queue);
        return;
     }
 
@@ -143,11 +239,35 @@ void    qmgr_queue_unthrottle(QMGR_QUEUE *queue)
      * Increase the destination's concurrency limit until we reach the
      * transport's concurrency limit. Allow for a margin the size of the
      * initial destination concurrency, so that we're not too gentle.
+     * 
+     * Why is the concurrency increment based on preferred concurrency and not
+     * on the number of outstanding delivery requests? The latter fluctuates
+     * wildly when deliveries complete in bursts (artificial benchmark
+     * measurements), and does not account for cached connections.
+     * 
+     * Keep the window within reasonable distance from actual concurrency
+     * otherwise negative feedback will be ineffective. This expression
+     * assumes that busy_refcount changes gradually. This is invalid when
+     * deliveries complete in bursts (artificial benchmark measurements).
      */
     if (transport->dest_concurrency_limit == 0
        || transport->dest_concurrency_limit > queue->window)
-       if (queue->window < queue->busy_refcount + transport->init_dest_concurrency)
-           queue->window++;
+       if (queue->window < queue->busy_refcount + transport->init_dest_concurrency) {
+           feedback = QMGR_FEEDBACK_VAL(qmgr_pos_feedback_idx, queue->window);
+           QMGR_LOG_FEEDBACK(feedback);
+           queue->success += feedback;
+           /* Prepare for overshoot (feedback > hysteresis, rounding error). */
+           while (queue->success >= var_qmgr_pos_hysteresis) {
+               queue->window += var_qmgr_pos_hysteresis;
+               queue->success -= var_qmgr_pos_hysteresis;
+               queue->failure = 0;
+           }
+           /* Prepare for overshoot. */
+           if (transport->dest_concurrency_limit > 0
+               && queue->window > transport->dest_concurrency_limit)
+               queue->window = transport->dest_concurrency_limit;
+       }
+    QMGR_LOG_WINDOW(queue);
 }
 
 /* qmgr_queue_throttle - handle destination delivery failure */
@@ -155,6 +275,7 @@ void    qmgr_queue_unthrottle(QMGR_QUEUE *queue)
 void    qmgr_queue_throttle(QMGR_QUEUE *queue, DSN *dsn)
 {
     const char *myname = "qmgr_queue_throttle";
+    double  feedback;
 
     /*
      * Sanity checks.
@@ -167,13 +288,43 @@ void    qmgr_queue_throttle(QMGR_QUEUE *queue, DSN *dsn)
                 myname, queue->name, dsn->status, dsn->reason);
 
     /*
-     * Decrease the destination's concurrency limit until we reach zero, at
-     * which point the destination is declared dead. Decrease the concurrency
-     * limit by one, instead of using actual concurrency - 1, to avoid
-     * declaring a host dead after just one single delivery failure.
+     * Don't restart the positive feedback hysteresis cycle with every
+     * negative feedback. Restart it only when we make a negative concurrency
+     * adjustment (i.e. at the start of a negative feedback hysteresis
+     * cycle). Otherwise positive feedback would be too weak (positive
+     * feedback does not take effect until the end of its hysteresis cycle).
      */
-    if (queue->window > 0)
-       queue->window--;
+
+    /*
+     * This queue is declared dead after a configurable number of
+     * pseudo-cohort failures.
+     */
+    if (queue->window > 0) {
+       queue->fail_cohorts += 1.0 / queue->window;
+       if (queue->fail_cohorts >= var_qmgr_sac_cohorts)
+           queue->window = 0;
+    }
+
+    /*
+     * Decrease the destination's concurrency limit until we reach 1. Base
+     * adjustments on the concurrency limit itself, instead of using the
+     * actual concurrency. The latter fluctuates wildly when deliveries
+     * complete in bursts (artificial benchmark measurements).
+     */
+    if (queue->window > 1) {
+       feedback = QMGR_FEEDBACK_VAL(qmgr_neg_feedback_idx, queue->window);
+       QMGR_LOG_FEEDBACK(feedback);
+       queue->failure -= feedback;
+       /* Prepare for overshoot (feedback > hysteresis, rounding error). */
+       while (queue->failure < 0) {
+           queue->window -= var_qmgr_neg_hysteresis;
+           queue->success = 0;
+           queue->failure += var_qmgr_neg_hysteresis;
+       }
+       /* Prepare for overshoot. */
+       if (queue->window < 1)
+           queue->window = 1;
+    }
 
     /*
      * Special case for a site that just was declared dead.
@@ -184,6 +335,7 @@ void    qmgr_queue_throttle(QMGR_QUEUE *queue, DSN *dsn)
                            (char *) queue, var_min_backoff_time);
        queue->dflags = 0;
     }
+    QMGR_LOG_WINDOW(queue);
 }
 
 /* qmgr_queue_done - delete in-core queue for site */
@@ -241,6 +393,7 @@ QMGR_QUEUE *qmgr_queue_create(QMGR_TRANSPORT *transport, const char *name,
     queue->busy_refcount = 0;
     queue->transport = transport;
     queue->window = transport->init_dest_concurrency;
+    queue->success = queue->failure = queue->fail_cohorts = 0;
     QMGR_LIST_INIT(queue->todo);
     QMGR_LIST_INIT(queue->busy);
     queue->dsn = 0;