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

io.fluxcapacitor.common.tracking.TrackerCluster Maven / Gradle / Ivy

There is a newer version: 0.1072.0
Show newest version
/*
 * Copyright (c) Flux Capacitor IP B.V. or its affiliates. All Rights Reserved.
 *
 * 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 io.fluxcapacitor.common.tracking;

import lombok.Getter;
import lombok.Value;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.stream.Stream;

import static java.util.Optional.ofNullable;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
import static java.util.stream.Stream.concat;

@Value
public class TrackerCluster {

    public static final int[] emptyRange = new int[]{0, 0};

    @Getter
    int segments;
    Map trackers;
    Map activeTrackers;

    public TrackerCluster(int segments) {
        this(segments, Collections.emptyMap(), Collections.emptyMap());
    }

    private TrackerCluster(int segments, Map trackers, Map activeTrackers) {
        this.segments = segments;
        this.trackers = trackers;
        this.activeTrackers = activeTrackers;
    }

    public TrackerCluster withActiveTracker(Tracker tracker) {
        if (!contains(tracker) || isActive(tracker)) {
            return withWaitingTracker(tracker).withActiveTracker(tracker);
        }
        Map activeTrackers = new HashMap<>(this.activeTrackers);
        activeTrackers.putIfAbsent(tracker, Instant.now());
        return new TrackerCluster(segments, trackers, activeTrackers);
    }

    public TrackerCluster withWaitingTracker(Tracker tracker) {
        SortedSet trackers = concat(this.trackers.keySet().stream().filter(
                c -> !c.equals(tracker)), Stream.of(tracker)).collect(toCollection(TreeSet::new));
        Map activeTrackers = new HashMap<>(this.activeTrackers);
        activeTrackers.remove(tracker);
        return recalculate(trackers, activeTrackers);
    }

    public TrackerCluster withoutTracker(Tracker tracker) {
        if (!contains(tracker)) {
            return this;
        }
        SortedSet trackers =
                this.trackers.keySet().stream().filter(c -> !c.equals(tracker)).collect(toCollection(TreeSet::new));
        Map activeTrackers = new HashMap<>(this.activeTrackers);
        activeTrackers.remove(tracker);
        return recalculate(trackers, activeTrackers);
    }

    public TrackerCluster purgeTrackers(Predicate predicate) {
        TrackerCluster result = this;
        for (Tracker tracker : trackers.keySet()) {
            if (predicate.test(tracker)) {
                result = result.withoutTracker(tracker);
            }
        }
        return result;
    }

    public TrackerCluster purgeCeasedTrackers(Instant threshold) {
        return purgeTrackers(t -> ofNullable(activeTrackers.get(t)).filter(p -> p.isBefore(threshold)).isPresent());
    }

    public Optional getProcessingDuration(Tracker tracker) {
        return ofNullable(activeTrackers.get(tracker)).map(t -> Duration.between(t, Instant.now()));
    }

    public int[] getSegment(Tracker tracker) {
        return ofNullable(trackers.get(tracker))
                .map(segment -> {
                    if (tracker.singleTracker()) {
                        return segment.contains(0) && segment.getLength() > 0 ? new int[]{0, segments} : emptyRange;
                    }
                    return segment.asArray();
                }).orElse(null);
    }

    public boolean contains(Tracker tracker) {
        return trackers.containsKey(tracker);
    }

    public boolean isActive(Tracker tracker) {
        return activeTrackers.containsKey(tracker);
    }

    public Set getTrackers() {
        return Collections.unmodifiableSet(trackers.keySet());
    }

    public boolean isEmpty() {
        return trackers.isEmpty();
    }

    private TrackerCluster recalculate(SortedSet trackers, Map activeTrackers) {
        if (trackers.isEmpty()) {
            return new TrackerCluster(segments);
        }
        Map constraints = activeTrackers.keySet().stream()
                .filter(c -> !Objects.equals(this.trackers.get(c), new Segment(0, 0)))
                .collect(toMap(identity(), this.trackers::get));

        TreeSet grid = createGrid(segments, trackers.size(), constraints.values());

        removeGridPoints(grid, trackers.size(), concat(Stream.of(0, segments), getIntersections(constraints.values())
                .stream()).collect(toSet()));

        List trackerSegments = toSegments(grid);

        Map adjustedConstraints = adjustConstraints(trackerSegments, constraints);
        Map listeningTrackers = new TreeMap<>(adjustedConstraints);
        List remainingSegments = new ArrayList<>(trackerSegments);
        remainingSegments.removeAll(listeningTrackers.values());
        trackers.stream().filter(t -> !adjustedConstraints.containsKey(t)).forEach(t -> listeningTrackers
                .put(t, remainingSegments.isEmpty() ? new Segment(0, 0) : remainingSegments.remove(0)));

        return new TrackerCluster(segments, listeningTrackers, activeTrackers);
    }

    private Map adjustConstraints(List segments, Map constraints) {
        Map result = new HashMap<>();
        constraints.forEach(
                ((tracker, segment) -> segments.stream().filter(newSegment -> newSegment.contains(segment.getStart()))
                        .findAny().ifPresent(newSegment -> result.put(tracker, newSegment))));
        return result;
    }

    //create a grid for tracker segments that includes the given constraints

    private static TreeSet createGrid(int segments, int trackers, Collection constraints) {
        //create evenly spaced grid for Tracker  segments. E.g. size 4 -> [0, 25, 50, 75, 100]
        TreeSet result = new TreeSet<>();
        int quotient = segments / trackers, remainder = segments % trackers;
        result.add(0);
        int last = 0;
        for (int i = 1; i <= trackers; i++) {
            result.add(last += trackers - i < remainder ? quotient + 1 : quotient);
        }

        //add constraints to grid. E.g. constraints: [0,33], [40,60] & [80,100] -> [0, 25, 33, 40, 50, 60, 75, 80, 100]
        constraints.forEach(segment -> Collections.addAll(result, segment.getStart(), segment.getEnd()));

        //remove all grid points between constraints. E.g. previous example becomes -> [0, 33, 40, 60, 75, 80, 100]
        result.removeIf(point -> constraints.stream()
                .anyMatch(segment -> point > segment.getStart() && point < segment.getEnd()));

        return result;
    }

    //remove grid points (except if constraints) until the grid size = number of trackers + 1

    private static void removeGridPoints(TreeSet grid, int trackers, Set constraints) {
        while (grid.size() > trackers + 1) {
            List segments = new ArrayList<>();
            Iterator iterator = grid.iterator();
            int start = iterator.next();
            while (iterator.hasNext()) {
                int end = iterator.next();
                segments.add(new Segment(start, end));
                start = end;
            }
            segments.stream().sorted(Comparator.comparing(Segment::getLength)).map(Segment::getEnd)
                    .filter(end -> !constraints.contains(end))
                    .findFirst().ifPresent(grid::remove);
        }
    }

    //convert grid to a list of touching segments

    private static List toSegments(SortedSet grid) {
        List result = new ArrayList<>();
        Iterator iterator = grid.iterator();
        int next = iterator.next();
        while (iterator.hasNext()) {
            result.add(new Segment(next, next = iterator.next()));
        }
        return result;
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    private static Set getIntersections(Collection segments) {
        Set result = new HashSet<>();
        segments.stream().sorted(Comparator.comparing(Segment::getStart)).reduce((a, b) -> {
            if (a.getEnd() == b.getStart()) {
                result.add(a.getEnd());
            }
            return b;
        });
        return result;
    }

    @Value
    static class Segment {
        int start, end;

        public Segment(int start, int end) {
            this.start = start;
            this.end = end;
        }

        public int getLength() {
            return end - start;
        }

        public boolean contains(int point) {
            return point >= start && point < end;
        }

        public int[] asArray() {
            return new int[]{start, end};
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy