MetricsMap.java

// Copyright (c) ZeroC, Inc.

package com.zeroc.Ice;

import com.zeroc.Ice.MetricsMap.SubMap;
import com.zeroc.IceMX.Metrics;
import com.zeroc.IceMX.MetricsFailures;
import com.zeroc.IceMX.MetricsHelper;
import com.zeroc.IceMX.Observer;

import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * @hidden Public because it's used by IceMX.
 */
public class MetricsMap<T extends Metrics> {
    /**
     * Represents a metrics entry within a metrics map.
     */
    public class Entry {
        Entry(T obj) {
            _object = obj;
        }

        /**
         * Records a failure for this metrics entry with the specified exception name.
         *
         * @param exceptionName the name of the exception that caused the failure
         */
        public void failed(String exceptionName) {
            synchronized (MetricsMap.this) {
                ++_object.failures;
                if (_failures == null) {
                    _failures = new HashMap<>();
                }
                Integer count = _failures.get(exceptionName);
                _failures.put(exceptionName, Integer.valueOf(count == null ? 1 : count + 1));
            }
        }

        /**
         * Gets a matching entry from a sub-map with the specified parameters.
         *
         * @param <S> the type of metrics
         * @param mapName the name of the sub-map
         * @param helper the metrics helper
         * @param cl the metrics class
         * @return the matching entry or null if no sub-map is found
         */
        @SuppressWarnings("unchecked")
        public <S extends Metrics> MetricsMap<S>.Entry getMatching(
                String mapName, MetricsHelper<S> helper, Class<S> cl) {
            SubMap<S> m;
            synchronized (MetricsMap.this) {
                m = _subMaps != null ? (SubMap<S>) _subMaps.get(mapName) : null;
                if (m == null) {
                    m = createSubMap(mapName, cl);
                    if (m == null) {
                        return null;
                    }
                    if (_subMaps == null) {
                        _subMaps = new HashMap<>();
                    }
                    _subMaps.put(mapName, m);
                }
            }
            return m.getMatching(helper);
        }

        /**
         * Detaches this entry and updates its total lifetime.
         *
         * @param lifetime the lifetime to add to the total lifetime
         */
        public void detach(long lifetime) {
            synchronized (MetricsMap.this) {
                _object.totalLifetime += lifetime;
                if (--_object.current == 0) {
                    detached(this);
                }
            }
        }

        /**
         * Executes a metrics update function on this entry's metrics object.
         *
         * @param func the update function to execute
         */
        public void execute(Observer.MetricsUpdate<T> func) {
            synchronized (MetricsMap.this) {
                func.update(_object);
            }
        }

        /**
         * Gets the metrics map that contains this entry.
         *
         * @return the parent metrics map
         */
        public MetricsMap<?> getMap() {
            return MetricsMap.this;
        }

        private MetricsFailures getFailures() {
            if (_failures == null) {
                return null;
            }
            var f = new MetricsFailures();
            f.id = _object.id;
            f.failures = new HashMap<>(_failures);
            return f;
        }

        private void attach(MetricsHelper<T> helper) {
            ++_object.total;
            ++_object.current;
            helper.initMetrics(_object);
        }

        private boolean isDetached() {
            return _object.current == 0;
        }

        @Override
        @SuppressWarnings("unchecked")
        public Metrics clone() {
            T metrics = (T) _object.clone();
            if (_subMaps != null) {
                for (SubMap<?> s : _subMaps.values()) {
                    s.addSubMapToMetrics(metrics);
                }
            }
            return metrics;
        }

        private T _object;
        private Map<String, Integer> _failures;
        private Map<String, SubMap<?>> _subMaps;
    }

    static class SubMap<S extends Metrics> {
        public SubMap(MetricsMap<S> map, java.lang.reflect.Field field) {
            _map = map;
            _field = field;
        }

        /**
         * Gets a matching entry using the specified metrics helper.
         *
         * @param helper the metrics helper
         * @return the matching entry
         */
        public MetricsMap<S>.Entry getMatching(MetricsHelper<S> helper) {
            return _map.getMatching(helper, null);
        }

        /**
         * Adds this sub-map to the specified metrics object.
         *
         * @param metrics the metrics object to add the sub-map to
         */
        public void addSubMapToMetrics(Metrics metrics) {
            try {
                _field.set(metrics, _map.getMetrics());
            } catch (Exception ex) {
                assert false;
            }
        }

