Cloud function is a sub-module of LeanEngine that allows you to run functions on the cloud in response to the requests made by clients. Before you continue, make sure you have read LeanEngine Overview.
When developing your application, you may need to write logic that:
Are shared by multiple platforms (like iOS, Android, and web) and you wish to write them only once.
Need to be modified frequently (like the sorting rules of a list) but you don't want to release a new version of client each time you make a change.
Demand high network traffic or computing power (like producing statistics on a huge table) which you don't want to run on clients.
Need to be triggered when certain events happen (hooking). For example, when a user deletes an account, you may wish to delete the entries in other tables that are related to this account as well.
Need to bypass certain restrictions set by ACL.
Need to be performed routinely. For example, you may wish to clean up inactive accounts every month.
With cloud function, you can deploy these types of logic written in any language (JavaScript, Python, PHP, or Java) on the cloud and have LeanEngine run them for you.
If you have no idea how to deploy your project to LeanEngine, take a look at LeanEngine Quick Start.
Other Languages
This guide uses Java as an example, but LeanEngine supports many other languages as well. You can choose the one you are familiar with for development:
LeanEngine offers two environments for each app: production environment and staging environment. When calling cloud functions within LeanEngine instances using SDK, no matter explicitly or implicitly (by triggering hooks), the SDK uses the function defined in the same environment as the instance. For example, if beforeDelete hook is defined and an object is deleted with SDK in the staging environment, the beforeDelete hook in the staging environment will be triggered.
When calling cloud functions outside of LeanEngine instances using SDK, no matter explicitly or implicitly, X-LC-Prod is set to be 1 by default, which means that the production environment will be used. For historical reasons, there are some differences between each SDK:
For Node.js, PHP, and Java SDKs, the production environment will always be used by default.
For Python SDK, when debugging locally with lean-cli, the staging environment will be used if it exists. Otherwise, the production environment will be used.
For Java example projects java-war-getting-started and spring-boot-getting-started, when debugging locally with lean-cli, the staging environment will be used if it exists. Otherwise, the production environment will be used (same as Python SDK).
You can specify the environment being used with SDK:
Apps with only trial instances would only have production environments. Please do not attempt to switch to staging environments.
Cloud Functions
In this example, a simple cloud function named hello is defined in src/main/java/cn/leancloud/demo/todo/Cloud.java. By doing so, clients running on all platforms will be able to call it and get the return value of it. The computing process of the function is done on the cloud side rather than on the client side, so there will be less burden on the clients.
Now let's look into a more complex example.
Imagine that you have an app that lets users review the movies they have watched. An object containing a single review of a movie may look like this:
{
"movie": "Despicable Me",
"stars": 5,
"comment": "These minions are so cute. I wish they are real and I can have them in my home!"
}
stars is the score given by the user, ranging from 1 to 5. If you want to obtain the average score of Despicable Me, one thing you can do is to have the client search for all the reviews of this movie and calculate the average score on the device. However, this requires all the reviews of this movie to be fetched to the client, which leads to unnecessary network traffic. With cloud function, you can simply have the client pass the name of the movie to the cloud and receive the calculated score only.
Cloud functions accept parameters in JSON objects which we can include the name of the movie in. All the methods defined in LeanStorage Java SDK can be used on LeanEngine, so we can write the cloud function averageStars like this:
@EngineFunction("averageStars")
public static float getAverageStars(@EngineFunctionParam("movie") String movie)
throws AVException {
AVQuery<AVObject> query = new AVQuery("Review");
query.whereEqualTo("movie", movie);
List<AVObject> reviews = query.find();
int sum = 0;
if (reviews == null && reviews.isEmpty()) {
return 0;
}
for (AVObject review : reviews) {
sum += review.getInt("star");
}
return sum / reviews.size();
}
Parameters and Return Values
The following parameters can be accessed within a cloud function:
@EngineFunctionParam: The parameters sent from the client.
EngineRequestContext: More information about the client. EngineRequestContext.getSessionToken() is the session token associated with the user logged in at the client side (according to the HTTP header X-LC-Session); EngineRequestContext.getRemoteAddress() is the IP address of the client.
Calling Cloud Functions with SDK
You can call cloud functions with any LeanCloud SDK:
// Construct the dictionary to be passed to the cloud
NSDictionary *dicParameters = [NSDictionary dictionaryWithObject:@"Despicable Me"
forKey:@"movie"];
// Call averageStars with parameters
[AVCloud callFunctionInBackground:@"averageStars"
withParameters:dicParameters
block:^(id object, NSError *error) {
if(error == nil){
// Success; object is the returned value from the cloud function
} else {
// Error handling
}
}];
var paramsJson = {
movie: "Despicable Me"
};
AV.Cloud.run('averageStars', paramsJson).then(function (data) {
// Success; data is the returned value from the cloud function
}, function (err) {
// Error handling
});
from leancloud import cloud
cloud.run('averageStars', movie='Despicable Me')
use \LeanCloud\Engine\Cloud;
$params = array(
"movie" => "Despicable Me"
);
Cloud::run("averageStars", $params);
// Construct the parameters to be passed to the cloud
Map<String, String> dicParameters = new HashMap<String, String>();
dicParameters.put("movie", "Despicable Me");
// Call averageStars with parameters
AVCloud.callFunctionInBackground("averageStars", dicParameters).subscribe(new Observer<AVObject>() {
@Override
public void onSubscribe(Disposable disposable) {
}
@Override
public void onNext(AVObject avObject) {
// succeed.
}
@Override
public void onError(Throwable throwable) {
// failed
}
@Override
public void onComplete() {
}
});
// Java SDK also supports caching results returned by cloud function, similar to AVQuery.
// For example, the following calls will use cached results if available,
// and the cached results will expire in 30 seconds (30000 ms).
AVCloud.callFunctionWithCacheInBackground("averageStars", dicParameters, AVQuery.CachePolicy.CACHE_ELSE_NETWORK, 30000)
.subscribe(new Observer<Object>() {
@Override
public void onSubscribe(Disposable disposable) {}
@Override
public void onNext(Object object) {
// succeed.
}
@Override
public void onError(Throwable throwable) {
// failed.
}
@Override
public void onComplete() {}
});
try {
Map response = await LCCloud.run('averageStars', params: { 'movie': '夏洛特烦恼' });
// deal with results
} on LCException catch (e) {
// deal with exceptions
}
By calling cloud functions with RPC, LeanEngine will automatically serialize the HTTP response body and the SDK will get the response in the format of AVObject:
var paramsJson = {
movie: "Despicable Me"
};
AV.Cloud.rpc('averageStars', paramsJson).then(function (object) {
// Success
}, function (error) {
// Error handling
});
from leancloud import cloud
cloud.rpc('averageStars', movie='Despicable Me')
// Not supported yet
// Construct parameters
Map<String, Object> dicParameters = new HashMap<>();
dicParameters.put("movie", "Despicable Me");
AVCloud.<AVObject>callRPCInBackground("averageStars", dicParameters).subscribe(new Observer<AVObject>() {
@Override
public void onSubscribe(Disposable disposable) {
}
@Override
public void onNext(AVObject avObject) {
// succeed.
}
@Override
public void onError(Throwable throwable) {
// failed
}
@Override
public void onComplete() {
}
});
// cached version (see "Calling Cloud Functions with SDK" section above)
AVCloud.<AVObject>callRPCWithCacheInBackground("averageStars", dicParameters, AVQuery.CachePolicy.CACHE_ELSE_NETWORK, 30000)
.subscribe(new Observer<AVObject>() {
@Override
public void onSubscribe(Disposable disposable) {}
@Override
public void onNext(AVObject avObject) {
// succeed.
}
@Override
public void onError(Throwable throwable) {
// failed
}
@Override
public void onComplete() {}
});
try {
LCObject response = await LCCloud.rpc('averageStars', params: { 'movie': '夏洛特烦恼' });
// deal with results
} on LCException catch (e) {
// deal with exceptions
}
Error Codes
You can customize error codes for cloud functions in accordance with HTTP status codes.
@EngineFunction("errorCode")
public static AVUser getCurrentUser() throws Exception {
String sessionToken = EngineRequestContext.getSessionToken();
AVUser u = null;
if (!StringUtil.isEmpty(sessionToken)) {
u = AVUser.becomeWithSessionToken(sessionToken);
}
if (u == null) {
throw new AVException(211, "Could not find the user.");
} else {
return u;
}
}
The client will receive { "code": 211, "error": "Could not find the user." } from the function above.
@EngineFunction()
public static void customErrorCode() throws Exception {
throw new AVException(123, "Custom error message.");
}
The client will receive { "code": 123, "error": "Custom error message." } from the function above.
Timeouts
The time limit for a cloud function to be processed is 15 seconds. If the cloud function does not make a response after this, HTTP error 503 will be triggered with the error message The request timed out on the server.
Handling Timeouts
We recommend that you have your application handle tasks asynchronously to avoid timeouts.
@EngineFunction("hello")
public static String hello(@EngineFunctionParam("name") String name) {
new Thread(() -> {
doSomeTimeConsumingThings();
}).start();
if (name == null) {
return "Hello World!";
}
return String.format("Hello %s!", name);
}
However, this usually does not make sense for before hooks.
Although asynchronous before hooks will not trigger timeout errors,
they also cannot interrupt the operation.
If you cannot optimize execution time for before hooks,
you have to use after hooks instead.
For example, to filter fake comments, one beforeSave hook needs to call a time-consuming third-party NLP API,
which may cause a timeout.
As a workaround, you can use an afterSave hook to call the third party after the comment is saved.
If that comment turns out to be a fake one, then delete it afterward.
Hooking
A hook can be automatically triggered when certain events happen (like before or after saving or updating an object). Keep in mind that:
Importing data on the dashboard will not trigger any hooks.
Dead loops may be caused if hooks are not appropriately defined.
Hooks cannot be applied to _Installation table.
For hooks starting with before (including onLogin), if an exception occurs inside the function, the data operation will be terminated. Therefore, you can reject certain data operations by having functions throw an error. For hooks starting with after (including onVerified), such exception will not terminate the data operation because the operation is already completed before the function is executed.
graph LR
A((save)) -->D{object}
D-->E(new)
E-->|beforeSave|H{error?}
H-->N(No)
N-->B[create new object on the cloud]
B -->|afterSave|C((done))
H-->Y(Yes)
Y-->Z((interrupted))
D-->F(existing)
F-->|beforeUpdate|I{error?}
I-->Y
I-->V(No)
V-->G[update existing object on the cloud]
G-->|afterUpdate|C
graph LR
A((delete))-->|beforeDelete|H{error?}
H-->Y(Yes)
Y-->Z((interrupted))
H-->N(No)
N-->B[delete object on the cloud]
B -->|afterDelete|C((done))
To ensure that hooks are triggered internally by LeanStorage services, our SDK will verify the source of each request. If the verification fails, the error message Hook key check failed will be returned. If you see such error message when debugging locally, make sure you are using our command-line interface for debugging.
beforeSave
You can perform an operation before an object is saved, like data verification and pre-processing. For example, a comment on a movie may be too long to be displayed on the client and needs to be cut off to 140 characters:
@EngineHook(className = "Review", type = EngineHookType.beforeSave)
public static AVObject reviewBeforeSaveHook(AVObject review) throws Exception {
if (AVUtils.isBlankString(review.getString("comment"))) {
throw new Exception("No comment provided!");
} else if (review.getString("comment").length() > 140) {
review.put("comment", review.getString("comment").substring(0, 140) + "…");
}
return review;
}
afterSave
You can perform an operation after an object is saved. For example, to update the total number of comments after a comment is created:
@EngineHook(className = "Review", type = EngineHookType.afterSave)
public static void reviewAfterSaveHook(AVObject review) throws Exception {
AVObject post = review.getAVObject("post");
post.fetch();
post.increment("comments");
post.save();
}
Or, to add a new field from for each new user:
@EngineHook(className = "_User", type = EngineHookType.afterSave)
public static void userAfterSaveHook(AVUser user) throws Exception {
user.put("from", "LeanCloud");
user.save();
}
beforeUpdate
You can perform an operation before an object is updated. You will be able to know which fields are updated, or reject the data operation:
@EngineHook(className = "Review", type = EngineHookType.beforeUpdate)
public static AVObject reviewBeforeUpdateHook(AVObject review) throws Exception {
List<String> updateKeys = EngineRequestContext.getUpdateKeys();
for (String key : updateKeys) {
// If comment is changed, check its length
if ("comment".equals(key) && review.getString("comment").length()>140) {
throw new Exception("Your comment cannot be longer than 140 characters.");
}
}
return review;
}
Do not attempt to modify review since changes made to it will not be saved to LeanStorage. If you want to reject the change, you can have the function throw an error.
The object passed into the function is a temporary object and may be different from the one saved to LeanStorage in the end (which may have atomic operations applied to).
afterUpdate
Dead loops may be triggered if this hook is not defined properly. This may lead to extra API calls or even extra bills. See Preventing Dead Loops for more details.
You can perform an operation after an object is updated. You will be able to know which fields are updated (same as beforeUpdate).
@EngineHook(className = "Review", type = EngineHookType.afterUpdate)
public static void reviewAfterUpdateHook(AVObject review) throws Exception {
List<String> updateKeys = EngineRequestContext.getUpdateKeys();
for (String key : updateKeys) {
if ("comment".equals(key) && review.getString("comment").length()<5) {
LogUtil.avlog.d(review.ObjectId + " seems like a spam: " + comment);
}
}
}
Preventing Dead Loops
You might be wondering why we can modify and save the post object in the afterUpdate hook without triggering this hook again. This is because LeanEngine automatically identifies and pre-processes all the post objects passed in by a hook to prevent the hook to be triggered again.
However, if the following situations happen, you still need to handle them by yourself:
You called fetch on the post objects passed in.
You constructed the post objects passed in by yourself with methods like AVObject.createWithoutData(String, String).
To prevent such objects from triggering certain hooks, you can call post.disableBeforeHook() or post.disableAfterHook() on them:
@EngineHook(className="Post", type = EngineHookType.afterUpdate)
public static void afterUpdatePost(AVObject post) throws AVException {
// Directly modifying and saving the object will not trigger afterUpdate
post.put("foo", "bar");
post.save();
// Use disableAfterHook if you called fetch on the object
post.fetch();
post.disableAfterHook();
post.put("foo", "bar");
// Use disableAfterHook if you constructed the object by yourself
post = AVObject.createWithoutData("Post", post.getObjectId());
post.disableAfterHook();
post.save();
}
beforeDelete
You can perform an operation before an object is deleted. For example, before an album is deleted, check if there are any photos in it:
@EngineHook(className = "Album", type = EngineHookType.beforeDelete)
public static AVObject albumBeforeDeleteHook(AVObject album) throws Exception {
AVQuery query = new AVQuery("Photo");
query.whereEqualTo("album", album);
int count = query.count();
if (count > 0) {
// The delete operation will be aborted
throw new Exception("Cannot delete an album if it still has photos in it.");
} else {
return album;
}
}
afterDelete
You can perform an operation after an object is deleted. For example, when an album is being deleted, instead of checking if there are any photos left, we directly delete all the photos in it:
@EngineHook(className = "Album", type = EngineHookType.afterDelete)
public static void albumAfterDeleteHook(AVObject album) throws Exception {
AVQuery query = new AVQuery("Photo");
query.whereEqualTo("album", album);
List<AVObject> result = query.find();
if (result != null && !result.isEmpty()) {
AVObject.deleteAll(result);
}
}
onVerified
You can perform an operation after a user's email or phone number is verified:
@EngineHook(className = "_User", type = EngineHookType.onVerifiedSMS)
public static void userOnVerifiedHook(AVUser user) throws Exception {
LOGGER.d("User " + user.getObjectId() + " is verified by SMS.");
}
@EngineHook(className = "_User", type = EngineHookType.onVerifiedEmail)
public static void userOnVerifiedHook(AVUser user) throws Exception {
LOGGER.d("User " + user.getObjectId() + " is verified by Email.");
}
Keep in mind that you don't have to update fields like emailVerified with this hook since the system automatically updates them.
onLogin
You can perform an operation before a user tries to log in. For example, to prevent blocked users from logging in:
@EngineHook(className = "_User", type = EngineHookType.onLogin)
public static AVUser userOnLoginHook(AVUser user) throws Exception {
if ("noLogin".equals(user.getUsername())) {
throw new Exception("Forbidden");
} else {
return user;
}
}
You can also verify access_token of a third party account using this hook.
Suppose there is a platform called Example with the following authData:
Be aware that the code above is for demonstration only.
We omitted a lot of logic for brevity,
such as verifying token's expiration date, handling user login in other ways, and recovering from network errors.
You can perform an operation after a message is delivered to the cloud but before it is delivered to the receiver. For example, to filter out certain keywords from the message:
You can perform an operation if a message is delivered to the receiver but the receiver is offline. For example, to slice the first 32 characters of the message as the title for the push notification:
@IMHook(type = IMHookType.receiversOffline)
public static Map<String, Object> onReceiversOffline(Map<String, Object> params) {
// content is the content of the message
String alert = (String)params.get("content");
if(alert.length() > 32){
alert = alert.substring(0, 32);
}
System.out.println(alert);
Map<String, Object> result = new HashMap<String, Object>();
JSONObject object = new JSONObject();
// Self-increase the number of unread messages
// Can be set to a fixed number
object.put("badge", "Increment");
object.put("sound", "default");
// Using development certificate
object.put("_profile", "dev");
object.put("alert", alert);
result.put("pushMessage", object.toString());
return result;
}
messageSent
You can perform an operation after a message is delivered to the receiver. For example, to print out a log in LeanEngine:
@IMHook(type = IMHookType.messageSent)
public static Map<String, Object> onMessageSent(Map<String, Object> params) {
System.out.println(params);
Map<String, Object> result = new HashMap<String, Object>();
// …
return result;
}
conversationStart
You can perform an operation after the signature verification (if enabled) of creating a conversation is completed but before the conversation is created. For example, to print out a log in LeanEngine:
@IMHook(type = IMHookType.conversationStart)
public static Map<String, Object> onConversationStart(Map<String, Object> params) {
System.out.println(params);
Map<String, Object> result = new HashMap<String, Object>();
// Refuse to create the conversation if it is initiated by Tom
if ("Tom".equals(params.get("initBy"))) {
result.put("reject", true);
// Custom error code
result.put("code", 9890);
}
return result;
}
conversationStarted
You can perform an operation after a conversation is created. For example, to print out a log in LeanEngine:
@IMHook(type = IMHookType.conversationStarted)
public static Map<String, Object> onConversationStarted(Map<String, Object> params) throws Exception {
System.out.println(params);
Map<String, Object> result = new HashMap<String, Object>();
String convId = (String)params.get("convId");
System.out.println(convId);
return result;
}
conversationAdd
You can perform an operation after the signature verification (if enabled) of adding a member into a conversation is completed but before the member is added. This will be triggered both when the member proactively joins the conversation or is added by another member. Keep in mind that this hook will not be triggered when creating a conversation with clientIds. For example, to print out a log in LeanEngine:
@IMHook(type = IMHookType.conversationAdd)
public static Map<String, Object> onConversationAdd(Map<String, Object> params) {
System.out.println(params);
String[] members = (String[])params.get("members");
Map<String, Object> result = new HashMap<String, Object>();
System.out.println("members");
System.out.println(members);
// Refuse to add the member if it is initiated by Tom
if ("Tom".equals(params.get("initBy"))) {
result.put("reject", true);
// Custom error code
result.put("code", 9890);
}
return result;
}
conversationRemove
You can perform an operation after the signature verification (if enabled) of removing a member from a conversation is completed but before the member is removed. This will not be triggered when the member proactively quits the conversation. For example, to print out a log in LeanEngine:
@IMHook(type = IMHookType.conversationRemove)
public static Map<String, Object> onConversationRemove(Map<String, Object> params) {
System.out.println(params);
String[] members = (String[])params.get("members");
Map<String, Object> result = new HashMap<String, Object>();
System.out.println("members");
// Refuse to remove the member if it is initiated by Tom
if ("Tom".equals(params.get("initBy"))) {
result.put("reject", true);
// Custom error code
result.put("code", 9892);
}
return result;
}
conversationUpdate
You can perform an operation before the name, custom attributes, or mention properties of a conversation (including notifications) are updated. For example, to print out a log in LeanEngine:
@IMHook(type = IMHookType.conversationUpdate)
public static Map<String, Object> onConversationUpdate(Map<String, Object> params) {
System.out.println(params);
Map<String, Object> result = new HashMap<String, Object>();
Map<String,Object> attr = (Map<String,Object>)params.get("attr");
System.out.println(attr);
// Map<String,Object> attrMap = (Map<String,Object>)JSON.parse(attr);
String name = (String)attr.get("name");
// System.out.println(attrMap);
System.out.println(name);
// Refuse to change the property if it is initiated by Tom
if ("Tom".equals(params.get("initBy"))) {
result.put("reject", true);
// Custom error code
result.put("code", 9893);
}
return result;
}
Error Codes for Hooks
You can define error codes for hooks like beforeSave:
@EngineHook(className = "Review", type = EngineHookType.beforeSave)
public static AVObject reviewBeforeSaveHook(AVObject review) throws Exception {
throw new AVException(123, "An error occurred.");
}
The client will receive Cloud Code validation failed. Error detail: { "code": 123, "message": "An error occurred." } as the response. You can retrieve the error message by slicing the string.
Timeouts for Hooks
The time limit for a hook to be processed is 3 seconds. If a hook is triggered by another cloud function (like a beforeSave or afterSave triggered by saving an object), the time limit of this hook will be further limited by the time remaining.
For example, if a beforeSave is triggered by a cloud function that has already taken 13 seconds, there will be only 2 seconds left for this hook. See Timeouts.
Scheduled Tasks
You can set up timers to schedule your cloud functions. For example, to clean up temporary data every night, to send push notifications to users every Monday, etc. The timer can be accurate to a second.
The time restrictions applied to ordinary cloud functions also apply to scheduled functions. See Timeouts.
If a timer triggers more than 30 400 (Bad Request) or 502 (Bad Gateway) errors within the past 24 hours, the system will disable it and send a email to you regarding the issue. The error log timerAction short-circuited and no fallback available will also be printed out in the web console.
After deploying your program to LeanEngine, go to your app's Dashboard > LeanEngine > Scheduled tasks and click on Create a timer to create a timer for a cloud function. For example, if we have a function named logTimer:
@EngineFunction("logTimer")
public static float logTimer throws Exception {
LogUtil.avlog.d("This log is printed by logTimer.");
}
You can specify the times a task gets triggered using one of the following expressions:
CRON expression
Interval in seconds
Take CRON expression as an example.
To print logs at 8am every Monday, create a timer for the function logTimer using CRON expression and enter 0 0 8 ? * MON for it.
When creating a timer, two optional options are available:
Execute Parameters: the parameters passed to the cloud function in a JSON object
Retry Policy: retry or cancel the task when it fails due to a cloud function timeout
After the timer is created, the dashboard will display Last executed and Next execution time.
Last executed is the time and result of the last execution.
Clicking on the details button will reveal more information about the task:
status: the status of the task; could be success or failed
uniqueId: a unique ID for the task
finishedAt: the exact time when the task finished (for successful tasks only)
statusCode: the HTTP status returned by the cloud function (for successful tasks only)
result: the response body returned by the cloud function (for successful tasks only)
error: error message (for failed tasks only)
retryAt: the time when the cloud will try to rerun the task (for failed tasks only)
If you want to suspend a timer temporarily, say, for debug purpose, you can click on the Disable button in the Status column.
Correspondingly, clicking on the Enable button will reactivate a suspended task.
If you want to modify or delete a task, you can click on the Edit or Delete button in the Operation column.
Special characters can be used in the following ways:
Character
Meaning
Usage
*
All values
All the values a field can have. For example, to run a task on every minute, set <minutes> to be *.
?
Unspecified value
Can be used on at most one of the two fields that accept this value. For example, to run a task on the 10th of every month regardless of what day it is, set <day-of-month> to be 10 and <day-of-week> to be ?.
-
Scope
For example, setting <hours> to be 10-12 means 10am, 11am, and 12pm.
,
Splitting multiple values
For example, setting <day-of-week> to be MON,WED,FRI means Monday, Wednesday, and Friday.
/
Interval
For example, setting <seconds> to be */15 means every 15 seconds starting from the 0th, which are the 0th, 15th, 30th, and 45th seconds.
Fields are concatenated with spaces. Values like JAN-DEC and SUN-SAT are case-insensitive (MON is the same as mon).
To illustrate:
Expression
Explanation
0 */5 * * * ?
Run a task every 5 minutes.
10 */5 * * * ?
Run a task every 5 minutes and the time to run it is always the 10th second of a minute (like 10:00:10, 10:05:10, etc.).
0 30 10-13 ? * WED,FRI
Run a task at the 10:30am, 11:30am, 12:30am, and 1:30pm of every Wednesday and Friday.
0 */30 8-9 5,20 * ?
Run a task every 30 minutes between 8am and 10am (8:00am, 8:30am, 9:00am, and 9:30am) on the 5th and 20th of every month.
The time zone followed by CRON expressions is UTC+0.
Using Master Key
Since cloud functions are running on the server side, we can assume that requests made by these functions are trustable. Therefore, you can enable global Master Key for all these requests, which will have your program ignore permission settings of classes and those done by ACL so it can access data in the cloud without any restrictions. The code below enables Master Key for your program:
// Often in src/…/AppInitListener.java
JavaRequestSignImplementation.instance().setUseMasterKey(true);
Java Cloud Function Guide
Cloud function is a sub-module of LeanEngine that allows you to run functions on the cloud in response to the requests made by clients. Before you continue, make sure you have read LeanEngine Overview.
When developing your application, you may need to write logic that:
With cloud function, you can deploy these types of logic written in any language (JavaScript, Python, PHP, or Java) on the cloud and have LeanEngine run them for you.
If you have no idea how to deploy your project to LeanEngine, take a look at LeanEngine Quick Start.
Other Languages
This guide uses Java as an example, but LeanEngine supports many other languages as well. You can choose the one you are familiar with for development:
Switching Environments
LeanEngine offers two environments for each app: production environment and staging environment. When calling cloud functions within LeanEngine instances using SDK, no matter explicitly or implicitly (by triggering hooks), the SDK uses the function defined in the same environment as the instance. For example, if
beforeDelete
hook is defined and an object is deleted with SDK in the staging environment, thebeforeDelete
hook in the staging environment will be triggered.When calling cloud functions outside of LeanEngine instances using SDK, no matter explicitly or implicitly,
X-LC-Prod
is set to be1
by default, which means that the production environment will be used. For historical reasons, there are some differences between each SDK:You can specify the environment being used with SDK:
Apps with only trial instances would only have production environments. Please do not attempt to switch to staging environments.
Cloud Functions
In this example, a simple cloud function named
hello
is defined insrc/main/java/cn/leancloud/demo/todo/Cloud.java
. By doing so, clients running on all platforms will be able to call it and get the return value of it. The computing process of the function is done on the cloud side rather than on the client side, so there will be less burden on the clients.Now let's look into a more complex example.
Imagine that you have an app that lets users review the movies they have watched. An object containing a single review of a movie may look like this:
stars
is the score given by the user, ranging from 1 to 5. If you want to obtain the average score of Despicable Me, one thing you can do is to have the client search for all the reviews of this movie and calculate the average score on the device. However, this requires all the reviews of this movie to be fetched to the client, which leads to unnecessary network traffic. With cloud function, you can simply have the client pass the name of the movie to the cloud and receive the calculated score only.Cloud functions accept parameters in JSON objects which we can include the name of the movie in. All the methods defined in LeanStorage Java SDK can be used on LeanEngine, so we can write the cloud function
averageStars
like this:Parameters and Return Values
The following parameters can be accessed within a cloud function:
@EngineFunctionParam
: The parameters sent from the client.EngineRequestContext
: More information about the client.EngineRequestContext.getSessionToken()
is the session token associated with the user logged in at the client side (according to the HTTP headerX-LC-Session
);EngineRequestContext.getRemoteAddress()
is the IP address of the client.Calling Cloud Functions with SDK
You can call cloud functions with any LeanCloud SDK:
Calling Cloud Functions with REST API
See LeanEngine REST API Guide.
Calling Cloud Functions on LeanEngine
You can call the cloud functions defined by
@EngineFunction
withAVCloud.callFunctionInBackground
:Calling Cloud Functions with RPC
By calling cloud functions with RPC, LeanEngine will automatically serialize the HTTP response body and the SDK will get the response in the format of
AVObject
:Error Codes
You can customize error codes for cloud functions in accordance with HTTP status codes.
The client will receive
{ "code": 211, "error": "Could not find the user." }
from the function above.The client will receive
{ "code": 123, "error": "Custom error message." }
from the function above.Timeouts
The time limit for a cloud function to be processed is 15 seconds. If the cloud function does not make a response after this, HTTP error
503
will be triggered with the error messageThe request timed out on the server
.Handling Timeouts
We recommend that you have your application handle tasks asynchronously to avoid timeouts.
However, this usually does not make sense for before hooks. Although asynchronous before hooks will not trigger timeout errors, they also cannot interrupt the operation. If you cannot optimize execution time for before hooks, you have to use after hooks instead. For example, to filter fake comments, one
beforeSave
hook needs to call a time-consuming third-party NLP API, which may cause a timeout. As a workaround, you can use anafterSave
hook to call the third party after the comment is saved. If that comment turns out to be a fake one, then delete it afterward.Hooking
A hook can be automatically triggered when certain events happen (like before or after saving or updating an object). Keep in mind that:
_Installation
table.For hooks starting with
before
(includingonLogin
), if an exception occurs inside the function, the data operation will be terminated. Therefore, you can reject certain data operations by having functions throw an error. For hooks starting withafter
(includingonVerified
), such exception will not terminate the data operation because the operation is already completed before the function is executed.To ensure that hooks are triggered internally by LeanStorage services, our SDK will verify the source of each request. If the verification fails, the error message
Hook key check failed
will be returned. If you see such error message when debugging locally, make sure you are using our command-line interface for debugging.beforeSave
You can perform an operation before an object is saved, like data verification and pre-processing. For example, a comment on a movie may be too long to be displayed on the client and needs to be cut off to 140 characters:
afterSave
You can perform an operation after an object is saved. For example, to update the total number of comments after a comment is created:
Or, to add a new field
from
for each new user:beforeUpdate
You can perform an operation before an object is updated. You will be able to know which fields are updated, or reject the data operation:
Do not attempt to modify
review
since changes made to it will not be saved to LeanStorage. If you want to reject the change, you can have the function throw an error.The object passed into the function is a temporary object and may be different from the one saved to LeanStorage in the end (which may have atomic operations applied to).
afterUpdate
Dead loops may be triggered if this hook is not defined properly. This may lead to extra API calls or even extra bills. See Preventing Dead Loops for more details.
You can perform an operation after an object is updated. You will be able to know which fields are updated (same as
beforeUpdate
).Preventing Dead Loops
You might be wondering why we can modify and save the
post
object in theafterUpdate
hook without triggering this hook again. This is because LeanEngine automatically identifies and pre-processes all thepost
objects passed in by a hook to prevent the hook to be triggered again.However, if the following situations happen, you still need to handle them by yourself:
fetch
on thepost
objects passed in.post
objects passed in by yourself with methods likeAVObject.createWithoutData(String, String)
.To prevent such objects from triggering certain hooks, you can call
post.disableBeforeHook()
orpost.disableAfterHook()
on them:beforeDelete
You can perform an operation before an object is deleted. For example, before an album is deleted, check if there are any photos in it:
afterDelete
You can perform an operation after an object is deleted. For example, when an album is being deleted, instead of checking if there are any photos left, we directly delete all the photos in it:
onVerified
You can perform an operation after a user's email or phone number is verified:
Keep in mind that you don't have to update fields like
emailVerified
with this hook since the system automatically updates them.onLogin
You can perform an operation before a user tries to log in. For example, to prevent blocked users from logging in:
You can also verify
access_token
of a third party account using this hook.Suppose there is a platform called Example with the following
authData
:and the platform name is
example_platform
. We can verify itsaccess_token
using the method below:Be aware that the code above is for demonstration only. We omitted a lot of logic for brevity, such as verifying token's expiration date, handling user login in other ways, and recovering from network errors.
Hooks for LeanMessage
See LeanMessage Overview > LeanEngine Hooks for more instructions on the methods introduced below.
messageReceived
You can perform an operation after a message is delivered to the cloud but before it is delivered to the receiver. For example, to filter out certain keywords from the message:
receiversOffline
You can perform an operation if a message is delivered to the receiver but the receiver is offline. For example, to slice the first 32 characters of the message as the title for the push notification:
messageSent
You can perform an operation after a message is delivered to the receiver. For example, to print out a log in LeanEngine:
conversationStart
You can perform an operation after the signature verification (if enabled) of creating a conversation is completed but before the conversation is created. For example, to print out a log in LeanEngine:
conversationStarted
You can perform an operation after a conversation is created. For example, to print out a log in LeanEngine:
conversationAdd
You can perform an operation after the signature verification (if enabled) of adding a member into a conversation is completed but before the member is added. This will be triggered both when the member proactively joins the conversation or is added by another member. Keep in mind that this hook will not be triggered when creating a conversation with
clientId
s. For example, to print out a log in LeanEngine:conversationRemove
You can perform an operation after the signature verification (if enabled) of removing a member from a conversation is completed but before the member is removed. This will not be triggered when the member proactively quits the conversation. For example, to print out a log in LeanEngine:
conversationUpdate
You can perform an operation before the name, custom attributes, or mention properties of a conversation (including notifications) are updated. For example, to print out a log in LeanEngine:
Error Codes for Hooks
You can define error codes for hooks like
beforeSave
:The client will receive
Cloud Code validation failed. Error detail: { "code": 123, "message": "An error occurred." }
as the response. You can retrieve the error message by slicing the string.Timeouts for Hooks
The time limit for a hook to be processed is 3 seconds. If a hook is triggered by another cloud function (like a
beforeSave
orafterSave
triggered by saving an object), the time limit of this hook will be further limited by the time remaining.For example, if a
beforeSave
is triggered by a cloud function that has already taken 13 seconds, there will be only 2 seconds left for this hook. See Timeouts.Scheduled Tasks
You can set up timers to schedule your cloud functions. For example, to clean up temporary data every night, to send push notifications to users every Monday, etc. The timer can be accurate to a second.
The time restrictions applied to ordinary cloud functions also apply to scheduled functions. See Timeouts.
If a timer triggers more than 30
400
(Bad Request) or502
(Bad Gateway) errors within the past 24 hours, the system will disable it and send a email to you regarding the issue. The error logtimerAction short-circuited and no fallback available
will also be printed out in the web console.After deploying your program to LeanEngine, go to your app's Dashboard > LeanEngine > Scheduled tasks and click on Create a timer to create a timer for a cloud function. For example, if we have a function named
logTimer
:You can specify the times a task gets triggered using one of the following expressions:
Take CRON expression as an example. To print logs at 8am every Monday, create a timer for the function
logTimer
using CRON expression and enter0 0 8 ? * MON
for it.When creating a timer, two optional options are available:
After the timer is created, the dashboard will display Last executed and Next execution time. Last executed is the time and result of the last execution. Clicking on the details button will reveal more information about the task:
status
: the status of the task; could besuccess
orfailed
uniqueId
: a unique ID for the taskfinishedAt
: the exact time when the task finished (for successful tasks only)statusCode
: the HTTP status returned by the cloud function (for successful tasks only)result
: the response body returned by the cloud function (for successful tasks only)error
: error message (for failed tasks only)retryAt
: the time when the cloud will try to rerun the task (for failed tasks only)You can see the logs of the timer in Dashboard > LeanEngine > App logs. For example:
If you want to suspend a timer temporarily, say, for debug purpose, you can click on the Disable button in the Status column. Correspondingly, clicking on the Enable button will reactivate a suspended task. If you want to modify or delete a task, you can click on the Edit or Delete button in the Operation column.
CRON Expressions
The basic syntax of a CRON expression is:
, - * /
, - * /
, - * /
, - * ? /
, - * /
, - ? /
Special characters can be used in the following ways:
*
<minutes>
to be*
.?
<day-of-month>
to be10
and<day-of-week>
to be?
.-
<hours>
to be10-12
means 10am, 11am, and 12pm.,
<day-of-week>
to beMON,WED,FRI
means Monday, Wednesday, and Friday./
<seconds>
to be*/15
means every 15 seconds starting from the 0th, which are the 0th, 15th, 30th, and 45th seconds.Fields are concatenated with spaces. Values like
JAN-DEC
andSUN-SAT
are case-insensitive (MON
is the same asmon
).To illustrate:
0 */5 * * * ?
10 */5 * * * ?
0 30 10-13 ? * WED,FRI
0 */30 8-9 5,20 * ?
The time zone followed by CRON expressions is
UTC+0
.Using Master Key
Since cloud functions are running on the server side, we can assume that requests made by these functions are trustable. Therefore, you can enable global Master Key for all these requests, which will have your program ignore permission settings of classes and those done by ACL so it can access data in the cloud without any restrictions. The code below enables Master Key for your program:
See ACL Guide and Using ACL in LeanEngine for more information regarding permission settings with LeanEngine.