Blade supports asynchronous command execution, allowing you to run long-running operations without blocking the main server thread.
The @Async Annotation
The @Async annotation marks a command for asynchronous execution:
/**
* This annotation is used to indicate that a command should be executed asynchronously.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Async {
}
Basic Usage
Simply add the @Async annotation to your command method:
@Command("database")
@Async
public void databaseCommand(@Sender Player player, @Name("query") String query) {
// This runs on a separate thread
DatabaseResult result = database.query(query);
player.sendMessage("Query returned " + result.size() + " rows");
}
When to Use Async
Use @Async for operations that might block or take time:
Database Operations
@Command("stats")
@Async
public void statsCommand(@Sender Player player, @Name("username") String username) {
// Database query - could take time
PlayerStats stats = database.getPlayerStats(username);
if (stats == null) {
player.sendMessage("Player not found!");
return;
}
player.sendMessage("Kills: " + stats.getKills());
player.sendMessage("Deaths: " + stats.getDeaths());
}
File I/O
@Command("export")
@Async
public void exportCommand(@Sender Player player) {
player.sendMessage("Exporting data...");
// File writing - I/O operation
try {
exportPlayerData(player.getUniqueId());
player.sendMessage("Export complete!");
} catch (IOException e) {
player.sendMessage("Export failed: " + e.getMessage());
}
}
HTTP Requests
@Command("verify")
@Async
public void verifyCommand(@Sender Player player, @Name("code") String code) {
// HTTP request - network operation
try {
boolean valid = apiClient.verifyCode(code);
if (valid) {
player.sendMessage("Code verified successfully!");
} else {
player.sendMessage("Invalid code!");
}
} catch (Exception e) {
player.sendMessage("Verification service unavailable");
}
}
Complex Calculations
@Command("calculate")
@Async
public void calculateCommand(
@Sender Player player,
@Name("iterations") @Range(min = 1, max = 1000000) int iterations
) {
player.sendMessage("Starting calculation...");
// CPU-intensive operation
double result = performComplexCalculation(iterations);
player.sendMessage("Result: " + result);
}
Thread Safety Considerations
When using @Async, your command executes on a separate thread, not the main server thread. You must be careful with thread safety.
Safe Operations
These operations are safe in async commands:
@Command("lookup")
@Async
public void lookupCommand(@Sender Player player, @Name("username") String username) {
// ✓ Sending messages to players is thread-safe in most platforms
player.sendMessage("Looking up " + username + "...");
// ✓ Database operations are designed for multi-threading
PlayerData data = database.getPlayerData(username);
// ✓ HTTP requests are safe
String info = httpClient.get("https://api.example.com/player/" + username);
player.sendMessage("Done!");
}
Unsafe Operations
These operations are NOT safe and should be wrapped in a scheduler:
@Command("unsafe-example")
@Async
public void unsafeExample(@Sender Player player) {
// ✗ WRONG: Modifying game state directly
player.setHealth(20.0); // Not thread-safe!
player.getInventory().clear(); // Not thread-safe!
player.teleport(location); // Not thread-safe!
}
Returning to Main Thread
When you need to modify game state, use the platform’s scheduler:
Bukkit/Paper/Spigot
@Command("heal")
@Async
public void healCommand(@Sender Player player) {
// Do async work first
boolean hasPermission = database.hasVipPermission(player.getUniqueId());
if (!hasPermission) {
player.sendMessage("You need VIP to use this!");
return;
}
// Switch back to main thread for game modifications
Bukkit.getScheduler().runTask(plugin, () -> {
player.setHealth(20.0);
player.setFoodLevel(20);
player.sendMessage("You have been healed!");
});
}
Fabric
@Command("heal")
@Async
public void healCommand(@Sender ServerPlayerEntity player) {
// Do async work
boolean hasPermission = checkPermissionAsync(player);
if (!hasPermission) {
player.sendMessage(Text.literal("No permission!"));
return;
}
// Switch to main thread
MinecraftServer server = player.getServer();
server.execute(() -> {
player.setHealth(20.0f);
player.sendMessage(Text.literal("Healed!"));
});
}
Velocity
@Command("broadcast")
@Async
public void broadcastCommand(@Sender Player player, @Name("message") @Greedy String message) {
// Do async work
String filtered = profanityFilter.filter(message);
// Switch to main thread
proxyServer.getScheduler()
.buildTask(plugin, () -> {
proxyServer.getAllPlayers().forEach(p ->
p.sendMessage(Component.text(filtered))
);
})
.schedule();
}
Error Handling
Always handle exceptions in async commands:
@Command("api-call")
@Async
public void apiCallCommand(@Sender Player player, @Name("endpoint") String endpoint) {
try {
player.sendMessage("Calling API...");
String response = httpClient.get(endpoint);
player.sendMessage("Response: " + response);
} catch (IOException e) {
player.sendMessage("Network error: " + e.getMessage());
logger.error("API call failed", e);
} catch (Exception e) {
player.sendMessage("An error occurred!");
logger.error("Unexpected error in API call", e);
}
}
Combining with Other Annotations
You can combine @Async with other annotations:
@Command("admin database")
@Permission("myplugin.admin.database")
@Description("Query the database")
@Async
public void adminDatabaseCommand(
@Sender Player player,
@Name("query") @Greedy String query
) {
// Permission checked BEFORE async execution
// Command runs asynchronously
try {
List<Map<String, Object>> results = database.executeQuery(query);
player.sendMessage("Query returned " + results.size() + " rows");
} catch (SQLException e) {
player.sendMessage("Query failed: " + e.getMessage());
}
}
Best Practices
- Only use for I/O or long operations: Don’t use
@Async for simple commands
- Handle exceptions: Always catch and handle potential errors
- Be thread-safe: Don’t modify game state directly from async threads
- Use schedulers: Return to the main thread when you need to modify game state
- Send progress updates: Let users know the command is working
- Set timeouts: Don’t let async operations run forever
Example: Complete Async Command
Here’s a complete example showing best practices:
@Command("backup")
@Permission("myplugin.admin.backup")
@Async
public void backupCommand(@Sender CommandSender sender) {
sender.sendMessage("Starting backup...");
try {
// Async file I/O
long startTime = System.currentTimeMillis();
File backup = backupManager.createBackup();
long duration = System.currentTimeMillis() - startTime;
// Upload to cloud storage (network I/O)
sender.sendMessage("Uploading to cloud storage...");
String url = cloudStorage.upload(backup);
// Success message
sender.sendMessage("Backup complete! (" + duration + "ms)");
sender.sendMessage("URL: " + url);
// Clean up old backups
backupManager.cleanOldBackups();
} catch (IOException e) {
sender.sendMessage("Backup failed: " + e.getMessage());
logger.error("Backup operation failed", e);
} catch (Exception e) {
sender.sendMessage("An unexpected error occurred!");
logger.error("Unexpected error during backup", e);
}
}