Object ::throttle (public)
::xotcl::THREAD ::throttle
Defined in
- Testcases:
-
No testcase defined.
Source code:
::nsf::object::alloc ::xotcl::THREAD ::throttle {set :community_id 2449
set :context_initialized 1
set :exithandler {ns_log notice "EXITHANDLER of slave thread SELF 1382542"}
set :initcmd {
package req XOTcl
namespace import -force ::xotcl::*
ns_thread name ::throttle
::xotcl::Object setExitHandler {ns_log notice "EXITHANDLER of slave thread ::throttle 1382542"}
set ::xotcl::currentScript /var/www/openacs.org/packages/xotcl-request-monitor/tcl/throttle_mod-procs.tcl
set ::xotcl::currentThread ::throttle
set ::never_blocked_fetchDest {image iframe script}
set ::monitor_urls {/ /register/ /dotlrn/}
set ::verbose_blocking 0
Class create ::package_parameter -parameter {{default ""} value name} -instproc defaultmethod {} {return ${:value}} -instproc update {value} {set :value $value} -instproc init {} {
set :name [namespace tail [self]]
set :value [::parameter::get_from_package_key -package_key "xotcl-request-monitor" -parameter ${:name} -default ${:default}]
}
::package_parameter log-dir -default [file dirname [file rootname [ns_config ns/parameters ServerLog]]]
::package_parameter do_double_click_prevention -default on
::package_parameter do_slowdown_overactive -default off
::package_parameter do_throttle -default on
::package_parameter do_track_activity -default off
::package_parameter max-stats-elements -default 5
::package_parameter max-url-stats -default 500
::package_parameter monitor_urls -default "/ /register/ /dotlrn/"
::package_parameter time-window -default 10
::package_parameter trend-elements -default 48
::package_parameter map-slow-pool-duration -default [expr {[ns_baseunit -time 12h]*1000}]
max-stats-elements proc update {value} {
next
Counter set_in_all_instances nr_stats_elements $value
}
trend-elements proc update {value} {
next
Counter set_in_all_instances nr_trend_elements $value
}
do_throttle proc update {value} {
next
throttler set do_throttle $value
}
do_double_click_prevention proc update {value} {
next
throttler set do_double_click_prevention $value
}
monitor_urls proc update {value} {
next
set ::monitor_urls $value
}
set ::logdir [log-dir]
if {![ad_file isdirectory $logdir]} {file mkdir $logdir}
Class create AsyncLogFile -parameter {filename {mode a}}
AsyncLogFile instproc init {} {
if {![info exists :filename]} {
set :filename $::logdir/[namespace tail [self]]
}
:open
}
if {[acs::icanuse ns_asynclogfile]} {
ns_log notice "... AsyncLogFile uses NaviServer ns_asynclogfile"
AsyncLogFile instproc open {} {
set :handle [ns_asynclogfile open ${:filename}]
}
AsyncLogFile instproc write {{-sanitize 0} msg} {
ns_asynclogfile write -sanitize $sanitize ${:handle} $msg\n
}
AsyncLogFile instproc destroy {} {
ns_asynclogfile close ${:handle}
next
}
} else {
ns_log notice "... AsyncLogFile uses bgdelivery"
AsyncLogFile instproc open {} {
set :handle [bgdelivery do AsyncDiskWriter new -autoflush true]
bgdelivery do ${:handle} open -filename ${:filename} -mode ${:mode}
}
AsyncLogFile instproc write {{-sanitize 0} msg} {
bgdelivery do ${:handle} async_write $msg\n
}
AsyncLogFile instproc destroy {} {
catch {bgdelivery do ${:handle} close}
next
}
}
AsyncLogFile create counter.log
AsyncLogFile create long-calls.log
AsyncLogFile create switches.log
Class create ThrottleStat -parameter { type requester timestamp ip_address url }
Class create Throttle -parameter {
{timeWindow 10}
{timeoutMs 2000}
{startThrottle 11}
{toMuch 10}
{alerts 0} {throttles 0} {rejects 0} {repeats 0}
}
Throttle instproc init {} {
set :do_throttle [do_throttle]
set :do_double_click_prevention [do_double_click_prevention]
Object create [self]::stats
Object create [self]::users
next
}
Throttle instproc add_statistics { type requester ip_address url query } {
incr :${type}s
set entry [ThrottleStat new -childof [self]::stats -type $type -requester $requester -timestamp [clock seconds] -ip_address $ip_address -url $url]
}
Throttle instproc url_statistics {{-flush 0}} {
set data [[self]::stats info children]
if { [llength $data] == 0} {
return $data
} elseif {$flush} {
foreach c $data {$c destroy}
return ""
} else {
foreach stat $data {
lappend output [list [$stat type] [$stat requester] [$stat timestamp] [$stat ip_address] [$stat url]]
}
return $output
}
}
Throttle instproc call_statistics {} {
set l [list]
foreach t {seconds minutes hours} {
lappend l [list $t [$t set last] [$t set trend] [$t set stats]]
}
return $l
}
Throttle instproc register_access {requestKey pa url community_id is_embedded_request} {
set obj [Users current_object]
$obj addKey $requestKey $pa $url $community_id $is_embedded_request
Users expSmooth [$obj point_in_time] $requestKey
}
Throttle instproc running {} {
array get :running_url
}
set ::threads_busy 0
set ::threads_current 0
set ::threads_datapoints 0 ;
if {[ns_info name] eq "NaviServer"} {
Throttle instproc server_threads {} {ns_server threads}
} else {
Throttle instproc server_threads {} {
return [concat {*}[ns_server threads]]
}
}
Throttle instproc update_threads_state {} {
array set threadInfo [:server_threads]
incr ::threads_busy [expr {$threadInfo(current) - $threadInfo(idle)}]
incr ::threads_current $threadInfo(current)
incr ::threads_datapoints
}
Throttle instproc thread_avgs {} {
return [list busy [format %.2f [expr {1.0 * $::threads_busy / $::threads_datapoints}]] current [format %.2f [expr {1.0 * $::threads_current / $::threads_datapoints}]]]
}
Throttle instproc throttle_check {requestKey pa url conn_time content_type community_id {context ""}} {
seconds ++
:update_threads_state
set fetchDest [expr {[dict exists $context Sec-Fetch-Dest] ? [dict get $context Sec-Fetch-Dest] : "document"}]
set range [expr {[dict exists $context Range] ? [dict get $context Range] : ""}]
set ajax_p [expr {[dict get $context X-Requested-With] eq "XMLHttpRequest"}]
if {!${:do_throttle}} {
return [list 0 0 0]
}
set var :running_url($requestKey,$url)
set overactive ov($requestKey,$url)
if {
$fetchDest in $::never_blocked_fetchDest
|| $range ne ""
|| [dict get $context pool] eq "fast"
|| $ajax_p
|| [string match "image/*" $content_type]
|| [string match "video/*" $content_type]
|| $content_type in {
application/vnd.apple.mpegurl
text/css
application/javascript
application/x-javascript
}
|| [string match "/SYSTEM/*" $url]
|| [string match "/shared/*" $url]
|| "/proctoring/upload" eq $url
} {
if {$::verbose_blocking && [info exists $var]} {
ns_log notice "request not blocked although apparently running: fetchDest $fetchDest $requestKey $url"
}
set $var $conn_time
return [list 0 0 0]
}
if {${:do_double_click_prevention} && [info exists $var]} {
minutes incr $overactive
return [list 0 0 1]
} elseif {$::verbose_blocking && [info exists $var]} {
ns_log notice "would block: fetchDest $fetchDest $requestKey $url"
}
if {[minutes exists $overactive] && [minutes set $overactive] > 15} {
ns_log notice "### request $overactive blocked since user has issued in this minute too many repeated requests"
return [list 0 0 2]
}
set $var $conn_time
:register_access $requestKey $pa $url $community_id 0 ;
if {[do_slowdown_overactive]} {
incr :alerts
if {[info exists :active($requestKey)]} {
lassign [set :active($requestKey)] to cnt
set retMs [expr {$cnt > ${:startThrottle} ? 500 : 0}]
after cancel $to
} else {
set retMs 0
set cnt 0
}
incr cnt
set to [after ${:timeoutMs} [list [self] cancel $requestKey]]
set :active($requestKey) [list $to $cnt]
if {$cnt <= ${:toMuch}} {
set cnt 0
}
return [list $cnt $retMs 0]
}
return [list 0 0 0]
}
Throttle instproc statistics {} {
return "<table>
<tr><td>Number of alerts:</td><td>[:alerts]</td></tr>
<tr><td>Number of throttles:</td><td>[:throttles]</td></tr>
<tr><td>Number of rejects:</td><td>[:rejects]</td></tr>
<tr><td>Number of repeats:</td><td>[:repeats]</td></tr>
</table>\n"
}
Throttle instproc cancel {requestKey} {
if {[info exists :active($requestKey)]} {
after cancel [lindex [set :active($requestKey)] 0]
unset :active($requestKey)
} else {
:log "+++ Cancel for $requestKey failed !!!"
}
}
Throttle instproc active { } {
return [array get :active]
}
Throttle instproc add_url_stat {method url partialtimes key pa content_type pool} {
unset -nocomplain :running_url($key,$url)
if {[string match "text/html*" $content_type]} {
[Users current_object] add_view $key
}
response_time_minutes add_url_stat $url [dict get $partialtimes ms] $key
}
Throttle instforward report_url_stats response_time_minutes %proc
Throttle instforward flush_url_stats response_time_minutes %proc
Throttle instforward last100 response_time_minutes %proc
Throttle create throttler
Class create ThrottleTrace
ThrottleTrace instproc log {msg} {
if {![info exists :traceFile]} {
set file $::logdir/calls
set :traceFile [open $file a]
set :traceCounter 0
}
puts ${:traceFile} $msg
}
ThrottleTrace instproc throttle_check args {
incr :traceCounter
:log "CALL ${:traceCounter} [self args]"
next
}
ThrottleTrace instproc add_url_stat args {
catch {:log "END ${:traceCounter} [self args]"}
next
}
Class create TraceLongCalls
TraceLongCalls set count 0
TraceLongCalls instproc log {msg} {
set entry "[clock format [clock seconds]] -- $msg"
long-calls.log write $entry
[self class] append log "$entry\n"
[self class] incr count
}
TraceLongCalls instproc add_url_stat {method url partialtimes key pa content_type pool} {
regexp {^([^?]+)[?]?(.*)$} $url . url query
set conntime [expr {int(([dict get $partialtimes runtime] + [dict get $partialtimes filtertime]) * 1000)}]
set totaltime [dict get $partialtimes ms]
if { $url in $::monitor_urls } {
incr ::agg_time($url) $totaltime
incr ::count(calls:$url)
}
::xo::remap_pool -runtime [dict get $partialtimes runtime] $method $url
if {$conntime > 3000
|| [dict get $partialtimes filtertime] > 1.0
|| [dict get $partialtimes queuetime] > 0.5
} {
if {$url eq "/register/"} {
set color unexpected
} elseif {$conntime > 7000} {
set color red
} elseif {$conntime > 5000} {
set color orange
} else {
set color yellow
}
incr ::count(longcalls:$color)
set ql [string length $query]
if {$ql > 0} {
if {$ql < 80} {
set loggedUrl $url?$query
} else {
set loggedUrl $url?[string range $query 0 77]...
}
} else {
set loggedUrl $url
}
if {[catch {:log [list $loggedUrl $partialtimes $key $pa $content_type $pool]} errorMsg]} {
ns_log error "long-call error: $errorMsg"
}
}
next
}
Class create BanUser
throttle do throttler mixin {BanUser TraceLongCalls}
Class create Counter -parameter {
report
timeoutMs
{stats ""}
{last ""}
{trend ""}
{c 0}
{logging 0}
{nr_trend_elements [trend-elements]}
{nr_stats_elements [max-stats-elements]}
} -ad_doc {
This class holds the counted statistics so they do not have to be computed
all the time from the list of requests.
The statistics holding objects are instances of this class and initialized and called after
the timeoutMS
@param report Report type of the instance. This could e.g. be hours and minutes
@param timeoutMS How often are the statistics for this report computed
@param stats stats keeps nr_stats_elements highest values with timestamp.
These hold a list of lists of the actual stats in the form {time value}.
Time is given like "Thu Sep 13 09:17:30 CEST 2007".
This is used for displaying the maximum values
@param trend trend keeps nr_trend_elements most recent values. This is used for displaying the graphics
@param c counter
@param logging If set to 1 the instance current value is logged to the counter.log file
@param nr_trend_elements Number of data points that are used for the trend calculation. The default of 48 translates into "48 minutes" for the Views per minute or 48 hours for the views per hour.
@param nr_stats_elements Number of data points for the stats values. The default of 5 will give you the highest datapoints over the whole period.
}
Counter ad_proc set_in_all_instances {var value} {
A helper function to set in all (direct or indirect) instances
an instance variable to the same value. This is used here
in combination with changing parameters
} {
foreach object [:allinstances] {
$object set $var $value
}
}
Counter instproc ++ {} {
incr :c
}
Counter instproc end {} {
if {[info exists :report]} {
[:report] incr c ${:c}
}
:finalize ${:c}
set :c 0
}
Counter instproc log_to_file {timestamp label value} {
set server [ns_info server]
counter.log write "$timestamp -- $server $label $value"
}
Counter instproc add_value {timestamp n} {
lappend :trend $n
set lt [llength ${:trend}]
if {$lt > ${:nr_trend_elements}} {
set :trend [lrange ${:trend} $lt-${:nr_trend_elements} end]
}
lappend :stats [list $timestamp $n]
set :stats [lrange [lsort -real -decreasing -index 1 ${:stats}] 0 ${:nr_stats_elements}-1]
}
Counter instproc finalize {n} {
if {[info exists :to]} {
after cancel ${:to}
set now [clock format [clock seconds]]
:add_value $now $n
catch {if {${:logging}} {:log_to_file $now [self] $n}}
} else {
ns_log notice "request-monitor: [self] has no timeout defined"
}
set :to [after ${:timeoutMs} [list [self] end]]
}
Counter instproc init {} {
set :to [after ${:timeoutMs} [list [self] end]]
next
}
Counter instproc destroy {} {
after cancel ${:to}
next
}
Counter create hours -timeoutMs [expr {60000*60}] -logging 1
Counter create minutes -timeoutMs 60000 -report hours -logging 1
Counter create seconds -timeoutMs 1000 -report minutes
minutes proc end {} {
unset -nocomplain :ov
next
}
Counter user_count_day -timeoutMs [expr {71000*60}] -logging 1
user_count_day proc end {} {
lassign [throttle users nr_users_per_day] auth ip
set now [clock format [clock seconds]]
:log_to_file $now [self]-non-auth $ip
set :c $auth
Users perDayCleanup
next
}
Class create MaxCounter -superclass Counter -parameter {{metric nr_active}} -instproc end {} {
set :c [Users ${:metric}]
if {[info exists :report]} {
if {[${:report} set c] < ${:c}} {
${:report} set c ${:c}
}
}
:finalize ${:c}
set :c 0
}
MaxCounter create user_count_hours -timeoutMs [expr {60000*60}] -logging 1
MaxCounter create user_count_minutes -timeoutMs 60000 -report user_count_hours -logging 1
MaxCounter create authenticated_count_hours -metric nr_authenticated -timeoutMs [expr {60000*60}] -logging 1
MaxCounter create authenticated_count_minutes -metric nr_authenticated -timeoutMs 60000 -report authenticated_count_hours -logging 1
Class create AvgCounter -superclass Counter -parameter {{t 0} {atleast 1}} -instproc end {} {
if {${:c} > 0} {
set avg [expr {int(${:t} * 1.0 / ${:c})}]
} else {
set avg 0
}
if {[info exists :report]} {
${:report} incr c ${:c}
${:report} incr t ${:t}
}
:finalize $avg
set :c 0
set :t 0
}
Class create UrlCounter -superclass AvgCounter -parameter {
{truncate_check 10}
{max_urls 0}
} -set seconds [clock seconds]
UrlCounter instproc add_url_stat {url ms requester} {
my ++
incr :t $ms
set now [clock seconds]
set order [expr {($now - [[self class] set seconds]) * 10000 + ${:c}}]
set :last100([expr {$order%99}]) [list $now $order $url $ms $requester]
set has_param [regexp {^(.*)[?]} $url _ url]
if {$has_param} {set url $url?...}
incr :stat($url) $ms
incr :cnt($url)
}
UrlCounter instproc last100 {} {
array get :last100
}
UrlCounter instproc flush_url_stats {} {
:log "flush_url_stats"
unset -nocomplain :stat
unset -nocomplain :cnt
}
UrlCounter instproc url_stats {} {
set result [list]
foreach url [array names :stat] {
lappend result [list $url [set :stat($url)] [set :cnt($url)]]
}
set result [lsort -real -decreasing -index 1 $result]
return $result
}
UrlCounter instproc check_truncate_stats {} {
set max [max-url-stats]
if {$max>1} {
set result [:url_stats]
set l [llength $result]
for {set i $max} {$i < $l} {incr i} {
set url [lindex $result $i 0]
unset :stat($url) :cnt($url)
}
set result [lrange $result 0 $max-1]
return $result
}
return ""
}
UrlCounter instproc cleanup_stats {} {
set time_window [time-window]
if {$time_window != [throttler timeWindow]} {
throttler timeWindow $time_window
after 0 [list Users purge_access_stats]
}
return ""
}
UrlCounter instproc report_url_stats {} {
set stats [:check_truncate_stats]
if {$stats eq ""} {
set stats [:url_stats]
}
return $stats
}
UrlCounter instproc finalize args {
next
after 0 [list [self] cleanup_stats]
}
UrlCounter create response_time_hours -timeoutMs [expr {60000*60}] -atleast 500 -logging 1
UrlCounter create response_time_minutes -timeoutMs 60000 -report response_time_hours -atleast 100 -logging 1
Class create Users -parameter {
point_in_time
{ip24 0}
{auth24 0}
} -ad_doc {
This class is responsible for the user tracking and is defined only
in a separate Tcl thread named <code>throttle</code>.
For each minute within the specified <code>time-window</code> an instance
of this class exists keeping various statistics.
When a minute ends the instance dropping out of the
time window is destroyed. The procs of this class can be
used to obtain various kinds of information.
@author Gustaf Neumann
@cvs-id $Id: throttle_mod-procs.tcl,v 1.72 2024/10/28 14:58:41 antoniop Exp $
}
Users set ip24 0
Users set auth24 0
Users ad_proc active {-full:switch} {
Return a list of lists containing information about current
users. If the switch 'full' is used this list contains these users
who have used the server within the monitoring time window (per
default: 10 minutes). Otherwise, just a list of requesters
(user_ids or peer addresses for unauthenticated requests) is
returned.
If "-full" is used for each requester the last peer address, the
last timestamp, the number of hits, a list of values for the
activity calculations and the number of ip-switches the user is
returned.
The activity calculations are performed on base of an exponential
smoothing algorithm which is calculated through an aggregated
value, a timestamp (in minutes) and the number of hits in the
monitored time window.
@return list with detailed user info
} {
if {$full} {
set info [list]
foreach key [array names :pa] {
set entry [list $key [set :pa($key)]]
foreach var [list timestamp hits expSmooth switches] {
set k ${var}($key)
lappend entry [expr {[info exists :$k] ? [set :$k] : 0}]
}
lappend info $entry
}
return $info
} else {
return [array names :pa]
}
}
Users proc unknown { obj args } {
:log "unknown called with $obj $args"
}
Users ad_proc nr_active {} {
@return number of active users (in time window)
} {
return [array size :pa]
}
Users ad_proc nr_authenticated {} {
@return number of authenticated users (in time window)
} {
return [lindex [:nr_users_time_window] 1]
}
Users ad_proc nr_users_time_window {} {
@return number of different IP addresses and authenticated users (in time window)
} {
set ip 0; set auth 0; set reverseAuthDict {}; set ipDict {}
foreach {k v} [array get :pa] {
if {[::xo::is_ip $k]} {
lappend ipDict $k $v
} else {
lappend reverseAuthDict $v $k
incr auth
}
}
foreach {k v} $ipDict {
if {![dict exists $reverseAuthDict $v]} {
incr ip
}
}
return [list $ip $auth]
}
Users ad_proc user_is_active {uid} {
@return boolean value whether user is active
} {
info exists :pa($uid)
}
Users ad_proc hits {uid} {
@param uid request key
@return Number of hits by this user (in time window)
} {
if {[info exists :hits($uid)]} {
return [set :hits($uid)]
} else {
return 0
}
}
Users ad_proc last_pa {uid} {
@param uid request key
@return last peer address of the specified users
} {
if {[info exists :pa($uid)]} {
return [set :pa($uid)]
} else {
return ""
}
}
Users proc last_click {uid} {
if {[info exists :timestamp($uid)]} {
return [set :timestamp($uid)]
} else {
return 0
}
}
Users proc last_requests {uid} {
set urls {}
if {[info exists :pa($uid)]} {
foreach i [Users info instances] {
if {[$i exists urls($uid)]} {
foreach u [$i set urls($uid)] { lappend urls $u }
}
}
set urls [lsort -index 0 $urls]
}
return $urls
}
Users proc active_communities {} {
foreach i [Users info instances] {
lappend communities [list [$i point_in_time] [$i array names in_community]]
foreach {c names} [$i array get in_community] {
lappend community($c) $names
}
}
return [array get community]
}
Users proc nr_active_communities {} {
foreach i [Users info instances] {
foreach c [$i array names in_community] {
set community($c) 1
}
}
set n [array size community]
return [incr n -1];
}
Users proc in_community {community_id} {
set users [list]
foreach i [Users info instances] {
if {[$i exists in_community($community_id)]} {
set time [$i point_in_time]
foreach u [$i set in_community($community_id)] {
lappend users [list $time $u]
}
}
}
return $users
}
Users proc current_object {} {
set now [clock seconds]
set mkey [expr { ($now / 60) % [throttler timeWindow]}]
set obj [self]::users::$mkey
if {$mkey ne ${:last_mkey}} {
if {${:last_mkey} ne ""} {:purge_access_stats}
if {[nsf::is object $obj]} {
$obj destroy
}
Users create $obj -point_in_time $now
set :last_mkey $mkey
}
return $obj
}
Users proc purge_access_stats {} {
set time [clock seconds]
set secs [expr {[throttler timeWindow] * 60}]
if { [info commands [self]::users::${:last_mkey}] ne ""
&& $time - [[self]::users::${:last_mkey} point_in_time] > $secs
} {
Object create [self]::users
} else {
foreach element [[self]::users info children] {
if { [$element point_in_time] < $time - $secs } {$element destroy}
}
}
}
Users proc community_access {requester pa community_id} {
[:current_object] community_access $requester $pa $community_id
}
Users proc entered_community {key now community_id data reason} {
set :user_in_community($key) [dict replace $data community_id $community_id community_clicks 1 community_start $now]
}
Users proc left_community {key pa now community_id data reason} {
set seconds [expr {$now - [dict get $data community_start]}]
set clicks [dict get $data community_clicks]
dict unset data community_start
dict unset data community_clicks
dict unset data community_id
set :user_in_community($key) $data
if {[do_track_activity] && $seconds > 0} {
xo::job_enqueue [list ::xo::request_monitor_record_community_activity $key $pa $community_id $seconds $clicks $reason]
}
}
Users proc left_system {key pa now data reason} {
if {[dict exists $data start]} {
set seconds [expr {$now - [dict get $data start]}]
set clicks [dict get $data clicks]
} else {
if {[info exists :timestamp($key)]} {
set seconds [expr {$now - [set :timestamp($key)]}]
set clicks 0
} else {
ns_log warning "could not determine online duration <$key> <$pa> data <$data>"
set seconds -1
set clicks -1
}
}
if {[do_track_activity] && $seconds > 0} {
xo::job_enqueue [list ::xo::request_monitor_record_activity $key $pa $seconds $clicks $reason]
}
unset -nocomplain :user_in_community($key) :refcount($key) :pa($key) :expSmooth($key) :switches($key)
}
Users instproc init {} {
next
set ms [expr {([time-window] * 60000) + 1000}]
after $ms [list [self class] current_object]
}
Users instproc community_access {key pa community_id} {
set class [self class]
set now [clock seconds]
set var user_in_community($key)
if {[$class exists $var]} {
if {[$class exists timestamp($key)] && [$class set timestamp($key)] == $now } {
return
}
set data [$class set $var]
set old_community_id [dict get $data community_id]
if {$old_community_id != $community_id} {
Users left_community $key $pa $now $old_community_id $data switch
dict incr data clicks
Users entered_community $key $now $community_id $data switch
} else {
dict incr data clicks
dict incr data community_clicks
$class set $var $data
}
} else {
set data [list start $now clicks 1]
Users entered_community $key $now $community_id $data new
set $var 1
}
set var :user_in_community($key,$community_id)
if {![info exists $var]} {
set $var 1
lappend :in_community($community_id) $key
}
}
Users instproc check_pa_change {key pa url} {
set class [self class]
if {[$class exists pa($key)]} {
if {[$class set pa($key)] ne $pa} {
$class incr switches($key)
set timestamp [clock format [clock seconds]]
switches.log write "$timestamp -- switch -- $key from [$class set pa($key)] to $pa $url"
}
} elseif {[$class exists pa($pa)]} {
if {[$class exists timestamp($pa)] && [clock seconds] - [$class set timestamp($pa)] < 60} {
if {[$class exists user_in_community($pa)]} {
$class set user_in_community($key) [$class set user_in_community($pa)]
}
$class incr ip24 -1
$class set pa($key) [$class set pa($pa)]
$class set timestamp($key) [$class set timestamp($pa)]
$class unset pa($pa)
$class unset timestamp($pa)
ns_log notice "UNSET timestamp($pa) turned into timestamp($key)"
}
}
}
Users instproc addKey {key pa url community_id is_embedded_request} {
set class [self class]
if {$key ne $pa} {
:check_pa_change $key $pa $url
}
set counter :active($key)
if {[incr $counter] == 1} {
$class incrRefCount $key $pa
}
if {!$is_embedded_request} {
set blacklisted_url [expr {[string match "/RrdGraphJS/public/*" $url]
|| [string match "/munin/*" $url]
}]
if {!$blacklisted_url} {
:community_access $key $pa $community_id
}
if {[string match "*/logout" $url]} {
set now [clock seconds]
set var user_in_community($key)
if {[$class exists $var]} {
set data [$class set $var]
if {[dict exists $data community_id]} {
Users left_community $key $pa $now [dict get $data community_id] $data logout
}
} else {
set data ""
}
Users left_system $key $pa $now $data logout
}
}
lappend :urls($key) [list ${:point_in_time} $url $pa]
$class incr hits($key)
$class set timestamp($key) [clock seconds]
}
Users instproc add_view {uid} {
incr :views($uid)
}
Users proc views_per_minute {uid} {
set mins 0
set views 0
set key views($uid)
foreach i [Users info instances] {
if {[$i exists $key]} {
incr mins
incr views [$i set $key]
}
}
if {$mins > 0} {
return [expr {$views*1.0/$mins}]
}
return 0
}
Users instproc destroy {} {
set class [self class]
if {[Users exists last_mkey] && [Users set last_mkey] eq [self]} {
Users set last_mkey ""
}
foreach key [array names :active] {
if {[::xo::is_ip $key]} {
set pa $key
} else {
set pa [expr {[$class exists pa($key)] ? [$class set pa($key)] : "unknown"}]
}
$class decrRefCount $key $pa [set :active($key)]
}
next
}
Users proc expSmooth {ts key} {
set mins [expr {$ts/60}]
if {[info exists :expSmooth($key)]} {
lassign [set :expSmooth($key)] _ aggval lastmins hits
set mindiff [expr {$mins-$lastmins}]
if {$mindiff == 0} {
incr hits
set retval [expr {$aggval*0.3 + $hits*0.7}]
} else {
set aggval [expr {$aggval*pow(0.3,$mindiff) + $hits*0.7}]
set hits 1
}
} else {
set hits 1
set aggval 1.0
}
if {![info exists retval]} {set retval $aggval}
set :expSmooth($key) [list $retval $aggval $mins $hits]
return $retval
}
Users proc incrRefCount {key pa} {
if {[incr :refcount($key)] == 1} {
if {![info exists :timestamp($key)]} {
if {[::xo::is_ip $key]} {incr :ip24} {incr :auth24}
}
}
set :pa($key) $pa
}
Users proc decrRefCount {key pa hitcount} {
if {[info exists :refcount($key)]} {
set x [incr :refcount($key) -1]
incr :hits($key) -$hitcount
if {$x < 1} {
set var :user_in_community($key)
if {[info exists $var]} {
set data [set $var]
Users left_community $key $pa [clock seconds] [dict get $data community_id] $data inactive
Users left_system $key $pa [clock seconds] $data inactive
} else {
Users left_system $key $pa [clock seconds] {} inactive
if {![::xo::is_ip $key]} {
set address [expr {[info exists :pa($pa)] ? "peer address [set :pa($pa)]" : ""}]
ns_log warning "no community info for $key available $address"
}
}
}
} else {
ns_log notice "no refcount for $key available, probably explicit logout"
}
}
Users proc forget_community {community_id} {
foreach {key data} [array get :user_in_community] {
if {[dict get $data community_id] == $community_id} {
unset -nocomplain :user_in_community($key)
}
}
foreach i [Users info instances] {
$i unset -nocomplain in_community($community_id)
}
}
Users proc compute_nr_users_per_day {} {
set :ip24 0
set :auth24 0
foreach i [array names :timestamp] {
if {[::xo::is_ip $i]} {incr :ip24} {incr :auth24}
}
}
Users proc nr_users_per_day {} {
return [list ${:ip24} ${:auth24}]
}
Users proc users_per_day {} {
set ip [list]; set auth [list]
foreach i [array names :timestamp] {
if {[::xo::is_ip $i]} {
set var ip
} else {
set var auth
}
lappend $var [list $i [set :timestamp($i)]]
}
return [list $ip $auth]
}
Users proc time_window_cleanup {} {
set now [clock seconds]
set maxdiff [expr {[throttler timeWindow] * 60}]
foreach i [array names :pa] {
if {![info exists :timestamp($i)]
|| ($now - [set :timestamp($i)] > $maxdiff)
} {
unset -nocomplain :pa($i) :refcount($i) :expSmooth($i) :switches($i)
}
}
foreach i [array names :refcount] {
if {![info exists :pa($i)]} {
unset :refcount($i)
}
}
}
Users proc perDayCleanup {} {
:time_window_cleanup
set :ip24 0
set :auth24 0
set secsPerDay [expr {3600*24}]
set now [clock seconds]
foreach i [array names :timestamp] {
if {$now - [set :timestamp($i)] > $secsPerDay} {
unset :timestamp($i)
} else {
if {[::xo::is_ip $i]} {incr :ip24} {incr :auth24}
}
}
dump write
}
namespace eval throttle_mod {}
d_proc -private ::throttle_mod::unmap_pool {
{-pool slow}
{-ms}
method
url
} {
Function within throttle monitor thread for registering pool
unmapping requests after a specified time. This function has to run
in this thread to be able to use "::after".
} {
if {![info exists ms]} {
set ms [::map-slow-pool-duration]
}
after $ms [list ::xo::unmap_pool -pool $pool $method $url]
ns_log notice "slow request: mapping of '$url' moved to '$pool' connection pool will be canceled in $ms ms"
}
Object create dump
dump set file ${logdir}/throttle-data.dump
dump proc read {} {
array set Users::timestamp [list]
if {[ad_file readable ${:file}]} {
if {[catch {source ${:file}} errorMsg]} {
ns_log error "during source of ${:file}:\n$errorMsg"
}
}
Users time_window_cleanup
Users compute_nr_users_per_day
if {[ad_file readable ${:file}] && ([clock seconds] - [ad_file mtime ${:file}] > 180)} {
Users unset -nocomplain user_in_community
}
}
dump proc collect {} {
set cmds {}
set o ::Users
foreach var [$o info vars] {
if {$var in {last_mkey hits}} {
continue
}
if {[$o array exists $var]} {
lappend cmds [list $o array set $var [$o array get $var]]
} else {
lappend cmds [list $o set $var [$o set $var]]
}
}
return $cmds
}
dump proc write {{-sync false}} {
set cmds [:collect]
if {$sync} {
set dumpFile [open ${:file} w]
puts -nonewline $dumpFile [join $cmds \n]\n
close $dumpFile
} else {
file delete -force -- ${:file}
set dumpFile [AsyncLogFile new -filename ${:file}]
foreach cmd $cmds {
$dumpFile write $cmd
}
$dumpFile destroy
}
}
Object create Users::users
Users set last_mkey ""
proc showTimers {} {
set _ ""
foreach t [after info] { append _ "$t [after info $t]\n" }
return $_
}
Class create Value -parameter {{value ""} {refresh 10000}}
Value instproc updateValue {} {set :handle [after ${:refresh} [list [self] updateValue]]}
Value create loadAvg
loadAvg proc updateValue {} {
set procloadavg /proc/loadavg
if {[ad_file readable $procloadavg]} {
set f [open $procloadavg];
set :value [lrange [read $f] 0 2]
close $f
}
next
}
loadAvg updateValue
set tail [::util::which tail]
if {[ad_file readable ${logdir}/counter.log] && $tail ne ""} {
ns_log notice "+++ request-monitor: initialize counters"
set number_of_lines [expr {182 * [trend-elements]}]
try {
exec $tail -n $number_of_lines ${logdir}/counter.log >${logdir}/counter-new.log
set f [open $logdir/counter-new.log]
while {-1 != [gets $f line]} {
regexp {(.*) -- (.*) ::(.*) (.*)} $line match timestamp server counter value
if {[nsf::is object $counter]} {
$counter add_value $timestamp $value
} elseif {![info exists complain($counter)]} {
ns_log notice "request-monitor: ignore reload of value $value for counter $counter"
set complain($counter) 1
}
}
} on error {errorMsg} {
ns_log Warning "+++ request-monitor: error initializing counters: $errorMsg"
} finally {
if {[info exists f]} {
close $f
unset f
}
}
}
dump read
::xotcl::Object setExitHandler {
ns_log notice "::throttle: exiting"
dump write -sync true
foreach obj [Users info instances] {$obj destroy}
ns_log notice "::throttle specific exist handler finished"
}
}
set :lightweight 0
set :method GET
set :mutex ns:mutex:tcl:7
set :pa 3.149.24.192
set :persistent 1
set :query proc=+Object+%3A%3Athrottle&source_p=1
set :requester 3.149.24.192
set :tid tid0x7f631fcbe700
set :url /api-doc/proc-view?proc=+Object+%3A%3Athrottle&source_p=1
set :user {client from 3.149.24.192}}
::throttle proc community_access community_id {
:get_context
if {${:community_id} eq ""} {
:users community_access ${:requester} ${:pa} $community_id
}
}
::throttle proc check {} {
:get_context
set hdrs [ns_conn headers]
lassign [:throttle_check ${:requester} ${:pa} ${:url} [ns_conn start] [ns_guesstype [ns_conn url]] ${:community_id} [list pool [ns_conn pool] Sec-Fetch-Dest [ns_set iget $hdrs Sec-Fetch-Dest] X-Requested-With [ns_set iget $hdrs X-Requested-With] Range [ns_set iget $hdrs Range] ]] toMuch ms repeat
if {$repeat > 0} {
:add_statistics repeat ${:requester} ${:pa} ${:url} ${:query}
if {$repeat > 1} {
set result 1
} else {
set result -1
}
} elseif {$toMuch} {
:log "*** we have to refuse user ${:requester} with $toMuch requests"
:add_statistics reject ${:requester} ${:pa} ${:url} ${:query}
set result $toMuch
} elseif {$ms} {
:log "*** we have to block user ${:requester} for $ms ms"
:add_statistics throttle ${:requester} ${:pa} ${:url} ${:query}
after $ms
:log "*** continue for user ${:requester}"
set result 0
} else {
set result 0
}
return $result
}
::throttle proc trace args {
:get_context
:add_url_stat ${:method} ${:url} [:partialtimes] ${:requester} ${:pa} [ns_set iget [ns_conn outputheaders] Content-Type] [ns_conn pool]
unset :context_initialized
return filter_ok
}
::throttle proc partialtimes {} {
set s [ns_conn start]
set d [ns_conn partialtimes]
set t [ns_time diff [ns_time get] $s]
lappend d ms [expr {[ns_time seconds $t]*1000 + [ns_time microseconds $t]/1000}] start $s
return $d
}
::throttle proc postauth args {
set r [:check]
if {$r < 0} {
set url ${:url}
catch {ns_log notice "blocked request for user ${:user} Sec-Fetch-Dest [ns_set iget [ns_conn headers] Sec-Fetch-Dest] url ${:url}"}
catch { ns_log notice ".... [ns_set array [ns_conn headers]]" }
ns_return 429 text/html "
<h1>[_ xotcl-request-monitor.repeated_operation]</h1>
[_ xotcl-request-monitor.operation_blocked]<p>"
return filter_return
} elseif {$r > 0} {
ns_return 429 text/html "
<h1>Invalid Operation</h1>
This web server is only open for interactive usage.<br>
Automated copying and mirroring is not allowed!<p>
Please slow down your requests...<p>"
return filter_return
} else {
return filter_ok
}
}
::throttle proc ms -start_time {
if {![info exists start_time]} {set start_time [ns_conn start]}
set t [ns_time diff [ns_time get] $start_time]
set ms [expr {[ns_time seconds $t]*1000 + [ns_time microseconds $t]/1000}]
return $ms
}
::throttle proc destroy {} {
ns_log notice throttle-DESTROY-shutdownpending->[ns_info shutdownpending]
if {[ns_info shutdownpending] && [nsv_exists ::xotcl::THREAD [self]]} {
set tid [nsv_get ::xotcl::THREAD [self]]
ns_log notice =========throttle-DESTROY-shutdown==========================$tid-??[::thread::exists $tid]
if {[::thread::exists $tid]} {
ns_log notice =========throttle-DESTROY-shutdown==========================THREAD-EXISTS
set refcount [::thread::release $tid]
ns_log notice throttle-DESTROY-shutdownpending->[ns_info shutdownpending]-refCount$refcount
}
}
next
}
::throttle proc get_context {} {
if {[info exists :context_initialized]} return
if {[ns_conn isconnected]} {
set :url [ns_conn url]
} else {
set :url /
}
set :method [ns_conn method]
set :community_id 0
if {[info exists ::ad_conn(package_id)]} {
set :community_id [ad_conn subsite_id]
set package_id [ad_conn package_id]
::xo::ConnectionContext require -package_id $package_id -url ${:url}
if {[info commands dotlrn_community::get_community_id_from_url] ne ""} {
set community_id [dotlrn_community::get_community_id_from_url -url ${:url}]
if {$community_id ne ""} {
set :community_id $community_id
}
}
} else {
::xo::ConnectionContext require -url ${:url}
}
set :requester [::xo::cc requester]
set :user [::xo::cc user]
set :query [ad_conn query]
set :pa [ad_conn peeraddr]
if {${:query} ne ""} {
append :url ?${:query}
}
set :context_initialized 1
}
::throttle forward nr_running %self do throttler array size running_url
::throttle forward thread_avgs %self do throttler %proc
::throttle forward server_threads %self do throttler %proc
::throttle forward throttle_check %self do throttler %proc
::throttle forward url_statistics %self do throttler %proc
::throttle forward on %self do throttler set do_throttle 1
::throttle forward max_values %self do %1 set stats
::throttle forward add_url_stat %self do throttler %proc
::throttle forward running %self do throttler %proc
::throttle forward flush_url_stats %self do throttler %proc
::throttle forward purge_access_stats %self do Users %proc
::throttle forward users %self do Users
::throttle forward add_statistics %self do throttler %proc
::throttle forward statistics %self do throttler %proc
::throttle forward off %self do throttler set do_throttle 0
::throttle forward report_url_stats %self do throttler %proc
::throttle forward last100 %self do throttler %proc
::throttle forward trend %self do %1 set trend
::throttle forward views_per_minute %self do Users %proc
::throttle forward user_is_active %self do Users %proc
XQL Not present:Generic, PostgreSQL, Oracle
[
hide source ]
| [
make this the default ]