blob: 81e8148ba294ee2a5226eadc05efb0a373d057dd [file] [log] [blame]
/*
* 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.solr.pkg;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import org.apache.solr.common.MapWriter;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.SolrCore;
import org.apache.solr.core.SolrResourceLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.lucene.util.IOUtils.closeWhileHandlingException;
/**
* The class that holds a mapping of various packages and classloaders
*/
public class PackageLoader implements Closeable {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static final String LATEST = "$LATEST";
private final CoreContainer coreContainer;
private final Map<String, Package> packageClassLoaders = new ConcurrentHashMap<>();
private PackageAPI.Packages myCopy = new PackageAPI.Packages();
private PackageAPI packageAPI;
public Optional<Package.Version> getPackageVersion(String pkg, String version) {
Package p = packageClassLoaders.get(pkg);
if(p == null) return Optional.empty();
return Optional.ofNullable(p.getVersion(version));
}
public PackageLoader(CoreContainer coreContainer) {
this.coreContainer = coreContainer;
packageAPI = new PackageAPI(coreContainer, this);
refreshPackageConf();
}
public PackageAPI getPackageAPI() {
return packageAPI;
}
public Package getPackage(String key) {
return packageClassLoaders.get(key);
}
@SuppressWarnings({"unchecked"})
public Map<String, Package> getPackages() {
return Collections.EMPTY_MAP;
}
public void refreshPackageConf() {
log.info("{} updated to version {}", ZkStateReader.SOLR_PKGS_PATH, packageAPI.pkgs.znodeVersion);
List<Package> updated = new ArrayList<>();
Map<String, List<PackageAPI.PkgVersion>> modified = getModified(myCopy, packageAPI.pkgs);
for (Map.Entry<String, List<PackageAPI.PkgVersion>> e : modified.entrySet()) {
if (e.getValue() != null) {
Package p = packageClassLoaders.get(e.getKey());
if (e.getValue() != null && p == null) {
packageClassLoaders.put(e.getKey(), p = new Package(e.getKey()));
}
p.updateVersions(e.getValue());
updated.add(p);
} else {
Package p = packageClassLoaders.remove(e.getKey());
if (p != null) {
//other classes are holding to a reference to this object
// they should know that this is removed
p.markDeleted();
closeWhileHandlingException(p);
}
}
}
for (SolrCore core : coreContainer.getCores()) {
core.getPackageListeners().packagesUpdated(updated);
}
myCopy = packageAPI.pkgs;
}
public Map<String, List<PackageAPI.PkgVersion>> getModified(PackageAPI.Packages old, PackageAPI.Packages newPkgs) {
Map<String, List<PackageAPI.PkgVersion>> changed = new HashMap<>();
for (Map.Entry<String, List<PackageAPI.PkgVersion>> e : newPkgs.packages.entrySet()) {
List<PackageAPI.PkgVersion> versions = old.packages.get(e.getKey());
if (versions != null) {
if (!Objects.equals(e.getValue(), versions)) {
if (log.isInfoEnabled()) {
log.info("Package {} is modified ", e.getKey());
}
changed.put(e.getKey(), e.getValue());
}
} else {
if (log.isInfoEnabled()) {
log.info("A new package: {} introduced", e.getKey());
}
changed.put(e.getKey(), e.getValue());
}
}
//some packages are deleted altogether
for (String s : old.packages.keySet()) {
if (!newPkgs.packages.keySet().contains(s)) {
log.info("Package: {} is removed althogether", s);
changed.put(s, null);
}
}
return changed;
}
public void notifyListeners(String pkg) {
Package p = packageClassLoaders.get(pkg);
if (p != null) {
List<Package> l = Collections.singletonList(p);
for (SolrCore core : coreContainer.getCores()) {
core.getPackageListeners().packagesUpdated(l);
}
}
}
/**
* represents a package definition in the packages.json
*/
public class Package implements Closeable {
final String name;
final Map<String, Version> myVersions = new ConcurrentHashMap<>();
private List<String> sortedVersions = new CopyOnWriteArrayList<>();
String latest;
private boolean deleted;
Package(String name) {
this.name = name;
}
public boolean isDeleted() {
return deleted;
}
public Set<String> allVersions() {
return myVersions.keySet();
}
private synchronized void updateVersions(List<PackageAPI.PkgVersion> modified) {
for (PackageAPI.PkgVersion v : modified) {
Version version = myVersions.get(v.version);
if (version == null) {
log.info("A new version: {} added for package: {} with artifacts {}", v.version, this.name, v.files);
Version ver = null;
try {
ver = new Version(this, v);
} catch (Exception e) {
log.error("package could not be loaded {}", ver, e);
continue;
}
myVersions.put(v.version, ver);
sortedVersions.add(v.version);
}
}
Set<String> newVersions = new HashSet<>();
for (PackageAPI.PkgVersion v : modified) {
newVersions.add(v.version);
}
for (String s : new HashSet<>(myVersions.keySet())) {
if (!newVersions.contains(s)) {
log.info("version: {} is removed from package: {}", s, this.name);
sortedVersions.remove(s);
Version removed = myVersions.remove(s);
if (removed != null) {
closeWhileHandlingException(removed);
}
}
}
sortedVersions.sort(String::compareTo);
if (sortedVersions.size() > 0) {
String latest = sortedVersions.get(sortedVersions.size() - 1);
if (!latest.equals(this.latest)) {
log.info("version: {} is the new latest in package: {}", latest, this.name);
}
this.latest = latest;
} else {
log.error("latest version: null");
latest = null;
}
}
public Version getLatest() {
return latest == null ? null : myVersions.get(latest);
}
public Version getVersion(String version) {
if(version == null) return getLatest();
return myVersions.get(version);
}
public Version getLatest(String lessThan) {
if (lessThan == null) {
return getLatest();
}
String latest = findBiggest(lessThan, new ArrayList<>(sortedVersions));
return latest == null ? null : myVersions.get(latest);
}
public String name() {
return name;
}
private void markDeleted() {
deleted = true;
myVersions.clear();
sortedVersions.clear();
latest = null;
}
@Override
public void close() throws IOException {
for (Version v : myVersions.values()) v.close();
}
public class Version implements MapWriter, Closeable {
private final Package parent;
private SolrResourceLoader loader;
private final PackageAPI.PkgVersion version;
@Override
public void writeMap(EntryWriter ew) throws IOException {
ew.put("package", parent.name());
version.writeMap(ew);
}
Version(Package parent, PackageAPI.PkgVersion v) {
this.parent = parent;
this.version = v;
List<Path> paths = new ArrayList<>();
List<String> errs = new ArrayList<>();
coreContainer.getPackageStoreAPI().validateFiles(version.files, true, s -> errs.add(s));
if(!errs.isEmpty()) {
throw new RuntimeException("Cannot load package: " +errs);
}
for (String file : version.files) {
paths.add(coreContainer.getPackageStoreAPI().getPackageStore().getRealpath(file));
}
loader = new PackageResourceLoader(
"PACKAGE_LOADER: " + parent.name() + ":" + version,
paths,
Paths.get(coreContainer.getSolrHome()),
coreContainer.getResourceLoader().getClassLoader());
}
public String getVersion() {
return version.version;
}
public PackageAPI.PkgVersion getPkgVersion(){
return version.copy();
}
@SuppressWarnings({"rawtypes"})
public Collection getFiles() {
return Collections.unmodifiableList(version.files);
}
public SolrResourceLoader getLoader() {
return loader;
}
@Override
public void close() throws IOException {
if (loader != null) {
closeWhileHandlingException(loader);
}
}
@Override
public String toString() {
return jsonStr();
}
}
}
static class PackageResourceLoader extends SolrResourceLoader {
List<Path> paths;
PackageResourceLoader(String name, List<Path> classpath, Path instanceDir, ClassLoader parent) {
super(name, classpath, instanceDir, parent);
this.paths = classpath;
}
@Override
public <T> boolean addToCoreAware(T obj) {
//do not do anything
//this class is not aware of a SolrCore and it is totally not tied to
// the lifecycle of SolrCore. So, this returns 'false' & it should be
// taken care of by the caller
return false;
}
@Override
public <T> boolean addToResourceLoaderAware(T obj) {
// do not do anything
// this should be invoked only after the init() is invoked.
// The caller should take care of that
return false;
}
@Override
public <T> void addToInfoBeans(T obj) {
//do not do anything. It should be handled externally
}
@Override
public InputStream openResource(String resource) {
return getClassLoader().getResourceAsStream(resource);
}
}
@SuppressWarnings("CompareToZero") // TODO either document why or fix this
private static String findBiggest(String lessThan, List<String> sortedList) {
String latest = null;
for (String v : sortedList) {
if (v.compareTo(lessThan) < 1) {
latest = v;
} else break;
}
return latest;
}
@Override
public void close() {
for (Package p : packageClassLoaders.values()) closeWhileHandlingException(p);
}
}