mirror of
				https://github.com/simon987/Architeuthis.git
				synced 2025-11-03 23:46:52 +00:00 
			
		
		
		
	Added host rules + minor refactoring
This commit is contained in:
		
							parent
							
								
									2b77188ef4
								
							
						
					
					
						commit
						7b0e9a0c13
					
				
							
								
								
									
										65
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										65
									
								
								README.md
									
									
									
									
									
								
							@ -12,6 +12,7 @@ and error handling. Built for automated web scraping.
 | 
				
			|||||||
* Strictly obeys configured rate-limiting for each IP & Host
 | 
					* Strictly obeys configured rate-limiting for each IP & Host
 | 
				
			||||||
* Seamless exponential backoff retries on timeout or error HTTP codes
 | 
					* Seamless exponential backoff retries on timeout or error HTTP codes
 | 
				
			||||||
* Requires no additional configuration for integration into existing programs
 | 
					* Requires no additional configuration for integration into existing programs
 | 
				
			||||||
 | 
					* Configurable per-host behavior
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Typical use case
 | 
					### Typical use case
 | 
				
			||||||

 | 
					
 | 
				
			||||||
@ -58,6 +59,51 @@ level=trace msg=Sleeping wait=433.394361ms
 | 
				
			|||||||
./reload.sh
 | 
					./reload.sh
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Rules
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Conditions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| Left operand | Description | Allowed operators | Right operand
 | 
				
			||||||
 | 
					| :--- | :--- | :--- | :---
 | 
				
			||||||
 | 
					| body | Contents of the response | `=`, `!=` | String w/ wildcard
 | 
				
			||||||
 | 
					| body | Contents of the response | `<`, `>` | float
 | 
				
			||||||
 | 
					| status | HTTP response code | `=`, `!=` | String w/ wildcard
 | 
				
			||||||
 | 
					| status | HTTP response code | `<`, `>` | float
 | 
				
			||||||
 | 
					| response_time | HTTP response code | `<`, `>` | duration (e.g. `20s`)
 | 
				
			||||||
 | 
					| header:`<header>` | Response header | `=`, `!=` | String w/ wildcard
 | 
				
			||||||
 | 
					| header:`<header>` | Response header | `<`, `>` | float
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Note that `response_time` can never be higher than the configured `timeout` value.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Examples:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```json
 | 
				
			||||||
 | 
					[
 | 
				
			||||||
 | 
					  {"condition":  "header:X-Test>10", "action":  "..."},
 | 
				
			||||||
 | 
					  {"condition":  "body=*Try again in a few minutes*", "action":  "..."},
 | 
				
			||||||
 | 
					  {"condition":  "response_time>10s", "action":  "..."},
 | 
				
			||||||
 | 
					  {"condition":  "status>500", "action":  "..."},
 | 
				
			||||||
 | 
					  {"condition":  "status=404", "action":  "..."},
 | 
				
			||||||
 | 
					  {"condition":  "status=40*", "action":  "..."}
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Actions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| Action | Description | `arg` value | 
 | 
				
			||||||
 | 
					| :--- | :--- | :--- |
 | 
				
			||||||
 | 
					| should_retry | Override default retry behavior for http errors (by default it retries on 403,408,429,444,499,>500)
 | 
				
			||||||
 | 
					| force_retry | Always retry (Up to retries_hard times)
 | 
				
			||||||
 | 
					| dont_retry | Immediately stop retrying
 | 
				
			||||||
 | 
					| multiply_every | Multiply the current limiter's 'every' value by `arg` | `1.5`(float)
 | 
				
			||||||
 | 
					| set_every | Set the current limiter's 'every' value to `arg` | `10s`(duration)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					In the event of a temporary network error, `should_retry` is ignored (it will always retry unless `dont_retry` is set)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Note that having too many rules for one host might negatively impact performance (especially the `body` condition for large requests)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Sample configuration
 | 
					### Sample configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```json
 | 
					```json
 | 
				
			||||||
@ -67,6 +113,7 @@ level=trace msg=Sleeping wait=433.394361ms
 | 
				
			|||||||
  "wait": "4s",
 | 
					  "wait": "4s",
 | 
				
			||||||
  "multiplier": 2.5,
 | 
					  "multiplier": 2.5,
 | 
				
			||||||
  "retries": 3,
 | 
					  "retries": 3,
 | 
				
			||||||
 | 
					  "retries_hard": 6,
 | 
				
			||||||
  "proxies": [
 | 
					  "proxies": [
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      "name": "squid_P0",
 | 
					      "name": "squid_P0",
 | 
				
			||||||
@ -83,7 +130,7 @@ level=trace msg=Sleeping wait=433.394361ms
 | 
				
			|||||||
      "every": "500ms",
 | 
					      "every": "500ms",
 | 
				
			||||||
      "burst": 25,
 | 
					      "burst": 25,
 | 
				
			||||||
      "headers": {
 | 
					      "headers": {
 | 
				
			||||||
        "User-Agent": "Some user agent",
 | 
					        "User-Agent": "Some user agent for all requests",
 | 
				
			||||||
        "X-Test": "Will be overwritten"
 | 
					        "X-Test": "Will be overwritten"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -94,6 +141,22 @@ level=trace msg=Sleeping wait=433.394361ms
 | 
				
			|||||||
      "headers": {
 | 
					      "headers": {
 | 
				
			||||||
        "X-Test": "Will overwrite default"
 | 
					        "X-Test": "Will overwrite default"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "host": ".s3.amazonaws.com",
 | 
				
			||||||
 | 
					      "every": "2s",
 | 
				
			||||||
 | 
					      "burst": 30,
 | 
				
			||||||
 | 
					      "rules": [
 | 
				
			||||||
 | 
					        {"condition": "status=403", "action": "dont_retry"}
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "host": ".www.instagram.com",
 | 
				
			||||||
 | 
					      "every": "4500ms",
 | 
				
			||||||
 | 
					      "burst": 3,
 | 
				
			||||||
 | 
					      "rules": [
 | 
				
			||||||
 | 
					        {"condition":  "body=*please try again in a few minutes*", "action": "multiply_every", "arg": "2"}
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  ]
 | 
					  ]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										305
									
								
								config.go
									
									
									
									
									
								
							
							
						
						
									
										305
									
								
								config.go
									
									
									
									
									
								
							@ -1,12 +1,18 @@
 | 
				
			|||||||
package main
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"github.com/pkg/errors"
 | 
				
			||||||
 | 
						"github.com/ryanuber/go-glob"
 | 
				
			||||||
	"github.com/sirupsen/logrus"
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
	"golang.org/x/time/rate"
 | 
						"golang.org/x/time/rate"
 | 
				
			||||||
	"io/ioutil"
 | 
						"io/ioutil"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
						"reflect"
 | 
				
			||||||
 | 
						"runtime"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@ -16,7 +22,47 @@ type HostConfig struct {
 | 
				
			|||||||
	EveryStr string            `json:"every"`
 | 
						EveryStr string            `json:"every"`
 | 
				
			||||||
	Burst    int               `json:"burst"`
 | 
						Burst    int               `json:"burst"`
 | 
				
			||||||
	Headers  map[string]string `json:"headers"`
 | 
						Headers  map[string]string `json:"headers"`
 | 
				
			||||||
 | 
						RawRules []*RawHostRule    `json:"rules"`
 | 
				
			||||||
	Every    time.Duration
 | 
						Every    time.Duration
 | 
				
			||||||
 | 
						Rules    []*HostRule
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type RawHostRule struct {
 | 
				
			||||||
 | 
						Condition string `json:"condition"`
 | 
				
			||||||
 | 
						Action    string `json:"action"`
 | 
				
			||||||
 | 
						Arg       string `json:"arg"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type HostRuleAction int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						DontRetry     HostRuleAction = 0
 | 
				
			||||||
 | 
						MultiplyEvery HostRuleAction = 1
 | 
				
			||||||
 | 
						SetEvery      HostRuleAction = 2
 | 
				
			||||||
 | 
						ForceRetry    HostRuleAction = 3
 | 
				
			||||||
 | 
						ShouldRetry   HostRuleAction = 4
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (a HostRuleAction) String() string {
 | 
				
			||||||
 | 
						switch a {
 | 
				
			||||||
 | 
						case DontRetry:
 | 
				
			||||||
 | 
							return "dont_retry"
 | 
				
			||||||
 | 
						case MultiplyEvery:
 | 
				
			||||||
 | 
							return "multiply_every"
 | 
				
			||||||
 | 
						case SetEvery:
 | 
				
			||||||
 | 
							return "set_every"
 | 
				
			||||||
 | 
						case ForceRetry:
 | 
				
			||||||
 | 
							return "force_retry"
 | 
				
			||||||
 | 
						case ShouldRetry:
 | 
				
			||||||
 | 
							return "should_retry"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "???"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type HostRule struct {
 | 
				
			||||||
 | 
						Matches func(r *RequestCtx) bool
 | 
				
			||||||
 | 
						Action  HostRuleAction
 | 
				
			||||||
 | 
						Arg     float64
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ProxyConfig struct {
 | 
					type ProxyConfig struct {
 | 
				
			||||||
@ -30,6 +76,7 @@ var config struct {
 | 
				
			|||||||
	WaitStr       string        `json:"wait"`
 | 
						WaitStr       string        `json:"wait"`
 | 
				
			||||||
	Multiplier    float64       `json:"multiplier"`
 | 
						Multiplier    float64       `json:"multiplier"`
 | 
				
			||||||
	Retries       int           `json:"retries"`
 | 
						Retries       int           `json:"retries"`
 | 
				
			||||||
 | 
						RetriesHard   int           `json:"retries_hard"`
 | 
				
			||||||
	Hosts         []*HostConfig `json:"hosts"`
 | 
						Hosts         []*HostConfig `json:"hosts"`
 | 
				
			||||||
	Proxies       []ProxyConfig `json:"proxies"`
 | 
						Proxies       []ProxyConfig `json:"proxies"`
 | 
				
			||||||
	Wait          int64
 | 
						Wait          int64
 | 
				
			||||||
@ -37,16 +84,202 @@ var config struct {
 | 
				
			|||||||
	DefaultConfig *HostConfig
 | 
						DefaultConfig *HostConfig
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func loadConfig() {
 | 
					func parseRule(raw *RawHostRule) (*HostRule, error) {
 | 
				
			||||||
 | 
						//TODO: for the love of god someone please refactor this func
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						rule := &HostRule{}
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch raw.Action {
 | 
				
			||||||
 | 
						case "should_retry":
 | 
				
			||||||
 | 
							rule.Action = ShouldRetry
 | 
				
			||||||
 | 
						case "dont_retry":
 | 
				
			||||||
 | 
							rule.Action = DontRetry
 | 
				
			||||||
 | 
						case "multiply_every":
 | 
				
			||||||
 | 
							rule.Action = MultiplyEvery
 | 
				
			||||||
 | 
							rule.Arg, err = strconv.ParseFloat(raw.Arg, 64)
 | 
				
			||||||
 | 
						case "set_every":
 | 
				
			||||||
 | 
							rule.Action = SetEvery
 | 
				
			||||||
 | 
							var duration time.Duration
 | 
				
			||||||
 | 
							duration, err = time.ParseDuration(raw.Arg)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							rule.Arg = 1 / duration.Seconds()
 | 
				
			||||||
 | 
						case "force_retry":
 | 
				
			||||||
 | 
							rule.Action = ForceRetry
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return nil, errors.Errorf("Invalid argument for action: %s", raw.Action)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case strings.Contains(raw.Condition, "!="):
 | 
				
			||||||
 | 
							op1Str, op2Str := split(raw.Condition, "!=")
 | 
				
			||||||
 | 
							op1Func := parseOperand1(op1Str)
 | 
				
			||||||
 | 
							if op1Func == nil {
 | 
				
			||||||
 | 
								return nil, errors.Errorf("Invalid rule: %s", raw.Condition)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if isGlob(op2Str) {
 | 
				
			||||||
 | 
								rule.Matches = func(ctx *RequestCtx) bool {
 | 
				
			||||||
 | 
									return !glob.Glob(op2Str, op1Func(ctx))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								op2Str = strings.Replace(op2Str, "\\*", "*", -1)
 | 
				
			||||||
 | 
								rule.Matches = func(ctx *RequestCtx) bool {
 | 
				
			||||||
 | 
									return op1Func(ctx) != op2Str
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case strings.Contains(raw.Condition, "="):
 | 
				
			||||||
 | 
							op1Str, op2Str := split(raw.Condition, "=")
 | 
				
			||||||
 | 
							op1Func := parseOperand1(op1Str)
 | 
				
			||||||
 | 
							if op1Func == nil {
 | 
				
			||||||
 | 
								return nil, errors.Errorf("Invalid rule: %s", raw.Condition)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if isGlob(op2Str) {
 | 
				
			||||||
 | 
								rule.Matches = func(ctx *RequestCtx) bool {
 | 
				
			||||||
 | 
									return glob.Glob(op2Str, op1Func(ctx))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								op2Str = strings.Replace(op2Str, "\\*", "*", -1)
 | 
				
			||||||
 | 
								rule.Matches = func(ctx *RequestCtx) bool {
 | 
				
			||||||
 | 
									return op1Func(ctx) == op2Str
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case strings.Contains(raw.Condition, ">"):
 | 
				
			||||||
 | 
							op1Str, op2Str := split(raw.Condition, ">")
 | 
				
			||||||
 | 
							op1Func := parseOperand1(op1Str)
 | 
				
			||||||
 | 
							if op1Func == nil {
 | 
				
			||||||
 | 
								return nil, errors.Errorf("Invalid rule: %s", raw.Condition)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							op2Num, err := parseOperand2(op1Str, op2Str)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							rule.Matches = func(ctx *RequestCtx) bool {
 | 
				
			||||||
 | 
								op1Num, err := strconv.ParseFloat(op1Func(ctx), 64)
 | 
				
			||||||
 | 
								handleRuleErr(err)
 | 
				
			||||||
 | 
								return op1Num > op2Num
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case strings.Contains(raw.Condition, "<"):
 | 
				
			||||||
 | 
							op1Str, op2Str := split(raw.Condition, "<")
 | 
				
			||||||
 | 
							op1Func := parseOperand1(op1Str)
 | 
				
			||||||
 | 
							if op1Func == nil {
 | 
				
			||||||
 | 
								return nil, errors.Errorf("Invalid rule: %s", raw.Condition)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							op2Num, err := parseOperand2(op1Str, op2Str)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							rule.Matches = func(ctx *RequestCtx) bool {
 | 
				
			||||||
 | 
								op1Num, err := strconv.ParseFloat(op1Func(ctx), 64)
 | 
				
			||||||
 | 
								handleRuleErr(err)
 | 
				
			||||||
 | 
								return op1Num < op2Num
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return rule, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func handleRuleErr(err error) {
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logrus.WithError(err).Warn("Error computing rule")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func split(str, subStr string) (string, string) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						str1 := str[:strings.Index(str, subStr)]
 | 
				
			||||||
 | 
						str2 := str[strings.Index(str, subStr)+len(subStr):]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return str1, str2
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func parseOperand2(op1, op2 string) (float64, error) {
 | 
				
			||||||
 | 
						if op1 == "response_time" {
 | 
				
			||||||
 | 
							res, err := time.ParseDuration(op2)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return -1, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return res.Seconds(), nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return strconv.ParseFloat(op2, 64)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func parseOperand1(op string) func(ctx *RequestCtx) string {
 | 
				
			||||||
 | 
						switch {
 | 
				
			||||||
 | 
						case op == "body":
 | 
				
			||||||
 | 
							return func(ctx *RequestCtx) string {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if ctx.Response == nil {
 | 
				
			||||||
 | 
									return ""
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								bodyBytes, err := ioutil.ReadAll(ctx.Response.Body)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return ""
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								err = ctx.Response.Body.Close()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return ""
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								ctx.Response.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return string(bodyBytes)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case op == "status":
 | 
				
			||||||
 | 
							return func(ctx *RequestCtx) string {
 | 
				
			||||||
 | 
								if ctx.Response == nil {
 | 
				
			||||||
 | 
									return ""
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return strconv.Itoa(ctx.Response.StatusCode)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case op == "response_time":
 | 
				
			||||||
 | 
							return func(ctx *RequestCtx) string {
 | 
				
			||||||
 | 
								return strconv.FormatFloat(time.Now().Sub(ctx.RequestTime).Seconds(), 'f', 6, 64)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case strings.HasPrefix(op, "header:"):
 | 
				
			||||||
 | 
							header := op[strings.Index(op, ":")+1:]
 | 
				
			||||||
 | 
							return func(ctx *RequestCtx) string {
 | 
				
			||||||
 | 
								if ctx.Response == nil {
 | 
				
			||||||
 | 
									return ""
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return ctx.Response.Header.Get(header)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func isGlob(op string) bool {
 | 
				
			||||||
 | 
						tmpStr := strings.Replace(op, "\\*", "_", -1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return strings.Contains(tmpStr, "*")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func loadConfig() error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	configFile, err := os.Open("config.json")
 | 
						configFile, err := os.Open("config.json")
 | 
				
			||||||
	handleErr(err)
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	configBytes, err := ioutil.ReadAll(configFile)
 | 
						configBytes, err := ioutil.ReadAll(configFile)
 | 
				
			||||||
	handleErr(err)
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err = json.Unmarshal(configBytes, &config)
 | 
						err = json.Unmarshal(configBytes, &config)
 | 
				
			||||||
	handleErr(err)
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	validateConfig()
 | 
						validateConfig()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -54,19 +287,53 @@ func loadConfig() {
 | 
				
			|||||||
	wait, err := time.ParseDuration(config.WaitStr)
 | 
						wait, err := time.ParseDuration(config.WaitStr)
 | 
				
			||||||
	config.Wait = int64(wait)
 | 
						config.Wait = int64(wait)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, conf := range config.Hosts {
 | 
						for i, conf := range config.Hosts {
 | 
				
			||||||
		if conf.EveryStr == "" {
 | 
							if conf.EveryStr == "" {
 | 
				
			||||||
			conf.Every = config.DefaultConfig.Every
 | 
								// Look 'upwards' for every
 | 
				
			||||||
 | 
								for _, prevConf := range config.Hosts[:i] {
 | 
				
			||||||
 | 
									if glob.Glob(prevConf.Host, conf.Host) {
 | 
				
			||||||
 | 
										conf.Every = prevConf.Every
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			conf.Every, err = time.ParseDuration(conf.EveryStr)
 | 
								conf.Every, err = time.ParseDuration(conf.EveryStr)
 | 
				
			||||||
			handleErr(err)
 | 
								handleErr(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if config.DefaultConfig != nil && conf.Burst == 0 {
 | 
							if conf.Burst == 0 {
 | 
				
			||||||
			conf.Burst = config.DefaultConfig.Burst
 | 
								// Look 'upwards' for burst
 | 
				
			||||||
 | 
								for _, prevConf := range config.Hosts[:i] {
 | 
				
			||||||
 | 
									if glob.Glob(prevConf.Host, conf.Host) {
 | 
				
			||||||
 | 
										conf.Burst = prevConf.Burst
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							if conf.Burst == 0 {
 | 
				
			||||||
 | 
								return errors.Errorf("Burst must be > 0 (Host: %s)", conf.Host)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, rawRule := range conf.RawRules {
 | 
				
			||||||
 | 
								r, err := parseRule(rawRule)
 | 
				
			||||||
 | 
								handleErr(err)
 | 
				
			||||||
 | 
								conf.Rules = append(conf.Rules, r)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								logrus.WithFields(logrus.Fields{
 | 
				
			||||||
 | 
									"arg":       r.Arg,
 | 
				
			||||||
 | 
									"action":    r.Action,
 | 
				
			||||||
 | 
									"matchFunc": runtime.FuncForPC(reflect.ValueOf(r.Matches).Pointer()).Name(),
 | 
				
			||||||
 | 
								}).Info("Rule")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							logrus.WithFields(logrus.Fields{
 | 
				
			||||||
 | 
								"every":   conf.Every,
 | 
				
			||||||
 | 
								"burst":   conf.Burst,
 | 
				
			||||||
 | 
								"headers": conf.Headers,
 | 
				
			||||||
 | 
								"host":    conf.Host,
 | 
				
			||||||
 | 
							}).Info("Host")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func validateConfig() {
 | 
					func validateConfig() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -91,18 +358,28 @@ func validateConfig() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func applyConfig(proxy *Proxy) {
 | 
					func applyConfig(proxy *Proxy) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, conf := range config.Hosts {
 | 
						//Reverse order
 | 
				
			||||||
		proxy.Limiters[conf.Host] = &ExpiringLimiter{
 | 
						for i := len(config.Hosts) - 1; i >= 0; i-- {
 | 
				
			||||||
			rate.NewLimiter(rate.Every(conf.Every), conf.Burst),
 | 
					
 | 
				
			||||||
			time.Now(),
 | 
							conf := config.Hosts[i]
 | 
				
			||||||
		}
 | 
					
 | 
				
			||||||
 | 
							proxy.Limiters = append(proxy.Limiters, &ExpiringLimiter{
 | 
				
			||||||
 | 
								HostGlob:  conf.Host,
 | 
				
			||||||
 | 
								IsGlob:    isGlob(conf.Host),
 | 
				
			||||||
 | 
								Limiter:   rate.NewLimiter(rate.Every(conf.Every), conf.Burst),
 | 
				
			||||||
 | 
								LastRead:  time.Now(),
 | 
				
			||||||
 | 
								CanDelete: false,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (b *Balancer) reloadConfig() {
 | 
					func (b *Balancer) reloadConfig() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	b.proxyMutex.Lock()
 | 
						b.proxyMutex.Lock()
 | 
				
			||||||
	loadConfig()
 | 
						err := loadConfig()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if b.proxies != nil {
 | 
						if b.proxies != nil {
 | 
				
			||||||
		b.proxies = b.proxies[:0]
 | 
							b.proxies = b.proxies[:0]
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										17
									
								
								config.json
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								config.json
									
									
									
									
									
								
							@ -4,6 +4,7 @@
 | 
				
			|||||||
  "wait": "4s",
 | 
					  "wait": "4s",
 | 
				
			||||||
  "multiplier": 2.5,
 | 
					  "multiplier": 2.5,
 | 
				
			||||||
  "retries": 3,
 | 
					  "retries": 3,
 | 
				
			||||||
 | 
					  "retries_hard": 6,
 | 
				
			||||||
  "proxies": [
 | 
					  "proxies": [
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      "name": "p0",
 | 
					      "name": "p0",
 | 
				
			||||||
@ -20,7 +21,10 @@
 | 
				
			|||||||
        "Cache-Control": "max-age=0",
 | 
					        "Cache-Control": "max-age=0",
 | 
				
			||||||
        "Connection": "keep-alive",
 | 
					        "Connection": "keep-alive",
 | 
				
			||||||
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0"
 | 
					        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0"
 | 
				
			||||||
      }
 | 
					      },
 | 
				
			||||||
 | 
					      "rules": [
 | 
				
			||||||
 | 
					        {"condition":  "response_time>10s", "action":  "dont_retry"}
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      "host": "*.reddit.com",
 | 
					      "host": "*.reddit.com",
 | 
				
			||||||
@ -36,13 +40,9 @@
 | 
				
			|||||||
      "host": ".pbs.twimg.com",
 | 
					      "host": ".pbs.twimg.com",
 | 
				
			||||||
      "every": "125ms"
 | 
					      "every": "125ms"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
      "host": "*.cdninstagram",
 | 
					 | 
				
			||||||
      "every": "250ms"
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      "host": ".www.instagram.com",
 | 
					      "host": ".www.instagram.com",
 | 
				
			||||||
      "every": "30s",
 | 
					      "every": "4500ms",
 | 
				
			||||||
      "burst": 3
 | 
					      "burst": 3
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@ -53,7 +53,10 @@
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
      "host": ".s3.amazonaws.com",
 | 
					      "host": ".s3.amazonaws.com",
 | 
				
			||||||
      "every": "10s",
 | 
					      "every": "10s",
 | 
				
			||||||
      "burst": 3
 | 
					      "burst": 1,
 | 
				
			||||||
 | 
					      "rules": [
 | 
				
			||||||
 | 
					        {"condition":  "status=403", "action":  "dont_retry"}
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  ]
 | 
					  ]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										18
									
								
								gc.go
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								gc.go
									
									
									
									
									
								
							@ -42,32 +42,20 @@ func cleanExpiredLimits(proxy *Proxy) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	const ttl = time.Hour
 | 
						const ttl = time.Hour
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	limits := make(map[string]*ExpiringLimiter, 0)
 | 
						var limits []*ExpiringLimiter
 | 
				
			||||||
	now := time.Now()
 | 
						now := time.Now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for host, limiter := range proxy.Limiters {
 | 
						for host, limiter := range proxy.Limiters {
 | 
				
			||||||
		if now.Sub(limiter.LastRead) > ttl && shouldPruneLimiter(host) {
 | 
							if now.Sub(limiter.LastRead) > ttl && limiter.CanDelete {
 | 
				
			||||||
			logrus.WithFields(logrus.Fields{
 | 
								logrus.WithFields(logrus.Fields{
 | 
				
			||||||
				"proxy":     proxy.Name,
 | 
									"proxy":     proxy.Name,
 | 
				
			||||||
				"limiter":   host,
 | 
									"limiter":   host,
 | 
				
			||||||
				"last_read": now.Sub(limiter.LastRead),
 | 
									"last_read": now.Sub(limiter.LastRead),
 | 
				
			||||||
			}).Trace("Pruning limiter")
 | 
								}).Trace("Pruning limiter")
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			limits[host] = limiter
 | 
								limits = append(limits, limiter)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	proxy.Limiters = limits
 | 
						proxy.Limiters = limits
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func shouldPruneLimiter(host string) bool {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Don't remove hosts that are coming from the config
 | 
					 | 
				
			||||||
	for _, conf := range config.Hosts {
 | 
					 | 
				
			||||||
		if conf.Host == host {
 | 
					 | 
				
			||||||
			return false
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return true
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										146
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										146
									
								
								main.go
									
									
									
									
									
								
							@ -23,6 +23,9 @@ type Balancer struct {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ExpiringLimiter struct {
 | 
					type ExpiringLimiter struct {
 | 
				
			||||||
 | 
						HostGlob  string
 | 
				
			||||||
 | 
						IsGlob    bool
 | 
				
			||||||
 | 
						CanDelete bool
 | 
				
			||||||
	Limiter   *rate.Limiter
 | 
						Limiter   *rate.Limiter
 | 
				
			||||||
	LastRead  time.Time
 | 
						LastRead  time.Time
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -30,11 +33,16 @@ type ExpiringLimiter struct {
 | 
				
			|||||||
type Proxy struct {
 | 
					type Proxy struct {
 | 
				
			||||||
	Name        string
 | 
						Name        string
 | 
				
			||||||
	Url         *url.URL
 | 
						Url         *url.URL
 | 
				
			||||||
	Limiters    map[string]*ExpiringLimiter
 | 
						Limiters    []*ExpiringLimiter
 | 
				
			||||||
	HttpClient  *http.Client
 | 
						HttpClient  *http.Client
 | 
				
			||||||
	Connections int
 | 
						Connections int
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type RequestCtx struct {
 | 
				
			||||||
 | 
						RequestTime time.Time
 | 
				
			||||||
 | 
						Response    *http.Response
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ByConnectionCount []*Proxy
 | 
					type ByConnectionCount []*Proxy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (a ByConnectionCount) Len() int {
 | 
					func (a ByConnectionCount) Len() int {
 | 
				
			||||||
@ -51,8 +59,13 @@ func (a ByConnectionCount) Less(i, j int) bool {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (p *Proxy) getLimiter(host string) *rate.Limiter {
 | 
					func (p *Proxy) getLimiter(host string) *rate.Limiter {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for hostGlob, limiter := range p.Limiters {
 | 
						for _, limiter := range p.Limiters {
 | 
				
			||||||
		if glob.Glob(hostGlob, host) {
 | 
							if limiter.IsGlob {
 | 
				
			||||||
 | 
								if glob.Glob(limiter.HostGlob, host) {
 | 
				
			||||||
 | 
									limiter.LastRead = time.Now()
 | 
				
			||||||
 | 
									return limiter.Limiter
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else if limiter.HostGlob == host {
 | 
				
			||||||
			limiter.LastRead = time.Now()
 | 
								limiter.LastRead = time.Now()
 | 
				
			||||||
			return limiter.Limiter
 | 
								return limiter.Limiter
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -65,14 +78,18 @@ func (p *Proxy) getLimiter(host string) *rate.Limiter {
 | 
				
			|||||||
func (p *Proxy) makeNewLimiter(host string) *ExpiringLimiter {
 | 
					func (p *Proxy) makeNewLimiter(host string) *ExpiringLimiter {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	newExpiringLimiter := &ExpiringLimiter{
 | 
						newExpiringLimiter := &ExpiringLimiter{
 | 
				
			||||||
 | 
							CanDelete: false,
 | 
				
			||||||
 | 
							HostGlob:  host,
 | 
				
			||||||
 | 
							IsGlob:    false,
 | 
				
			||||||
		LastRead:  time.Now(),
 | 
							LastRead:  time.Now(),
 | 
				
			||||||
		Limiter:   rate.NewLimiter(rate.Every(config.DefaultConfig.Every), config.DefaultConfig.Burst),
 | 
							Limiter:   rate.NewLimiter(rate.Every(config.DefaultConfig.Every), config.DefaultConfig.Burst),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	p.Limiters[host] = newExpiringLimiter
 | 
						p.Limiters = append([]*ExpiringLimiter{newExpiringLimiter}, p.Limiters...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	logrus.WithFields(logrus.Fields{
 | 
						logrus.WithFields(logrus.Fields{
 | 
				
			||||||
		"host":  host,
 | 
							"host":  host,
 | 
				
			||||||
 | 
							"every": config.DefaultConfig.Every,
 | 
				
			||||||
	}).Trace("New limiter")
 | 
						}).Trace("New limiter")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return newExpiringLimiter
 | 
						return newExpiringLimiter
 | 
				
			||||||
@ -96,7 +113,18 @@ func (b *Balancer) chooseProxy() *Proxy {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	sort.Sort(ByConnectionCount(b.proxies))
 | 
						sort.Sort(ByConnectionCount(b.proxies))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	p0 := b.proxies[0]
 | 
						proxyWithLeastConns := b.proxies[0]
 | 
				
			||||||
 | 
						proxiesWithSameConnCount := b.getProxiesWithSameConnCountAs(proxyWithLeastConns)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(proxiesWithSameConnCount) > 1 {
 | 
				
			||||||
 | 
							return proxiesWithSameConnCount[rand.Intn(len(proxiesWithSameConnCount))]
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return proxyWithLeastConns
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *Balancer) getProxiesWithSameConnCountAs(p0 *Proxy) []*Proxy {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	proxiesWithSameConnCount := make([]*Proxy, 0)
 | 
						proxiesWithSameConnCount := make([]*Proxy, 0)
 | 
				
			||||||
	for _, p := range b.proxies {
 | 
						for _, p := range b.proxies {
 | 
				
			||||||
		if p.Connections != p0.Connections {
 | 
							if p.Connections != p0.Connections {
 | 
				
			||||||
@ -104,12 +132,7 @@ func (b *Balancer) chooseProxy() *Proxy {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		proxiesWithSameConnCount = append(proxiesWithSameConnCount, p)
 | 
							proxiesWithSameConnCount = append(proxiesWithSameConnCount, p)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						return proxiesWithSameConnCount
 | 
				
			||||||
	if len(proxiesWithSameConnCount) > 1 {
 | 
					 | 
				
			||||||
		return proxiesWithSameConnCount[rand.Intn(len(proxiesWithSameConnCount))]
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		return p0
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func New() *Balancer {
 | 
					func New() *Balancer {
 | 
				
			||||||
@ -157,21 +180,61 @@ func New() *Balancer {
 | 
				
			|||||||
	return balancer
 | 
						return balancer
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func applyHeaders(r *http.Request) *http.Request {
 | 
					func getConfsMatchingRequest(r *http.Request) []*HostConfig {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	sHost := simplifyHost(r.Host)
 | 
						sHost := simplifyHost(r.Host)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						configs := make([]*HostConfig, 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, conf := range config.Hosts {
 | 
						for _, conf := range config.Hosts {
 | 
				
			||||||
		if glob.Glob(conf.Host, sHost) {
 | 
							if glob.Glob(conf.Host, sHost) {
 | 
				
			||||||
 | 
								configs = append(configs, conf)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return configs
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func applyHeaders(r *http.Request, configs []*HostConfig) *http.Request {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, conf := range configs {
 | 
				
			||||||
		for k, v := range conf.Headers {
 | 
							for k, v := range conf.Headers {
 | 
				
			||||||
			r.Header.Set(k, v)
 | 
								r.Header.Set(k, v)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return r
 | 
						return r
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func computeRules(ctx *RequestCtx, configs []*HostConfig) (dontRetry, forceRetry bool,
 | 
				
			||||||
 | 
						limitMultiplier, newLimit float64, shouldRetry bool) {
 | 
				
			||||||
 | 
						dontRetry = false
 | 
				
			||||||
 | 
						forceRetry = false
 | 
				
			||||||
 | 
						shouldRetry = false
 | 
				
			||||||
 | 
						limitMultiplier = 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, conf := range configs {
 | 
				
			||||||
 | 
							for _, rule := range conf.Rules {
 | 
				
			||||||
 | 
								if rule.Matches(ctx) {
 | 
				
			||||||
 | 
									switch rule.Action {
 | 
				
			||||||
 | 
									case DontRetry:
 | 
				
			||||||
 | 
										dontRetry = true
 | 
				
			||||||
 | 
									case MultiplyEvery:
 | 
				
			||||||
 | 
										limitMultiplier = rule.Arg
 | 
				
			||||||
 | 
									case SetEvery:
 | 
				
			||||||
 | 
										newLimit = rule.Arg
 | 
				
			||||||
 | 
									case ForceRetry:
 | 
				
			||||||
 | 
										forceRetry = true
 | 
				
			||||||
 | 
									case ShouldRetry:
 | 
				
			||||||
 | 
										shouldRetry = true
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (p *Proxy) processRequest(r *http.Request) (*http.Response, error) {
 | 
					func (p *Proxy) processRequest(r *http.Request) (*http.Response, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	p.Connections += 1
 | 
						p.Connections += 1
 | 
				
			||||||
@ -179,25 +242,41 @@ func (p *Proxy) processRequest(r *http.Request) (*http.Response, error) {
 | 
				
			|||||||
		p.Connections -= 1
 | 
							p.Connections -= 1
 | 
				
			||||||
	}()
 | 
						}()
 | 
				
			||||||
	retries := 0
 | 
						retries := 0
 | 
				
			||||||
 | 
						additionalRetries := 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	p.waitRateLimit(r)
 | 
						configs := getConfsMatchingRequest(r)
 | 
				
			||||||
	proxyReq := applyHeaders(cloneRequest(r))
 | 
						sHost := simplifyHost(r.Host)
 | 
				
			||||||
 | 
						limiter := p.getLimiter(sHost)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						proxyReq := applyHeaders(cloneRequest(r), configs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for {
 | 
						for {
 | 
				
			||||||
 | 
							p.waitRateLimit(limiter)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if retries >= config.Retries {
 | 
							if retries >= config.Retries+additionalRetries || retries > config.RetriesHard {
 | 
				
			||||||
			return nil, errors.Errorf("giving up after %d retries", config.Retries)
 | 
								return nil, errors.Errorf("giving up after %d retries", retries)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		resp, err := p.HttpClient.Do(proxyReq)
 | 
							ctx := &RequestCtx{
 | 
				
			||||||
 | 
								RequestTime: time.Now(),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							var err error
 | 
				
			||||||
 | 
							ctx.Response, err = p.HttpClient.Do(proxyReq)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			if isPermanentError(err) {
 | 
								if isPermanentError(err) {
 | 
				
			||||||
				return nil, err
 | 
									return nil, err
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			wait := waitTime(retries)
 | 
								dontRetry, forceRetry, limitMultiplier, newLimit, _ := computeRules(ctx, configs)
 | 
				
			||||||
 | 
								if forceRetry {
 | 
				
			||||||
 | 
									additionalRetries += 1
 | 
				
			||||||
 | 
								} else if dontRetry {
 | 
				
			||||||
 | 
									return nil, errors.Errorf("Applied dont_retry rule for (%s)", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								p.applyLimiterRules(newLimit, limiter, limitMultiplier)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								wait := waitTime(retries)
 | 
				
			||||||
			logrus.WithError(err).WithFields(logrus.Fields{
 | 
								logrus.WithError(err).WithFields(logrus.Fields{
 | 
				
			||||||
				"wait": wait,
 | 
									"wait": wait,
 | 
				
			||||||
			}).Trace("Temporary error during request")
 | 
								}).Trace("Temporary error during request")
 | 
				
			||||||
@ -207,27 +286,45 @@ func (p *Proxy) processRequest(r *http.Request) (*http.Response, error) {
 | 
				
			|||||||
			continue
 | 
								continue
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if isHttpSuccessCode(resp.StatusCode) {
 | 
							// Compute rules
 | 
				
			||||||
 | 
							dontRetry, forceRetry, limitMultiplier, newLimit, shouldRetry := computeRules(ctx, configs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			return resp, nil
 | 
							if forceRetry {
 | 
				
			||||||
		} else if shouldRetryHttpCode(resp.StatusCode) {
 | 
								additionalRetries += 1
 | 
				
			||||||
 | 
							} else if dontRetry {
 | 
				
			||||||
 | 
								return nil, errors.Errorf("Applied dont_retry rule")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							p.applyLimiterRules(newLimit, limiter, limitMultiplier)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if isHttpSuccessCode(ctx.Response.StatusCode) {
 | 
				
			||||||
 | 
								return ctx.Response, nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							} else if forceRetry || shouldRetry || shouldRetryHttpCode(ctx.Response.StatusCode) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			wait := waitTime(retries)
 | 
								wait := waitTime(retries)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			logrus.WithFields(logrus.Fields{
 | 
								logrus.WithFields(logrus.Fields{
 | 
				
			||||||
				"wait":   wait,
 | 
									"wait":   wait,
 | 
				
			||||||
				"status": resp.StatusCode,
 | 
									"status": ctx.Response.StatusCode,
 | 
				
			||||||
			}).Trace("HTTP error during request")
 | 
								}).Trace("HTTP error during request")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			time.Sleep(wait)
 | 
								time.Sleep(wait)
 | 
				
			||||||
			retries += 1
 | 
								retries += 1
 | 
				
			||||||
			continue
 | 
								continue
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			return nil, errors.Errorf("HTTP error: %d", resp.StatusCode)
 | 
								return nil, errors.Errorf("HTTP error: %d", ctx.Response.StatusCode)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p *Proxy) applyLimiterRules(newLimit float64, limiter *rate.Limiter, limitMultiplier float64) {
 | 
				
			||||||
 | 
						if newLimit != 0 {
 | 
				
			||||||
 | 
							limiter.SetLimit(rate.Limit(newLimit))
 | 
				
			||||||
 | 
						} else if limitMultiplier != 1 {
 | 
				
			||||||
 | 
							limiter.SetLimit(limiter.Limit() * rate.Limit(1/limitMultiplier))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (b *Balancer) Run() {
 | 
					func (b *Balancer) Run() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	//b.Verbose = true
 | 
						//b.Verbose = true
 | 
				
			||||||
@ -285,7 +382,6 @@ func NewProxy(name, stringUrl string) (*Proxy, error) {
 | 
				
			|||||||
		Name:       name,
 | 
							Name:       name,
 | 
				
			||||||
		Url:        parsedUrl,
 | 
							Url:        parsedUrl,
 | 
				
			||||||
		HttpClient: httpClient,
 | 
							HttpClient: httpClient,
 | 
				
			||||||
		Limiters:   make(map[string]*ExpiringLimiter),
 | 
					 | 
				
			||||||
	}, nil
 | 
						}, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										12
									
								
								retry.go
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								retry.go
									
									
									
									
									
								
							@ -3,10 +3,10 @@ package main
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"github.com/sirupsen/logrus"
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
 | 
						"golang.org/x/time/rate"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"math"
 | 
						"math"
 | 
				
			||||||
	"net"
 | 
						"net"
 | 
				
			||||||
	"net/http"
 | 
					 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"syscall"
 | 
						"syscall"
 | 
				
			||||||
@ -80,17 +80,9 @@ func waitTime(retries int) time.Duration {
 | 
				
			|||||||
	return time.Duration(config.Wait * int64(math.Pow(config.Multiplier, float64(retries))))
 | 
						return time.Duration(config.Wait * int64(math.Pow(config.Multiplier, float64(retries))))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (p *Proxy) waitRateLimit(r *http.Request) {
 | 
					func (p *Proxy) waitRateLimit(limiter *rate.Limiter) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	sHost := simplifyHost(r.Host)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	limiter := p.getLimiter(sHost)
 | 
					 | 
				
			||||||
	reservation := limiter.Reserve()
 | 
						reservation := limiter.Reserve()
 | 
				
			||||||
	if !reservation.OK() {
 | 
					 | 
				
			||||||
		logrus.WithFields(logrus.Fields{
 | 
					 | 
				
			||||||
			"host": sHost,
 | 
					 | 
				
			||||||
		}).Warn("Could not get reservation, make sure that burst is > 0")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	delay := reservation.Delay()
 | 
						delay := reservation.Delay()
 | 
				
			||||||
	if delay > 0 {
 | 
						if delay > 0 {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										17
									
								
								test/web.py
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								test/web.py
									
									
									
									
									
								
							@ -1,6 +1,7 @@
 | 
				
			|||||||
from flask import Flask, Response
 | 
					 | 
				
			||||||
import time
 | 
					import time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from flask import Flask, Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app = Flask(__name__)
 | 
					app = Flask(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -10,6 +11,18 @@ def slow():
 | 
				
			|||||||
    return "Hello World!"
 | 
					    return "Hello World!"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route("/echo/<text>")
 | 
				
			||||||
 | 
					def echo(text):
 | 
				
			||||||
 | 
					    return text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.route("/echoh/<text>")
 | 
				
			||||||
 | 
					def echoh(text):
 | 
				
			||||||
 | 
					    return Response(response="see X-Test header", status=404, headers={
 | 
				
			||||||
 | 
					        "X-Test": text,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.route("/500")
 | 
					@app.route("/500")
 | 
				
			||||||
def e500():
 | 
					def e500():
 | 
				
			||||||
    return Response(status=500)
 | 
					    return Response(status=500)
 | 
				
			||||||
@ -22,7 +35,7 @@ def e404():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@app.route("/403")
 | 
					@app.route("/403")
 | 
				
			||||||
def e403():
 | 
					def e403():
 | 
				
			||||||
    return Response(status=404)
 | 
					    return Response(status=403)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == "__main__":
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user