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

org.apache.pulsar.metadata.tableview.impl.MetadataStoreTableViewImpl Maven / Gradle / Ivy

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.pulsar.metadata.tableview.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
import lombok.Builder;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.mutable.MutableBoolean;
import org.apache.pulsar.common.util.FutureUtil;
import org.apache.pulsar.metadata.api.CacheGetResult;
import org.apache.pulsar.metadata.api.MetadataCache;
import org.apache.pulsar.metadata.api.MetadataCacheConfig;
import org.apache.pulsar.metadata.api.MetadataStore;
import org.apache.pulsar.metadata.api.MetadataStoreException;
import org.apache.pulsar.metadata.api.MetadataStoreTableView;
import org.apache.pulsar.metadata.api.NotificationType;

@Slf4j
public class MetadataStoreTableViewImpl implements MetadataStoreTableView {

    private static final int FILL_TIMEOUT_IN_MILLIS = 300_000;
    private static final int MAX_CONCURRENT_METADATA_OPS_DURING_FILL = 50;
    private static final long CACHE_REFRESH_FREQUENCY_IN_MILLIS = 600_000;
    private final ConcurrentMap data;
    private final Map immutableData;
    private final String name;
    private final MetadataStore store;
    private final MetadataCache cache;
    private final Predicate listenPathValidator;
    private final BiPredicate conflictResolver;
    private final List> tailItemListeners;
    private final List> existingItemListeners;
    private final long timeoutInMillis;
    private final String pathPrefix;

    /**
     * Construct MetadataStoreTableViewImpl.
     *
     * @param clazz                 clazz of the value type
     * @param name                  metadata store tableview name
     * @param store                 metadata store
     * @param pathPrefix            metadata store path prefix
     * @param listenPathValidator   path validator to listen
     * @param conflictResolver      resolve conflicts for concurrent puts
     * @param tailItemListeners     listener for tail item(recently updated) notifications
     * @param existingItemListeners listener for existing items in metadata store
     * @param timeoutInMillis       timeout duration for each sync operation.
     * @throws MetadataStoreException if init fails.
     */
    @Builder
    public MetadataStoreTableViewImpl(@NonNull Class clazz,
                                      @NonNull String name,
                                      @NonNull MetadataStore store,
                                      @NonNull String pathPrefix,
                                      @NonNull BiPredicate conflictResolver,
                                      Predicate listenPathValidator,
                                      List> tailItemListeners,
                                      List> existingItemListeners,
                                      long timeoutInMillis) {
        this.name = name;
        this.data = new ConcurrentHashMap<>();
        this.immutableData = Collections.unmodifiableMap(data);
        this.pathPrefix = pathPrefix;
        this.conflictResolver = conflictResolver;
        this.listenPathValidator = listenPathValidator;
        this.tailItemListeners = new ArrayList<>();
        if (tailItemListeners != null) {
            this.tailItemListeners.addAll(tailItemListeners);
        }
        this.existingItemListeners = new ArrayList<>();
        if (existingItemListeners != null) {
            this.existingItemListeners.addAll(existingItemListeners);
        }
        this.timeoutInMillis = timeoutInMillis;
        this.store = store;
        this.cache = store.getMetadataCache(clazz,
                MetadataCacheConfig.builder()
                        .expireAfterWriteMillis(-1)
                        .refreshAfterWriteMillis(CACHE_REFRESH_FREQUENCY_IN_MILLIS)
                        .asyncReloadConsumer(this::consumeAsyncReload)
                        .build());
        store.registerListener(this::handleNotification);
    }

    public void start() throws MetadataStoreException {
        fill();
    }

    private void consumeAsyncReload(String path, Optional> cached) {
        if (!isValidPath(path)) {
            return;
        }
        String key = getKey(path);
        var val = getValue(cached);
        handleTailItem(key, val);
    }

    private boolean isValidPath(String path) {
        if (listenPathValidator != null && !listenPathValidator.test(path)) {
            return false;
        }
        return true;
    }

    private T getValue(Optional> cached) {
        return cached.map(CacheGetResult::getValue).orElse(null);
    }

    boolean updateData(String key, T cur) {
        MutableBoolean updated = new MutableBoolean();
        data.compute(key, (k, prev) -> {
            if (Objects.equals(prev, cur)) {
                if (log.isDebugEnabled()) {
                    log.debug("{} skipped item key={} value={} prev={}",
                            name, key, cur, prev);
                }
                updated.setValue(false);
                return prev;
            } else {
                updated.setValue(true);
                return cur;
            }
        });
        return updated.booleanValue();
    }

