##// END OF EJS Templates
feat(archive-cache): added option to define a configurable top-level bucket for all shards
super-admin -
r5445:fdcdfe77 default
parent child Browse files
Show More
@@ -1,221 +1,222 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import os
19 import os
20 import tempfile
20 import tempfile
21 import logging
21 import logging
22
22
23 from pyramid.settings import asbool
23 from pyramid.settings import asbool
24
24
25 from rhodecode.config.settings_maker import SettingsMaker
25 from rhodecode.config.settings_maker import SettingsMaker
26 from rhodecode.config import utils as config_utils
26 from rhodecode.config import utils as config_utils
27
27
28 log = logging.getLogger(__name__)
28 log = logging.getLogger(__name__)
29
29
30
30
31 def sanitize_settings_and_apply_defaults(global_config, settings):
31 def sanitize_settings_and_apply_defaults(global_config, settings):
32 """
32 """
33 Applies settings defaults and does all type conversion.
33 Applies settings defaults and does all type conversion.
34
34
35 We would move all settings parsing and preparation into this place, so that
35 We would move all settings parsing and preparation into this place, so that
36 we have only one place left which deals with this part. The remaining parts
36 we have only one place left which deals with this part. The remaining parts
37 of the application would start to rely fully on well-prepared settings.
37 of the application would start to rely fully on well-prepared settings.
38
38
39 This piece would later be split up per topic to avoid a big fat monster
39 This piece would later be split up per topic to avoid a big fat monster
40 function.
40 function.
41 """
41 """
42 jn = os.path.join
42 jn = os.path.join
43
43
44 global_settings_maker = SettingsMaker(global_config)
44 global_settings_maker = SettingsMaker(global_config)
45 global_settings_maker.make_setting('debug', default=False, parser='bool')
45 global_settings_maker.make_setting('debug', default=False, parser='bool')
46 debug_enabled = asbool(global_config.get('debug'))
46 debug_enabled = asbool(global_config.get('debug'))
47
47
48 settings_maker = SettingsMaker(settings)
48 settings_maker = SettingsMaker(settings)
49
49
50 settings_maker.make_setting(
50 settings_maker.make_setting(
51 'logging.autoconfigure',
51 'logging.autoconfigure',
52 default=False,
52 default=False,
53 parser='bool')
53 parser='bool')
54
54
55 logging_conf = jn(os.path.dirname(global_config.get('__file__')), 'logging.ini')
55 logging_conf = jn(os.path.dirname(global_config.get('__file__')), 'logging.ini')
56 settings_maker.enable_logging(logging_conf, level='INFO' if debug_enabled else 'DEBUG')
56 settings_maker.enable_logging(logging_conf, level='INFO' if debug_enabled else 'DEBUG')
57
57
58 # Default includes, possible to change as a user
58 # Default includes, possible to change as a user
59 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
59 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
60 log.debug(
60 log.debug(
61 "Using the following pyramid.includes: %s",
61 "Using the following pyramid.includes: %s",
62 pyramid_includes)
62 pyramid_includes)
63
63
64 settings_maker.make_setting('rhodecode.edition', 'Community Edition')
64 settings_maker.make_setting('rhodecode.edition', 'Community Edition')
65 settings_maker.make_setting('rhodecode.edition_id', 'CE')
65 settings_maker.make_setting('rhodecode.edition_id', 'CE')
66
66
67 if 'mako.default_filters' not in settings:
67 if 'mako.default_filters' not in settings:
68 # set custom default filters if we don't have it defined
68 # set custom default filters if we don't have it defined
69 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
69 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
70 settings['mako.default_filters'] = 'h_filter'
70 settings['mako.default_filters'] = 'h_filter'
71
71
72 if 'mako.directories' not in settings:
72 if 'mako.directories' not in settings:
73 mako_directories = settings.setdefault('mako.directories', [
73 mako_directories = settings.setdefault('mako.directories', [
74 # Base templates of the original application
74 # Base templates of the original application
75 'rhodecode:templates',
75 'rhodecode:templates',
76 ])
76 ])
77 log.debug(
77 log.debug(
78 "Using the following Mako template directories: %s",
78 "Using the following Mako template directories: %s",
79 mako_directories)
79 mako_directories)
80
80
81 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
81 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
82 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
82 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
83 raw_url = settings['beaker.session.url']
83 raw_url = settings['beaker.session.url']
84 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
84 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
85 settings['beaker.session.url'] = 'redis://' + raw_url
85 settings['beaker.session.url'] = 'redis://' + raw_url
86
86
87 settings_maker.make_setting('__file__', global_config.get('__file__'))
87 settings_maker.make_setting('__file__', global_config.get('__file__'))
88
88
89 # TODO: johbo: Re-think this, usually the call to config.include
89 # TODO: johbo: Re-think this, usually the call to config.include
90 # should allow to pass in a prefix.
90 # should allow to pass in a prefix.
91 settings_maker.make_setting('rhodecode.api.url', '/_admin/api')
91 settings_maker.make_setting('rhodecode.api.url', '/_admin/api')
92
92
93 # Sanitize generic settings.
93 # Sanitize generic settings.
94 settings_maker.make_setting('default_encoding', 'UTF-8', parser='list')
94 settings_maker.make_setting('default_encoding', 'UTF-8', parser='list')
95 settings_maker.make_setting('gzip_responses', False, parser='bool')
95 settings_maker.make_setting('gzip_responses', False, parser='bool')
96 settings_maker.make_setting('startup.import_repos', 'false', parser='bool')
96 settings_maker.make_setting('startup.import_repos', 'false', parser='bool')
97
97
98 # statsd
98 # statsd
99 settings_maker.make_setting('statsd.enabled', False, parser='bool')
99 settings_maker.make_setting('statsd.enabled', False, parser='bool')
100 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
100 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
101 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
101 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
102 settings_maker.make_setting('statsd.statsd_prefix', '')
102 settings_maker.make_setting('statsd.statsd_prefix', '')
103 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
103 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
104
104
105 settings_maker.make_setting('vcs.svn.compatible_version', '')
105 settings_maker.make_setting('vcs.svn.compatible_version', '')
106 settings_maker.make_setting('vcs.svn.proxy.enabled', True, parser='bool')
106 settings_maker.make_setting('vcs.svn.proxy.enabled', True, parser='bool')
107 settings_maker.make_setting('vcs.svn.proxy.host', 'http://svn:8090', parser='string')
107 settings_maker.make_setting('vcs.svn.proxy.host', 'http://svn:8090', parser='string')
108 settings_maker.make_setting('vcs.hooks.protocol', 'http')
108 settings_maker.make_setting('vcs.hooks.protocol', 'http')
109 settings_maker.make_setting('vcs.hooks.host', '*')
109 settings_maker.make_setting('vcs.hooks.host', '*')
110 settings_maker.make_setting('vcs.scm_app_implementation', 'http')
110 settings_maker.make_setting('vcs.scm_app_implementation', 'http')
111 settings_maker.make_setting('vcs.server', '')
111 settings_maker.make_setting('vcs.server', '')
112 settings_maker.make_setting('vcs.server.protocol', 'http')
112 settings_maker.make_setting('vcs.server.protocol', 'http')
113 settings_maker.make_setting('vcs.server.enable', 'true', parser='bool')
113 settings_maker.make_setting('vcs.server.enable', 'true', parser='bool')
114 settings_maker.make_setting('vcs.hooks.direct_calls', 'false', parser='bool')
114 settings_maker.make_setting('vcs.hooks.direct_calls', 'false', parser='bool')
115 settings_maker.make_setting('vcs.start_server', 'false', parser='bool')
115 settings_maker.make_setting('vcs.start_server', 'false', parser='bool')
116 settings_maker.make_setting('vcs.backends', 'hg, git, svn', parser='list')
116 settings_maker.make_setting('vcs.backends', 'hg, git, svn', parser='list')
117 settings_maker.make_setting('vcs.connection_timeout', 3600, parser='int')
117 settings_maker.make_setting('vcs.connection_timeout', 3600, parser='int')
118
118
119 settings_maker.make_setting('vcs.methods.cache', True, parser='bool')
119 settings_maker.make_setting('vcs.methods.cache', True, parser='bool')
120
120
121 # repo_store path
121 # repo_store path
122 settings_maker.make_setting('repo_store.path', '/var/opt/rhodecode_repo_store')
122 settings_maker.make_setting('repo_store.path', '/var/opt/rhodecode_repo_store')
123 # Support legacy values of vcs.scm_app_implementation. Legacy
123 # Support legacy values of vcs.scm_app_implementation. Legacy
124 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
124 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
125 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
125 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
126 scm_app_impl = settings['vcs.scm_app_implementation']
126 scm_app_impl = settings['vcs.scm_app_implementation']
127 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
127 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
128 settings['vcs.scm_app_implementation'] = 'http'
128 settings['vcs.scm_app_implementation'] = 'http'
129
129
130 settings_maker.make_setting('appenlight', False, parser='bool')
130 settings_maker.make_setting('appenlight', False, parser='bool')
131
131
132 temp_store = tempfile.gettempdir()
132 temp_store = tempfile.gettempdir()
133 tmp_cache_dir = jn(temp_store, 'rc_cache')
133 tmp_cache_dir = jn(temp_store, 'rc_cache')
134
134
135 # save default, cache dir, and use it for all backends later.
135 # save default, cache dir, and use it for all backends later.
136 default_cache_dir = settings_maker.make_setting(
136 default_cache_dir = settings_maker.make_setting(
137 'cache_dir',
137 'cache_dir',
138 default=tmp_cache_dir, default_when_empty=True,
138 default=tmp_cache_dir, default_when_empty=True,
139 parser='dir:ensured')
139 parser='dir:ensured')
140
140
141 # exception store cache
141 # exception store cache
142 settings_maker.make_setting(
142 settings_maker.make_setting(
143 'exception_tracker.store_path',
143 'exception_tracker.store_path',
144 default=jn(default_cache_dir, 'exc_store'), default_when_empty=True,
144 default=jn(default_cache_dir, 'exc_store'), default_when_empty=True,
145 parser='dir:ensured'
145 parser='dir:ensured'
146 )
146 )
147
147
148 settings_maker.make_setting(
148 settings_maker.make_setting(
149 'celerybeat-schedule.path',
149 'celerybeat-schedule.path',
150 default=jn(default_cache_dir, 'celerybeat_schedule', 'celerybeat-schedule.db'), default_when_empty=True,
150 default=jn(default_cache_dir, 'celerybeat_schedule', 'celerybeat-schedule.db'), default_when_empty=True,
151 parser='file:ensured'
151 parser='file:ensured'
152 )
152 )
153
153
154 settings_maker.make_setting('exception_tracker.send_email', False, parser='bool')
154 settings_maker.make_setting('exception_tracker.send_email', False, parser='bool')
155 settings_maker.make_setting('exception_tracker.email_prefix', '[RHODECODE ERROR]', default_when_empty=True)
155 settings_maker.make_setting('exception_tracker.email_prefix', '[RHODECODE ERROR]', default_when_empty=True)
156
156
157 # sessions, ensure file since no-value is memory
157 # sessions, ensure file since no-value is memory
158 settings_maker.make_setting('beaker.session.type', 'file')
158 settings_maker.make_setting('beaker.session.type', 'file')
159 settings_maker.make_setting('beaker.session.data_dir', jn(default_cache_dir, 'session_data'))
159 settings_maker.make_setting('beaker.session.data_dir', jn(default_cache_dir, 'session_data'))
160
160
161 # cache_general
161 # cache_general
162 settings_maker.make_setting('rc_cache.cache_general.backend', 'dogpile.cache.rc.file_namespace')
162 settings_maker.make_setting('rc_cache.cache_general.backend', 'dogpile.cache.rc.file_namespace')
163 settings_maker.make_setting('rc_cache.cache_general.expiration_time', 60 * 60 * 12, parser='int')
163 settings_maker.make_setting('rc_cache.cache_general.expiration_time', 60 * 60 * 12, parser='int')
164 settings_maker.make_setting('rc_cache.cache_general.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_general.db'))
164 settings_maker.make_setting('rc_cache.cache_general.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_general.db'))
165
165
166 # cache_perms
166 # cache_perms
167 settings_maker.make_setting('rc_cache.cache_perms.backend', 'dogpile.cache.rc.file_namespace')
167 settings_maker.make_setting('rc_cache.cache_perms.backend', 'dogpile.cache.rc.file_namespace')
168 settings_maker.make_setting('rc_cache.cache_perms.expiration_time', 60 * 60, parser='int')
168 settings_maker.make_setting('rc_cache.cache_perms.expiration_time', 60 * 60, parser='int')
169 settings_maker.make_setting('rc_cache.cache_perms.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_perms_db'))
169 settings_maker.make_setting('rc_cache.cache_perms.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_perms_db'))
170
170
171 # cache_repo
171 # cache_repo
172 settings_maker.make_setting('rc_cache.cache_repo.backend', 'dogpile.cache.rc.file_namespace')
172 settings_maker.make_setting('rc_cache.cache_repo.backend', 'dogpile.cache.rc.file_namespace')
173 settings_maker.make_setting('rc_cache.cache_repo.expiration_time', 60 * 60 * 24 * 30, parser='int')
173 settings_maker.make_setting('rc_cache.cache_repo.expiration_time', 60 * 60 * 24 * 30, parser='int')
174 settings_maker.make_setting('rc_cache.cache_repo.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_repo_db'))
174 settings_maker.make_setting('rc_cache.cache_repo.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_repo_db'))
175
175
176 # cache_license
176 # cache_license
177 settings_maker.make_setting('rc_cache.cache_license.backend', 'dogpile.cache.rc.file_namespace')
177 settings_maker.make_setting('rc_cache.cache_license.backend', 'dogpile.cache.rc.file_namespace')
178 settings_maker.make_setting('rc_cache.cache_license.expiration_time', 60 * 5, parser='int')
178 settings_maker.make_setting('rc_cache.cache_license.expiration_time', 60 * 5, parser='int')
179 settings_maker.make_setting('rc_cache.cache_license.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_license_db'))
179 settings_maker.make_setting('rc_cache.cache_license.arguments.filename', jn(default_cache_dir, 'rhodecode_cache_license_db'))
180
180
181 # cache_repo_longterm memory, 96H
181 # cache_repo_longterm memory, 96H
182 settings_maker.make_setting('rc_cache.cache_repo_longterm.backend', 'dogpile.cache.rc.memory_lru')
182 settings_maker.make_setting('rc_cache.cache_repo_longterm.backend', 'dogpile.cache.rc.memory_lru')
183 settings_maker.make_setting('rc_cache.cache_repo_longterm.expiration_time', 345600, parser='int')
183 settings_maker.make_setting('rc_cache.cache_repo_longterm.expiration_time', 345600, parser='int')
184 settings_maker.make_setting('rc_cache.cache_repo_longterm.max_size', 10000, parser='int')
184 settings_maker.make_setting('rc_cache.cache_repo_longterm.max_size', 10000, parser='int')
185
185
186 # sql_cache_short
186 # sql_cache_short
187 settings_maker.make_setting('rc_cache.sql_cache_short.backend', 'dogpile.cache.rc.memory_lru')
187 settings_maker.make_setting('rc_cache.sql_cache_short.backend', 'dogpile.cache.rc.memory_lru')
188 settings_maker.make_setting('rc_cache.sql_cache_short.expiration_time', 30, parser='int')
188 settings_maker.make_setting('rc_cache.sql_cache_short.expiration_time', 30, parser='int')
189 settings_maker.make_setting('rc_cache.sql_cache_short.max_size', 10000, parser='int')
189 settings_maker.make_setting('rc_cache.sql_cache_short.max_size', 10000, parser='int')
190
190
191 # archive_cache
191 # archive_cache
192 settings_maker.make_setting('archive_cache.locking.url', 'redis://redis:6379/1')
192 settings_maker.make_setting('archive_cache.locking.url', 'redis://redis:6379/1')
193 settings_maker.make_setting('archive_cache.backend.type', 'filesystem')
193 settings_maker.make_setting('archive_cache.backend.type', 'filesystem')
194
194
195 settings_maker.make_setting('archive_cache.filesystem.store_dir', jn(default_cache_dir, 'archive_cache'), default_when_empty=True,)
195 settings_maker.make_setting('archive_cache.filesystem.store_dir', jn(default_cache_dir, 'archive_cache'), default_when_empty=True,)
196 settings_maker.make_setting('archive_cache.filesystem.cache_shards', 8, parser='int')
196 settings_maker.make_setting('archive_cache.filesystem.cache_shards', 8, parser='int')
197 settings_maker.make_setting('archive_cache.filesystem.cache_size_gb', 10, parser='float')
197 settings_maker.make_setting('archive_cache.filesystem.cache_size_gb', 10, parser='float')
198 settings_maker.make_setting('archive_cache.filesystem.eviction_policy', 'least-recently-stored')
198 settings_maker.make_setting('archive_cache.filesystem.eviction_policy', 'least-recently-stored')
199
199
200 settings_maker.make_setting('archive_cache.filesystem.retry', False, parser='bool')
200 settings_maker.make_setting('archive_cache.filesystem.retry', False, parser='bool')
201 settings_maker.make_setting('archive_cache.filesystem.retry_backoff', 1, parser='int')
201 settings_maker.make_setting('archive_cache.filesystem.retry_backoff', 1, parser='int')
202 settings_maker.make_setting('archive_cache.filesystem.retry_attempts', 10, parser='int')
202 settings_maker.make_setting('archive_cache.filesystem.retry_attempts', 10, parser='int')
203
203
204 settings_maker.make_setting('archive_cache.objectstore.url', jn(default_cache_dir, 'archive_cache'), default_when_empty=True,)
204 settings_maker.make_setting('archive_cache.objectstore.url', jn(default_cache_dir, 'archive_cache'), default_when_empty=True,)
205 settings_maker.make_setting('archive_cache.objectstore.key', '')
205 settings_maker.make_setting('archive_cache.objectstore.key', '')
206 settings_maker.make_setting('archive_cache.objectstore.secret', '')
206 settings_maker.make_setting('archive_cache.objectstore.secret', '')
207 settings_maker.make_setting('archive_cache.objectstore.bucket_root', 'rhodecode-archive-cache')
207 settings_maker.make_setting('archive_cache.objectstore.bucket_shards', 8, parser='int')
208 settings_maker.make_setting('archive_cache.objectstore.bucket_shards', 8, parser='int')
208
209
209 settings_maker.make_setting('archive_cache.objectstore.cache_size_gb', 10, parser='float')
210 settings_maker.make_setting('archive_cache.objectstore.cache_size_gb', 10, parser='float')
210 settings_maker.make_setting('archive_cache.objectstore.eviction_policy', 'least-recently-stored')
211 settings_maker.make_setting('archive_cache.objectstore.eviction_policy', 'least-recently-stored')
211
212
212 settings_maker.make_setting('archive_cache.objectstore.retry', False, parser='bool')
213 settings_maker.make_setting('archive_cache.objectstore.retry', False, parser='bool')
213 settings_maker.make_setting('archive_cache.objectstore.retry_backoff', 1, parser='int')
214 settings_maker.make_setting('archive_cache.objectstore.retry_backoff', 1, parser='int')
214 settings_maker.make_setting('archive_cache.objectstore.retry_attempts', 10, parser='int')
215 settings_maker.make_setting('archive_cache.objectstore.retry_attempts', 10, parser='int')
215
216
216 settings_maker.env_expand()
217 settings_maker.env_expand()
217
218
218 # configure instance id
219 # configure instance id
219 config_utils.set_instance_id(settings)
220 config_utils.set_instance_id(settings)
220
221
221 return settings
222 return settings
@@ -1,166 +1,167 b''
1 # Copyright (C) 2015-2024 RhodeCode GmbH
1 # Copyright (C) 2015-2024 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import codecs
19 import codecs
20 import hashlib
20 import hashlib
21 import logging
21 import logging
22 import os
22 import os
23
23
24 import fsspec
24 import fsspec
25
25
26 from .base import BaseCache, BaseShard
26 from .base import BaseCache, BaseShard
27 from ..utils import ShardFileReader, NOT_GIVEN
27 from ..utils import ShardFileReader, NOT_GIVEN
28 from ...type_utils import str2bool
28 from ...type_utils import str2bool
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32
32
33 class FileSystemShard(BaseShard):
33 class FileSystemShard(BaseShard):
34
34
35 def __init__(self, index, directory, **settings):
35 def __init__(self, index, directory, **settings):
36 self._index = index
36 self._index = index
37 self._directory = directory
37 self._directory = directory
38 self.storage_type = 'directory'
38 self.storage_type = 'directory'
39 self.fs = fsspec.filesystem('file')
39 self.fs = fsspec.filesystem('file')
40
40
41 @property
41 @property
42 def directory(self):
42 def directory(self):
43 """Cache directory."""
43 """Cache directory."""
44 return self._directory
44 return self._directory
45
45
46 def _get_keyfile(self, archive_key) -> tuple[str, str]:
46 def _get_keyfile(self, archive_key) -> tuple[str, str]:
47 key_file = f'{archive_key}.{self.key_suffix}'
47 key_file = f'{archive_key}.{self.key_suffix}'
48 return key_file, os.path.join(self.directory, key_file)
48 return key_file, os.path.join(self.directory, key_file)
49
49
50 def _get_writer(self, path, mode):
50 def _get_writer(self, path, mode):
51 for count in range(1, 11):
51 for count in range(1, 11):
52 try:
52 try:
53 # Another cache may have deleted the directory before
53 # Another cache may have deleted the directory before
54 # the file could be opened.
54 # the file could be opened.
55 return self.fs.open(path, mode)
55 return self.fs.open(path, mode)
56 except OSError:
56 except OSError:
57 if count == 10:
57 if count == 10:
58 # Give up after 10 tries to open the file.
58 # Give up after 10 tries to open the file.
59 raise
59 raise
60 continue
60 continue
61
61
62 def _write_file(self, full_path, iterator, mode):
62 def _write_file(self, full_path, iterator, mode):
63 # ensure dir exists
63 # ensure dir exists
64 destination, _ = os.path.split(full_path)
64 destination, _ = os.path.split(full_path)
65 if not self.fs.exists(destination):
65 if not self.fs.exists(destination):
66 self.fs.makedirs(destination)
66 self.fs.makedirs(destination)
67
67
68 writer = self._get_writer(full_path, mode)
68 writer = self._get_writer(full_path, mode)
69
69
70 digest = hashlib.sha256()
70 digest = hashlib.sha256()
71 with writer:
71 with writer:
72 size = 0
72 size = 0
73 for chunk in iterator:
73 for chunk in iterator:
74 size += len(chunk)
74 size += len(chunk)
75 digest.update(chunk)
75 digest.update(chunk)
76 writer.write(chunk)
76 writer.write(chunk)
77 writer.flush()
77 writer.flush()
78 # Get the file descriptor
78 # Get the file descriptor
79 fd = writer.fileno()
79 fd = writer.fileno()
80
80
81 # Sync the file descriptor to disk, helps with NFS cases...
81 # Sync the file descriptor to disk, helps with NFS cases...
82 os.fsync(fd)
82 os.fsync(fd)
83 sha256 = digest.hexdigest()
83 sha256 = digest.hexdigest()
84 log.debug('written new archive cache under %s, sha256: %s', full_path, sha256)
84 log.debug('written new archive cache under %s, sha256: %s', full_path, sha256)
85 return size, sha256
85 return size, sha256
86
86
87 def store(self, key, value_reader, metadata: dict | None = None):
87 def store(self, key, value_reader, metadata: dict | None = None):
88 return self._store(key, value_reader, metadata, mode='xb')
88 return self._store(key, value_reader, metadata, mode='xb')
89
89
90 def fetch(self, key, retry=NOT_GIVEN, retry_attempts=NOT_GIVEN, retry_backoff=1) -> tuple[ShardFileReader, dict]:
90 def fetch(self, key, retry=NOT_GIVEN, retry_attempts=NOT_GIVEN, retry_backoff=1) -> tuple[ShardFileReader, dict]:
91 return self._fetch(key, retry, retry_attempts, retry_backoff)
91 return self._fetch(key, retry, retry_attempts, retry_backoff)
92
92
93 def remove(self, key):
93 def remove(self, key):
94 return self._remove(key)
94 return self._remove(key)
95
95
96 def random_filename(self):
96 def random_filename(self):
97 """Return filename and full-path tuple for file storage.
97 """Return filename and full-path tuple for file storage.
98
98
99 Filename will be a randomly generated 28 character hexadecimal string
99 Filename will be a randomly generated 28 character hexadecimal string
100 with ".archive_cache" suffixed. Two levels of sub-directories will be used to
100 with ".archive_cache" suffixed. Two levels of sub-directories will be used to
101 reduce the size of directories. On older filesystems, lookups in
101 reduce the size of directories. On older filesystems, lookups in
102 directories with many files may be slow.
102 directories with many files may be slow.
103 """
103 """
104
104
105 hex_name = codecs.encode(os.urandom(16), 'hex').decode('utf-8')
105 hex_name = codecs.encode(os.urandom(16), 'hex').decode('utf-8')
106
106
107 archive_name = hex_name[4:] + '.archive_cache'
107 archive_name = hex_name[4:] + '.archive_cache'
108 filename = f"{hex_name[:2]}/{hex_name[2:4]}/{archive_name}"
108 filename = f"{hex_name[:2]}/{hex_name[2:4]}/{archive_name}"
109
109
110 full_path = os.path.join(self.directory, filename)
110 full_path = os.path.join(self.directory, filename)
111 return archive_name, full_path
111 return archive_name, full_path
112
112
113 def __repr__(self):
113 def __repr__(self):
114 return f'{self.__class__.__name__}(index={self._index}, dir={self.directory})'
114 return f'{self.__class__.__name__}(index={self._index}, dir={self.directory})'
115
115
116
116
117 class FileSystemFanoutCache(BaseCache):
117 class FileSystemFanoutCache(BaseCache):
118 shard_name = 'shard_%03d'
118
119
119 def __init__(self, locking_url, **settings):
120 def __init__(self, locking_url, **settings):
120 """
121 """
121 Initialize file system cache instance.
122 Initialize file system cache instance.
122
123
123 :param str locking_url: redis url for a lock
124 :param str locking_url: redis url for a lock
124 :param settings: settings dict
125 :param settings: settings dict
125
126
126 """
127 """
127 self._locking_url = locking_url
128 self._locking_url = locking_url
128 self._config = settings
129 self._config = settings
129 cache_dir = self.get_conf('archive_cache.filesystem.store_dir')
130 cache_dir = self.get_conf('archive_cache.filesystem.store_dir')
130 directory = str(cache_dir)
131 directory = str(cache_dir)
131 directory = os.path.expanduser(directory)
132 directory = os.path.expanduser(directory)
132 directory = os.path.expandvars(directory)
133 directory = os.path.expandvars(directory)
133 self._directory = directory
134 self._directory = directory
134 self._storage_path = directory
135 self._storage_path = directory
135
136
136 # check if it's ok to write, and re-create the archive cache
137 # check if it's ok to write, and re-create the archive cache
137 if not os.path.isdir(self._directory):
138 if not os.path.isdir(self._directory):
138 os.makedirs(self._directory, exist_ok=True)
139 os.makedirs(self._directory, exist_ok=True)
139
140
140 self._count = int(self.get_conf('archive_cache.filesystem.cache_shards', pop=True))
141 self._count = int(self.get_conf('archive_cache.filesystem.cache_shards', pop=True))
141
142
142 self._eviction_policy = self.get_conf('archive_cache.filesystem.eviction_policy', pop=True)
143 self._eviction_policy = self.get_conf('archive_cache.filesystem.eviction_policy', pop=True)
143 self._cache_size_limit = self.gb_to_bytes(int(self.get_conf('archive_cache.filesystem.cache_size_gb')))
144 self._cache_size_limit = self.gb_to_bytes(int(self.get_conf('archive_cache.filesystem.cache_size_gb')))
144
145
145 self.retry = str2bool(self.get_conf('archive_cache.filesystem.retry', pop=True))
146 self.retry = str2bool(self.get_conf('archive_cache.filesystem.retry', pop=True))
146 self.retry_attempts = int(self.get_conf('archive_cache.filesystem.retry_attempts', pop=True))
147 self.retry_attempts = int(self.get_conf('archive_cache.filesystem.retry_attempts', pop=True))
147 self.retry_backoff = int(self.get_conf('archive_cache.filesystem.retry_backoff', pop=True))
148 self.retry_backoff = int(self.get_conf('archive_cache.filesystem.retry_backoff', pop=True))
148
149
149 log.debug('Initializing archival cache instance under %s', self._directory)
150 log.debug('Initializing archival cache instance under %s', self._directory)
150 self._shards = tuple(
151 self._shards = tuple(
151 FileSystemShard(
152 FileSystemShard(
152 index=num,
153 index=num,
153 directory=os.path.join(directory, 'shard_%03d' % num),
154 directory=os.path.join(directory, self.shard_name % num),
154 **settings,
155 **settings,
155 )
156 )
156 for num in range(self._count)
157 for num in range(self._count)
157 )
158 )
158 self._hash = self._shards[0].hash
159 self._hash = self._shards[0].hash
159
160
160 def _get_shard(self, key) -> FileSystemShard:
161 def _get_shard(self, key) -> FileSystemShard:
161 index = self._hash(key) % self._count
162 index = self._hash(key) % self._count
162 shard = self._shards[index]
163 shard = self._shards[index]
163 return shard
164 return shard
164
165
165 def _get_size(self, shard, archive_path):
166 def _get_size(self, shard, archive_path):
166 return os.stat(archive_path).st_size
167 return os.stat(archive_path).st_size
@@ -1,150 +1,158 b''
1 # Copyright (C) 2015-2024 RhodeCode GmbH
1 # Copyright (C) 2015-2024 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import codecs
19 import codecs
20 import hashlib
20 import hashlib
21 import logging
21 import logging
22 import os
22 import os
23
23
24 import fsspec
24 import fsspec
25
25
26 from .base import BaseCache, BaseShard
26 from .base import BaseCache, BaseShard
27 from ..utils import ShardFileReader, NOT_GIVEN
27 from ..utils import ShardFileReader, NOT_GIVEN
28 from ...type_utils import str2bool
28 from ...type_utils import str2bool
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32
32
33 class S3Shard(BaseShard):
33 class S3Shard(BaseShard):
34
34
35 def __init__(self, index, bucket, **settings):
35 def __init__(self, index, bucket, **settings):
36 self._index = index
36 self._index = index
37 self._bucket = bucket
37 self._bucket = bucket
38 self.storage_type = 'bucket'
38 self.storage_type = 'bucket'
39
39
40 endpoint_url = settings.pop('archive_cache.objectstore.url')
40 endpoint_url = settings.pop('archive_cache.objectstore.url')
41 key = settings.pop('archive_cache.objectstore.key')
41 key = settings.pop('archive_cache.objectstore.key')
42 secret = settings.pop('archive_cache.objectstore.secret')
42 secret = settings.pop('archive_cache.objectstore.secret')
43
43
44 # TODO: Add it all over the place...
45 self._bucket_root = settings.pop('archive_cache.objectstore.bucket_root')
46
44 self.fs = fsspec.filesystem('s3', anon=False, endpoint_url=endpoint_url, key=key, secret=secret)
47 self.fs = fsspec.filesystem('s3', anon=False, endpoint_url=endpoint_url, key=key, secret=secret)
45
48
46 @property
49 @property
47 def bucket(self):
50 def bucket(self):
48 """Cache bucket."""
51 """Cache bucket."""
49 return self._bucket
52 return os.path.join(self._bucket_root, self._bucket)
50
53
51 def _get_keyfile(self, archive_key) -> tuple[str, str]:
54 def _get_keyfile(self, archive_key) -> tuple[str, str]:
52 key_file = f'{archive_key}-{self.key_suffix}'
55 key_file = f'{archive_key}-{self.key_suffix}'
53 return key_file, os.path.join(self.bucket, key_file)
56 return key_file, os.path.join(self.bucket, key_file)
54
57
55 def _get_writer(self, path, mode):
58 def _get_writer(self, path, mode):
56 return self.fs.open(path, 'wb')
59 return self.fs.open(path, 'wb')
57
60
58 def _write_file(self, full_path, iterator, mode):
61 def _write_file(self, full_path, iterator, mode):
62 if self._bucket_root:
63 if not self.fs.exists(self._bucket_root):
64 self.fs.mkdir(self._bucket_root)
65
59 # ensure bucket exists
66 # ensure bucket exists
60 destination = self.bucket
67 destination = self.bucket
61 if not self.fs.exists(destination):
68 if not self.fs.exists(destination):
62 self.fs.mkdir(destination, s3_additional_kwargs={})
69 self.fs.mkdir(destination, s3_additional_kwargs={})
63
70
64 writer = self._get_writer(full_path, mode)
71 writer = self._get_writer(full_path, mode)
65
72
66 digest = hashlib.sha256()
73 digest = hashlib.sha256()
67 with writer:
74 with writer:
68 size = 0
75 size = 0
69 for chunk in iterator:
76 for chunk in iterator:
70 size += len(chunk)
77 size += len(chunk)
71 digest.update(chunk)
78 digest.update(chunk)
72 writer.write(chunk)
79 writer.write(chunk)
73
80
74 sha256 = digest.hexdigest()
81 sha256 = digest.hexdigest()
75 log.debug('written new archive cache under %s, sha256: %s', full_path, sha256)
82 log.debug('written new archive cache under %s, sha256: %s', full_path, sha256)
76 return size, sha256
83 return size, sha256
77
84
78 def store(self, key, value_reader, metadata: dict | None = None):
85 def store(self, key, value_reader, metadata: dict | None = None):
79 return self._store(key, value_reader, metadata, mode='wb')
86 return self._store(key, value_reader, metadata, mode='wb')
80
87
81 def fetch(self, key, retry=NOT_GIVEN, retry_attempts=NOT_GIVEN, retry_backoff=1) -> tuple[ShardFileReader, dict]:
88 def fetch(self, key, retry=NOT_GIVEN, retry_attempts=NOT_GIVEN, retry_backoff=1) -> tuple[ShardFileReader, dict]:
82 return self._fetch(key, retry, retry_attempts, retry_backoff)
89 return self._fetch(key, retry, retry_attempts, retry_backoff)
83
90
84 def remove(self, key):
91 def remove(self, key):
85 return self._remove(key)
92 return self._remove(key)
86
93
87 def random_filename(self):
94 def random_filename(self):
88 """Return filename and full-path tuple for file storage.
95 """Return filename and full-path tuple for file storage.
89
96
90 Filename will be a randomly generated 28 character hexadecimal string
97 Filename will be a randomly generated 28 character hexadecimal string
91 with ".archive_cache" suffixed. Two levels of sub-directories will be used to
98 with ".archive_cache" suffixed. Two levels of sub-directories will be used to
92 reduce the size of directories. On older filesystems, lookups in
99 reduce the size of directories. On older filesystems, lookups in
93 directories with many files may be slow.
100 directories with many files may be slow.
94 """
101 """
95
102
96 hex_name = codecs.encode(os.urandom(16), 'hex').decode('utf-8')
103 hex_name = codecs.encode(os.urandom(16), 'hex').decode('utf-8')
97
104
98 archive_name = hex_name[4:] + '.archive_cache'
105 archive_name = hex_name[4:] + '.archive_cache'
99 filename = f"{hex_name[:2]}-{hex_name[2:4]}-{archive_name}"
106 filename = f"{hex_name[:2]}-{hex_name[2:4]}-{archive_name}"
100
107
101 full_path = os.path.join(self.bucket, filename)
108 full_path = os.path.join(self.bucket, filename)
102 return archive_name, full_path
109 return archive_name, full_path
103
110
104 def __repr__(self):
111 def __repr__(self):
105 return f'{self.__class__.__name__}(index={self._index}, bucket={self.bucket})'
112 return f'{self.__class__.__name__}(index={self._index}, bucket={self.bucket})'
106
113
107
114
108 class ObjectStoreCache(BaseCache):
115 class ObjectStoreCache(BaseCache):
116 shard_name = 'shard-bucket-%03d'
109
117
110 def __init__(self, locking_url, **settings):
118 def __init__(self, locking_url, **settings):
111 """
119 """
112 Initialize objectstore cache instance.
120 Initialize objectstore cache instance.
113
121
114 :param str locking_url: redis url for a lock
122 :param str locking_url: redis url for a lock
115 :param settings: settings dict
123 :param settings: settings dict
116
124
117 """
125 """
118 self._locking_url = locking_url
126 self._locking_url = locking_url
119 self._config = settings
127 self._config = settings
120
128
121 objectstore_url = self.get_conf('archive_cache.objectstore.url')
129 objectstore_url = self.get_conf('archive_cache.objectstore.url')
122 self._storage_path = objectstore_url
130 self._storage_path = objectstore_url
123
131
124 self._count = int(self.get_conf('archive_cache.objectstore.bucket_shards', pop=True))
132 self._count = int(self.get_conf('archive_cache.objectstore.bucket_shards', pop=True))
125
133
126 self._eviction_policy = self.get_conf('archive_cache.objectstore.eviction_policy', pop=True)
134 self._eviction_policy = self.get_conf('archive_cache.objectstore.eviction_policy', pop=True)
127 self._cache_size_limit = self.gb_to_bytes(int(self.get_conf('archive_cache.objectstore.cache_size_gb')))
135 self._cache_size_limit = self.gb_to_bytes(int(self.get_conf('archive_cache.objectstore.cache_size_gb')))
128
136
129 self.retry = str2bool(self.get_conf('archive_cache.objectstore.retry', pop=True))
137 self.retry = str2bool(self.get_conf('archive_cache.objectstore.retry', pop=True))
130 self.retry_attempts = int(self.get_conf('archive_cache.objectstore.retry_attempts', pop=True))
138 self.retry_attempts = int(self.get_conf('archive_cache.objectstore.retry_attempts', pop=True))
131 self.retry_backoff = int(self.get_conf('archive_cache.objectstore.retry_backoff', pop=True))
139 self.retry_backoff = int(self.get_conf('archive_cache.objectstore.retry_backoff', pop=True))
132
140
133 log.debug('Initializing archival cache instance under %s', objectstore_url)
141 log.debug('Initializing archival cache instance under %s', objectstore_url)
134 self._shards = tuple(
142 self._shards = tuple(
135 S3Shard(
143 S3Shard(
136 index=num,
144 index=num,
137 bucket='rhodecode-archivecache-%03d' % num,
145 bucket=self.shard_name % num,
138 **settings,
146 **settings,
139 )
147 )
140 for num in range(self._count)
148 for num in range(self._count)
141 )
149 )
142 self._hash = self._shards[0].hash
150 self._hash = self._shards[0].hash
143
151
144 def _get_shard(self, key) -> S3Shard:
152 def _get_shard(self, key) -> S3Shard:
145 index = self._hash(key) % self._count
153 index = self._hash(key) % self._count
146 shard = self._shards[index]
154 shard = self._shards[index]
147 return shard
155 return shard
148
156
149 def _get_size(self, shard, archive_path):
157 def _get_size(self, shard, archive_path):
150 return shard.fs.info(archive_path)['size']
158 return shard.fs.info(archive_path)['size']
General Comments 0
You need to be logged in to leave comments. Login now