All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hivemq.client.internal.mqtt.handler.publish.outgoing.MqttTopicAliasAutoMapping Maven / Gradle / Ivy

Go to download

HiveMQ MQTT Client is an MQTT 5.0 and MQTT 3.1.1 compatible and feature-rich high-performance Java client library with different API flavours and backpressure support

There is a newer version: 1.3.3
Show newest version
/*
 * Copyright 2018 dc-square and the HiveMQ MQTT Client Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.hivemq.client.internal.mqtt.handler.publish.outgoing;

import com.hivemq.client.internal.mqtt.datatypes.MqttTopicImpl;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.HashMap;
import java.util.Map;
import java.util.TreeSet;

import static com.hivemq.client.internal.mqtt.message.publish.MqttStatefulPublish.*;

/**
 * @author Silvio Giebl
 */
public class MqttTopicAliasAutoMapping implements MqttTopicAliasMapping {

    private static final byte OVERSIZE = 8; // TODO configurable
    private static final byte RETAIN = 10; // TODO configurable
    private static final byte OVERWRITE_TRIES = 3; // TODO configurable

    private final int topicAliasMaximum;
    private final @NotNull Map map = new HashMap<>();
    private @Nullable Entry lowest; // entry with lowest priority
    private long accessCounter; // strictly incremented
    private byte overwriteTries;// attempts to overwrite the entry with lowest priority out of these with a topic alias
    private byte fullOverwriteTries; // attempts to overwrite the entry with lowest priority out of all entries

    public MqttTopicAliasAutoMapping(final int topicAliasMaximum) {
        this.topicAliasMaximum = topicAliasMaximum;
    }

    @Override
    public int getTopicAliasMaximum() {
        return topicAliasMaximum;
    }

    @Override
    public int onPublish(final @NotNull MqttTopicImpl topic) {
        accessCounter++;
        final String topicString = topic.toString();
        final Entry entry = map.get(topicString);
        if (entry != null) { // entry already present
            final long priority = entry.access(accessCounter);
            if ((entry.topicAlias != DEFAULT_NO_TOPIC_ALIAS) && (entry.lower != null) &&
                    (entry.lower.topicAlias == DEFAULT_NO_TOPIC_ALIAS)) {
                if (overwriteTries > 0) {
                    overwriteTries--; // reduce chance to overwrite entry with lowest priority & topic alias if accessed
                }
            } else if ((entry == lowest) && (fullOverwriteTries > 0)) {
                fullOverwriteTries--; // reduce chance to overwrite entry with lowest priority if accessed
            }
            swapNewer(entry, priority);
            return entry.topicAlias; // topic alias is 0 if entry is part of oversize
        }
        if (map.size() < topicAliasMaximum + OVERSIZE) {
            final Entry newEntry = new Entry(accessCounter);
            if (map.size() < topicAliasMaximum) { // still unused topic aliases available
                newEntry.setNewTopicAlias(map.size() + 1);
            }
            map.put(topicString, newEntry);
            if (lowest != null) {
                newEntry.higher = lowest;
                lowest.lower = newEntry;
            }
            lowest = newEntry;
            swapNewer(newEntry, newEntry.priority());
            return newEntry.topicAlias;
        }
        fullOverwriteTries++;
        if (fullOverwriteTries < OVERWRITE_TRIES) {
            return DEFAULT_NO_TOPIC_ALIAS; // do not overwrite entry with lowest priority immediately
        }
        fullOverwriteTries = 0;
        map.values().remove(lowest);
        final Entry newEntry = new Entry(accessCounter);
        map.put(topicString, newEntry);
        if (lowest != null) {
            newEntry.topicAlias = lowest.topicAlias;
            final Entry higher = lowest.higher;
            newEntry.higher = higher;
            if (higher != null) {
                higher.lower = newEntry;
            }
        }
        lowest = newEntry;
        return newEntry.topicAlias;
    }

    private void swapNewer(final @NotNull Entry entry, final long priority) {
        while (true) {
            final Entry higher = entry.higher;
            if (higher == null) {
                break;
            }
            final long newerPriority = higher.priority(accessCounter);
            if (newerPriority >= priority) {
                break;
            }
            if ((entry.topicAlias == DEFAULT_NO_TOPIC_ALIAS) && (higher.topicAlias != DEFAULT_NO_TOPIC_ALIAS)) {
                overwriteTries++;
                if (overwriteTries < OVERWRITE_TRIES) {
                    break; // do not swap immediately if entry would overwrite the topic alias of the next entry
                }
                overwriteTries = 0;
                entry.setNewTopicAlias(higher.topicAlias);
                higher.topicAlias = DEFAULT_NO_TOPIC_ALIAS;
            }
            higher.lower = entry.lower;
            entry.higher = higher.higher;
            entry.lower = higher;
            higher.higher = entry;
            if (entry == lowest) {
                lowest = higher;
            }
        }
    }

    @Override
    public @NotNull String toString() {
        final TreeSet> sorted = new TreeSet<>((o1, o2) -> {
            Entry entry = o1.getValue().higher;
            while (entry != null) {
                if (entry == o2.getValue()) {
                    return 1;
                }
                entry = entry.higher;
            }
            return -1;
        });
        sorted.addAll(map.entrySet());
        final StringBuilder s = new StringBuilder();
        for (final Map.Entry entry : sorted) {
            s.append(" -> ").append(entry.toString());
        }
        return s.toString();
    }

    static class Entry {

        int topicAlias;
        long used; // number of accesses, decays over time
        long access; // stamp when the entry was last accessed
        @Nullable Entry higher; // entry with the next higher priority
        @Nullable Entry lower; // entry with the next lower priority

        Entry(final long accessCounter) {
            topicAlias = DEFAULT_NO_TOPIC_ALIAS;
            used = 1;
            access = accessCounter;
        }

        void setNewTopicAlias(final int topicAlias) {
            this.topicAlias = topicAlias | TOPIC_ALIAS_FLAG_NEW;
        }

        long access(final long accessCounter) {
            update(accessCounter);
            topicAlias &= TOPIC_ALIAS_FLAG; // clear NEW_TOPIC_ALIAS bit
            used++;
            access = accessCounter;
            return priority();
        }

        long priority(final long accessCounter) {
            update(accessCounter);
            return priority();
        }

        long priority() {
            return used + access;
        }

        private void update(final long accessCounter) {
            final long decay = Math.max(accessCounter - access - RETAIN, 0);
            used = Math.max(used - decay, 1);
        }

        @Override
        public @NotNull String toString() {
            final int topicAlias = this.topicAlias & TOPIC_ALIAS_FLAG;
            return ((topicAlias == DEFAULT_NO_TOPIC_ALIAS) ? "-" : topicAlias) + " (used: " + used + ", access: " +
                    access + ")";
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy