forked from Automattic/vip-go-mu-plugins
-
Notifications
You must be signed in to change notification settings - Fork 0
/
alloptions-limit.php
178 lines (143 loc) · 5.76 KB
/
alloptions-limit.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
<?php
/**
* Plugin Name: VIP AllOptions Safeguard
* Description: Provides warnings and notifications for wp_options exceeding limits.
* Author: Automattic
* License: GPL version 2 or later - http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*/
namespace Automattic\VIP\AllOptions;
use Automattic\VIP\Utils\Alerts;
require_once __DIR__ . '/lib/utils/class-alerts.php';
add_action( 'plugins_loaded', __NAMESPACE__ . '\run_alloptions_safeguard' );
define( 'VIP_ALLOPTIONS_ERROR_THRESHOLD', 1000000 );
/**
* The purpose of this limit is to safe-guard against a barrage of requests with cache sets for values that are too large.
* Because WP would keep trying to set the data to Memcached, potentially resulting in Memcached (and site's) performance degradation.
*/
function run_alloptions_safeguard() {
if ( defined( 'WP_CLI' ) && WP_CLI ) {
return;
}
// Uncompressed size thresholds.
// Warn should *always* be =< die
$alloptions_size_warn = MB_IN_BYTES * 2.5;
// To avoid performing a potentially expensive calculation of the compressed size we use 4MB uncompressed (which is likely less than 1MB compressed)
$alloptions_size_die = MB_IN_BYTES * 4;
$alloptions_size = wp_cache_get( 'alloptions_size' );
// Cache miss
if ( false === $alloptions_size ) {
$alloptions = maybe_serialize( wp_load_alloptions() );
$alloptions_size = strlen( $alloptions );
wp_cache_add( 'alloptions_size', $alloptions_size, '', 60 );
}
$warning = $alloptions_size > $alloptions_size_warn;
$maybe_blocked = $alloptions_size > $alloptions_size_die;
$really_blocked = false;
$alloptions_size_compressed = 0;
if ( ! $warning ) {
return;
}
if ( $maybe_blocked ) {
// It's likely at this point the site is already experiencing performance degradation.
// We're using gzdeflate here because pecl-memcache uses Zlib compression for large values.
// See https://github.com/websupport-sk/pecl-memcache/blob/e014963c1360d764e3678e91fb73d03fc64458f7/src/memcache_pool.c#L303-L354
$alloptions_size_compressed = wp_cache_get( 'alloptions_size_compressed' );
if ( ! $alloptions_size_compressed ) {
$alloptions_size_deflated = gzdeflate( maybe_serialize( wp_load_alloptions() ) );
$alloptions_size_compressed = false !== $alloptions_size_deflated ? strlen( $alloptions_size_deflated ) : VIP_ALLOPTIONS_ERROR_THRESHOLD - 1;
wp_cache_add( 'alloptions_size_compressed', $alloptions_size_compressed, '', 60 );
}
}
if ( $alloptions_size_compressed >= VIP_ALLOPTIONS_ERROR_THRESHOLD ) {
$really_blocked = true;
}
// NOTE - This function has built-in rate limiting so it's ok to call on every request
alloptions_safeguard_notify( $alloptions_size, $alloptions_size_compressed, $really_blocked );
// Will exit with a 503
if ( $really_blocked ) {
alloptions_safeguard_die();
}
}
/**
* Show error page and exit
*/
function alloptions_safeguard_die() {
// 503 Service Unavailable - prevent caching, indexing, etc and alert Varnish of the problem
http_response_code( 503 );
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- no need to escape the premade HTML file
echo file_get_contents( __DIR__ . '/errors/alloptions-limit.html' );
exit;
}
/**
* Send notification
*
* @param int $size Uncompressed sized of alloptions, in bytes
* @param int $size_compressed Compressed size of alloption, in bytes.
* HOWEVER, this is only set if $size meets a threshold.
* @see run_alloptions_safeguard()
* @param bool $really_blocked True if the options size is large enough to cause site to be blocked from loading.
*/
function alloptions_safeguard_notify( $size, $size_compressed = 0, $really_blocked = true ) {
global $wpdb;
$throttle_was_set = wp_cache_add( 'alloptions', 1, 'throttle', 30 * MINUTE_IN_SECONDS );
// If adding to cache failed, we're already throttled (unless the operation actually failed),
// so return without doing anything.
if ( false === $throttle_was_set ) {
return;
}
/**
* Fires under alloptions warning conditions
*
* @param bool $really_blocked False if alloptions size is large. True if site loading is being blocked.
*/
do_action( 'vip_alloptions_safeguard_notify', $really_blocked );
$is_vip_env = ( defined( 'WPCOM_IS_VIP_ENV' ) && true === WPCOM_IS_VIP_ENV );
$environment = ( ( defined( 'VIP_GO_ENV' ) && VIP_GO_ENV ) ? VIP_GO_ENV : 'unknown' );
$site_id = defined( 'FILES_CLIENT_SITE_ID' ) ? FILES_CLIENT_SITE_ID : false;
// Send notices to VIP staff if this is happening on VIP-hosted sites
if (
! $is_vip_env ||
! $site_id ||
! defined( 'ALERT_SERVICE_ADDRESS' ) ||
! ALERT_SERVICE_ADDRESS ||
'production' !== $environment
) {
return;
}
$subject = 'ALLOPTIONS: %1$s (%2$s VIP Go site ID: %3$s';
if ( 0 !== $wpdb->blogid ) {
$subject .= ", blog ID {$wpdb->blogid}";
}
$subject .= ') options is up to %4$s';
$subject = sprintf(
$subject,
esc_url( home_url() ),
esc_html( $environment ),
(int) $site_id,
size_format( $size )
);
if ( $really_blocked ) {
$priority = 'P2';
$description = sprintf( 'The size of AllOptions has breached %s bytes', VIP_ALLOPTIONS_ERROR_THRESHOLD );
} elseif ( $size_compressed > 0 ) {
$priority = 'P3';
$description = sprintf( 'The size of AllOptions is at %1$s bytes (compressed), %2$s bytes (uncompressed)', $size_compressed, $size );
} else {
$priority = 'P5';
$description = sprintf( 'The size of AllOptions is at %1$s bytes (uncompressed)', $size );
}
// Send to OpsGenie
$alerts = Alerts::instance();
$alerts->opsgenie(
$subject,
array(
'alias' => 'alloptions/' . $site_id,
'description' => $description,
'entity' => (string) $site_id,
'priority' => $priority,
'source' => 'sites/alloptions-size',
),
'alloptions-size-alert',
'10'
);
}