/*
* 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.karaf.bundle.command;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.karaf.shell.api.action.Action;
import org.apache.karaf.shell.api.action.Command;
import org.apache.karaf.shell.api.action.Option;
import org.apache.karaf.shell.api.console.Session;
import org.apache.karaf.shell.api.action.lifecycle.Reference;
import org.apache.karaf.shell.api.action.lifecycle.Service;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.FrameworkEvent;
import org.osgi.framework.FrameworkListener;
import org.osgi.framework.wiring.FrameworkWiring;
@Command(scope = "bundle", name = "load-test", description = "Load test bundle lifecycle")
@Service
public class LoadTest implements Action {
@Option(name = "--threads", description = "number of concurrent threads")
int threads = 2;
@Option(name = "--delay", description = "maximum delay between actions")
int delay = 1;
@Option(name = "--iterations", description = "number of iterations per thread")
int iterations = 100;
@Option(name = "--refresh", description = "percentage of bundle refresh vs restart")
int refresh = 20;
@Option(name = "--excludes", description = "List of bundles (ids or symbolic names) to exclude")
List<String> excludes = Arrays.asList("0", "org.ops4j.pax.url.mvn", "org.ops4j.pax.logging.pax-logging-api", "org.ops4j.pax.logging.pax-logging-log4j2");
@Reference
Session session;
@Reference
BundleContext bundleContext;
@Override
public Object execute() throws Exception {
if (!confirm(session)) {
return null;
}
final BundleContext bundleContext = this.bundleContext.getBundle(0).getBundleContext();
final FrameworkWiring wiring = bundleContext.getBundle().adapt(FrameworkWiring.class);
final CountDownLatch latch = new CountDownLatch(threads);
final Bundle[] bundles = bundleContext.getBundles();
final AtomicBoolean[] locks = new AtomicBoolean[bundles.length];
for (int b = 0; b < locks.length; b++) {
locks[b] = new AtomicBoolean(true);
// Avoid touching excluded bundles
if (excludes.contains(Long.toString(bundles[b].getBundleId()))
|| excludes.contains(bundles[b].getSymbolicName())) {
continue;
}
// Only touch active bundles
if (bundles[b].getState() != Bundle.ACTIVE) {
continue;
}
// Now set the lock to available
locks[b].set(false);
}
for (int i = 0; i < threads; i++) {
new Thread() {
public void run() {
try {
Random rand = new Random();
for (int j = 0; j < iterations; j++) {
for (;;) {
int b = rand.nextInt(bundles.length);
if (locks[b].compareAndSet(false, true)) {
try {
// Only touch active bundles
if (bundles[b].getState() != Bundle.ACTIVE) {
continue;
}
if (rand.nextInt(100) < refresh) {
try {
bundles[b].update();
final CountDownLatch latch = new CountDownLatch(1);
wiring.refreshBundles(Collections.singletonList(bundles[b]), new FrameworkListener() {
public void frameworkEvent(FrameworkEvent event) {
latch.countDown();
}
});
latch.await();
} finally {
while (true) {
try {
bundles[b].start(Bundle.START_TRANSIENT);
break;
} catch (Exception e) {
Thread.sleep(1);
}
}
}
} else {
try {
bundles[b].stop(Bundle.STOP_TRANSIENT);
} finally {
while (true) {
try {
bundles[b].start(Bundle.START_TRANSIENT);
break;
} catch (Exception e) {
Thread.sleep(1);
}
}
}
}
Thread.sleep(rand.nextInt(delay));
} catch (Exception e) {
boolean ignore = false;
if (e instanceof BundleException && e.getMessage() != null) {
String msg = e.getMessage();
if ("Cannot acquire global lock to update the bundle.".equals(msg) ||
"Unable to acquire global lock for resolve.".equals(msg) ||
msg.matches("Bundle .* cannot be update, since it is either starting or stopping.")) {
ignore = true;
}
}
if (!ignore) {
e.printStackTrace();
}
} finally {
locks[b].set(false);
}
}
break;
}
}
} catch (Throwable t) {
t.printStackTrace();
} finally {
latch.countDown();
}
}
}.start();
}
new Thread() {
@Override
public void run() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.err.println("Load test finished");
}
}.start();
return null;
}
private boolean confirm(Session session) throws IOException {
for (;;) {
String msg = "You are about to perform a start/stop/refresh load test on bundles.\nDo you wish to continue (yes/no): ";
String str = session.readLine(msg, null);
if ("yes".equalsIgnoreCase(str)) {
return true;
}
if ("no".equalsIgnoreCase(str)) {
return false;
}
}
}
}