        private final MetricsMap<S> _map;
        private final java.lang.reflect.Field _field;
    }

    static class SubMapCloneFactory<S extends Metrics> {
        public SubMapCloneFactory(MetricsMap<S> map, java.lang.reflect.Field field) {
            _map = map;
            _field = field;
        }

        public SubMap<S> create() {
            return new SubMap<S>(new MetricsMap<S>(_map), _field);
        }

        private final MetricsMap<S> _map;
        private final java.lang.reflect.Field _field;
    }

    static class SubMapFactory<S extends Metrics> {
        SubMapFactory(Class<S> cl, java.lang.reflect.Field field) {
            _class = cl;
            _field = field;
        }

        SubMapCloneFactory<S> createCloneFactory(String subMapPrefix, Properties properties) {
            return new SubMapCloneFactory<S>(
                new MetricsMap<S>(subMapPrefix, _class, properties, null), _field);
        }

        private final Class<S> _class;
        private final java.lang.reflect.Field _field;
    }

    MetricsMap(
            String mapPrefix,
            Class<T> cl,
            Properties props,
            Map<String, SubMapFactory<?>> subMaps) {
        MetricsAdminI.validateProperties(mapPrefix, props);
        _properties = props.getPropertiesForPrefix(mapPrefix);

        _retain = props.getPropertyAsIntWithDefault(mapPrefix + "RetainDetached", 10);
        _accept = parseRule(props, mapPrefix + "Accept");
        _reject = parseRule(props, mapPrefix + "Reject");
        _groupByAttributes = new ArrayList<>();
        _groupBySeparators = new ArrayList<>();
        _class = cl;

        String groupBy = props.getPropertyWithDefault(mapPrefix + "GroupBy", "id");
        if (!groupBy.isEmpty()) {
            String v = "";
            boolean attribute =
                Character.isLetter(groupBy.charAt(0)) || Character.isDigit(groupBy.charAt(0));
            if (!attribute) {
                _groupByAttributes.add("");
            }

            for (char p : groupBy.toCharArray()) {
                boolean isAlphaNum = Character.isLetter(p) || Character.isDigit(p) || p == '.';
                if (attribute && !isAlphaNum) {
                    _groupByAttributes.add(v);
                    v = "" + p;
                    attribute = false;
                } else if (!attribute && isAlphaNum) {
                    _groupBySeparators.add(v);
                    v = "" + p;
                    attribute = true;
                } else {
                    v += p;
                }
            }

            if (attribute) {
                _groupByAttributes.add(v);
            } else {
                _groupBySeparators.add(v);
            }
        }

        if (subMaps != null && !subMaps.isEmpty()) {
            _subMaps = new HashMap<>();

            List<String> subMapNames = new ArrayList<>();
            for (Map.Entry<String, SubMapFactory<?>> e : subMaps.entrySet()) {
                subMapNames.add(e.getKey());
                String subMapsPrefix = mapPrefix + "Map.";
                String subMapPrefix = subMapsPrefix + e.getKey() + '.';
                if (props.getPropertiesForPrefix(subMapPrefix).isEmpty()) {
                    if (props.getPropertiesForPrefix(subMapsPrefix).isEmpty()) {
                        subMapPrefix = mapPrefix;
                    } else {
                        continue; // This sub-map isn't configured.
                    }
                }

                _subMaps.put(e.getKey(), e.getValue().createCloneFactory(subMapPrefix, props));
            }
        } else {
            _subMaps = null;
        }
    }

    MetricsMap(MetricsMap<T> map) {
        _properties = map._properties;
        _groupByAttributes = map._groupByAttributes;
        _groupBySeparators = map._groupBySeparators;
        _retain = map._retain;
        _accept = map._accept;
        _reject = map._reject;
        _class = map._class;
        _subMaps = map._subMaps;
    }

    Map<String, String> getProperties() {
        return _properties;
    }

    synchronized Metrics[] getMetrics() {
        Metrics[] metrics = new Metrics[_objects.size()];
        int i = 0;
        for (Entry e : _objects.values()) {
            metrics[i++] = e.clone();
        }
        return metrics;
    }

    synchronized MetricsFailures[] getFailures() {
        List<MetricsFailures> failures = new ArrayList<>();
        for (Entry e : _objects.values()) {
            MetricsFailures f = e.getFailures();
            if (f != null) {
                failures.add(f);
            }
        }
        return failures.toArray(new MetricsFailures[failures.size()]);
    }

