Cloudless Scheduler
Introduction
Like all serverless functions, Fluence Cloudless Functions are event-based. That is, something needs to happen in order to trigger the function. An event trigger can originate in a multitude of contexts, such as a browser click or data change in a spreadsheet. The Fluence Cloudless Scheduler implements an API for . In addition to event-based triggers, the Cloudless Scheduler can also accommodate time-based triggers, similar to cronjobs, such as time elapsed, specific dates or interval.
The Cloudless Scheduler is implemented by means of spells: stateful, event-driven Aqua scripts with dedicated kv storage per spell and a trigger configuration defining events. The remainder of this chapter introduces the spells so you can Implement Cloudless Scheduler functionality, if needed by your Cloudless App.
Create your first Spell: step-by-step
First let's take a look at how to create a simple spell from the Fluence CLI template.
-
Initialize a project with
fluence init -t minimal
The spell library dependency should be added to the
fluence.yaml
configuration file automatically:yamlversion: 8aquaDependencies:"@fluencelabs/aqua-lib": 0.9.1"@fluencelabs/spell": 0.6.9yamlversion: 8aquaDependencies:"@fluencelabs/aqua-lib": 0.9.1"@fluencelabs/spell": 0.6.9 -
Initialize a new spell template with
fluence spell new myFirstSpell
.This command modifies the
fluence.yaml
file, adding the spell to the list of the project’s spells and the default deal we deploy later.yaml# FCLI version: 0.15.5version: 8deployments:myDeployment:targetWorkers: 1pricePerWorkerEpoch: 0.00001initialBalance: 0.001services: []spells: [ myFirstSpell ]spells:myFirstSpell: # <-- added the new spell to the list of project's spellsget: src/spells/myFirstSpell # <-- the path to the spell directoryyaml# FCLI version: 0.15.5version: 8deployments:myDeployment:targetWorkers: 1pricePerWorkerEpoch: 0.00001initialBalance: 0.001services: []spells: [ myFirstSpell ]spells:myFirstSpell: # <-- added the new spell to the list of project's spellsget: src/spells/myFirstSpell # <-- the path to the spell directoryThis command will create a directory for the spell and its configuration in the
src/spells
directory:bash$ tree src/spellssrc/spells└── myFirstSpell├── spell.aqua└── spell.yamlbash$ tree src/spellssrc/spells└── myFirstSpell├── spell.aqua└── spell.yamlThe path to the spell directory is also set in the
fluence.yaml
file and can be manually modified if needed. -
The spell script is located in the
spell.aqua
file:aqua-- FCLI 0.15.5aqua MyFirstSpell-- Note: spell main function must be exportedexport spellimport Spell from "@fluencelabs/spell/spell_service.aqua" -- (1)func spell(): -- (2)Spell "myFirstSpell" -- (3)Spell.store_log("Spell 'myFirstSpell' is working!") -- (4)aqua-- FCLI 0.15.5aqua MyFirstSpell-- Note: spell main function must be exportedexport spellimport Spell from "@fluencelabs/spell/spell_service.aqua" -- (1)func spell(): -- (2)Spell "myFirstSpell" -- (3)Spell.store_log("Spell 'myFirstSpell' is working!") -- (4)Let’s review the most important parts of the file:
-
Importing the (Spell Service API)[#how-to-resolve-your-spell-service] is required to use logging capabilities, private spell storage, etc. Note that you can implement spells without ever interacting with the Spell Service if you don’t need the features it provides.
-
The
spell
function here is an entry point to the spell’s execution. The main function has several features we explore in the later sections.You can modify the name of the main function to any valid name, but you also need to modify the name in the
spell.yaml
configuration file. -
To write logs in the Spell Log Storage, you need to resolve the Spell Service that belongs to the executing spell using the Spell’s Name.
Note that you have several options to resolve the correct spell service, which we’ll explore later.
-
Here, we call the function provided by the Spell Service to store the message in the logs. This message will be added to the logs each spell runs.
-
-
The spell configuration is located in the
spell.yaml
file and defines several important aspects of the spell execution. Let’s look at the content of the default spell configuration file:yamlversion: 0aquaFilePath: ./spell.aqua # <-- (1)function: spell # <-- (2)clock: # <-- (3)periodSec: 60endDelaySec: 1800yamlversion: 0aquaFilePath: ./spell.aqua # <-- (1)function: spell # <-- (2)clock: # <-- (3)periodSec: 60endDelaySec: 1800aquaFilePath
must be set to the aqua file with the main spell function; the path is relative to thespell.yaml
path, so./spell.aqua
works since it’s in the same directory.function
must be set to the main spell function, by default,spell
we have seen when we look into thespell.aqua
file. You choose any valid name you want.clock
defines the configuration of when and how your spell is run.-
periodSec
defines how often your spell is run.periodSec: 60
means that the spell is run every minute. -
endDelaySec
defines when the spell must be stopped from execution.endDelaySec: 1800
means that the spell will no longer be periodically executed in 30 minutes.::⚠️ This delay is evaluated on the client, so the 30 minutes are counted since you created your deal, not since the spell was installed on a peer. :::
-
-
To deploy the deal, call
fluence deal deploy
. -
You can check the logs of your spell with
fluence deal logs myDeployment myFirstSpell
:Example output:
bash$ fluence deal logs myDeployment myFirstSpellConnecting to random local relay: /ip4/127.0.0.1/tcp/9993/ws/p2p/12D3KooWDd7zwsdYart7z9MQ2yEagjafPHLxbzNkNP7wYxJxvKiNConnectedmyDeployment (host_id: 12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAe, worker_id: 12D3KooWRRyM8XNhCFTQaqMQpg483Bq8pdfveeMKNxx4hArrNq7u, spell_id: 275382d7-cb42-402c-ad80-85a546ecfe51):2024-02-05 15:36:22 Spell 'myFirstSpell' is working!bash$ fluence deal logs myDeployment myFirstSpellConnecting to random local relay: /ip4/127.0.0.1/tcp/9993/ws/p2p/12D3KooWDd7zwsdYart7z9MQ2yEagjafPHLxbzNkNP7wYxJxvKiNConnectedmyDeployment (host_id: 12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAe, worker_id: 12D3KooWRRyM8XNhCFTQaqMQpg483Bq8pdfveeMKNxx4hArrNq7u, spell_id: 275382d7-cb42-402c-ad80-85a546ecfe51):2024-02-05 15:36:22 Spell 'myFirstSpell' is working!You can also check how the process of installing your spell is happening by checking the
worker-spell
logs withfluence deal logs
Example output:
bash$ fluence deal logsConnecting to random local relay: /ip4/127.0.0.1/tcp/9993/ws/p2p/12D3KooWDd7zwsdYart7z9MQ2yEagjafPHLxbzNkNP7wYxJxvKiNConnectedmyDeployment (host_id: 12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAe, worker_id: 12D3KooWRRyM8XNhCFTQaqMQpg483Bq8pdfveeMKNxx4hArrNq7u, spell_id: 4f35fd35-6be3-4a0b-8426-c0f9031d1cfa):2024-02-05 15:36:16 Installing worker for deal 0xeb92a1b5c10ad7bfdcaf23cb7dda9ea062cd07e82024-02-05 15:36:16 parsed worker definition bafkreifgq24izy6wu22dt6wbrentfcjkmyunof32ospc3qzlkl4adg6tcm {"services": [],"spells": [{"config": "bafkreibk6jbl6qwrp2hktbjkzfctfsyeblffrt3pcm5kte367pes5inhpm","init_args": "bafkreicecnx2gvntm6fbcrvnc336qze6st5u7qq7457igegamd3bzkx7ri","name": "myFirstSpell","script": "bafkreiddhrqqyxhfuyg3egfh34lkhktjplzxjmbmkm22kfjpviory7pjbi"}]}2024-02-05 15:36:18 Deploying spell myFirstSpell2024-02-05 15:36:22 Installed spell myFirstSpell 275382d7-cb42-402c-ad80-85a546ecfe512024-02-05 15:36:23 Installation finished2024-02-05 15:36:23 Worker installation finished with status {"message": "","state": "INSTALLATION_SUCCESSFUL","timestamp": 1707147383}bash$ fluence deal logsConnecting to random local relay: /ip4/127.0.0.1/tcp/9993/ws/p2p/12D3KooWDd7zwsdYart7z9MQ2yEagjafPHLxbzNkNP7wYxJxvKiNConnectedmyDeployment (host_id: 12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAe, worker_id: 12D3KooWRRyM8XNhCFTQaqMQpg483Bq8pdfveeMKNxx4hArrNq7u, spell_id: 4f35fd35-6be3-4a0b-8426-c0f9031d1cfa):2024-02-05 15:36:16 Installing worker for deal 0xeb92a1b5c10ad7bfdcaf23cb7dda9ea062cd07e82024-02-05 15:36:16 parsed worker definition bafkreifgq24izy6wu22dt6wbrentfcjkmyunof32ospc3qzlkl4adg6tcm {"services": [],"spells": [{"config": "bafkreibk6jbl6qwrp2hktbjkzfctfsyeblffrt3pcm5kte367pes5inhpm","init_args": "bafkreicecnx2gvntm6fbcrvnc336qze6st5u7qq7457igegamd3bzkx7ri","name": "myFirstSpell","script": "bafkreiddhrqqyxhfuyg3egfh34lkhktjplzxjmbmkm22kfjpviory7pjbi"}]}2024-02-05 15:36:18 Deploying spell myFirstSpell2024-02-05 15:36:22 Installed spell myFirstSpell 275382d7-cb42-402c-ad80-85a546ecfe512024-02-05 15:36:23 Installation finished2024-02-05 15:36:23 Worker installation finished with status {"message": "","state": "INSTALLATION_SUCCESSFUL","timestamp": 1707147383}
Spell Configuration in Fluence CLI
Spell Triggers Configuration
The spell.yaml
file allows you to set up the Spell Trigger, aka how and on what conditions your spells will be run.
Timer-based Spell Trigger provides an opportunity to trigger spells at a certain time with a certain regularity or end execution at a certain date.
To setup a timer-based spell trigger for your spells, you can use the following fields in your spell.yaml
file:
startDelaySec
andstartTimestamp
define when to run the spell (you can use only one of them),endDelaySec
andendTimestamp
define when to stop running the spell (you can use only one of them),periodSec
defines how often to run the spell.
For example, if you want to setup a spell that runs only once, you can use the following setup:
yaml
clock:periodSec: 0
yaml
clock:periodSec: 0
On the other hand, if you want your spell to run every two minutes indefinitely, you can do:
yaml
clock:periodSec: 120
yaml
clock:periodSec: 120
Next, if we want the spell to start on New Year’s Eve and end in a year, we can add the date:
yaml
clock:periodSec: 120startTimestamp: 2024-12-31endTimestamp: 2025-12-31
yaml
clock:periodSec: 120startTimestamp: 2024-12-31endTimestamp: 2025-12-31
You can also use startDelaySec
and endDelaySec
fields to setup the times more flexibly than just setting the timestamps:
yaml
clock:periodSec: 120startDelaySec: 86400endDelaySec: 1209600
yaml
clock:periodSec: 120startDelaySec: 86400endDelaySec: 1209600
In this example, the spell will start executing in approximately a day after the spell is deployed to the Network and will end in two weeks.
Warning: under the hood, the timestamps are evaluated from startDelaySec
and endDelaySec
on your local computer when you call fluence deal deploy
, so when you see startDelaySec: 60
, it means that the spell start time is the time when you initiated the deployment plus 1 minute. So, if you want to use startDelaySec
, you must choose bigger numbers for the delay since some time will definitely be spent on deployment.
Timer-based Spell Trigger Restrictions
To create valid Timer-Based Spell Trigger Configurations, you need to consider several rules:
-
the end dates set in
endDelaySec
andendTimestamp
must be later than the start dates instartDelaySec
andstartTimestamp
, -
the end date cannot precede the spell deployment date
Note that the past check happens during the spell installation on a specific provider, and if this check fails, the provider won’t be able to install your spell, so you must be careful when using the end date settings. ::: -
the period used in
periodSec
must be less than 100 yearsThat’s just some sensible upper bound to avoid strange behaviors. Please write the developers if your business logic requires more.
Other Important Fields
Spell Setup
-
aquaFilePath
— a path to the aqua file with the spell main function -
function
— a name of the spell main function -
version
— Fluence CLI-specific version of the spell configuration. Note that it’s not relevant to the spell versioning.
Inital Arguments
initArgs
field describes a key-value map that will be passed to spell on installation. The map can contain strings, numbers, and complex objects.
-
Example
Add this in your
spell.yaml
:yamlinitArgs:some_value: "hello"num_value: 35object:name: "Joe"age: 35children:- "Marie"- "Tom"dogs:- name: "Anna"- name: "Petr"yamlinitArgs:some_value: "hello"num_value: 35object:name: "Joe"age: 35children:- "Marie"- "Tom"dogs:- name: "Anna"- name: "Petr"The spell that uses these values can be like this:
aquaaqua MyFirstSpellexport spellimport "@fluencelabs/aqua-lib/builtin.aqua"import "@fluencelabs/spell/spell_service.aqua"data Dog:name: stringdata Person:name: stringage: u32children: []stringdogs: []Dogfunc spell(some_value: string, num_value: u32, object: Person):Spell "self"log_msg <- Debug.stringify(["Some value: ", some_value, "Num value:", num_value, "Object:", object])Spell.store_log(log_msg)aquaaqua MyFirstSpellexport spellimport "@fluencelabs/aqua-lib/builtin.aqua"import "@fluencelabs/spell/spell_service.aqua"data Dog:name: stringdata Person:name: stringage: u32children: []stringdogs: []Dogfunc spell(some_value: string, num_value: u32, object: Person):Spell "self"log_msg <- Debug.stringify(["Some value: ", some_value, "Num value:", num_value, "Object:", object])Spell.store_log(log_msg)-
Output
bash$ fluence deal logs myDeployment myFirstSpellConnecting to random local relay: /ip4/127.0.0.1/tcp/9991/ws/p2p/12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAeConnectedmyDeployment (host_id: 12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAe, worker_id: 12D3KooWBPfFGP7y1jiKgQ4DEkuS87KsfcPt5DDVoWUiwNtbzYFE, spell_id: e1a2df12-efcd-4a13-be4d-dfc8d1caf3b8):2024-02-09 16:56:13 Some value: hello Num value: 35 Object: {"age": 35,"children": ["Marie","Tom"],"dogs": [{"name": "Anna"},{"name": "Petr"}],"name": "Joe"}bash$ fluence deal logs myDeployment myFirstSpellConnecting to random local relay: /ip4/127.0.0.1/tcp/9991/ws/p2p/12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAeConnectedmyDeployment (host_id: 12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAe, worker_id: 12D3KooWBPfFGP7y1jiKgQ4DEkuS87KsfcPt5DDVoWUiwNtbzYFE, spell_id: e1a2df12-efcd-4a13-be4d-dfc8d1caf3b8):2024-02-09 16:56:13 Some value: hello Num value: 35 Object: {"age": 35,"children": ["Marie","Tom"],"dogs": [{"name": "Anna"},{"name": "Petr"}],"name": "Joe"}
-
Spell Basics
In this section, we will describe how to use Spell Service's features.
You can find the actual Spell Service API for your version here.
How to resolve your Spell Service
In the template example, we saw the way to find the spell service that belongs to the spell script:
aqua
func spell():Spell "myFirstSpell"
aqua
func spell():Spell "myFirstSpell"
Here, we use the spell name or alias to find the spell service. In the same way, you can find the spell from your command line using Fluence CLI. However, there are two other spell-specific ways to do so:
-
Use
spell_id
of your spell, automatically available as an argument of the main function of your spell:aquafunc spell(spell_id: string):Spell spell_idaquafunc spell(spell_id: string):Spell spell_id -
Use reserved names
"spell"
or"self"
:aquafunc spell():Spell "spell"aquafunc spell():Spell "spell"
Spell Key-Value Storage API
The major spell feature is storage: your spells can preserve their inner state between executions and make decisions based on the saved data. The data is preserved during the provider peers' reboots and is restricted to the paid amount of disk spac.
The storage is presented as an extended Key-Value dictionary where you can store values of several types:
- strings
-
API
aquadata StringValue:-- The stored value, an empty string if the success isn't true or the value is absentvalue: string-- True if the `get` operation was successfulsuccess: bool-- Error message if the success flag is false, an empty string otherwiseerror: string-- True if there's not such value for the given keyabsent: boolservice Spell:-- Load the string by the given `key` from the storageget_string(key: string) -> StringValue-- Store the `value` string by the given `key`set_string(key: string, value: string) -> UnitValueaquadata StringValue:-- The stored value, an empty string if the success isn't true or the value is absentvalue: string-- True if the `get` operation was successfulsuccess: bool-- Error message if the success flag is false, an empty string otherwiseerror: string-- True if there's not such value for the given keyabsent: boolservice Spell:-- Load the string by the given `key` from the storageget_string(key: string) -> StringValue-- Store the `value` string by the given `key`set_string(key: string, value: string) -> UnitValue -
Example
aquaaqua MyFirstSpellexport spellimport "@fluencelabs/spell/spell_service.aqua"import "@fluencelabs/aqua-lib/builtin.aqua"func spell():Spell "self"key = "key"result <- Spell.get_string(key)if result.success:if result.absent:Spell.set_string("key", ")")else:Spell.store_log(result.value)new_value <- Op.concat_strings(result.value, ")")Spell.set_string("key", new_value)aquaaqua MyFirstSpellexport spellimport "@fluencelabs/spell/spell_service.aqua"import "@fluencelabs/aqua-lib/builtin.aqua"func spell():Spell "self"key = "key"result <- Spell.get_string(key)if result.success:if result.absent:Spell.set_string("key", ")")else:Spell.store_log(result.value)new_value <- Op.concat_strings(result.value, ")")Spell.set_string("key", new_value)-
Output
aqua$ fluence deal logs myDeployment myFirstSpellConnecting to random local relay: /ip4/127.0.0.1/tcp/9992/ws/p2p/12D3KooWMNJvkaLpUKzK64CgX1x9PNdy3vLCWFgLSpB2S7ymVhECConnectedmyDeployment (host_id: 12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAe, worker_id: 12D3KooWRRyM8XNhCFTQaqMQpg483Bq8pdfveeMKNxx4hArrNq7u, spell_id: 6d8fc228-562a-4284-8d9b-f293c2f12785):2024-02-07 08:16:04 )2024-02-07 08:17:04 ))2024-02-07 08:18:04 )))aqua$ fluence deal logs myDeployment myFirstSpellConnecting to random local relay: /ip4/127.0.0.1/tcp/9992/ws/p2p/12D3KooWMNJvkaLpUKzK64CgX1x9PNdy3vLCWFgLSpB2S7ymVhECConnectedmyDeployment (host_id: 12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAe, worker_id: 12D3KooWRRyM8XNhCFTQaqMQpg483Bq8pdfveeMKNxx4hArrNq7u, spell_id: 6d8fc228-562a-4284-8d9b-f293c2f12785):2024-02-07 08:16:04 )2024-02-07 08:17:04 ))2024-02-07 08:18:04 )))
-
-
u32
numbers-
API:
aquadata U32Value:-- The stored value, zero if the success isn't true or the value is absentvalue: u32-- True if the `get` operation was successfulsuccess: bool-- Error message if the success flag is false, an empty string otherwiseerror: string-- True if there's not such value for the given keyabsent: boolservice Spell:get_u32(key: string) -> U32Valueset_u32(key: string, value: u32) -> UnitValueaquadata U32Value:-- The stored value, zero if the success isn't true or the value is absentvalue: u32-- True if the `get` operation was successfulsuccess: bool-- Error message if the success flag is false, an empty string otherwiseerror: string-- True if there's not such value for the given keyabsent: boolservice Spell:get_u32(key: string) -> U32Valueset_u32(key: string, value: u32) -> UnitValue -
Example
aquaaqua MyFirstSpellexport spellimport "@fluencelabs/spell/spell_service.aqua"import "@fluencelabs/aqua-lib/builtin.aqua"func spell():Spell "self"key = "key"result <- Spell.get_u32(key)if result.success:if result.absent:Spell.set_u32("key", 0)else:msg <- Debug.stringify(result.value)Spell.store_log(msg)new_value = result.value + 1Spell.set_u32("key", new_value)aquaaqua MyFirstSpellexport spellimport "@fluencelabs/spell/spell_service.aqua"import "@fluencelabs/aqua-lib/builtin.aqua"func spell():Spell "self"key = "key"result <- Spell.get_u32(key)if result.success:if result.absent:Spell.set_u32("key", 0)else:msg <- Debug.stringify(result.value)Spell.store_log(msg)new_value = result.value + 1Spell.set_u32("key", new_value)-
Output
aqua$ fluence deal logs myDeployment myFirstSpellConnecting to random local relay: /ip4/127.0.0.1/tcp/9993/ws/p2p/12D3KooWDd7zwsdYart7z9MQ2yEagjafPHLxbzNkNP7wYxJxvKiNConnectedmyDeployment (host_id: 12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAe, worker_id: 12D3KooWRRyM8XNhCFTQaqMQpg483Bq8pdfveeMKNxx4hArrNq7u, spell_id: a8451478-b3b3-46f1-b45d-dae7dcf36a24):2024-02-07 08:23:06 "0"2024-02-07 08:24:06 "1"2024-02-07 08:25:06 "2"2024-02-07 08:26:06 "3"aqua$ fluence deal logs myDeployment myFirstSpellConnecting to random local relay: /ip4/127.0.0.1/tcp/9993/ws/p2p/12D3KooWDd7zwsdYart7z9MQ2yEagjafPHLxbzNkNP7wYxJxvKiNConnectedmyDeployment (host_id: 12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAe, worker_id: 12D3KooWRRyM8XNhCFTQaqMQpg483Bq8pdfveeMKNxx4hArrNq7u, spell_id: a8451478-b3b3-46f1-b45d-dae7dcf36a24):2024-02-07 08:23:06 "0"2024-02-07 08:24:06 "1"2024-02-07 08:25:06 "2"2024-02-07 08:26:06 "3"
-
-
- lists of
strings
-
API
aquadata StringListValue:-- List of strings stored by the given key, empty if there's no such keyvalue: []stringsuccess: boolerror: stringservice Spell:-- Load the last value of the listlist_pop_string(key: string) -> StringValue-- Store the value at the end of the listlist_push_string(key: string, value: string) -> UnitValue-- Load all the values of the listlist_get_strings(key: string) -> StringListValue-- Remove a `value` from the `key` list-- Note that it must be the exact valuelist_remove_string(key: string, value: string) -> UnitValueaquadata StringListValue:-- List of strings stored by the given key, empty if there's no such keyvalue: []stringsuccess: boolerror: stringservice Spell:-- Load the last value of the listlist_pop_string(key: string) -> StringValue-- Store the value at the end of the listlist_push_string(key: string, value: string) -> UnitValue-- Load all the values of the listlist_get_strings(key: string) -> StringListValue-- Remove a `value` from the `key` list-- Note that it must be the exact valuelist_remove_string(key: string, value: string) -> UnitValue -
Example
aquaaqua MyFirstSpellexport spellimport "@fluencelabs/spell/spell_service.aqua"import "@fluencelabs/aqua-lib/builtin.aqua"func spell():Spell "self"key = "key"result <- Spell.list_get_strings(key)if result.success:if result.value.length < 5:Spell.list_push_string(key, ")")else:Spell.list_pop_string(key)msg <- Debug.stringify(result.value)Spell.store_log(msg)aquaaqua MyFirstSpellexport spellimport "@fluencelabs/spell/spell_service.aqua"import "@fluencelabs/aqua-lib/builtin.aqua"func spell():Spell "self"key = "key"result <- Spell.list_get_strings(key)if result.success:if result.value.length < 5:Spell.list_push_string(key, ")")else:Spell.list_pop_string(key)msg <- Debug.stringify(result.value)Spell.store_log(msg)-
Output
aqua$ fluence deal logs myDeployment myFirstSpellConnecting to random local relay: /ip4/127.0.0.1/tcp/9992/ws/p2p/12D3KooWMNJvkaLpUKzK64CgX1x9PNdy3vLCWFgLSpB2S7ymVhECConnectedmyDeployment (host_id: 12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAe, worker_id: 12D3KooWRRyM8XNhCFTQaqMQpg483Bq8pdfveeMKNxx4hArrNq7u, spell_id: 1e62bbc3-d368-4030-a877-261a748bb66a):2024-02-07 08:51:452024-02-07 08:52:45 )2024-02-07 08:53:46 ) )2024-02-07 08:54:46 ) ) )2024-02-07 08:55:46 ) ) ) )2024-02-07 08:56:46 ) ) ) ) )2024-02-07 08:57:46 ) ) ) )2024-02-07 08:58:46 ) ) ) ) )2024-02-07 08:59:46 ) ) ) )2024-02-07 09:00:46 ) ) ) ) )2024-02-07 09:01:46 ) ) ) )2024-02-07 09:02:46 ) ) ) ) )aqua$ fluence deal logs myDeployment myFirstSpellConnecting to random local relay: /ip4/127.0.0.1/tcp/9992/ws/p2p/12D3KooWMNJvkaLpUKzK64CgX1x9PNdy3vLCWFgLSpB2S7ymVhECConnectedmyDeployment (host_id: 12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAe, worker_id: 12D3KooWRRyM8XNhCFTQaqMQpg483Bq8pdfveeMKNxx4hArrNq7u, spell_id: 1e62bbc3-d368-4030-a877-261a748bb66a):2024-02-07 08:51:452024-02-07 08:52:45 )2024-02-07 08:53:46 ) )2024-02-07 08:54:46 ) ) )2024-02-07 08:55:46 ) ) ) )2024-02-07 08:56:46 ) ) ) ) )2024-02-07 08:57:46 ) ) ) )2024-02-07 08:58:46 ) ) ) ) )2024-02-07 08:59:46 ) ) ) )2024-02-07 09:00:46 ) ) ) ) )2024-02-07 09:01:46 ) ) ) )2024-02-07 09:02:46 ) ) ) ) )
-
-
To remove the key from the storage, you can use the remove_key
function.
-
API
aquaservice Spell:remove_key(key: string) -> UnitValueaquaservice Spell:remove_key(key: string) -> UnitValue
To check if a key exists, you can call the exists
function.
-
API
aquaservice Spell:exists(key: string) -> BoolValueaquaservice Spell:exists(key: string) -> BoolValue
Spell Storage Access Permissions
The Spell Service protects spell data from writing from arbitrary sources. The writing rules are the following:
-
the spell can write to any key in its own KV
-
the particles sent from the same worker the target spell is install on can write to the keys with
w_
andhw_
prefixFor example, another spell installed on the same worker can write to the keys
w_message
orhw_update
. -
the particles on the host on which the target spell’s worker is installed can access the keys with
h_
andhw_
prefix.For example, a system spell can write to the keys
h_hello_from_host
orhw_any_valid_name
.
However**, everyone can read** anything from the spell’s storage, so you must not store in the KV private information like keys. You should consider using other methods.
Spell Logs API
The Spell Service also provides additional storage for logs. The storage is restricted to 500 entries; older logs are deleted automatically when new ones are added.
-
API
aquadata Log:-- when the log was createdtimestamp: u64-- the log messagemessage: stringdata GetLogsResult:logs: []Logsuccess: boolerror: stringservice Spell:-- Store a log.-- Note that only a **spell** can store the log in its logs storagestore_log(log: string) -> UnitValue-- Return all logs ordered by timestamp ascending.get_logs() -> GetLogsResultaquadata Log:-- when the log was createdtimestamp: u64-- the log messagemessage: stringdata GetLogsResult:logs: []Logsuccess: boolerror: stringservice Spell:-- Store a log.-- Note that only a **spell** can store the log in its logs storagestore_log(log: string) -> UnitValue-- Return all logs ordered by timestamp ascending.get_logs() -> GetLogsResult -
Example
Note that the spell that just writes to its logs is the default template created by the Fluence CLI. Here’s another — though almost identical — way to write a simple spell with logging:
aquaaqua MyFirstSpellexport spellimport "@fluencelabs/spell/spell_service.aqua"func spell():Spell "self"Spell.store_log("I was run!")aquaaqua MyFirstSpellexport spellimport "@fluencelabs/spell/spell_service.aqua"func spell():Spell "self"Spell.store_log("I was run!")You can read the logs using the Fluence CLI command like this:
bash$ fluence deal logs myDeployment myFirstSpellConnecting to random local relay: /ip4/127.0.0.1/tcp/9991/ws/p2p/12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAeConnectedmyDeployment (host_id: 12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAe, worker_id: 12D3KooWBPfFGP7y1jiKgQ4DEkuS87KsfcPt5DDVoWUiwNtbzYFE, spell_id: b030cf69-7a35-4eeb-bad1-981797e67eec):2024-02-09 15:49:15 I was run!bash$ fluence deal logs myDeployment myFirstSpellConnecting to random local relay: /ip4/127.0.0.1/tcp/9991/ws/p2p/12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAeConnectedmyDeployment (host_id: 12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAe, worker_id: 12D3KooWBPfFGP7y1jiKgQ4DEkuS87KsfcPt5DDVoWUiwNtbzYFE, spell_id: b030cf69-7a35-4eeb-bad1-981797e67eec):2024-02-09 15:49:15 I was run!Or you can get the logs manually via Aqua. Copy this code into your
src/aqua/main.aqua
:aquaaqua Mainimport "@fluencelabs/aqua-lib/builtin.aqua"import "@fluencelabs/aqua-lib/subnet.aqua"import "@fluencelabs/spell/spell_service.aqua"use "deals.aqua"export testSpellfunc testSpell() -> []GetLogsResult:deals <- Deals.get()dealId = deals.myDeployment!.dealIdOriginallogs: *GetLogsResulton HOST_PEER_ID:subnet <- Subnet.resolve(dealId)if subnet.success == false:Console.print(["Failed to resolve subnet: ", subnet.error])for w <- subnet.workers:on w.worker_id! via w.host_id:Spell "myFirstSpell"logs <<- Spell.get_logs()<- logsaquaaqua Mainimport "@fluencelabs/aqua-lib/builtin.aqua"import "@fluencelabs/aqua-lib/subnet.aqua"import "@fluencelabs/spell/spell_service.aqua"use "deals.aqua"export testSpellfunc testSpell() -> []GetLogsResult:deals <- Deals.get()dealId = deals.myDeployment!.dealIdOriginallogs: *GetLogsResulton HOST_PEER_ID:subnet <- Subnet.resolve(dealId)if subnet.success == false:Console.print(["Failed to resolve subnet: ", subnet.error])for w <- subnet.workers:on w.worker_id! via w.host_id:Spell "myFirstSpell"logs <<- Spell.get_logs()<- logsThen call it via the Fluence CLI:
bash$ fluence run -f 'testSpell()' -i src/aqua/main.aquaConnecting to random local relay: /ip4/127.0.0.1/tcp/9991/ws/p2p/12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAeConnected[{"error": "","logs": [{"message": "I was run!","timestamp": 1707493755}],"success": true}]bash$ fluence run -f 'testSpell()' -i src/aqua/main.aquaConnecting to random local relay: /ip4/127.0.0.1/tcp/9991/ws/p2p/12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAeConnected[{"error": "","logs": [{"message": "I was run!","timestamp": 1707493755}],"success": true}]
Spell Mailbox API
The Spell Service provides the Mailbox API to receive messages from the outside world. Note that the mailbox keeps only 50 last messages in the storage.
-
API
aquadata MailboxMessage:-- How put the message into the mailboxinit_peer_id: string-- When the message was puttimestamp: u64-- The messagemessage: stringdata GetMailboxResult:messages: []MailboxMessagesuccess: boolerror: stringdata PopMailboxResult:-- Contain the earliest message in the mailboxmessage: []MailboxMessagesuccess: boolabsent: boolerror: stringservice Spell:-- Push a message to the mailbox.-- Mailbox keeps 50 latest messages.push_mailbox(message: string) -> UnitValue-- Get the latest mailbox message and remove it from the mailbox.-- result.absent is true if there are no messages in the mailbox.-- Only the spell can pop the messages from the mailboxpop_mailbox() -> PopMailboxResult-- Get all messages from the mailbox in FIFO order.get_mailbox() -> GetMailboxResultaquadata MailboxMessage:-- How put the message into the mailboxinit_peer_id: string-- When the message was puttimestamp: u64-- The messagemessage: stringdata GetMailboxResult:messages: []MailboxMessagesuccess: boolerror: stringdata PopMailboxResult:-- Contain the earliest message in the mailboxmessage: []MailboxMessagesuccess: boolabsent: boolerror: stringservice Spell:-- Push a message to the mailbox.-- Mailbox keeps 50 latest messages.push_mailbox(message: string) -> UnitValue-- Get the latest mailbox message and remove it from the mailbox.-- result.absent is true if there are no messages in the mailbox.-- Only the spell can pop the messages from the mailboxpop_mailbox() -> PopMailboxResult-- Get all messages from the mailbox in FIFO order.get_mailbox() -> GetMailboxResult -
Example
The Spell Script:
aquaaqua MyFirstSpellexport spellimport "@fluencelabs/aqua-lib/builtin.aqua"import "@fluencelabs/spell/spell_service.aqua"func spell():Spell "self"mailbox_result <- Spell.pop_mailbox()if mailbox_result.success && !mailbox_result.absent:message = mailbox_result.message!log_msg <- Debug.stringify(["Got message:", message.message, "from:", message.init_peer_id])Spell.store_log(log_msg)aquaaqua MyFirstSpellexport spellimport "@fluencelabs/aqua-lib/builtin.aqua"import "@fluencelabs/spell/spell_service.aqua"func spell():Spell "self"mailbox_result <- Spell.pop_mailbox()if mailbox_result.success && !mailbox_result.absent:message = mailbox_result.message!log_msg <- Debug.stringify(["Got message:", message.message, "from:", message.init_peer_id])Spell.store_log(log_msg)This script will add a log each time it finds the message in the mailbox.
We can put the message ourselves via Fluence CLI. Add this into your
src/aqua/main.aqua
:aquaaqua Mainimport "@fluencelabs/aqua-lib/builtin.aqua"import "@fluencelabs/aqua-lib/subnet.aqua"import "@fluencelabs/spell/spell_service.aqua"use "deals.aqua"export testSpellfunc testSpell():deals <- Deals.get()dealId = deals.myDeployment!.dealIdOriginalon HOST_PEER_ID:subnet <- Subnet.resolve(dealId)if subnet.success == false:Console.print(["Failed to resolve subnet: ", subnet.error])for w <- subnet.workers:on w.worker_id! via w.host_id:Spell "myFirstSpell"Spell.push_mailbox("hello from the client!")aquaaqua Mainimport "@fluencelabs/aqua-lib/builtin.aqua"import "@fluencelabs/aqua-lib/subnet.aqua"import "@fluencelabs/spell/spell_service.aqua"use "deals.aqua"export testSpellfunc testSpell():deals <- Deals.get()dealId = deals.myDeployment!.dealIdOriginalon HOST_PEER_ID:subnet <- Subnet.resolve(dealId)if subnet.success == false:Console.print(["Failed to resolve subnet: ", subnet.error])for w <- subnet.workers:on w.worker_id! via w.host_id:Spell "myFirstSpell"Spell.push_mailbox("hello from the client!")And call the Fluence CLI command:
fluence run -f 'testSpell()' -i src/aqua/main.aqua
When the spell wakes up, it’ll see the message and notify about it in the log. We can check the log using the usual Fluence CLI command.-
Output
bash$ fluence deal logs myDeployment myFirstSpellConnecting to random local relay: /ip4/127.0.0.1/tcp/9992/ws/p2p/12D3KooWMNJvkaLpUKzK64CgX1x9PNdy3vLCWFgLSpB2S7ymVhECConnectedmyDeployment (host_id: 12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAe, worker_id: 12D3KooWBPfFGP7y1jiKgQ4DEkuS87KsfcPt5DDVoWUiwNtbzYFE, spell_id: 8e00fe09-b6d8-4c81-b925-e3fb82d2317c):2024-02-09 16:18:13 Got message: hello from the client! from: 12D3KooWQc1TuBEi54HNVuerVSZAMD3sDd4ZzTLan4tAgNp9SHi7bash$ fluence deal logs myDeployment myFirstSpellConnecting to random local relay: /ip4/127.0.0.1/tcp/9992/ws/p2p/12D3KooWMNJvkaLpUKzK64CgX1x9PNdy3vLCWFgLSpB2S7ymVhECConnectedmyDeployment (host_id: 12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAe, worker_id: 12D3KooWBPfFGP7y1jiKgQ4DEkuS87KsfcPt5DDVoWUiwNtbzYFE, spell_id: 8e00fe09-b6d8-4c81-b925-e3fb82d2317c):2024-02-09 16:18:13 Got message: hello from the client! from: 12D3KooWQc1TuBEi54HNVuerVSZAMD3sDd4ZzTLan4tAgNp9SHi7
-
Tips and Tricks
Storing objects in the Spell Service Storage
Although the Spell Storage API mainly provides functions for storing strings, it’s possible to store any object using Nox JSON builtins:
aqua
service Json("json"):-- Parse the given string into a JSON objectparse(str: string) -> ⊤-- Create a string from the given JSON objectstringify(obj: ⊤) -> string
aqua
service Json("json"):-- Parse the given string into a JSON objectparse(str: string) -> ⊤-- Create a string from the given JSON objectstringify(obj: ⊤) -> string
Note that this service isn’t in the aqua-lib since it’s too generic, although the stringify
function is available via Debug.stringify()
.
To parse a specific object, you need to provide a service that references the “json”
builtin and the parse
function returning the required type:
aqua
data MyObject:name: stringservice MyObjectJson("json"):-- Note that here we return MyObject, not some generic typeparse(str: string) -> MyObject
aqua
data MyObject:name: stringservice MyObjectJson("json"):-- Note that here we return MyObject, not some generic typeparse(str: string) -> MyObject
If you want to parse several objects, you need to provide the instance of JSON service for each of them:
aqua
data MyObject:name: stringdata MyAnotherObject:counter: u32service MyObjectJson("json"):parse(str: string) -> MyObject-- Note that you can use any unique nameservice MyAnotherObjectJson("json"):parse(str: string) -> MyAnotherObject
aqua
data MyObject:name: stringdata MyAnotherObject:counter: u32service MyObjectJson("json"):parse(str: string) -> MyObject-- Note that you can use any unique nameservice MyAnotherObjectJson("json"):parse(str: string) -> MyAnotherObject
-
Example
aquaaqua MyFirstSpellexport spellimport "@fluencelabs/aqua-lib/builtin.aqua"import "@fluencelabs/spell/spell_service.aqua"-- The object we want to storedata Object:name: stringcounter: u32-- To parse the object from string, we need to provide the service-- that will parseservice ObjectJson("json"):parse(encoded: string) -> Objectfunc log(msg: []string):Spell "self"str <- Debug.stringify(msg)Spell.store_log(str)func spell():Spell "self"result <- Spell.get_string("object")if result.success && !result.absent:log(["Got: ", result.value])try:object <- ObjectJson.parse(result.value)object2 = Object(name = object.name, counter = object.counter + 1)object2_str <- Debug.stringify(object2)Spell.set_string("object", object2_str)catch e:log(["Can't parse the object"])else:object = Object(name = "test", counter = 0)object_str <- Debug.stringify(object)log(["Created object:", object_str])Spell.set_string("object", object_str)aquaaqua MyFirstSpellexport spellimport "@fluencelabs/aqua-lib/builtin.aqua"import "@fluencelabs/spell/spell_service.aqua"-- The object we want to storedata Object:name: stringcounter: u32-- To parse the object from string, we need to provide the service-- that will parseservice ObjectJson("json"):parse(encoded: string) -> Objectfunc log(msg: []string):Spell "self"str <- Debug.stringify(msg)Spell.store_log(str)func spell():Spell "self"result <- Spell.get_string("object")if result.success && !result.absent:log(["Got: ", result.value])try:object <- ObjectJson.parse(result.value)object2 = Object(name = object.name, counter = object.counter + 1)object2_str <- Debug.stringify(object2)Spell.set_string("object", object2_str)catch e:log(["Can't parse the object"])else:object = Object(name = "test", counter = 0)object_str <- Debug.stringify(object)log(["Created object:", object_str])Spell.set_string("object", object_str)-
Output
bash$ fluence deal logs myDeployment myFirstSpellConnecting to random local relay: /ip4/127.0.0.1/tcp/9993/ws/p2p/12D3KooWDd7zwsdYart7z9MQ2yEagjafPHLxbzNkNP7wYxJxvKiNConnectedmyDeployment (host_id: 12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAe, worker_id: 12D3KooWT1v3MHQWNqVdRPtK7ntozb3JegdPo9cfM3tXkNAE3CYp, spell_id: 782d9ff1-a243-4447-a6b6-51a4f40966e1):2024-02-19 12:43:33 Created object: {"counter":0,"name":"test"}2024-02-19 12:44:33 Got: {"counter":0,"name":"test"}2024-02-19 12:45:34 Got: {"counter":1,"name":"test"}2024-02-19 12:46:34 Got: {"counter":2,"name":"test"}2024-02-19 12:47:34 Got: {"counter":3,"name":"test"}2024-02-19 12:48:34 Got: {"counter":4,"name":"test"}bash$ fluence deal logs myDeployment myFirstSpellConnecting to random local relay: /ip4/127.0.0.1/tcp/9993/ws/p2p/12D3KooWDd7zwsdYart7z9MQ2yEagjafPHLxbzNkNP7wYxJxvKiNConnectedmyDeployment (host_id: 12D3KooWFS4WXar3f1SWCykTUy9cVKNU8x1yDA18ZYp86mXTUyAe, worker_id: 12D3KooWT1v3MHQWNqVdRPtK7ntozb3JegdPo9cfM3tXkNAE3CYp, spell_id: 782d9ff1-a243-4447-a6b6-51a4f40966e1):2024-02-19 12:43:33 Created object: {"counter":0,"name":"test"}2024-02-19 12:44:33 Got: {"counter":0,"name":"test"}2024-02-19 12:45:34 Got: {"counter":1,"name":"test"}2024-02-19 12:46:34 Got: {"counter":2,"name":"test"}2024-02-19 12:47:34 Got: {"counter":3,"name":"test"}2024-02-19 12:48:34 Got: {"counter":4,"name":"test"}
-