    private void handleTailItem(String key, T val) {
        if (updateData(key, val)) {
            if (log.isDebugEnabled()) {
                log.debug("{} applying item key={} value={}",
                        name,
                        key,
                        val);
            }
            for (var listener : tailItemListeners) {
                try {
                    listener.accept(key, val);
                } catch (Throwable e) {
                    log.error("{} failed to listen tail item key:{}, val:{}",
                            name,
                            key, val, e);
                }
            }
        }

    }

    private CompletableFuture doHandleNotification(String path) {
        if (!isValidPath(path)) {
            return CompletableFuture.completedFuture(null);
        }
        return cache.get(path).thenAccept(valOpt -> {
            String key = getKey(path);
            var val = valOpt.orElse(null);
            handleTailItem(key, val);
        }).exceptionally(e -> {
            log.error("{} failed to handle notification for path:{}", name, path, e);
            return null;
        });
    }

    private void handleNotification(org.apache.pulsar.metadata.api.Notification notification) {

        if (notification.getType() == NotificationType.ChildrenChanged) {
            return;
        }

        String path = notification.getPath();

        doHandleNotification(path);
    }


    private CompletableFuture handleExisting(String path) {
        if (!isValidPath(path)) {
            return CompletableFuture.completedFuture(null);
        }
        return cache.get(path)
                .thenAccept(valOpt -> {
                    valOpt.ifPresent(val -> {
                        String key = getKey(path);
                        updateData(key, val);
                        if (log.isDebugEnabled()) {
                            log.debug("{} applying existing item key={} value={}",
                                    name,
                                    key,
                                    val);
                        }
                        for (var listener : existingItemListeners) {
                            try {
                                listener.accept(key, val);
                            } catch (Throwable e) {
                                log.error("{} failed to listen existing item key:{}, val:{}", name, key, val,
                                        e);
                                throw e;
                            }
                        }
                    });
                });
    }

    private void fill() throws MetadataStoreException {
        final var deadline = System.currentTimeMillis() + FILL_TIMEOUT_IN_MILLIS;
        log.info("{} start filling existing items under the pathPrefix:{}", name, pathPrefix);
        ConcurrentLinkedDeque q = new ConcurrentLinkedDeque<>();
        List> futures = new ArrayList<>();
        q.add(pathPrefix);
        LongAdder count = new LongAdder();
        while (!q.isEmpty()) {
            var now = System.currentTimeMillis();
            if (now >= deadline) {
                String err = name + " failed to fill existing items in "
                        + TimeUnit.MILLISECONDS.toSeconds(FILL_TIMEOUT_IN_MILLIS) + " secs. Filled count:"
                        + count.sum();
                log.error(err);
                throw new MetadataStoreException(err);
            }
            int size = Math.min(MAX_CONCURRENT_METADATA_OPS_DURING_FILL, q.size());
            for (int i = 0; i < size; i++) {
                String path = q.poll();
                futures.add(store.getChildren(path)
                        .thenCompose(children -> {
                            // The path is leaf
                            if (children.isEmpty()) {
                                count.increment();
                                return handleExisting(path);
                            } else {
                                for (var child : children) {
                                    q.add(path + "/" + child);
                                }
                                return CompletableFuture.completedFuture(null);
                            }
                        }));
            }
            try {
                FutureUtil.waitForAll(futures).get(
                        Math.min(timeoutInMillis, deadline - now),
                        TimeUnit.MILLISECONDS);
            } catch (Throwable e) {
                Throwable c = FutureUtil.unwrapCompletionException(e);
                log.error("{} failed to fill existing items", name, c);
                throw new MetadataStoreException(c);
            }
            futures.clear();
        }
        log.info("{} completed filling existing items with size:{}", name, count.sum());
    }


    private String getPath(String key) {
        return pathPrefix + "/" + key;
    }

    private String getKey(String path) {
        return path.replaceFirst(pathPrefix + "/", "");
    }

    public boolean exists(String key) {
        return immutableData.containsKey(key);
    }

    public T get(String key) {
        return data.get(key);
    }

    public CompletableFuture put(String key, T value) {
        String path = getPath(key);
        return cache.readModifyUpdateOrCreate(path, (old) -> {
            if (conflictResolver.test(old.orElse(null), value)) {
                return value;
            } else {
                throw new ConflictException(
                        String.format("Failed to update from old:%s to value:%s", old, value));
            }
        }).thenCompose(__ -> doHandleNotification(path)); // immediately notify local tableview
    }

    public CompletableFuture delete(String key) {
        String path = getPath(key);
        return cache.delete(path)
                .thenCompose(__ -> doHandleNotification(path)); // immediately notify local tableview
    }

    public int size() {
        return immutableData.size();
    }

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

    public Set> entrySet() {
        return immutableData.entrySet();
    }

    public Set keySet() {
        return immutableData.keySet();
    }

    public Collection values() {
        return immutableData.values();
    }

    public void forEach(BiConsumer action) {
        immutableData.forEach(action);
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy