/* * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * * WSO2 Inc. 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.wso2.siddhi.core.debugger; import org.apache.log4j.Logger; import org.wso2.siddhi.core.config.ExecutionPlanContext; import org.wso2.siddhi.core.event.ComplexEvent; import org.wso2.siddhi.core.util.snapshot.SnapshotService; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicBoolean; /** * SiddhiDebugger adds checkpoints, remove checkpoints and provide traversal functions like next and play. * The given operations are: * - next: Debug the next checkpoint * - play: Return to the same */ public class SiddhiDebugger { /** * Logger to log events. */ private static final Logger log = Logger.getLogger(SiddhiDebugger.class); /** * Thread local flag to indicate whether the next endpoint must be blocked or not. */ private static final ThreadLocal<Boolean> threadLocalNextFlag = new ThreadLocal<Boolean>() { @Override protected Boolean initialValue() { return false; } }; /** * Semaphore is used to pause the Siddhi thread at breakpoints. * The lock is acquired in {@link SiddhiDebugger#checkBreakPoint(String, QueryTerminal, ComplexEvent)} and * released in {@link SiddhiDebugger#next()} and {@link SiddhiDebugger#play()}. */ private Semaphore breakPointLock = new Semaphore(0); /** * A map containing breakpoint name and the AtomicBoolean indicates whether the point has an active checkpoint. */ private HashMap<String, AtomicBoolean> acquiredBreakPointsMap = new HashMap<String, AtomicBoolean>(); /** * A flag to indicate whether the user called the next method or not. */ private volatile AtomicBoolean enableNext = new AtomicBoolean(false); /** * Debug caller to send the {@link ComplexEvent} for debugging. */ private SiddhiDebuggerCallback siddhiDebuggerCallback; /** * Snapshot service to retrieve the internal states of queries. */ private SnapshotService snapshotService; /** * Create a new SiddhiDebugger instance for the given {@link ExecutionPlanContext}. * * @param executionPlanContext the ExecutionPlanContext */ public SiddhiDebugger(ExecutionPlanContext executionPlanContext) { this.snapshotService = executionPlanContext.getSnapshotService(); } /** * Acquire the given breakpoint. * * @param queryName name of the Siddhi query * @param queryTerminal IN or OUT endpoint of the query */ public void acquireBreakPoint(String queryName, QueryTerminal queryTerminal) { // TODO: Validate the query name String breakpointName = createBreakpointName(queryName, queryTerminal); AtomicBoolean breakPointLock = acquiredBreakPointsMap.get(breakpointName); if (breakPointLock == null) { breakPointLock = new AtomicBoolean(true); acquiredBreakPointsMap.put(breakpointName, breakPointLock); } else { breakPointLock.set(true); } } /** * Release the given breakpoint from the SiddhiDebugger. * * @param queryName name of the Siddhi query * @param queryTerminal IN or OUT endpoint of the query */ public void releaseBreakPoint(String queryName, QueryTerminal queryTerminal) { // TODO: Validate the query name acquiredBreakPointsMap.remove(createBreakpointName(queryName, queryTerminal)); } /** * Release all the breakpoints from the Siddhi debugger. This may required to before stopping the debugger. */ public void releaseAllBreakPoints() { acquiredBreakPointsMap.clear(); } /** * Check for active breakpoint at the given endpoint and if there is an active checkpoint, block the thread and * send the event for debug callback. * * @param queryName name of the Siddhi query * @param queryTerminal IN or OUT endpoint of the query * @param complexEvent the {@link ComplexEvent} which is waiting at the endpoint */ public void checkBreakPoint(String queryName, QueryTerminal queryTerminal, ComplexEvent complexEvent) { // Prevent other threads from calling this method simultaneously synchronized (this) { // Create the breakpoint name for the given query at the given endpoint String breakpointName = createBreakpointName(queryName, queryTerminal); // Get the breakpoint atomic boolean using the name AtomicBoolean breakpoint = acquiredBreakPointsMap.get(breakpointName); // Check whether this endpoint comes after the next call of another breakpoint boolean isNext = isNextEnabled(); if (isNext) { // Reset next flag of this thread setNextEnabled(false); } if (breakpoint != null && breakpoint.get() || isNext) { // An active breakpoint is available or next method was called last time // Pass the debugger, query name, breakpoint and the events to the callback if (siddhiDebuggerCallback != null) { siddhiDebuggerCallback.debugEvent(complexEvent, queryName, queryTerminal, this); } try { // Lock the breakpoint so that the thread cannot progress further until it is released by next or // play methods. breakPointLock.acquire(); // After releasing the lock check how the lock was released. If it is by next, set the thread // local flag to true. if (this.enableNext.get()) { // Must be assigned from the current thread. Do not move it to next or play methods setNextEnabled(true); this.enableNext.set(false); } } catch (InterruptedException e) { log.error("Error in acquiring breakpoint lock at " + breakpointName); } } } } /** * Release the current lock and wait for the events arrive to the next point. For this to work, the next endpoint * is not required to be a checkpoint marked by the user. * For example, if user adds breakpoint only for the IN of query 1, next will track the event in OUT of query 1. */ public void next() { this.enableNext.set(true); this.breakPointLock.release(); } /** * Release the current lock and wait for the next event arrive to the same break point. */ public void play() { this.breakPointLock.release(); } /** * A callback to be called by the Siddhi debugger when reaching an active breakpoint. * * @param siddhiDebuggerCallback the SiddhiDebuggerCallback */ public void setDebuggerCallback(SiddhiDebuggerCallback siddhiDebuggerCallback) { this.siddhiDebuggerCallback = siddhiDebuggerCallback; } /** * Get all the events stored in the {@link org.wso2.siddhi.core.util.snapshot.Snapshotable} entities of the given * query. * * @param queryName name of the Siddhi query * @return QueryState internal state of the query */ public Map<String, Object> getQueryState(String queryName) { return this.snapshotService.queryState(queryName); } /** * Determine whether this checkpoint is enabled by the previous checkpoint. * * @return true of this is the next point otherwise false * @see SiddhiDebugger#setNextEnabled(boolean) */ private boolean isNextEnabled() { return threadLocalNextFlag.get(); } /** * Enable or disable checkpoint at the next end point. * * @param nextEnabled true or false indicating whether the next checkpoint is enabled or not * @see SiddhiDebugger#isNextEnabled() */ private void setNextEnabled(boolean nextEnabled) { threadLocalNextFlag.set(nextEnabled); } /** * Create the breakpoint name for the given query at the given end point. * * @param queryName the Siddhi query name * @param queryTerminal the IN or OUT end point of the query * @return the breakpoint name */ private String createBreakpointName(String queryName, QueryTerminal queryTerminal) { return queryName + ":" + queryTerminal; } /** * SiddhiDebugger allows to add breakpoints only at the beginning and the end of a query. */ public enum QueryTerminal { IN, OUT } }