From 568479d4da7501425c60a36c122af2f1373c341d Mon Sep 17 00:00:00 2001 From: Jeff Huffman Date: Wed, 13 Nov 2024 00:46:38 -0500 Subject: [PATCH] make newsfeed update delay adaptive per-feed --- homeConfigurations/tejing/news/default.nix | 3 - homeConfigurations/tejing/news/rc.public.nix | 11 +- homeConfigurations/tejing/news/rc.secret.nix | Bin 17783 -> 19194 bytes homeModules/sfeed/default.nix | 233 +++++++++++++++---- homeModules/sfeed/regressions.awk | 132 +++++++++++ 5 files changed, 325 insertions(+), 54 deletions(-) create mode 100644 homeModules/sfeed/regressions.awk diff --git a/homeConfigurations/tejing/news/default.nix b/homeConfigurations/tejing/news/default.nix index c7c3079..8eb9584 100644 --- a/homeConfigurations/tejing/news/default.nix +++ b/homeConfigurations/tejing/news/default.nix @@ -39,9 +39,6 @@ in # Enable my sfeed module my.sfeed.enable = true; - # Update every hour, at a non-round time - my.sfeed.update = "*:45:35"; - # Pass useful args through to submodule config my.sfeed.rc._module.args = { inherit pkgs my; }; my.sfeed.rc.imports = [ diff --git a/homeConfigurations/tejing/news/rc.public.nix b/homeConfigurations/tejing/news/rc.public.nix index 8b5ab67..e4c17dc 100644 --- a/homeConfigurations/tejing/news/rc.public.nix +++ b/homeConfigurations/tejing/news/rc.public.nix @@ -56,15 +56,14 @@ in }; # Gets around cloudflare's bot detection, last time I checked - fetch.imitate_browser = { + helper.curl_cf = { code = '' - curl --no-alpn -fsSLm 15 -A ${escapeShellArg innocuousUserAgent} "$2"''; + curl --no-alpn -fsSL -A ${escapeShellArg innocuousUserAgent} "$@"''; inputs = [ pkgs.curl ]; }; - fetch.imitate_browser_post = { + fetch.imitate_browser = { code = '' - curl --no-alpn -fsSLm 15 -A ${escapeShellArg innocuousUserAgent} -X POST "$2"''; - inputs = [ pkgs.curl ]; + curl_cf -m 15 "$2"''; }; # A good general pattern for scraping html pages @@ -114,7 +113,7 @@ in helper.normalize_dates = { code = ''awk -F'\t' -v OFS=$'\t' -f '' + pkgs.writeText "sfeed_process_dates.awk" '' BEGIN { - datecmd = "${pkgs.coreutils}/bin/date -f- +%s" + datecmd = "${pkgs.coreutils}/bin/date -uf- +%s" PROCINFO[datecmd, "pty"] = 1 } { diff --git a/homeConfigurations/tejing/news/rc.secret.nix b/homeConfigurations/tejing/news/rc.secret.nix index fd6b67121f55ed4f29a7dec335178d6a66627b1f..7431b811053d03f4a32c9d41b5d3eb36807408a6 100644 GIT binary patch literal 19194 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 V@=w8hYl<8Cs=V(Und5SmCiu)Qan1k$ literal 17783 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 OTklD7a%x!InDXxEI&c60 diff --git a/homeModules/sfeed/default.nix b/homeModules/sfeed/default.nix index 31f63e9..f2eb737 100644 --- a/homeModules/sfeed/default.nix +++ b/homeModules/sfeed/default.nix @@ -8,7 +8,7 @@ let optionalString optionalAttrs splitString mapAttrsToList groupBy' nameValuePair removeSuffix unique filterAttrs makeBinPath mkEnableOption mkOption mkIf; - inherit (lib.types) attrsOf submodule str ints listOf package nullOr submoduleWith; + inherit (lib.types) attrsOf submodule str ints float listOf package nullOr submoduleWith; inherit (pkgs) resholve; # Changing these 2 lines will safely relocate this module's options @@ -33,8 +33,7 @@ let defineFunctions = funcs: concatMapStringsSep "\n" ({name, value}: '' ${name}() { - ${indentString " " '' - ${removeSuffix "\n" value}''} + ${indentString " " (removeSuffix "\n" value)} }'') (if isList funcs then funcs @@ -44,12 +43,9 @@ let # like branches caseStatement = word: default: branches: '' case ${word} in - ${indentString " " '' - ${concatMapStringsSep "\n" ({ patterns, commands }: '' + ${indentString " " (concatMapStringsSep "\n" ({ patterns, commands }: '' ${patterns}) - ${indentString " " '' - ${removeSuffix "\n" commands} - ;;''}'') + ${indentString " " (removeSuffix "\n" commands + "\n;;")}'') (mapAttrsToList (_: patterns: { patterns = concatMapStringsSep "|" escapeShellArg patterns; @@ -63,44 +59,116 @@ let (mapAttrsToList (pattern: commands: { inherit pattern commands; }) branches)) - ++ [ { patterns = "*"; commands = default; } ])}''} + ++ [ { patterns = "*"; commands = default; } ]))} esac''; - # Code for the feed() function - feed_function = defineFunctions { - feed = '' - [ "$(jobs | wc -l)" -ge ${toString cfg.jobs} ] && wait -nf - _feed "$@" & + settings = concatMapStringsSep "\n" ({name,value}: "${name}=${escapeShellArg value}") (mapAttrsToList nameValuePair { + maxjobs = cfg.jobs; + sfeedpath="${config.home.homeDirectory}/.sfeed/feeds"; + stamppath="${config.home.homeDirectory}/.sfeed/stamps"; + }); + + makedirs = concatMapStringsSep "\n" (dir: "mkdir -p ${dir}") [ + ''"$stamppath"'' + ]; + + cp_function = defineFunctions { + cp_function = '' + test -n "$(declare -f "$1")" || return + eval "''${_/#"$1"/"$2"}" ''; }; + # Code for the _feed() function + feed_function = '' + cp_function _feed __feed + ${defineFunctions { + "_feed" = '' + local name="$(printf '%s' "$1" | tr '/' '_')" + local file="$sfeedpath/$name" + local stamp="$stamppath/$name" + if __feed "$@"; then + touch "$file" + rm -f "$stamp" + return 0 + else + touch "$stamp" + return 1 + fi + ''; + }}''; + + # Code for the adaptfeed() function + adaptfeed_function = defineFunctions { + adaptfeed = '' + local file="''${sfeedpath}/$(printf '%s' "''${10}" | tr '/' '_')" + local stamp="''${sfeedpath%%/feeds}/stamps/$(printf '%s' "''${10}" | tr '/' '_')" + mkdir -p "''${sfeedpath%%/feeds}/stamps" + + if [ -e "$file" ]; then + local lastchecked="$(stat -c'%Y' "$file")" + else + local lastchecked=0 + fi + + if [ -e "$stamp" ]; then + local lastfailed="$(stat -c'%Y' "$stamp")" + else + local lastfailed=0 + fi + + # Temporarily set pipefail + local restore_opts="$(shopt -po pipefail)";set -o pipefail + + if sort -k1,1n "$file" | awk -F'\t' -v recentdiv="$1" -v recentmin="$2" -v regressiondiv="$3" -v regressionmin="$4" -v maxdelay="$5" -v firststeplength="$6" -v weightdoublingtime="$7" -v backofffactor="$8" -v deferprobablility="$9" -v lastchecked="$lastchecked" -v lastfailed="$lastfailed" -f ${./regressions.awk}; then + # Restore pipefail setting + eval "$restore_opts" + else + # Restore pipefail setting + eval "$restore_opts" + + shift 9 + feed "$@" || return + fi + ''; + }; + + # Command to go in the feeds() function, for a given feed + feed_command = name: {url, baseurl, encoding, adapt, ... }: + (if adapt.enable then + "adaptfeed " + escapeShellArgs [ + adapt.recentdiv + adapt.recentmin + adapt.regressiondiv + adapt.regressionmin + adapt.maxdelay + adapt.firststeplength + adapt.weightdoublingtime + adapt.backofffactor + adapt.deferprobablility + ] + else + "feed" + ) + " " + escapeShellArgs ( + [ name url ] ++ + (if encoding != "" + then [ baseurl encoding ] + else + (if baseurl != "" + then [ baseurl ] + else [] + ) + ) + ); + # Code for the feeds() function feeds_function = defineFunctions { feeds = if length (attrValues cfg.rc.feeds) == 0 then ":" else - (concatMapStringsSep "\n" - (feed_args: "feed " + escapeShellArgs feed_args) - (mapAttrsToList - (name: {url, baseurl ? "", encoding ? "", ... }: - [ name url ] ++ - (if encoding != "" - then [ baseurl encoding ] - else - (if baseurl != "" - then [ baseurl ] - else []))) - cfg.rc.feeds)); + concatStringsSep "\n" (mapAttrsToList feed_command cfg.rc.feeds); }; # Code for all helper functions defined through cfg.rc.helper - helper_functions = defineFunctions - (mapAttrs (_: v: v.code) cfg.rc.helper - // optionalAttrs - (any - (feed: any (name: hasAttr name feed) overrideableFuncs) - (attrValues cfg.rc.feeds)) - {cp_function = '' - test -n "$(declare -f "$1")" || return - eval "''${_/#"$1"/"$2"}"'';}); + helper_functions = defineFunctions (mapAttrs (_: v: v.code) cfg.rc.helper); # Code to define all configured implementations for ${name} and override the ${name}() function functionOverride = name: optionalString @@ -124,15 +192,19 @@ let # Generate the entire sfeedrc file sfeedrc = resholve.writeScript "sfeedrc" { interpreter = "none"; - inputs = unique (concatMap (x: if x ? inputs then x.inputs else []) (concatMap attrValues (attrValues cfg.rc))); + inputs = unique ([ pkgs.gawk ] ++ concatMap (x: if x ? inputs then x.inputs else []) (concatMap attrValues (attrValues cfg.rc))); execer = unique (concatMap (x: if x ? execer then x.execer else []) (concatMap attrValues (attrValues cfg.rc))); - fake.function = [ "_feed" ] ++ map (name: "__${name}") overrideableFuncs; + fake.function = [ "feed" "__feed" ] ++ map (name: "__${name}") overrideableFuncs; } (concatStringsSep "\n\n" (map (removeSuffix "\n") (filter (x: x != "") ( [ + settings + makedirs + cp_function feed_function + adaptfeed_function feeds_function helper_functions ] ++ map functionOverride overrideableFuncs)))); @@ -160,6 +232,64 @@ let type = str; description = "URL of the feed"; }; + baseurl = mkOption { + type = str; + default = ""; + description = "Base URL of the feed. If set to the empty string, the feed url is used."; + }; + encoding = mkOption { + type = str; + default = ""; + description = "Encoding of the feed. If set to the empty string, it is autodetected, falling back to utf-8."; + }; + adapt = { + enable = mkEnableOption "adapting the timing of checks for this feed to its history" // {default = true;}; + recentdiv = mkOption { + type = ints.positive; + default = 15; + description = "Divide the time from latest entry to latest check by this to determine recentdelay"; + }; + recentmin = mkOption { + type = ints.unsigned; + default = 60*60*24; + description = "Clamp recentdelay to be at least this"; + }; + regressiondiv = mkOption { + type = ints.positive; + default = 50; + description = "Divide the regression-estimated time per release by this to determine regressiondelay"; + }; + regressionmin = mkOption { + type = ints.unsigned; + default = 60*30; + description = "Clamp regressiondelay to be at least this"; + }; + maxdelay = mkOption { + type = ints.unsigned; + default = 60*60*24*30; + description = "Clamp the minimum of recentdelay and regressiondelay to be at most this, giving the final check delay"; + }; + firststeplength = mkOption { + type = ints.unsigned; + default = 60*60; + description = "The regression should consider the history to start this amount of time before the first entry"; + }; + weightdoublingtime = mkOption { + type = ints.positive; + default = 60*60*24*90; + description = "The doubling time of the exponential time-weighting used in the regression"; + }; + backofffactor = mkOption { + type = float; + default = 1.2; + description = "Check again after a failure if time since last success is more than this multiple of the time between last success and last failure."; + }; + deferprobablility = mkOption { + type = float; + default = 0.1; + description = "Probability (between 0 and 1) with which to randomly defer checks when they are otherwise considered ready. Breaks up cadence between feeds over time."; + }; + }; } // listToAttrs (map (func: nameValuePair func (mkOption { type = nullOr str; default = null; @@ -205,14 +335,24 @@ in { options = putopt { enable = mkEnableOption "sfeed"; - update = mkOption { - type = str; - default = "hourly"; - description = "systemd calendar event string describing when to update feeds (see man systemd.time)"; + update.averagedelay = mkOption { + type = ints.positive; + default = 600; + description = "Average delay, in seconds, between runs of the service."; + }; + update.deviation = mkOption { + type = ints.unsigned; + default = 30; + description = "How long, in seconds, to randomly deviate either way on the delay."; + }; + update.accuracy = mkOption { + type = ints.unsigned; + default = 5; + description = "How long, in seconds, to non-randomly deviate either way on the delay to coalesce with other wakeups."; }; jobs = mkOption { type = ints.positive; - default = 12; + default = 16; description = "number of simultaneous fetches to run"; }; rc = mkOption { @@ -231,12 +371,13 @@ in home.file.".sfeed/sfeedrc".source = sfeedrc; systemd.user.services.sfeed_update = { - Unit.Description = "news feed update"; + Unit.Description = "news feed updater"; Service.Environment = [ "PATH=${makeBinPath (attrValues { inherit (pkgs) sfeed curl glibc # for iconv + findutils # for xargs coreutils ; })}" ]; @@ -245,8 +386,10 @@ in systemd.user.timers.sfeed_update = { Unit.Description = "news feed update timer"; Install.WantedBy = [ "timers.target" ]; - Timer.OnCalendar = cfg.update; - Timer.Persistent = true; + Timer.OnActiveSec = 0; + Timer.OnUnitInactiveSec = cfg.update.averagedelay - cfg.update.deviation - cfg.update.accuracy; + Timer.RandomizedDelaySec = 2 * cfg.update.deviation; + Timer.AccuracySec = 2 * cfg.update.accuracy; }; }; } diff --git a/homeModules/sfeed/regressions.awk b/homeModules/sfeed/regressions.awk new file mode 100644 index 0000000..e4955a7 --- /dev/null +++ b/homeModules/sfeed/regressions.awk @@ -0,0 +1,132 @@ +@namespace "regression" + +# Does a weighted linear regression of a step function f(t) with a weight function of exp(r*t) + +# The core regression formula used is: +# y = b1*x + b0 +# b0 = (Σ[y]*Σ[x^2] - Σ[x]*Σ[x*y]) / (Σ[1]*Σ[x^2] - Σ[x]^2) +# b1 = (Σ[1]*Σ[x*y] - Σ[x]*Σ[y]) / (Σ[1]*Σ[x^2] - Σ[x]^2) +# where Σ[g(x,y)] = ∫[t₀ -> t] exp(r*x)*g(x,f(x))*dx + +# Persistent variables used by the algorithm: +# r = growth constant of the weight function +# irt = r*t of the first step's start time +# prt = r*t of the last step's end time +# sy = (∫[irt/r -> prt/r] exp(r*x) *f(x)*dx)*r /exp(prt) +# sxy = (∫[irt/r -> prt/r] exp(r*x)*x*f(x)*dx)*r^2/exp(prt) + +# Components of the formula in terms of those variables: +# with drt = irt - prt +# with ef = exp(drt) +# Σ[1] = exp(prt)/r*(1-ef) +# Σ[y] = exp(prt)/r*sy +# Σ[x] = exp(prt)/r^2*(prt-1-ef*(irt-1)) +# Σ[x*y] = exp(prt)/r^2*sxy +# Σ[x^2] = exp(prt)/r^3*(prt^2-2*prt+2-ef*(irt^2-2*irt+2)) + +# b0 = (Σ[y]*Σ[x^2] - Σ[x]*Σ[x*y]) / (Σ[1]*Σ[x^2] - Σ[x]^2) +# = (exp(prt)/r*sy*exp(prt)/r^3*(prt^2-2*prt+2-ef*(irt^2-2*irt+2)) - exp(prt)/r^2*(prt-1-ef*(irt-1))*exp(prt)/r^2*sxy) / +# (exp(prt)/r*(1-ef)*exp(prt)/r^3*(prt^2-2*prt+2-ef*(irt^2-2*irt+2)) - (exp(prt)/r^2*(prt-1-ef*(irt-1)))^2) +# = (sy*(prt^2-2*prt+2-ef*(irt^2-2*irt+2)) - (prt-1-ef*(irt-1))*sxy) / ((1-ef)*(prt^2-2*prt+2-ef*(irt^2-2*irt+2)) - (prt-1-ef*(irt-1))^2) +# = (sy*(prt^2-ef*irt^2)+(sxy+2*sy)*(1-prt-ef*(1-irt))) / (ef*(ef-drt^2-2)+1) +# b1 = (Σ[1]*Σ[x*y] - Σ[x]*Σ[y]) / (Σ[1]*Σ[x^2] - Σ[x]^2) +# = (exp(prt)/r*(1-ef)*exp(prt)/r^2*sxy - exp(prt)/r^2*(prt-1-ef*(irt-1))*exp(prt)/r*sy) / +# (exp(prt)/r*(1-ef)*exp(prt)/r^3*(prt^2-2*prt+2-ef*(irt^2-2*irt+2)) - (exp(prt)/r^2*(prt-1-ef*(irt-1)))^2) +# = r*((1-ef)*sxy - (prt-1-ef*(irt-1))*sy)/((1-ef)*(prt^2-2*prt+2-ef*(irt^2-2*irt+2)) - ((prt-1-ef*(irt-1)))^2) +# = r*((1-ef)*(sxy+sy)+(ef*irt-prt)*sy) / (ef*(ef-drt^2-2)+1) + +# a version of exp() that doesn't warn about rounding to zero +BEGIN { exp_warn_breakpoint = -6554261109157969/8796093022208; } +function exp_no_warn(x) { + if (x < exp_warn_breakpoint) return 0; + return exp(x); +} + +# configure the weight function +function config(dt) { + # calculate the growth constant of the exponential growth of the weight function, in terms of the given doubling time + r = log(2)/dt; +} + +# initialize regression, with a given start time for the (not yet created) first step +function init(t) { + irt = prt = r*t; + sy = sxy = 0; +} + +# add a step of the step function, ending at the given time, with the given value +function step(t, v) { + rt = r*t; + drt = prt-rt; + if (drt > 0) { + printf "Warning: Entries are not in time order! Skipping.\n" >>"/dev/stderr"; + return; + } + ef = exp_no_warn(drt); + sy = ef*(sy - v ) + v; + sxy = ef*(sxy - v*(prt - 1)) + v*(rt - 1); + prt = rt; +} + +# calculate the reciprocal of the slope of the linear regression result +function recip_slope() { + drt = irt - prt; + ef = exp_no_warn(drt); + divisor = r*((1 - ef)*(sxy + sy) + (ef*irt - prt)*sy); + if (divisor == 0) return strtonum("+inf"); + return (ef*(ef - drt^2 - 2) + 1)/divisor; +} + +@namespace "awk" + +function min(x, y) { + if (x <= y) + return x; + else + return y; +} + +function max(x, y) { + if (x >= y) + return x; + else + return y; +} + +BEGIN { + regression::config(weightdoublingtime); +} + +NR==1 { + regression::init($1 - firststeplength); + count = 0; +} + +{ + regression::step($1, count); + count += 1; + lastentry = $1; +} + +END { + regression::step(lastchecked, count); + + # Regression-based estimator + regressiondelay = regression::recip_slope()/regressiondiv; + regressiondelay = max(regressiondelay,regressionmin); + + # Estimator based on time since last entry + recentdelay = (lastchecked-lastentry)/recentdiv; + recentdelay = max(recentdelay,recentmin); + + # Use the minimum of the 2 estimators + checkdelay = min(regressiondelay,recentdelay); + checkdelay = min(checkdelay,maxdelay); + + now = systime(); + srand(now + count); # Probably random enough for what we need. + if (now >= max(lastchecked,lastfailed) + checkdelay || (lastfailed > lastchecked && (now-lastchecked)/(lastfailed-lastchecked) > backofffactor)) + exit (rand() > deferprobablility) # Fail, indicating it's time to re-check, but occasionally put it off, to break up cadence between feeds over time. + else + exit 0 # Succeed, indicating it can wait till later +}