Skip to content

Commit 947cfb3

Browse files
authored
Merge pull request #214 from plone/deploy_fixes
Try to fix frontend node image build with buster -> bookworm exclude backend maintenance scripts for now, too much hassle, been there, don that :-/
2 parents de66623 + 2821dc9 commit 947cfb3

6 files changed

Lines changed: 485 additions & 6 deletions

File tree

backend/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@ inituser
4545
pip-wheel-metadata
4646
pyvenv.cfg
4747
.python-version
48+
instance-local.yaml

backend/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pyflakes = ["+*"]
3838
"flake8-*" = ["+*"]
3939

4040
[tool.plone-code-analysis]
41-
paths = "src/ploneorg/setup.py src/ploneorg/src/ scripts/"
41+
paths = "src/ploneorg/setup.py src/ploneorg/src/"
4242
paths_pyroma = "src/ploneorg"
4343

4444
[tool.pytest.ini_options]
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
"""
2+
Run this as a `zopectl run` script via for example:
3+
4+
$ bin/instance run catalogoptimise.py
5+
6+
Note that it does actual transaction commits.
7+
"""
8+
9+
from Acquisition import aq_base
10+
from BTrees.IOBTree import IOBTree
11+
from BTrees.OOBTree import OOBTree
12+
from datetime import datetime
13+
from Products.ZCatalog.ZCatalog import ZCatalog
14+
from Products.ZCTextIndex.Lexicon import Lexicon
15+
from Products.ZCTextIndex.ZCTextIndex import ZCTextIndex
16+
17+
import transaction
18+
19+
20+
def blen(bucket, track_objects=False):
21+
distribution = {}
22+
objects = []
23+
while True:
24+
bucket_len = len(bucket)
25+
if distribution.get(bucket_len):
26+
distribution[bucket_len] += 1
27+
else:
28+
distribution[bucket_len] = 1
29+
if track_objects:
30+
objects.append(bucket)
31+
bucket = bucket._next
32+
if bucket is None:
33+
break
34+
return (distribution, objects)
35+
36+
37+
def get_max_bucket_size(data):
38+
# Data is tree or treeset.
39+
# We calculate instead of hardcoding because values can be patched.
40+
tmp = data.__class__()
41+
if hasattr(tmp, "items"):
42+
update = lambda x: (x, x)
43+
else:
44+
update = lambda x: x
45+
count = 0
46+
tmp.update([update(count)])
47+
bucket = tmp._firstbucket
48+
while bucket._next is None:
49+
count += 1
50+
tmp.update([update(count)])
51+
# Buckets are split on count
52+
return count
53+
54+
55+
def get_bucket_sizes(bucket):
56+
sizes = []
57+
while bucket is not None:
58+
sizes.append(len(bucket))
59+
bucket = bucket._next
60+
return sizes
61+
62+
63+
def new_tree(old_tree, modfactor=9):
64+
# Fill the tree in a two-step process, which should result in better
65+
# fill rates
66+
klass = old_tree.__class__
67+
new = klass()
68+
count = 0
69+
tmp = []
70+
# If the last bucket is not 50% full after first run (it is fuller), it is likely
71+
# to split on second run, and the last 3 buckets will have lower fill rates,
72+
# instead of just the last one.
73+
# Idea: keep the tmp the same size as max and start on 2nd run inbetween 1st run
74+
# but with a max size delay
75+
if hasattr(old_tree, "items"):
76+
# BTree
77+
for k, v in old_tree.items():
78+
modcount = count % modfactor
79+
if modcount % 2 == 0:
80+
new[k] = v
81+
else:
82+
tmp.append((k, v))
83+
count += 1
84+
else:
85+
# Tree set
86+
for k in old_tree.keys():
87+
modcount = count % modfactor
88+
if modcount % 2 == 0:
89+
new.insert(k)
90+
else:
91+
tmp.append(k)
92+
count += 1
93+
94+
# Before adding the rest of the data, we need to make sure the last bucket
95+
# is not more than 50% full.
96+
# Add and remove synthetic values to provoke a bucket split
97+
maxsize = get_max_bucket_size(new)
98+
maxkey = new.maxKey()
99+
if isinstance(maxkey, int):
100+
synthetic = range(
101+
maxkey + 1, maxkey + 2 + (maxsize - get_bucket_sizes(new._firstbucket)[-1])
102+
)
103+
elif isinstance(maxkey, basestring): # noqa: F821
104+
synthetic = [
105+
maxkey + str(x)
106+
for x in range((maxsize - get_bucket_sizes(new._firstbucket)[-1]) + 1)
107+
]
108+
else:
109+
synthetic = []
110+
111+
if hasattr(new, "items"):
112+
for s in synthetic:
113+
new[s] = 0
114+
for s in synthetic:
115+
del new[s]
116+
else:
117+
for s in synthetic:
118+
new.insert(s)
119+
for s in synthetic:
120+
new.remove(s)
121+
122+
# Add the rest of the data
123+
new.update(tmp)
124+
125+
# Verify data
126+
assert len(old_tree) == len(new)
127+
return new
128+
129+
130+
def optimize_tree(parent, k, v, attr=True):
131+
transaction.begin()
132+
bucket = getattr(v, "_firstbucket", None)
133+
if bucket is None:
134+
return 0
135+
readCurrent = getattr(bucket._p_jar, "readCurrent", None)
136+
if readCurrent is not None:
137+
track_objects = True
138+
else:
139+
track_objects = False
140+
before_distribution, objects = blen(bucket, track_objects=track_objects)
141+
142+
# do we have bucket lengths more than one which exist and aren't 90% full?
143+
# we assume here that 90% is one of 27, 54 or 108
144+
try:
145+
unoptimized = any([a % 9 for a, b in before_distribution.items() if b > 1])
146+
except NameError:
147+
# Python 2.4 doesn't have any, we'll just loop over all items
148+
unoptimized = bool([a % 9 for a, b in before_distribution.items() if b > 1])
149+
150+
if unoptimized:
151+
# Gather stats used to figure out modfactor
152+
before = sum(before_distribution.values())
153+
maxsize = get_max_bucket_size(v)
154+
averagesize = (
155+
sum([kk * vv for kk, vv in before_distribution.items()]) * 1.0 / before
156+
)
157+
bucketsizes = [
158+
x
159+
for sublist in [
160+
(kk,) * vv for kk, vv in sorted(before_distribution.items())
161+
]
162+
for x in sublist
163+
]
164+
median = bucketsizes[before / 2]
165+
166+
# Filling the tree in a two-step process. The first time we set up the tree,
167+
# values are inserted sequentially, resulting in 50% fill rate.
168+
# The second time we fill up with additional values to get fill rate higher
169+
# than 50%.
170+
# We want to set optimal fill rates based on current fill rate.
171+
# Fill rates of 55% or below indicates sequential index like dateindex
172+
# and we want 100% fill rate, otherwise 90% is good.
173+
avgrate = float(averagesize) / maxsize
174+
medianrate = float(median) / maxsize
175+
if avgrate < 0.55 or medianrate < 0.55 or medianrate > 0.95:
176+
modfactor = 2 # same number of items in both runs gives 100% fill
177+
else:
178+
modfactor = 9 # 5 in first run and 4 in second run gives 90% fill rate
179+
180+
new = new_tree(v, modfactor)
181+
after_distribution, _ = blen(new._firstbucket)
182+
after = sum(after_distribution.values())
183+
if after < before:
184+
if readCurrent is not None:
185+
for obj in objects:
186+
readCurrent(obj)
187+
if attr:
188+
setattr(parent, k, new)
189+
else:
190+
parent[k] = new
191+
parent._p_changed = True
192+
many_buckets = {}
193+
few_buckets = []
194+
for k, v in after_distribution.items():
195+
if v > 1:
196+
many_buckets[k] = v
197+
else:
198+
few_buckets.append(k)
199+
newaveragesize = (
200+
sum([kk * vv for kk, vv in after_distribution.items()]) * 1.0 / after
201+
)
202+
newavgrate = float(newaveragesize) / maxsize
203+
print(
204+
"New buckets {fill size: count}: %s\nSingle buckets: %s\n"
205+
"fill: before %.3f after %.3f"
206+
% (str(many_buckets), str(few_buckets), avgrate, newavgrate)
207+
)
208+
transaction.commit()
209+
return before - after
210+
211+
conn = parent._p_jar
212+
if conn:
213+
conn.cacheGC()
214+
transaction.abort()
215+
return 0
216+
217+
218+
def optimize(obj, no_data=False):
219+
obj = aq_base(obj)
220+
result = 0
221+
obj._p_activate()
222+
for k, v in obj.__dict__.items():
223+
if no_data and k == "data":
224+
# data blows up memory too much
225+
continue
226+
result += optimize_tree(obj, k, v)
227+
# handle sets inside *OBTrees
228+
if isinstance(v, (IOBTree, OOBTree)):
229+
obj._p_activate()
230+
new_v = obj.__dict__[k]
231+
for k2, v2 in new_v.iteritems():
232+
result += optimize_tree(new_v, k2, v2, attr=False)
233+
print("Optimized away %s buckets in %s" % (result, obj))
234+
return result
235+
236+
237+
# Loop over all Plone sites
238+
for site in app.values(): # noqa: F821
239+
if not site.meta_type == "Plone Site":
240+
continue
241+
242+
site_id = site.getId()
243+
now = datetime.now().isoformat()
244+
print('%s - Starting for site "%s" ...' % (now, site_id))
245+
combined = 0
246+
for zcatalog in site.values():
247+
if not isinstance(zcatalog, ZCatalog):
248+
continue
249+
zcatalog_id = zcatalog.getId()
250+
now = datetime.now().isoformat()
251+
print('%s - Optimizing "%s"' % (now, zcatalog_id))
252+
catalog = zcatalog._catalog
253+
# optimize paths, uids, data - skip data for portal_catalog
254+
combined += optimize(catalog, no_data=zcatalog_id == "portal_catalog")
255+
# optimize lexica
256+
for obj in zcatalog.values():
257+
if isinstance(obj, Lexicon):
258+
combined += optimize(obj)
259+
# optimize indexes
260+
for index in catalog.indexes.values():
261+
if isinstance(index, ZCTextIndex):
262+
combined += optimize(index.index)
263+
else:
264+
combined += optimize(index)
265+
print('Optimized away %s buckets for site "%s"' % (combined, site_id))
266+
267+
print("%s - Finishing..." % datetime.now().isoformat())
268+
transaction.commit()

0 commit comments

Comments
 (0)