    synchronized MetricsFailures getFailures(String id) {
        Entry e = _objects.get(id);
        if (e != null) {
            return e.getFailures();
        }
        return null;
    }

    /**
     * Creates a sub-map with the specified name and metrics class.
     *
     * @param <S> the type of metrics
     * @param subMapName the name of the sub-map to create
     * @param cl the metrics class
     * @return the created sub-map or null if no factory is available
     */
    @SuppressWarnings("unchecked")
    public <S extends Metrics> SubMap<S> createSubMap(String subMapName, Class<S> cl) {
        if (_subMaps == null) {
            return null;
        }
        SubMapCloneFactory<S> factory = (SubMapCloneFactory<S>) _subMaps.get(subMapName);
        if (factory != null) {
            return factory.create();
        }
        return null;
    }

    /**
     * Gets a matching entry for the specified metrics helper and previous entry.
     *
     * @param helper the metrics helper
     * @param previous the previous entry or null
     * @return the matching entry or null if no match is found
     */
    public Entry getMatching(MetricsHelper<T> helper, Entry previous) {
        //
        // Check the accept and reject filters.
        //
        for (Map.Entry<String, Pattern> e : _accept.entrySet()) {
            if (!match(e.getKey(), e.getValue(), helper, false)) {
                return null;
            }
        }

        for (Map.Entry<String, Pattern> e : _reject.entrySet()) {
            if (match(e.getKey(), e.getValue(), helper, true)) {
                return null;
            }
        }

        //
        // Compute the key from the GroupBy property.
        //
        String key;
        try {
            if (_groupByAttributes.size() == 1) {
                key = helper.resolve(_groupByAttributes.get(0));
            } else {
                StringBuilder os = new StringBuilder();
                Iterator<String> q = _groupBySeparators.iterator();
                for (String p : _groupByAttributes) {
                    os.append(helper.resolve(p));
                    if (q.hasNext()) {
                        os.append(q.next());
                    }
                }
                key = os.toString();
            }
        } catch (Exception ex) {
            return null;
        }

        //
        // Lookup the metrics object.
        //
        synchronized (this) {
            if (previous != null && previous._object.id.equals(key)) {
                assert (_objects.get(key) == previous);
                return previous;
            }

            Entry e = _objects.get(key);
            if (e == null) {
                try {
                    T t = _class.getDeclaredConstructor().newInstance();
                    t.id = key;
                    e = new Entry(t);
                    _objects.put(key, e);
                } catch (Exception ex) {
                    assert false;
                }
            }
            e.attach(helper);
            return e;
        }
    }

    private void detached(Entry entry) {
        if (_retain == 0) {
            return;
        }

        if (_detachedQueue == null) {
            _detachedQueue = new LinkedList<>();
        }
        assert (_detachedQueue.size() <= _retain);

        // Compress the queue by removing entries which are no longer detached.
        Iterator<Entry> p = _detachedQueue.iterator();
        while (p.hasNext()) {
            Entry e = p.next();
            if (e == entry || !e.isDetached()) {
                p.remove();
            }
        }

        // If there's still no room, remove the oldest entry (at the front).
        if (_detachedQueue.size() == _retain) {
            _objects.remove(_detachedQueue.pollFirst()._object.id);
        }

        // Add the entry at the back of the queue.
        _detachedQueue.add(entry);
    }

    private Map<String, Pattern> parseRule(
            Properties properties, String name) {
        Map<String, Pattern> pats = new HashMap<>();
        Map<String, String> rules = properties.getPropertiesForPrefix(name + '.');
        for (Map.Entry<String, String> e : rules.entrySet()) {
            pats.put(
                e.getKey().substring(name.length() + 1),
                Pattern.compile(e.getValue()));
        }
        return pats;
    }

    private boolean match(
            String attribute,
            Pattern regex,
            MetricsHelper<T> helper,
            boolean reject) {
        String value;
        try {
            value = helper.resolve(attribute);
        } catch (Exception ex) {
            return !reject;
        }
        return regex.matcher(value).matches();
    }

    private final Map<String, String> _properties;
    private final List<String> _groupByAttributes;
    private final List<String> _groupBySeparators;
    private final int _retain;
    private final Map<String, Pattern> _accept;
    private final Map<String, Pattern> _reject;
    private final Class<T> _class;

    private final Map<String, Entry> _objects = new HashMap<>();
    private final Map<String, SubMapCloneFactory<?>> _subMaps;
    private Deque<Entry> _detachedQueue;
}