Skip to content

Commit

Permalink
- isInThread BIF
Browse files Browse the repository at this point in the history
- ThreadGroup control for cfthreads
- Logging thread exceptions
  • Loading branch information
lmajano committed Apr 19, 2024
1 parent 900b3ce commit 18e51bd
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* [BoxLang]
*
* Copyright [2023] [Ortus Solutions, Corp]
*
* 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 ortus.boxlang.runtime.bifs.global.async;

import ortus.boxlang.runtime.bifs.BIF;
import ortus.boxlang.runtime.bifs.BoxBIF;
import ortus.boxlang.runtime.context.IBoxContext;
import ortus.boxlang.runtime.context.RequestBoxContext;
import ortus.boxlang.runtime.scopes.ArgumentsScope;

@BoxBIF
public class IsInThread extends BIF {

/**
* Constructor
*/
public IsInThread() {
super();
}

/**
* Verifies if the calling execution code is running in a thread or not.
*
* @param context The context in which the BIF is being invoked.
* @param arguments Argument scope for the BIF.
*
* @return True, if the calling code is running in a thread; false, otherwise.
*/
public Object _invoke( IBoxContext context, ArgumentsScope arguments ) {
return context
.getParentOfType( RequestBoxContext.class )
.getThreadManager()
.isInThread();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import java.util.List;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ortus.boxlang.runtime.components.Attribute;
import ortus.boxlang.runtime.components.BoxComponent;
import ortus.boxlang.runtime.components.Component;
Expand All @@ -40,15 +43,9 @@
public class Thread extends Component {

/**
* --------------------------------------------------------------------------
* Constants
* --------------------------------------------------------------------------
* Logger
*/

/**
* The prefix for thread names
*/
public static final String DEFAULT_THREAD_PREFIX = "BL-Thread-";
private static final Logger logger = LoggerFactory.getLogger( Thread.class );

/**
* --------------------------------------------------------------------------
Expand Down Expand Up @@ -97,9 +94,7 @@ public Thread() {
* @attribute.priority The priority of the thread. The default value is "normal". The following are the possible values: "high", "low", "normal".
*
* @attribute.timeout The number of milliseconds to wait for the thread to finish. If the thread does not finish within the specified time, the thread
* is
* terminated. If the timeout attribute is not specified, the thread runs until it finishes.
*
* is terminated. If the timeout attribute is not specified, the thread runs until it finishes.
*/
public BodyResult _invoke( IBoxContext context, IStruct attributes, ComponentBody body, IStruct executionState ) {
Key action = Key.of( attributes.getAsString( Key.action ) );
Expand Down Expand Up @@ -135,33 +130,42 @@ public BodyResult _invoke( IBoxContext context, IStruct attributes, ComponentBod
private void run( IBoxContext context, String name, String priority, IStruct attributes, ComponentBody body ) {
RequestThreadManager threadManager = context.getParentOfType( RequestBoxContext.class ).getThreadManager();

// generate random name if not set or empty
// generate random name if not set or empty: anonymous thread
if ( name == null || name.isEmpty() ) {
name = DEFAULT_THREAD_PREFIX + java.util.UUID.randomUUID().toString();
name = java.util.UUID.randomUUID().toString();
}
final Key nameKey = Key.of( name );
// Generate a new thread context of execution
ThreadBoxContext tContext = new ThreadBoxContext( context, threadManager, nameKey );
// Create a new thread
java.lang.Thread thread = new java.lang.Thread( () -> {
StringBuffer buffer = new StringBuffer();
Throwable exception = null;
try {
processBody( tContext, body, buffer );
} catch ( AbortException e ) {
// Nothing to do here
} catch ( Throwable e ) {
exception = e;
} finally {
threadManager.completeThread(
nameKey,
buffer.toString(),
exception,
java.lang.Thread.interrupted()
);
}
},
DEFAULT_THREAD_PREFIX + name );
// Create a new thread definition
java.lang.Thread thread = new java.lang.Thread(
// thread group
threadManager.getThreadGroup(),
// Runnable Proxy
() -> {
StringBuffer buffer = new StringBuffer();
Throwable exception = null;
try {
processBody( tContext, body, buffer );
} catch ( AbortException e ) {
// We log it so we can potentially find out why it was aborted
logger.error( "Thread [{}] aborted at stacktrace: {}", nameKey.getName(), e.getStackTrace() );
} catch ( Throwable e ) {
exception = e;
logger.error( "Thread [{}] terminated with exception: {}", nameKey.getName(), e.getMessage() );
logger.error( "-> Exception", e );
} finally {
threadManager.completeThread(
nameKey,
buffer.toString(),
exception,
java.lang.Thread.interrupted()
);
}
},
// Name
threadManager.DEFAULT_THREAD_PREFIX + name
);

// Set the priority of the thread if it's not the default
thread.setPriority( switch ( priority ) {
Expand Down
51 changes: 46 additions & 5 deletions src/main/java/ortus/boxlang/runtime/util/RequestThreadManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,42 @@
public class RequestThreadManager {

/**
* The threads we are managing
* --------------------------------------------------------------------------
* Public Constants
* --------------------------------------------------------------------------
*/
private Map<Key, IStruct> threads = new ConcurrentHashMap<>();

/**
* The thread scope
* The prefix for thread names
*/
protected IScope threadScope = new ThreadScope();
public static final String DEFAULT_THREAD_PREFIX = "BL-Thread-";

/**
* The default time to wait for a thread to stop when terminating
*/
private static final long DEFAULT_THREAD_WAIT_TIME = 3000;
public static final long DEFAULT_THREAD_WAIT_TIME = 3000;

/**
* --------------------------------------------------------------------------
* Private Properties
* --------------------------------------------------------------------------
*/

/**
* The threads we are managing will be stored here
*/
protected Map<Key, IStruct> threads = new ConcurrentHashMap<>();

/**
* The thread scope
*/
protected IScope threadScope = new ThreadScope();

/**
* The thread group for the threads created by this manager
* TODO: Move to SingleThreadExecutors for better control
*/
private static final ThreadGroup THREAD_GROUP = new ThreadGroup( "BL-Threads" );

/**
* Registers a thread with the manager
Expand Down Expand Up @@ -251,6 +274,24 @@ public IScope getThreadScope() {
return this.threadScope;
}

/**
* Get the thread group for the threads created by this manager
*
* @return The thread group
*/
public ThreadGroup getThreadGroup() {
return THREAD_GROUP;
}

/**
* Verify if the current thread is in a thread.
*
* @return true if the current thread is in a thread
*/
public boolean isInThread() {
return Thread.currentThread().getThreadGroup() == THREAD_GROUP;
}

/**
* Gets the names of the threads
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package ortus.boxlang.runtime.bifs.global.async;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import ortus.boxlang.runtime.BoxRuntime;
import ortus.boxlang.runtime.context.IBoxContext;
import ortus.boxlang.runtime.context.ScriptingRequestBoxContext;
import ortus.boxlang.runtime.scopes.IScope;
import ortus.boxlang.runtime.scopes.Key;
import ortus.boxlang.runtime.scopes.VariablesScope;

public class isInThreadTest {

static BoxRuntime instance;
IBoxContext context;
IScope variables;
static Key result = new Key( "result" );

@BeforeAll
public static void setUp() {
instance = BoxRuntime.getInstance( true );
}

@AfterAll
public static void teardown() {

}

@BeforeEach
public void setupEach() {
context = new ScriptingRequestBoxContext( instance.getRuntimeContext() );
variables = context.getScopeNearby( VariablesScope.name );
}

@DisplayName( "It can verify you are not in a thread" )
@Test
public void testNotInThread() {
// @formatter:off
instance.executeSource(
"""
result = isInThread();
""",
context );
// @formatter:on

Boolean resultValue = variables.getAsBoolean( result );
assert ( resultValue == false );
}

@DisplayName( "It can verify you are in a thread" )
@Test
public void testInThread() {
// @formatter:off
instance.executeSource(
"""
result = false;
thread name="testThread"{
result = isInThread();
}
// join
thread action="join" name="testThread";
""",
context );
// @formatter:on

Boolean resultValue = variables.getAsBoolean( result );
assert ( resultValue == true );
}

}

0 comments on commit 18e51bd

Please sign in to comment.