merge: tiered proxy with circuit breaker for Playtomic extractor
This commit is contained in:
100
.env.dev.sops
100
.env.dev.sops
@@ -1,72 +1,74 @@
|
|||||||
#ENC[AES256_GCM,data:rfm9xw==,iv:yWV+DjVlLNdDXw8brZZ98NGMr5pF88Oy14laCyF9XSk=,tag:EKvfFOCjrJD8NTQ/gOym7A==,type:comment]
|
#ENC[AES256_GCM,data:tO7tjA==,iv:LuaS9vf2rGblW1DTmC/Ih2YmKMCM4yslxCjh5tA9Y2g=,tag:Pj1QU5RWt6g10pCyhLDSkw==,type:comment]
|
||||||
APP_NAME=ENC[AES256_GCM,data:H4Ho9hHoL4Fo+4c=,iv:hBnuls1xYBtHMxU/womw+Om3JR0yrKXp7+VeiLcZiyM=,tag:oKJiE3VekDwMjpF+evoumQ==,type:str]
|
APP_NAME=ENC[AES256_GCM,data:C+gQ1rAnQU7OyOc=,iv:zHplisKDP20NKHjkPMx//z9xtHj/FQadm7lK+URboEo=,tag:cY9lKxBtm8QdGtEi9L/bPA==,type:str]
|
||||||
SECRET_KEY=ENC[AES256_GCM,data:+5bv1jlS+1DnmKxVxebcdJ7+ADJjvgk3hOqUM5LnB/0=,iv:Y9obfg6ttf12J6L3hgVJr1S4tJoayFHpp2zfUgT1Vek=,tag:RWN2OMZqpNm0uY4YqV9FZg==,type:str]
|
SECRET_KEY=ENC[AES256_GCM,data:8J1enTv/0q4w+IZUl6F9A/35HxtVAj1KwL08KpAjuAk=,iv:eu2Q8RJ8rDAdABsQRPauFRhOviEGVNtZwCBhM3wKp7o=,tag:NDLHZn5h9lg/91+aoQlKyQ==,type:str]
|
||||||
BASE_URL=ENC[AES256_GCM,data:DSgPTAuGfA9/ntDJ5JT34zVbJush,iv:Lh6vcDVfPfPBi0Bwd34h2CQX+D8bxqWF8O47Oid8EHg=,tag:PoP8DUX+GsAmc7Ntweeing==,type:str]
|
BASE_URL=ENC[AES256_GCM,data:GSKgprsXcZnYd88t0Kj8ahG2kYg8,iv:dBea81bLhWjtYRVOJGdou+y9WiVUdi6Mvz5QdqaR5Ok=,tag:GhH9veEOHr9wR6YjepbqVQ==,type:str]
|
||||||
DEBUG=ENC[AES256_GCM,data:ibm6FA==,iv:nhDzB8x3pe6ehhU69S1ZN+cNN7Cchj7MK+8NUea3Zug=,tag:kcL4esX+uPNdifenuW0TMQ==,type:str]
|
DEBUG=ENC[AES256_GCM,data:zy3fFw==,iv:D7eJH06aqgwfVK56I1B7MkBcj/NZ8bU1E4Q8ag3gr5w=,tag:OYx2XqxpkDdKP+Si/If6Wg==,type:str]
|
||||||
#ENC[AES256_GCM,data:LdfNHhD4n53JP3blJVGX7lfg2DuaUsFp0p0mI0SPjCOlgJI661jGXjSpvtZP+3Fh7g09KzUNjlhFuugR7082u5WRiQx6ks2BKA==,iv:lHqw7UTr68hZLYyYYbJuB/Mqfyds87NOPl0yE5x4eyU=,tag:rDrOSOqyBIPFq1BC4Ktw8A==,type:comment]
|
#ENC[AES256_GCM,data:RLUbwoSZS+C0Y9+00yiY2RJJp0MRfP2SxLCUw4TmScCqRMq/OAB54Zb10B8j4asqpOPiIl9iOTgZIq3o9JuEXWmc35c9axITfw==,iv:a8KPtv1UKjPPnNcNIKESMugmhW5jzB48ww9ohVjOA9g=,tag:bFaAuQk9VWiuXrG2NzaaJw==,type:comment]
|
||||||
ADMIN_EMAILS=ENC[AES256_GCM,data:0jOhhL5ncjyx7c3hGg==,iv:UdU6U7Qz50KL+Aa1UPo1Vvo0Rhb5aT0MdxN2sFW/sMc=,tag:3LhGEXuXTu2mtMJbnFFsSw==,type:str]
|
ADMIN_EMAILS=ENC[AES256_GCM,data:W0fdsQZCiU/re+wCBA==,iv:aExW76GQodDwAsOk3Sh0R7CRHCf62duDRAkdUe+dobY=,tag:DaPw2bUp3NNMPAI8DuBLcA==,type:str]
|
||||||
#ENC[AES256_GCM,data:AyuwH3wRLrh7,iv:4A/7vGSqb7CVLePYrgKqGJIz1hqJwC0v5ikKtOhMLUM=,tag:ADfmbAXEZIF8HObsTM3DEg==,type:comment]
|
#ENC[AES256_GCM,data:iH5Wz+Eb9/13,iv:6GncS8xJ1IxdGNtwj6MSL5XGiiULImuUVEi7l5m3LB4=,tag:ri93HPghLkvHezMzrg7pwA==,type:comment]
|
||||||
DATABASE_PATH=ENC[AES256_GCM,data:zLXck4opQIMGFqc=,iv:mqX3ONrD/hph6teavhSh9m30FAR3hIxQdeeJb4SnOR4=,tag:LgT7C816/wQlYHECZ+1gww==,type:str]
|
DATABASE_PATH=ENC[AES256_GCM,data:LM5Ya/grtmCyoZs=,iv:B+hSWR57zebC3z4BOlcL62DSugmQLqfOM6k9dl2qT4Y=,tag:pKxijkQugOIyEJvVtiearQ==,type:str]
|
||||||
#ENC[AES256_GCM,data:wiAYWd0=,iv:ngyBfrG1QEBh5TXulXlCKSuzRccFhxYs8GPozCz5Uqo=,tag:kdwHR1nQm0bmTbbTrEUqDg==,type:comment]
|
#ENC[AES256_GCM,data:WUeUFso=,iv:iR8B4lq4ZP96+uAduwRXT9B3XU3DsIwiQSU/Nhj7PL4=,tag:jqladSMwaJtoPxvCjrfcLQ==,type:comment]
|
||||||
MAGIC_LINK_EXPIRY_MINUTES=ENC[AES256_GCM,data:Tvc=,iv:eV62HtqgApJXdiHTWLeWj+ESCK3GK4OyHmSgNd6gsxw=,tag:TJAH5lUx/XWKtDu2Vt/mBw==,type:str]
|
MAGIC_LINK_EXPIRY_MINUTES=ENC[AES256_GCM,data:Wfk=,iv:ntrM1YPNh1N10IJfOv5oYz5ndJykriNNPierlnjC8H8=,tag:ibWA++PyqptDNhUAiC8yvQ==,type:str]
|
||||||
SESSION_LIFETIME_DAYS=ENC[AES256_GCM,data:3gk=,iv:gtuxbN4TPF6kuEr3W8WVxH9cDYl4KyYqFpUrIPCjalo=,tag:QE3OBTJoQfG2GcD/UXBVBw==,type:str]
|
SESSION_LIFETIME_DAYS=ENC[AES256_GCM,data:QMg=,iv:ywt+hBTfLss2CfJ6beBegEdbw/qDw3HEuZigVe6/37w=,tag:vTtukJdxr6QsBQ0WyjjHGA==,type:str]
|
||||||
#ENC[AES256_GCM,data:dEwIOVzHW+whu48/e81j,iv:v9Du6iruCsArD8F/JCf/xy9xxzdV23wCnfHCkuIgPY4=,tag:w7RVK4a0pxmkNDwLTn3mRA==,type:comment]
|
#ENC[AES256_GCM,data:9IbZDceOMDVQ+buGmgKk,iv:z7JbXd6ygsgK1pf+bWf18Fjwu/aaPnMpXV1PwzNU/Qo=,tag:71XQhXEES5uDR2zmztlsJQ==,type:comment]
|
||||||
#ENC[AES256_GCM,data:+aUQXMWkNNHvFcINaDzgi85NLBsSHISWr3oAkB5qJwqKv+uC+MtqkQ2rVaDbfBAfKUKU6r64ShcWiFc/p7ikNEIaeVQYNK8JQ+Tn+A==,iv:w7G2AF3FPh7Qe++HhFuuLQjZB4vrcrG0uI37L2rXuDA=,tag:zU0czMUSD0qKIT8sLKviWA==,type:comment]
|
#ENC[AES256_GCM,data:5jXlwMXRE8S6Jx/+8pd1zwiYfMDPrF2p2vN4phAF3dMvzTAl2UECbb+Uq3p01zC/jc5kpx82kVGmjrigQbFRUINNk10IyHdb9ywpiQ==,iv:mI44N+RvStxDXjU9SIzyt+WqVPVoXroe/Uhtm6B38ck=,tag:VUysMS9Zv+zSIoghLlPXSg==,type:comment]
|
||||||
#
|
#
|
||||||
#ENC[AES256_GCM,data:yHT0cngMLYrKnRX+theDXOQP7Y8jVK2f/2dwbHRq8Z5spK2IUsOChU2WYpuGWFqMMkXPwjiy9Oe7ABArRdxfX849gwuhXt9Y+re45N6o3Wo=,iv:u3TWONVcLrKVAypavvk7ioTLePlqdJByjxOwU128ubU=,tag:lfsQxzorm/d+bWQAXUK2Qg==,type:comment]
|
#ENC[AES256_GCM,data:cDhpSGp27nwp7pafyktXVjAH0DogxTlSJ4RHR1iufQwIv30PCRlNmicBGeS2+N3rvKQj1X+oAabYFYXsmP6O9rVXRsqvo380fLhd0F6fzxM=,iv:X63vVcfNYOSnhKukE16RlbzzZkvk1n92Ujups4UL+Ns=,tag:jg2dextzUmUeJk7z+fZjow==,type:comment]
|
||||||
#ENC[AES256_GCM,data:ua/erKnlXzB+Wae1Jwdr4Xg0Cy3xIiaEpPJefBq1eEOm54ZTNeqZGFfYS7LuijgueuOdq/Bmacq56sfNbiEEbUeehf5f,iv:GWuX4YPyG/9KBdK5RNrH+hFB5QXc+Ep8Ao2mYm6/EFU=,tag:dTPTsWaloxADlplot5SNfw==,type:comment]
|
#ENC[AES256_GCM,data:rBOS1sz/zIaXT+Bc0XvuxgSbr6kgjrtxowCp3TTRq510YESXnY6w8v++0q89lLaEOaBMEfKvehscrGeI9bp4a+g0FA4p,iv:vPDVzz81rES+EmIzWo36vhAOipHMUhyTQJvexCWQvxY=,tag:uhCJgZCU3GrM5n98be/v3A==,type:comment]
|
||||||
#ENC[AES256_GCM,data:K5FoYsnz7dZBR/HZh4KX93WipQDQpvUP+o3xDUNALCm2zZbtQ8pPmTwbug7NpJnrm75MGkI=,iv:pxm+cMZhJOzI5Uys25JRL2g2allAHG5v6VFqNpyvtrI=,tag:GTy5/FtmT/sqADxoX3Yg1g==,type:comment]
|
#ENC[AES256_GCM,data:b4xnKixaM9wGxcK+Q74+rs9jZXWkmAufUrYGOrjcd0dR8UuZPKsEq2m336O+rl3AnOH44AI=,iv:l+iGNfUq3007Mdb1A+PFYMKzPLJyRJLN+W7Ew5026xg=,tag:BGODJsjM6dMEnVTCD8OQ+w==,type:comment]
|
||||||
#ENC[AES256_GCM,data:Apk0ukZWg1ZguSFLaRmjDq4hw+RQc1CnSqcJAybC7m+SDW0nEkMY953nVkqY7oDvKeuhEIUySg8=,iv:3QOosRhjJHve9xw8y3rD4guZ5cv9B9uCJaMHqgkyO1A=,tag:fETCJefFt1G5fIonehAlXQ==,type:comment]
|
#ENC[AES256_GCM,data:h3aIplJ6ggIZ6AcbFNLJfWoIU6eddtotRLRQmbsOYX4aBW1QG8tR1It+YPCGswjCPqHAOnoCK7A=,iv:7BPHezVScIiMfzs2hXqPELNqelm1aBfu+GPYo1mp17I=,tag:iIM4kbSiFRSqLqd7J/5O+A==,type:comment]
|
||||||
#ENC[AES256_GCM,data:PsEk9JIsPZJ8sHjVLugtfCiKx1ulPXjjtz2+zS1fKrlAgDTUJ7Pe0vCuSgemfCZG5hlfK/DN8ows5oaeb28=,iv:d8S9eXEM+U5vBiDj8nCv949qFiec1deN348QUDZMDII=,tag:tJAn2b2U5OmaWSWBJRmfAw==,type:comment]
|
#ENC[AES256_GCM,data:uZ0fYJFN8Ts+ybDY4t4+bpvAfysMSk9EaIgKXwkckjLAaGw98o05bnBi5bZdfWu7766e6tKWadZ30Xen2Po=,iv:uzxi3awzBLnn18S5B4mvKekpiQuwZOb0aLQ0X1G10Uk=,tag:CMB7PIKcMB8LRCPTfBQTZw==,type:comment]
|
||||||
#ENC[AES256_GCM,data:sCb3wLMhMX592Si7cIPgvB2hfl94qWNWifpDVgpkdMyF5y15PS+SZ1ouetU7Gi7UEVzwWxuct80=,iv:LpSZ+QZG/VqK1cUxVakdIi0bRjwBPMCLNEPr7D5xIu8=,tag:XPwk5T66WmViYf0faBsm9A==,type:comment]
|
#ENC[AES256_GCM,data:jbrU62G5xMmR07WsOaVfV1G4O3CtWViOqKsYDBzSaMldZmcts29ee+zZAvvJ0KzJzQlgb4ib4kU=,iv:Ach1vZiBsulqSYpS8arKP5uIa2XvrNinGCktfXSkt3s=,tag:rG2kP6DRQrgI/xw4B0By2A==,type:comment]
|
||||||
#ENC[AES256_GCM,data:Ki5g8TW+PpA/WUDk5zfZdRJrXfY24L6hSTG82PtwxbzZwKxuj/URwS7SxJYJMfuMYvOIcz/l+GHX7iKf6/SLIM12u454hoTj,iv:gMePS2eidgVf6ccnNUlJlGjUAcm48H2mCVGlxbgZxFI=,tag:hPXD2Lq+iWut+Vt2YU/LRg==,type:comment]
|
#ENC[AES256_GCM,data:F7Jf895irD1ukk6BlS8vpmb9BG+lRhMeYpnAqyROuZF9iUuppkwU34fZ3APpseoAlbc/ipyaPFIVJs83nMEjaWXGBtkh//0W,iv:6OTb4ElAUxJdp0jZ/wJ/v13uqsteeXxn1uhKnAQyFCs=,tag:Qi6ZCceMxoS8tgxrcg62DA==,type:comment]
|
||||||
#
|
#
|
||||||
#ENC[AES256_GCM,data:97HOuSOoYawq8c9bZgNaPEVLxJ2Wew/IbljdGuevTk3cSLeFJw1Ih+JELvBMfN7s6GaTzI5NXt0+fQGA7AnihKJw5vQPshUMTXI=,iv:gFNm/7O6GxLDhqtkBXMXts5XC22qhagCr7y1QIw+k8I=,tag:eKhFM/n25eROi+JoYN3ZBA==,type:comment]
|
#ENC[AES256_GCM,data:kwl6bmCm7M/SWJwNTMLKApVIaRlrEFu4TCxbyYA4VkE0YsKKnigqu1shhOZ1By2dSpB8O2ai7UleYHmDhttP+wKkwowE/8JZ7AA=,iv:FegyAuJ+x4CBk1nWuUXFbrlx40oabgnKJQitIK3J17E=,tag:ls6ryKivVSZX6gLbnkL2Ig==,type:comment]
|
||||||
RESEND_API_KEY=
|
RESEND_API_KEY=
|
||||||
EMAIL_FROM=ENC[AES256_GCM,data:8D4sqeCDj0dw1Kh0sHi9h3q4ckg=,iv:GlWgA3OzZUgMbg5MQwpiiWpn20at/tkgxbpR16io5qo=,tag:Qpq6B2tjzPXBiMzvNgjSaQ==,type:str]
|
EMAIL_FROM=ENC[AES256_GCM,data:VfDztVDKyi5UA6naoCF7u/J7sP8=,iv:vHm1yLqTJppte0n7z4Pr3LPoYgg8idyDHOHFYreC3bc=,tag:+hh5lsTroRjmsSU0paSxZg==,type:str]
|
||||||
LEADS_EMAIL=ENC[AES256_GCM,data:Pj74LSKvkjJ48RLqUuAPpOzrgLI=,iv:iZinDeQbqL1DfbqYu0Duux5GQNRBYG2JhTXdjXQgpOA=,tag:ZWXSf4WlSL0wykOVPFjf3A==,type:str]
|
LEADS_EMAIL=ENC[AES256_GCM,data:tKfcRjekFpyokkUPQVvp6LoUlgA=,iv:2kD4gnm4pPNwAjdHu2GijnmjpFB5IWS69u2fd3IGTnU=,tag:lv+so7I6uYk7x4pdWznRsQ==,type:str]
|
||||||
RESEND_WEBHOOK_SECRET=
|
RESEND_WEBHOOK_SECRET=
|
||||||
#ENC[AES256_GCM,data:gjvHsdCmiGT0hw/lvUuu7yMfXWMBjvwAvvwTl0RpZxptLiG7Wz4s6A5saBnZCZbnvrHpXoJJ2lyPqWdt7XsnpRBAQQ==,iv:jMWd+hNbwtB4DCUM+pjTihrRSSCVr+qNuoAT4pZp7QQ=,tag:ncxbxmCRgiR1VNuGZTu0mg==,type:comment]
|
#ENC[AES256_GCM,data:tGSs8NPV6sn7gd73ra5pwZc6H966bFzBWQq7LGCkJfZ+3HbkMpXjVbfl4dXvzzFJpD8SNRSxlQlLRTdsXvg9b1c4Hw==,iv:09XPpnPnvBRrTCJ/1SiVl+QRnp66jbGnSdWKiQ5zls8=,tag:Rzt6YlKWRwPdVvWCaYF5dA==,type:comment]
|
||||||
#ENC[AES256_GCM,data:prllSnPTUbTo1E26lMhrbrrgTmdCK+E+Z++N7BhW4tXJBFpSgJ/vmrISWB/X6cLdYgIewyByPPmKlHCoLsAVwdb5/uDeIKKwdJXp,iv:c2JxexmzVOeiicv5b+0SXq9ylTRk9+ad2umgsJ/5IPo=,tag:uD2m9Ghbt6ZxV7cupFJpvw==,type:comment]
|
#ENC[AES256_GCM,data:2GtBzKD3qNwMMWqp8BR/kY4BmaGGykzKTYKTVZh0OB0oaD9Vn8OFeUzS2+ZZFo8HnYS8mplDbyfEvQQn3nQe1nZFc0Y7WqfAmR1O,iv:IqGpYYWdsf7VBifNVHj8BlhVAPiu4S4j/5G3BPyK+mA=,tag:YB5ml+tfDsf8J71Igplc2A==,type:comment]
|
||||||
#ENC[AES256_GCM,data:NU2hol5nqs8ffhhDqAXZg48eFzPTw14gO4zuyJhlO075bC18EIMi+2xz0Hg7CC5aa+AuywdjSXeO92j1CvM4YKfng63biI4b/NIdXQ==,iv:IYA4mY+V8jQD/jElsgbwa5fRQ336XSzYv3q9OlZ/DG0=,tag:OXZwhCSmSADMxDotptCKvA==,type:comment]
|
#ENC[AES256_GCM,data:DkjB7T1KLXyt9iN+aamrlIxHuGuUHwCdI+YbkJk8iI1nbjhx337mPsJCVVm1sQFEKjTHHK6jicADv3aNeiGGOE1E1DDFmkSjOGLdjA==,iv:g2oFzHPCAYtHthRqUthC0EVcMnz336qNV/DvtzzAOZs=,tag:wIjTcIPHz5752/MvZlkXsQ==,type:comment]
|
||||||
#ENC[AES256_GCM,data:xRM7eWDm3yrN4gdmWR6nlafMlL5F+0CbNa+45c6dU53fwlf5eFnpKF2700/8XwWe5h6s,iv:v5/tdyxozElqXLjC4Dr1HzHVPwI7e9DgK53nB77pArs=,tag:md4YiR0gkSaTiEMhrgV/4w==,type:comment]
|
#ENC[AES256_GCM,data:N4pNW0vpIpaROwZh8ZGLwfs33GUxCoLdJDdt3iL3RsedbjhTLgGTszGJwcRzA93X3Tv1,iv:awW1hwGPuqlRIRr8kOKpVwFfROWi+hLCt17MtBpzZEA=,tag:GyyR+k8MJ3BP7B7CVyHWKg==,type:comment]
|
||||||
PADDLE_API_KEY=
|
PADDLE_API_KEY=
|
||||||
PADDLE_CLIENT_TOKEN=
|
PADDLE_CLIENT_TOKEN=
|
||||||
PADDLE_WEBHOOK_SECRET=
|
PADDLE_WEBHOOK_SECRET=
|
||||||
PADDLE_NOTIFICATION_SETTING_ID=
|
PADDLE_NOTIFICATION_SETTING_ID=
|
||||||
PADDLE_ENVIRONMENT=ENC[AES256_GCM,data:YzbXeOJr4Q==,iv:eZ0lAAfjVTtHHEkBR80fZACE6VTXrow4bnogAz8VI48=,tag:Uxs9oEZZI5jBloSSlOPoLg==,type:str]
|
PADDLE_ENVIRONMENT=ENC[AES256_GCM,data:set1HXmlEw==,iv:8OzhWk2wrd/NusWHqS4TpRUZ1UK4mMp9uBEOAAUHu6s=,tag:BE+O5UL98SwcHn0b0HLN1g==,type:str]
|
||||||
#ENC[AES256_GCM,data:JZ+dTFncUwrhh5kdBeKbHkPk4HNOu5Ka7l8IhPnkcpbC4+opxuviWV8QXG/lcOlg9SN004FQ83kOPjrI,iv:3txict0Am1Gp/qNFgB5d7d46nVLtyBBixXdJjGiRoVo=,tag:Y1D5nzSveG0c3Qoyvy59lQ==,type:comment]
|
#ENC[AES256_GCM,data:K6HI/gEdbbsu4qHjR3OPKwF/Vsp8IaDI+bPZCfn3sPfyapkPPLasC9kjqonOBHBfb49RRx3pezBcOXdo,iv:ESwxwPZeW6mnmfhliszcbcMld5osRgLSeZR9oECjGhs=,tag:Ex8R0N0fhBZjjS2Eaytb9w==,type:comment]
|
||||||
UMAMI_API_URL=ENC[AES256_GCM,data:VLov17JIMAAmiv0Rq8TR637k1ablVBtJ9GKgWQ==,iv:MqV0T/4xqWit/vZm+sMu0LNTzCH3ILFCivQnD8LTpXA=,tag:5VdbDoubaaFy67+y4u0EQQ==,type:str]
|
UMAMI_API_URL=ENC[AES256_GCM,data:f6Dma4mYHV1HsYbXupk+23GWTpAmwi+epzns0g==,iv:M3GIeZuRiQeYDK19rgwZ79U7DNrBmzsRCRGHYfiwiV0=,tag:eyAu7CTVFNocNSOi8q0/qg==,type:str]
|
||||||
UMAMI_API_TOKEN=
|
UMAMI_API_TOKEN=
|
||||||
#ENC[AES256_GCM,data:hxmk761Ynp57ssLcCIM=,iv:ApzwBN4h8ZU7XvJEG3V8Jr+OH3yiTxq2hx0ts+1MP0M=,tag:msUwMv/h8Z5pyrKrYzyjHw==,type:comment]
|
#ENC[AES256_GCM,data:w7NL+dPofzgK3m+2Fsk=,iv:XyqxWvW/Xk0ktIAW4erwhz9Z6Ujxj6eofrvfOlcBkDU=,tag:4yJ/e8KkSfJD6EkF6JRyZg==,type:comment]
|
||||||
RATE_LIMIT_REQUESTS=ENC[AES256_GCM,data:p0XT,iv:FXMjZ/Vi0O3ZvvgT9P12fYV57ksWkIKKHsXTFAtJ1BQ=,tag:oHLBpOp6WIGxxtkEdJyutA==,type:str]
|
RATE_LIMIT_REQUESTS=ENC[AES256_GCM,data:3AsS,iv:vJHCK/qRR8J2vOJddh9OelVDqWossNkqIWStqLpG3x0=,tag:eEze/syOX3Qr6+z4vk3IVg==,type:str]
|
||||||
RATE_LIMIT_WINDOW=ENC[AES256_GCM,data:+6s=,iv:vwMf5cyfkwxSB4mA8/OJabURcGHHQNS5I9jIA+CP74I=,tag:HwT077P9h6+YkIwVJe9HTg==,type:str]
|
RATE_LIMIT_WINDOW=ENC[AES256_GCM,data:ACE=,iv:LL5LOIO2LfMm+VaMOwpwKBu6GiPQy3ybKLy7uorJge8=,tag:TPbo4Tne2O0NDHz2XZjG8Q==,type:str]
|
||||||
#ENC[AES256_GCM,data:DWa+fY4uRmAYEQyxQUepVUiZ23Kw10IqUdiZvqqo5lKn83IPOqqutM7TO//QSeCCGtExATPXx4WumfsWWfAnyfH5W/LndJ1AlwpVOoVtpWI=,iv:Uj80Naei97O7hGyEcxfr6iFzgERUCEkRu1iKH4DuJX4=,tag:xcz9zPrEEauLyRtoVffsjQ==,type:comment]
|
#ENC[AES256_GCM,data:KsGKuhBQMb07rP7NVQqhLRq4gGMBbbGvjwRK+hvuQt7oGLsLcthsTnMdR3BOXwv3b5GFOUzmLcTHLieXBYc+PeaslfXTTTgLxLIw60WbLes=,iv:QWq6V47ZUpbb0g4vsY6G/z3N+YB6M2qb35dcrc4Z0n0=,tag:1PDVfb7esfjRUS/pjdvyyw==,type:comment]
|
||||||
LITESTREAM_R2_BUCKET=
|
LITESTREAM_R2_BUCKET=
|
||||||
LITESTREAM_R2_ACCESS_KEY_ID=
|
LITESTREAM_R2_ACCESS_KEY_ID=
|
||||||
LITESTREAM_R2_SECRET_ACCESS_KEY=
|
LITESTREAM_R2_SECRET_ACCESS_KEY=
|
||||||
LITESTREAM_R2_ENDPOINT=
|
LITESTREAM_R2_ENDPOINT=
|
||||||
#ENC[AES256_GCM,data:Wh8wbI1ONGwz7YmTh3g=,iv:hsgNVrb1ZmItszvWSW5XozSTSoORc48ePg/L7wk3k0E=,tag:ryrs1Myp6JMNO4PppsEy5A==,type:comment]
|
#ENC[AES256_GCM,data:Uu1arRNv56qWcjLzwBo=,iv:lUVJ6FYL821POzm5IFCej8Hn/4u+n7CxKURxjCdrxOY=,tag:g8yWw8d2TJFBHpqJC1Su6w==,type:comment]
|
||||||
DUCKDB_PATH=ENC[AES256_GCM,data:MA6E5KnIZxOd1rOA5cLGk0oXoTk5,iv:Q+xoCHnf6x4ismgOqXSqePEV2T5RV8J2KIyD+Pdidbs=,tag:80GfhXAZ4AeXQ4HEz72K9g==,type:str]
|
DUCKDB_PATH=ENC[AES256_GCM,data:mjde2EBgYYDt/+ct/co1LRAw+1hW,iv:Gc4c4EShP+M2Qr6kyDzH9l4OK5FEWBZu+Pzf6/yvCiw=,tag:kiFxuDrLoqYMHlGULx94Uw==,type:str]
|
||||||
SERVING_DUCKDB_PATH=ENC[AES256_GCM,data:ubhnX43J3bEw1g2xJDhQWJiLNYrd,iv:Z9ltDGDYhTl98Pg18wCmU+Qxco8+PKleZ/SkhD9XGCs=,tag:7BtwbKiHcM17DpvnwLSB/Q==,type:str]
|
SERVING_DUCKDB_PATH=ENC[AES256_GCM,data:xaqVNsB5/AjpYA3vqKlyRyLU0ooK,iv:Xayq0hEICs2NnqPEFOO1MUPLUjDsuCdsvlFd7jAm5eY=,tag:1Pkn3tnuSqDkIaUK/+VaPA==,type:str]
|
||||||
LANDING_DIR=ENC[AES256_GCM,data:e+ZJClS0YWbTOgVo,iv:Mh13edgTjG/cW/0hsdvM32uQOlBJwVpC2nju67+n84Q=,tag:S6/50MMQOmxMmpCC7XRavQ==,type:str]
|
LANDING_DIR=ENC[AES256_GCM,data:mPJitWncQfQCijay,iv:dzk3i0yHRNw4TGT/iYYqAd+S1J4NcOdKtFw32eHmNdo=,tag:4kzYlrq9rBTLH40sibHXcA==,type:str]
|
||||||
#ENC[AES256_GCM,data:hrtFixymQ0XR1t288qEETWAajvEe13tZlSAmnwpaEGr11wzr+b6rd7QHc+enb2/lkSOcnIKRxCOZtu6y+tEAGuZImpijf5+Lza8=,iv:CLniQ1Jf6JcmzgHpzbCn9WFTJOPLGTFXu/5jVCdIrtQ=,tag:vv5Hwu2TGQeGRejEOwa/dg==,type:comment]
|
#ENC[AES256_GCM,data:2hG2+eK9l6YzRybTFEEUBrirw/cCU0G/Dw9hKILHIvBtw3qATM8mgSyS4CEaNlfM5VReUr86XT0nnTdhAPxsx0aTKY6oGYx5X9k=,iv:snCIvgehKVYuzvr6iOB0Ggrga/DrFUrCbGJU+ggWtW8=,tag:Rs27oLsFWY8HhggJCUuGNQ==,type:comment]
|
||||||
REPO_DIR=ENC[AES256_GCM,data:ZA==,iv:TH+5LUPD7fKSj+kgtFCmsxEbG0sO5gtNPBi0k5yuiAc=,tag:pVy/nn7YalVV44zELdMIyg==,type:str]
|
REPO_DIR=ENC[AES256_GCM,data:+Q==,iv:uzfcmAkRXEHtl7mxrXMuThTPQwbmlAvG7mAh4FmcKeE=,tag:yBE749xyNFOXg7Xi83CsdA==,type:str]
|
||||||
WORKFLOWS_PATH=ENC[AES256_GCM,data:KcdUD1rSa1VBKzktiuHGA+a/cI7m/GWXkrKr50NhgQ==,iv:VgF8+wZmg61+sVoHeL2U7PJuTQ5UuOeonaTPX7mdHBA=,tag:CIRYpkEFnbCFN8+zvRZVag==,type:str]
|
WORKFLOWS_PATH=ENC[AES256_GCM,data:bM9tttWpHbxWag80ohDTre3lGn8IOHISEjYspu4B9Q==,iv:9uczfTWn27L253iUo87YYSG3Eoq9SGGzcHjBufKCUcc=,tag:T9hnCnv0f2KQn+nPigYFIg==,type:str]
|
||||||
ALERT_WEBHOOK_URL=
|
ALERT_WEBHOOK_URL=
|
||||||
NTFY_TOKEN=
|
NTFY_TOKEN=
|
||||||
#ENC[AES256_GCM,data:407y6mp/tJLef0I=,iv:661eXVnxobVG8pWCYq3MZ6WO9yYzdMBskwtReeiVe+Y=,tag:+6W6h8EGU68KZ7Vz8QozNw==,type:comment]
|
#ENC[AES256_GCM,data:A/KnoE68ouBRyj0=,iv:v2DINooD2UeZrQ9+7Y+PQZVyy0PdLFYV7yh3IQ+E2bQ=,tag:7H1cFfvNpbtv6VYIQVIVJQ==,type:comment]
|
||||||
PROXY_URLS=
|
PROXY_URLS=
|
||||||
EXTRACT_WORKERS=ENC[AES256_GCM,data:Cg==,iv:w6JWrCAfBJuUS7Kwc4JsvCCbYGU4FIc18JTd7C6kiak=,tag:yJ53uTn9cs+GtghTj9tjxA==,type:str]
|
EXTRACT_WORKERS=ENC[AES256_GCM,data:VQ==,iv:LIlnbGE3SqsnOE+w1P5RLyn7dZ10BIYfDQM9Rn64MiY=,tag:kW5aRY2t18TO0UMWkVC2hw==,type:str]
|
||||||
RECHECK_WINDOW_MINUTES=ENC[AES256_GCM,data:/3Q=,iv:xe8I/VBCGK49qTDY2Ehci9jrY4j1gPOzbx39mAJjf0w=,tag:Ekx3G4cs60CvCehCJuyl9Q==,type:str]
|
RECHECK_WINDOW_MINUTES=ENC[AES256_GCM,data:+K8=,iv:emFyt6Q6VXE1RgRSRGUxQNniSycBjYaByWMhCm97ZgA=,tag:M88Q61Kvz96MbSc1k4K5QA==,type:str]
|
||||||
#ENC[AES256_GCM,data:K8tsERccx4RgTurYruu6tctL6+sHz471+pAWRDld9sLTBFWD1HgTd2MtGqybuEJyU6lp4/fYPXZKOc6sff9EaqiK52dGtqbe,iv:2hlV+RsPEBOx90ZnBx4Hb2tPdrJqyri4Ic/cxReiV2o=,tag:lHT4nLUsb//ncn/E4irfWg==,type:comment]
|
PROXY_URLS_FALLBACK=
|
||||||
|
CIRCUIT_BREAKER_THRESHOLD=
|
||||||
|
#ENC[AES256_GCM,data:9D+Mxt1FmY/wXf5oHY20aYBeaVp/7eelKFxgJ6w4FCgAvVv1PYSi1ddsBghwAkIBVJdICRe6JXKtU7266nknY0MRxlfOIuyj,iv:I76ax9TNOgFrES4mJNKkNFzN3yUcLGhUPB4lrME0RX8=,tag:U5QyhZzMKBp0henjzgzkrA==,type:comment]
|
||||||
GSC_SERVICE_ACCOUNT_PATH=
|
GSC_SERVICE_ACCOUNT_PATH=
|
||||||
GSC_SITE_URL=
|
GSC_SITE_URL=
|
||||||
BING_WEBMASTER_API_KEY=
|
BING_WEBMASTER_API_KEY=
|
||||||
BING_SITE_URL=
|
BING_SITE_URL=
|
||||||
sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYR0RXNG5kczUyUVp5VnE3\nRUdFdmxpY2I2SUNPR1JVbkZYd0pMblFpV3lZCitMNnl6OFdIT1hCdVN6Z0lzVU5D\nK2FzcFpWdUJsZ09ubXYvemVuT0dEUG8KLS0tIGcrcER0dUVnSEdHS21MQlVzLzQr\nc1VnTERieVJNTE5UUjNXTWVESzVUcE0KYyHa1Y42l54gblStQHKKPZZ0FJJBr9FT\n68A1DVRU/zXgvO/wkBaumKqBDQqMVKOPzQGRggb+RoQtlVEfU57DGA==\n-----END AGE ENCRYPTED FILE-----\n
|
sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFUVVkYnFIZTVIWnNOZytS\neFhGVXN6WkUvQWF3aWxiOHV0eXJ1Z1E4aHc4CitTOEtGUHFieG5LS3JtZWRYeEh5\nOUt1eUlhWTY2ZjlJazZhY3dsYW5FVEEKLS0tIEliUmV3d1oyVFZkUG1FWlRYUkFB\nWU15cnBwTCsybk8wc3VQM0lPRGwrU2cKJdd6xG43194DOswmYMz06zVPm62Ahp1o\nlENmD1yG1c0aU/ZdVGs7wUoY7L5GglHVM60uc9AR+UiLqiG+Qlcnrg==\n-----END AGE ENCRYPTED FILE-----\n
|
||||||
sops_age__list_0__map_recipient=age1f5002gj4s78jju45jd28kuejtcfhn5cdujz885fl7z2p9ym68pnsgky87a
|
sops_age__list_0__map_recipient=age1f5002gj4s78jju45jd28kuejtcfhn5cdujz885fl7z2p9ym68pnsgky87a
|
||||||
sops_lastmodified=2026-02-23T21:01:40Z
|
sops_lastmodified=2026-02-24T15:01:12Z
|
||||||
sops_mac=ENC[AES256_GCM,data:xehhYZcf8o/AWztlWOM/QGUl/SGf2ZXXJHl0GOiZ5s/VfItoXGx0elcV13wWnlMLOb4oRnFzblt8J0IgqCINDdKsh4JHDqKAEVjBm0cTulA6ZmKELB4hopPZve3c9FwU0AAO7jKWJpNzg0ymIxNvF05JwZKL3ILr+55s9Tun7BE=,iv:VcMqkoaLgn5P8ds/oRfObnf6uDnULBSJMJgrozDyw78=,tag:84UvqnHen+qe7rS/8HffFQ==,type:str]
|
sops_mac=ENC[AES256_GCM,data:le95na6NB5eTvbQengN0crIn279mfnV50ceb/vZ1Ia5MAIZ8f2bmgXwuYYDpMqCMP4iP93gS1Veht10QWyc3VrjTTNUQq7BOau8zaBXjlsaUD65mhkPxtl9RS+c9rA1bHhZPwz2Hq/7+goGIvmpsupqjYeyqxQS63khiwy0+nZ0=,iv:99wtdYrYQFUkGVXym5L1jQnxrjfvZ2ifdP1Pm3vxJ7M=,tag:fOn1Rt6Md108Xb3ohBBGaQ==,type:str]
|
||||||
sops_unencrypted_suffix=_unencrypted
|
sops_unencrypted_suffix=_unencrypted
|
||||||
sops_version=3.12.1
|
sops_version=3.12.1
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from pathlib import Path
|
|||||||
import niquests
|
import niquests
|
||||||
|
|
||||||
from ._shared import HTTP_TIMEOUT_SECONDS, USER_AGENT, run_extractor, setup_logging
|
from ._shared import HTTP_TIMEOUT_SECONDS, USER_AGENT, run_extractor, setup_logging
|
||||||
from .proxy import load_proxy_urls, make_round_robin_cycler
|
from .proxy import load_fallback_proxy_urls, load_proxy_urls, make_tiered_cycler
|
||||||
from .utils import get_last_cursor, landing_path, write_gzip_atomic
|
from .utils import get_last_cursor, landing_path, write_gzip_atomic
|
||||||
|
|
||||||
logger = setup_logging("padelnomics.extract.playtomic_availability")
|
logger = setup_logging("padelnomics.extract.playtomic_availability")
|
||||||
@@ -42,6 +42,12 @@ MAX_VENUES_PER_RUN = 20_000
|
|||||||
MAX_RETRIES_PER_VENUE = 2
|
MAX_RETRIES_PER_VENUE = 2
|
||||||
MAX_WORKERS = int(os.environ.get("EXTRACT_WORKERS", "1"))
|
MAX_WORKERS = int(os.environ.get("EXTRACT_WORKERS", "1"))
|
||||||
RECHECK_WINDOW_MINUTES = int(os.environ.get("RECHECK_WINDOW_MINUTES", "90"))
|
RECHECK_WINDOW_MINUTES = int(os.environ.get("RECHECK_WINDOW_MINUTES", "90"))
|
||||||
|
CIRCUIT_BREAKER_THRESHOLD = int(os.environ.get("CIRCUIT_BREAKER_THRESHOLD", "10"))
|
||||||
|
|
||||||
|
# Parallel mode submits futures in batches so the circuit breaker can stop
|
||||||
|
# new submissions after it opens. Already-inflight futures in the current
|
||||||
|
# batch still complete.
|
||||||
|
PARALLEL_BATCH_SIZE = 100
|
||||||
|
|
||||||
# Thread-local storage for per-worker sessions
|
# Thread-local storage for per-worker sessions
|
||||||
_thread_local = threading.local()
|
_thread_local = threading.local()
|
||||||
@@ -169,10 +175,15 @@ def _fetch_venues_parallel(
|
|||||||
start_min_str: str,
|
start_min_str: str,
|
||||||
start_max_str: str,
|
start_max_str: str,
|
||||||
worker_count: int,
|
worker_count: int,
|
||||||
proxy_cycler,
|
cycler: dict,
|
||||||
|
fallback_urls: list[str],
|
||||||
) -> tuple[list[dict], int]:
|
) -> tuple[list[dict], int]:
|
||||||
"""Fetch availability for multiple venues in parallel.
|
"""Fetch availability for multiple venues in parallel.
|
||||||
|
|
||||||
|
Submits futures in batches of PARALLEL_BATCH_SIZE. After each batch
|
||||||
|
completes, checks the circuit breaker: if it opened and there is no
|
||||||
|
fallback configured, stops submitting further batches.
|
||||||
|
|
||||||
Returns (venues_data, venues_errored).
|
Returns (venues_data, venues_errored).
|
||||||
"""
|
"""
|
||||||
venues_data: list[dict] = []
|
venues_data: list[dict] = []
|
||||||
@@ -181,20 +192,32 @@ def _fetch_venues_parallel(
|
|||||||
lock = threading.Lock()
|
lock = threading.Lock()
|
||||||
|
|
||||||
def _worker(tenant_id: str) -> dict | None:
|
def _worker(tenant_id: str) -> dict | None:
|
||||||
proxy_url = proxy_cycler()
|
proxy_url = cycler["next_proxy"]()
|
||||||
return _fetch_venue_availability(tenant_id, start_min_str, start_max_str, proxy_url)
|
return _fetch_venue_availability(tenant_id, start_min_str, start_max_str, proxy_url)
|
||||||
|
|
||||||
with ThreadPoolExecutor(max_workers=worker_count) as pool:
|
with ThreadPoolExecutor(max_workers=worker_count) as pool:
|
||||||
futures = {pool.submit(_worker, tid): tid for tid in tenant_ids}
|
for batch_start in range(0, len(tenant_ids), PARALLEL_BATCH_SIZE):
|
||||||
|
# Stop submitting new work if circuit is open with no fallback
|
||||||
|
if cycler["is_fallback_active"]() and not fallback_urls:
|
||||||
|
logger.error(
|
||||||
|
"Circuit open with no fallback — stopping after %d/%d venues",
|
||||||
|
completed_count, len(tenant_ids),
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
for future in as_completed(futures):
|
batch = tenant_ids[batch_start:batch_start + PARALLEL_BATCH_SIZE]
|
||||||
|
batch_futures = {pool.submit(_worker, tid): tid for tid in batch}
|
||||||
|
|
||||||
|
for future in as_completed(batch_futures):
|
||||||
result = future.result()
|
result = future.result()
|
||||||
with lock:
|
with lock:
|
||||||
completed_count += 1
|
completed_count += 1
|
||||||
if result is not None:
|
if result is not None:
|
||||||
venues_data.append(result)
|
venues_data.append(result)
|
||||||
|
cycler["record_success"]()
|
||||||
else:
|
else:
|
||||||
venues_errored += 1
|
venues_errored += 1
|
||||||
|
cycler["record_failure"]()
|
||||||
|
|
||||||
if completed_count % 500 == 0:
|
if completed_count % 500 == 0:
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -249,10 +272,11 @@ def extract(
|
|||||||
if resume_index > 0:
|
if resume_index > 0:
|
||||||
venues_to_process = venues_to_process[resume_index:]
|
venues_to_process = venues_to_process[resume_index:]
|
||||||
|
|
||||||
# Determine parallelism
|
# Set up tiered proxy cycler with circuit breaker
|
||||||
proxy_urls = load_proxy_urls()
|
proxy_urls = load_proxy_urls()
|
||||||
|
fallback_urls = load_fallback_proxy_urls()
|
||||||
worker_count = min(MAX_WORKERS, len(proxy_urls)) if proxy_urls else 1
|
worker_count = min(MAX_WORKERS, len(proxy_urls)) if proxy_urls else 1
|
||||||
proxy_cycler = make_round_robin_cycler(proxy_urls)
|
cycler = make_tiered_cycler(proxy_urls, fallback_urls, CIRCUIT_BREAKER_THRESHOLD)
|
||||||
|
|
||||||
start_min_str = start_min.strftime("%Y-%m-%dT%H:%M:%S")
|
start_min_str = start_min.strftime("%Y-%m-%dT%H:%M:%S")
|
||||||
start_max_str = start_max.strftime("%Y-%m-%dT%H:%M:%S")
|
start_max_str = start_max.strftime("%Y-%m-%dT%H:%M:%S")
|
||||||
@@ -260,21 +284,25 @@ def extract(
|
|||||||
if worker_count > 1:
|
if worker_count > 1:
|
||||||
logger.info("Parallel mode: %d workers, %d proxies", worker_count, len(proxy_urls))
|
logger.info("Parallel mode: %d workers, %d proxies", worker_count, len(proxy_urls))
|
||||||
venues_data, venues_errored = _fetch_venues_parallel(
|
venues_data, venues_errored = _fetch_venues_parallel(
|
||||||
venues_to_process, start_min_str, start_max_str, worker_count, proxy_cycler,
|
venues_to_process, start_min_str, start_max_str, worker_count, cycler, fallback_urls,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Serial mode — same as before but uses shared fetch function
|
|
||||||
logger.info("Serial mode: 1 worker, %d venues", len(venues_to_process))
|
logger.info("Serial mode: 1 worker, %d venues", len(venues_to_process))
|
||||||
venues_data = []
|
venues_data = []
|
||||||
venues_errored = 0
|
venues_errored = 0
|
||||||
for i, tenant_id in enumerate(venues_to_process):
|
for i, tenant_id in enumerate(venues_to_process):
|
||||||
result = _fetch_venue_availability(
|
result = _fetch_venue_availability(
|
||||||
tenant_id, start_min_str, start_max_str, proxy_cycler(),
|
tenant_id, start_min_str, start_max_str, cycler["next_proxy"](),
|
||||||
)
|
)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
venues_data.append(result)
|
venues_data.append(result)
|
||||||
|
cycler["record_success"]()
|
||||||
else:
|
else:
|
||||||
venues_errored += 1
|
venues_errored += 1
|
||||||
|
circuit_opened = cycler["record_failure"]()
|
||||||
|
if circuit_opened and not fallback_urls:
|
||||||
|
logger.error("Circuit open with no fallback — writing partial results")
|
||||||
|
break
|
||||||
|
|
||||||
if (i + 1) % 100 == 0:
|
if (i + 1) % 100 == 0:
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -390,24 +418,30 @@ def extract_recheck(
|
|||||||
start_min_str = window_start.strftime("%Y-%m-%dT%H:%M:%S")
|
start_min_str = window_start.strftime("%Y-%m-%dT%H:%M:%S")
|
||||||
start_max_str = window_end.strftime("%Y-%m-%dT%H:%M:%S")
|
start_max_str = window_end.strftime("%Y-%m-%dT%H:%M:%S")
|
||||||
|
|
||||||
# Determine parallelism
|
# Set up tiered proxy cycler with circuit breaker
|
||||||
proxy_urls = load_proxy_urls()
|
proxy_urls = load_proxy_urls()
|
||||||
|
fallback_urls = load_fallback_proxy_urls()
|
||||||
worker_count = min(MAX_WORKERS, len(proxy_urls)) if proxy_urls else 1
|
worker_count = min(MAX_WORKERS, len(proxy_urls)) if proxy_urls else 1
|
||||||
proxy_cycler = make_round_robin_cycler(proxy_urls)
|
cycler = make_tiered_cycler(proxy_urls, fallback_urls, CIRCUIT_BREAKER_THRESHOLD)
|
||||||
|
|
||||||
if worker_count > 1 and len(venues_to_recheck) > 10:
|
if worker_count > 1 and len(venues_to_recheck) > 10:
|
||||||
venues_data, venues_errored = _fetch_venues_parallel(
|
venues_data, venues_errored = _fetch_venues_parallel(
|
||||||
venues_to_recheck, start_min_str, start_max_str, worker_count, proxy_cycler,
|
venues_to_recheck, start_min_str, start_max_str, worker_count, cycler, fallback_urls,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
venues_data = []
|
venues_data = []
|
||||||
venues_errored = 0
|
venues_errored = 0
|
||||||
for tid in venues_to_recheck:
|
for tid in venues_to_recheck:
|
||||||
result = _fetch_venue_availability(tid, start_min_str, start_max_str, proxy_cycler())
|
result = _fetch_venue_availability(tid, start_min_str, start_max_str, cycler["next_proxy"]())
|
||||||
if result is not None:
|
if result is not None:
|
||||||
venues_data.append(result)
|
venues_data.append(result)
|
||||||
|
cycler["record_success"]()
|
||||||
else:
|
else:
|
||||||
venues_errored += 1
|
venues_errored += 1
|
||||||
|
circuit_opened = cycler["record_failure"]()
|
||||||
|
if circuit_opened and not fallback_urls:
|
||||||
|
logger.error("Circuit open with no fallback — writing partial recheck results")
|
||||||
|
break
|
||||||
|
|
||||||
# Write recheck file
|
# Write recheck file
|
||||||
recheck_hour = now.hour
|
recheck_hour = now.hour
|
||||||
|
|||||||
@@ -6,12 +6,20 @@ When unset, all functions return None/no-op — extractors fall back to direct r
|
|||||||
Two routing modes:
|
Two routing modes:
|
||||||
round-robin — distribute requests evenly across proxies (default)
|
round-robin — distribute requests evenly across proxies (default)
|
||||||
sticky — same key always maps to same proxy (for session-tracked sites)
|
sticky — same key always maps to same proxy (for session-tracked sites)
|
||||||
|
|
||||||
|
Tiered proxy with circuit breaker:
|
||||||
|
Primary tier (PROXY_URLS) is used by default — typically cheap datacenter proxies.
|
||||||
|
Fallback tier (PROXY_URLS_FALLBACK) activates once consecutive failures >= threshold.
|
||||||
|
Once the circuit opens it stays open for the duration of the run (no auto-recovery).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def load_proxy_urls() -> list[str]:
|
def load_proxy_urls() -> list[str]:
|
||||||
"""Read PROXY_URLS env var (comma-separated). Returns [] if unset.
|
"""Read PROXY_URLS env var (comma-separated). Returns [] if unset.
|
||||||
@@ -23,6 +31,17 @@ def load_proxy_urls() -> list[str]:
|
|||||||
return urls
|
return urls
|
||||||
|
|
||||||
|
|
||||||
|
def load_fallback_proxy_urls() -> list[str]:
|
||||||
|
"""Read PROXY_URLS_FALLBACK env var (comma-separated). Returns [] if unset.
|
||||||
|
|
||||||
|
Used as the residential/reliable fallback tier when the primary tier fails.
|
||||||
|
Format: http://user:pass@host:port or socks5://host:port
|
||||||
|
"""
|
||||||
|
raw = os.environ.get("PROXY_URLS_FALLBACK", "")
|
||||||
|
urls = [u.strip() for u in raw.split(",") if u.strip()]
|
||||||
|
return urls
|
||||||
|
|
||||||
|
|
||||||
def make_round_robin_cycler(proxy_urls: list[str]):
|
def make_round_robin_cycler(proxy_urls: list[str]):
|
||||||
"""Thread-safe round-robin proxy cycler.
|
"""Thread-safe round-robin proxy cycler.
|
||||||
|
|
||||||
@@ -42,6 +61,87 @@ def make_round_robin_cycler(proxy_urls: list[str]):
|
|||||||
return next_proxy
|
return next_proxy
|
||||||
|
|
||||||
|
|
||||||
|
def make_tiered_cycler(
|
||||||
|
primary_urls: list[str],
|
||||||
|
fallback_urls: list[str],
|
||||||
|
threshold: int,
|
||||||
|
) -> dict:
|
||||||
|
"""Thread-safe tiered proxy cycler with circuit breaker.
|
||||||
|
|
||||||
|
Uses primary_urls until consecutive failures >= threshold, then switches
|
||||||
|
permanently to fallback_urls for the rest of the run. No auto-recovery —
|
||||||
|
once the circuit opens it stays open to avoid flapping.
|
||||||
|
|
||||||
|
Returns a dict of callables:
|
||||||
|
next_proxy() -> str | None — returns URL from the active tier
|
||||||
|
record_success() — resets consecutive failure counter
|
||||||
|
record_failure() -> bool — increments counter; True if circuit just opened
|
||||||
|
is_fallback_active() -> bool — whether fallback tier is currently active
|
||||||
|
|
||||||
|
If primary_urls is empty: always returns from fallback_urls (no circuit breaker needed).
|
||||||
|
If both are empty: next_proxy() always returns None.
|
||||||
|
"""
|
||||||
|
assert threshold > 0, f"threshold must be positive, got {threshold}"
|
||||||
|
|
||||||
|
lock = threading.Lock()
|
||||||
|
state = {
|
||||||
|
"consecutive_failures": 0,
|
||||||
|
"fallback_active": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
primary_cycle = itertools.cycle(primary_urls) if primary_urls else None
|
||||||
|
fallback_cycle = itertools.cycle(fallback_urls) if fallback_urls else None
|
||||||
|
|
||||||
|
# No primary proxies — skip circuit breaker, use fallback directly
|
||||||
|
if not primary_urls:
|
||||||
|
state["fallback_active"] = True
|
||||||
|
|
||||||
|
def next_proxy() -> str | None:
|
||||||
|
with lock:
|
||||||
|
if state["fallback_active"]:
|
||||||
|
return next(fallback_cycle) if fallback_cycle else None
|
||||||
|
return next(primary_cycle) if primary_cycle else None
|
||||||
|
|
||||||
|
def record_success() -> None:
|
||||||
|
with lock:
|
||||||
|
state["consecutive_failures"] = 0
|
||||||
|
|
||||||
|
def record_failure() -> bool:
|
||||||
|
"""Increment failure counter. Returns True if circuit just opened."""
|
||||||
|
with lock:
|
||||||
|
if state["fallback_active"]:
|
||||||
|
# Already on fallback — don't trip the circuit again
|
||||||
|
return False
|
||||||
|
state["consecutive_failures"] += 1
|
||||||
|
if state["consecutive_failures"] >= threshold:
|
||||||
|
state["fallback_active"] = True
|
||||||
|
if fallback_urls:
|
||||||
|
logger.warning(
|
||||||
|
"Circuit open after %d consecutive failures — "
|
||||||
|
"switching to fallback residential proxies",
|
||||||
|
state["consecutive_failures"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
"Circuit open after %d consecutive failures — "
|
||||||
|
"no fallback configured, aborting run",
|
||||||
|
state["consecutive_failures"],
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_fallback_active() -> bool:
|
||||||
|
with lock:
|
||||||
|
return state["fallback_active"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"next_proxy": next_proxy,
|
||||||
|
"record_success": record_success,
|
||||||
|
"record_failure": record_failure,
|
||||||
|
"is_fallback_active": is_fallback_active,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def make_sticky_selector(proxy_urls: list[str]):
|
def make_sticky_selector(proxy_urls: list[str]):
|
||||||
"""Consistent-hash proxy selector — same key always maps to same proxy.
|
"""Consistent-hash proxy selector — same key always maps to same proxy.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user