diff --git a/.gitignore b/.gitignore index c1cc042ee9..692f684ce6 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,9 @@ dependency-reduced-pom.xml hs_err_pid* +# Development +repository/scripts/hazelcast-init/alfresco-hazelcast-config.xml + # Alfresco runtime alf_data diff --git a/repository/scripts/hazelcast-init/alfresco-hazelcast-template.xml b/repository/scripts/hazelcast-init/alfresco-hazelcast-template.xml new file mode 100644 index 0000000000..22d7e03e4c --- /dev/null +++ b/repository/scripts/hazelcast-init/alfresco-hazelcast-template.xml @@ -0,0 +1,24 @@ + + + + Replace this with a secure value + + + ${alfresco.hazelcast.mancenter.url} + + + + + 1 + + com.hazelcast.spi.merge.PutIfAbsentMergePolicy + + + 172800 + 0 + + + \ No newline at end of file diff --git a/repository/scripts/hazelcast-init/ci-caches.properties b/repository/scripts/hazelcast-init/ci-caches.properties new file mode 100644 index 0000000000..ba65e9a066 --- /dev/null +++ b/repository/scripts/hazelcast-init/ci-caches.properties @@ -0,0 +1,21 @@ +# These caches definitions are automatically adapted to XML configuration +# and used for integration test purposes. Be mindful when changing these! + +# cache to test max items setting +cache.maxItemsCache.maxItems=1000 +cache.maxItemsCache.timeToLiveSeconds=0 +cache.maxItemsCache.maxIdleSeconds=0 +cache.maxItemsCache.cluster.type=fully-distributed +cache.maxItemsCache.backup-count=1 +cache.maxItemsCache.eviction-policy=LRU +cache.maxItemsCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy +cache.maxItemsCache.readBackupData=false + +# cache to test time to live setting +cache.ttlCache.timeToLiveSeconds=1 +cache.ttlCache.maxIdleSeconds=0 +cache.ttlCache.cluster.type=fully-distributed +cache.ttlCache.backup-count=1 +cache.ttlCache.eviction-policy=LRU +cache.ttlCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy +cache.ttlCache.readBackupData=false \ No newline at end of file diff --git a/repository/scripts/hazelcast-init/generate-hazelcast-config.py b/repository/scripts/hazelcast-init/generate-hazelcast-config.py new file mode 100755 index 0000000000..3e71778ce3 --- /dev/null +++ b/repository/scripts/hazelcast-init/generate-hazelcast-config.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python +"""Python script to convert Hazelcast cache definitions from Java properties to XML""" +__author__="Domenico Sibilio" + +import collections +import configparser +import xml.etree.ElementTree as ET +from xml.dom import minidom +from typing import List +from pathlib import Path +import argparse + +ROOT_PATH = Path(__file__).parent +CONFIG_PATH = ROOT_PATH / 'ci-caches.properties' +OUTPUT_PATH = ROOT_PATH / 'alfresco-hazelcast-config.xml' +TEMPLATE_PATH = ROOT_PATH / 'alfresco-hazelcast-template.xml' +TEMPLATE_PLACEHOLDER = '' +PROPS_TO_XML = { + # time-to-live-seconds with value = x + 'timeToLiveSeconds': 'time-to-live-seconds', + # max-idle-seconds with value = x + 'maxIdleSeconds': 'max-idle-seconds', + # backup-count with value = x + 'backup-count': 'backup-count', + # read-backup-data with value = x + 'readBackupData': 'read-backup-data', + # merge-policy with value = x + 'merge-policy': 'merge-policy', + # eviction with eviction-policy=x and max-size-policy=PER_NODE and size=${maxItems} + 'eviction-policy': 'eviction', + # near-cache.eviction max-size-policy=ENTRY_COUNT, eviction-policy=LRU and size = x + 'nearCache.maxSize': 'size', + # near-cache.max-idle-seconds with value = x + 'nearCache.maxIdleSeconds': 'max-idle-seconds', + # near-cache.time-to-live-seconds with value = x + 'nearCache.timeToLiveSeconds': 'time-to-live-seconds', +} + + +def get_prop(prop_key: str): + # shortcut to get the property within the default section + return config.get('default', prop_key) + + +def get_cache_name(sections: List[str]): + # get the cache name given the full property string split by '.' + return '.'.join(sections[0:get_cache_name_index(sections)+1]) + + +def get_cache_name_index(sections: List[str]): + # returns the index where the cache name ends + # given the full property string split by '.' + for i, e in enumerate(sections): + if e.endswith('Cache'): + return i + + return 1 + + +def get_key(sections: List[str]): + # get the property key name given the full property string split by '.' + cn_index = get_cache_name_index(sections) + key_sections = sections[cn_index+1::] + return '.'.join(key_sections) + + +def prettify(xml_string: str): + # format and indent an xml string + prettified_xml = minidom.parseString(xml_string).toprettyxml(indent=' ') + return '\n'.join([line for line in prettified_xml.splitlines() if line.strip()]) + + +# entrypoint +parser = argparse.ArgumentParser(description='A Python script to generate XML Hazelcast cache definitions starting from Java properties.') +parser.add_argument('-s', '--source', default=CONFIG_PATH, help='path to the Java properties file to convert') +args = parser.parse_args() + +source_path = args.source + +# add dummy section to properties +with open(source_path, 'r') as f: + cache_props = '[default]\n' + f.read() + +config = configparser.ConfigParser() +# preserve property case +config.optionxform = str +# parse config file +config.read_string(cache_props) + +# group properties by cache name +props_by_cache = collections.defaultdict(list) +for item in config.items('default'): + sections = item[0].split('.') + if sections[0] == 'cache': + cache_name = get_cache_name(sections) + key = get_key(sections) + value = item[1] + props_by_cache[cache_name].append((key, value)) + +# read template file +with open(TEMPLATE_PATH, 'r') as input: + template = input.read() + +# perform template substitutions to apply the caches.properties configuration +map_configs = '' +for cache, props in props_by_cache.items(): + props = dict(props) + if(props.get('cluster.type') == 'fully-distributed'): + map = ET.Element('map', name=cache) + near_cache = None + for prop, value in props.items(): + xml_prop = PROPS_TO_XML.get(prop) + # handle eviction configuration + if prop == 'eviction-policy': + ET.SubElement(map, xml_prop, + {'eviction-policy': value, + 'max-size-policy': 'PER_NODE', + 'size': props.get('maxItems') if props.get('maxItems') else '0'}) + # handle near-cache configuration + elif prop.startswith('nearCache'): + if near_cache is None: + near_cache = ET.SubElement(map, 'near-cache') + if prop.split('.')[1] == 'maxSize': + ET.SubElement(near_cache, 'eviction', + {'max-size-policy': 'ENTRY_COUNT', + 'eviction-policy': 'LRU', + xml_prop: value}) + else: + ET.SubElement(near_cache, xml_prop).text = value + # handle basic map configuration + elif xml_prop: + ET.SubElement(map, xml_prop).text = value + ET.SubElement(map, 'per-entry-stats-enabled').text = 'true' + map_configs += minidom.parseString(ET.tostring(map)).childNodes[0].toprettyxml(indent=' ') + +template = template.replace(TEMPLATE_PLACEHOLDER, map_configs) + +# produce actual Hazelcast config file +with open(OUTPUT_PATH, 'w') as output: + output.write(prettify(template)) + print(f"Generated XML Hazelcast config: {OUTPUT_PATH}") diff --git a/repository/src/main/resources/alfresco/caches.properties b/repository/src/main/resources/alfresco/caches.properties index f7d776c5b9..ad12d905ff 100644 --- a/repository/src/main/resources/alfresco/caches.properties +++ b/repository/src/main/resources/alfresco/caches.properties @@ -686,7 +686,6 @@ cache.hbClusterUsageCache.backup-count=1 cache.hbClusterUsageCache.eviction-policy=NONE cache.hbClusterUsageCache.merge-policy=com.hazelcast.spi.merge.PutIfAbsentMergePolicy cache.hbClusterUsageCache.readBackupData=false -q # # Query accelerator cluster cache