Creating an interactive serverless application using WebSocket
- Getting started
- Prepare the environment
- Create Managed Service for YDB databases
- Create a Data Streams data stream
- Create a Yandex Lockbox secret
- Deploy the project
- Create access keys for service accounts
- Create a new secret version and deploy your project again
- Create an API Gateway API gateway
- Connect a domain to a Telegram bot
- Test the app
- How to delete the resources you created
In this tutorial, you will deploy an online game based on Node.js using WebSocket.
The game's static resources will be stored in a Object Storage bucket and its data in Managed Service for YDB databases. The game data will be transferred in Data Streams streams and handled by Cloud Functions functions. For messaging between the app components, we will use a Message Queue queue. Secrets are securely delivered to the app using Yandex Lockbox. An API Gateway API gateway will accept user requests and redirect them to Cloud Functions functions.
Game user authorization is based on integration with Telegram.
To create an online game:
- Prepare the environment.
- Create Yandex Managed Service for YDB databases.
- Create a Yandex Data Streams data stream.
- Create a Yandex Lockbox secret.
- Deploy the project.
- Create access keys for service accounts.
- Create a new secret version and deploy your project again.
- Create an Yandex API Gateway API gateway.
- Connect a domain to a Telegram bot.
- Test your application.
If you no longer need the resources you created, delete them.
Getting started
Sign up for Yandex Cloud and create a billing account:
- Go to the management console
and log in to Yandex Cloud or create an account if you do not have one yet. - On the Yandex Cloud Billing
page, make sure you have a billing account linked and it has theACTIVE
orTRIAL_ACTIVE
status. If you do not have a billing account, create one.
If you have an active billing account, you can go to the cloud page
Learn more about clouds and folders.
Required paid resources
The infrastructure support cost for this tutorial includes:
- Fee for data operations and the amount of stored data (see Yandex Managed Service for YDB pricing).
- Fee for using a data stream (see Yandex Data Streams pricing).
- Secret storage fees (see Yandex Lockbox pricing).
- Fee for data storage and operations with data (see Yandex Object Storage pricing).
- Fee for requests to the created API gateways and outgoing traffic (see Yandex API Gateway pricing).
- Fee for queue requests and outgoing traffic (see Yandex Message Queue pricing).
- Fee for function calls and computing resources allocated to execute the functions (see Yandex Cloud Functions pricing).
Prepare the environment
- Install the WSL utility
to run a Linux environment. - Run the Linux subsystem (by default, Ubuntu).
- Next, configure the environment as described in this guide for Linux.
Note
If you use a distribution other than Ubuntu, install the specified utilities using your package manager commands.
-
Install the following utilities in the specified order using commands in the terminal:
-
WebStorm
or IntelliJ IDEA Community Edition :sudo snap install webstorm --classic
-
sudo apt-get install curl git -y
-
The jq
utility:sudo apt-get install jq
-
curl https://storage.yandexcloud.net/yandexcloud-yc/install.sh | bash exec -l $SHELL yc version
-
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" --output "AWSCLIV2.pkg" sudo installer -pkg AWSCLIV2.pkg -target /
-
curl --silent --show-error --location https://storage.yandexcloud.net/yandexcloud-ydb/install.sh | bash
-
Node.js
16.16.0
or higher:sudo apt-get install curl curl --silent --location https://deb.nodesource.com/setup_16.x | sudo -E bash sudo apt-get install nodejs node -v npm -v
-
sudo npm install -g typescript
-
-
Create a Yandex Cloud CLI profile with basic parameters.
-
Install the following utilities in the specified order using commands in the terminal:
-
/bin/bash -c "$(curl --fail --silent --show-error --location https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
-
WebStorm
or IntelliJ IDEA Community Edition :brew install --cask webstorm
-
brew install curl git
-
The jq
utility:brew install jq
-
curl https://storage.yandexcloud.net/yandexcloud-yc/install.sh | bash exec -l $SHELL yc version
-
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" --output "AWSCLIV2.pkg" sudo installer -pkg AWSCLIV2.pkg -target /
-
curl --silent --show-error --location https://storage.yandexcloud.net/yandexcloud-ydb/install.sh | bash
-
Node.js
16.16.0
or higher:brew install node@16 brew install nvm node -v npm -v
If you are using Zsh, run this command:
echo 'export NVM_DIR=~/.nvm' >> ~/.zshrc echo '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"' >> ~/.zshrc source ~/.zshrc
-
npm install -g typescript
-
-
Create a Yandex Cloud CLI profile with basic parameters.
Download a project
Clone the yc-serverless-game
repository
git clone https://github.com/yandex-cloud-examples/yc-serverless-game.git
Register the Telegram bot
Register your bot in Telegram and get a token.
-
To register the new bot, launch the BotFather
bot and run this command:/newbot
-
In the
name
field, specify a name for the bot you are creating, e.g.,Serverless Game With WebSockets
. This is the name users will see when communicating with the bot. -
In the
username
field, enter a username for the new bot, e.g.,ServerlessGameWithWebSocketsBot
. You can use the username to search for the bot in Telegram. The username must end with...Bot
or..._bot
. -
You will get the
t.me/ServerlessGameWithWebSocketsBot
address and token in response. -
Save the username to the
TG_BOT_LOGIN
variable:echo "export TG_BOT_LOGIN=<bot_username>" >> ~/.bashrc && . ~/.bashrc echo $TG_BOT_LOGIN
-
Save the token to the
TG_BOT_TOKEN
variable:echo "export TG_BOT_TOKEN=<token>" >> ~/.bashrc && . ~/.bashrc echo $TG_BOT_TOKEN
Get an access token
Get an IAM token and save it to the YC_IAM_TOKEN
variable:
echo "export YC_IAM_TOKEN=$(yc iam create-token)" >> ~/.bashrc && . ~/.bashrc
echo $YC_IAM_TOKEN
Get an OAUTH token and save it to the OAUTH_TOKEN
variable:
echo "export OAUTH_TOKEN=$(yc config get token)" >> ~/.bashrc && . ~/.bashrc
echo $OAUTH_TOKEN
Create a service account
-
Create a service account named
sls-deploy
and save its name to theSERVICE_ACCOUNT_GAME
variable:export SERVICE_ACCOUNT_GAME=$(yc iam service-account create --name sls-deploy \ --description "service account for serverless game" \ --format json | jq -r .) echo $SERVICE_ACCOUNT_GAME
-
The
id
line in the command output will show the service account ID. Save it to theSERVICE_ACCOUNT_GAME_ID
variable:echo "export SERVICE_ACCOUNT_GAME_ID=<service_account_ID>" >> ~/.bashrc && . ~/.bashrc
-
Assign the
editor
role to the service account:echo "export YC_FOLDER_ID=$(yc config get folder-id)" >> ~/.bashrc && . ~/.bashrc echo $YC_FOLDER_ID echo "export YC_CLOUD_ID=$(yc config get cloud-id)" >> ~/.bashrc && . ~/.bashrc echo $YC_CLOUD_ID yc resource-manager folder add-access-binding $YC_FOLDER_ID \ --subject serviceAccount:$SERVICE_ACCOUNT_GAME_ID \ --role editor
-
Create an authorized key for the service account:
yc iam key create \ --service-account-name sls-deploy \ --output sls-deploy.sa echo "export SA_KEY_FILE=$PWD/sls-deploy.sa" >> ~/.bashrc && . ~/.bashrc echo $SA_KEY_FILE
-
Create a static access key for your service account:
yc iam access-key create --service-account-id $SERVICE_ACCOUNT_GAME_ID
Result:
access_key: id: ajeibet3219******** service_account_id: ajehr1tv2eo1******** created_at: "2023-03-13T10:20:58.471421425Z" key_id: YCAS33CT2mPCVFh3K******** secret: YCNhBcdvfDdssIuBa-FDl6zZz0MSky********
-
Save the key ID (
key_id
) to theAWS_ACCESS_KEY_ID
variable and the secret key (secret
) to theAWS_SECRET_ACCESS_KEY
variable:echo "export AWS_ACCESS_KEY_ID=<key_ID>" >> ~/.bashrc && . ~/.bashrc echo $AWS_ACCESS_KEY_ID echo "export AWS_SECRET_ACCESS_KEY=<secret_key>" >> ~/.bashrc && . ~/.bashrc echo $AWS_SECRET_ACCESS_KEY
-
Set up the AWS CLI:
aws configure
Enter the following parameters:
AWS Access Key ID
: Service account access key ID (key_id
) you got earlier.AWS Secret Access Key
: Service account secret key (secret
) you got earlier.Default region name
: Use theru-central1
value.Default output format
: Leave empty.
-
Check the configuration:
echo $AWS_ACCESS_KEY_ID echo $AWS_SECRET_ACCESS_KEY aws configure list
Create Managed Service for YDB databases
Create a database named game-data
to store the game data and a database named data-streams
for a Data Streams stream.
-
Create a database named
game-data
in serverless mode:yc ydb database create game-data --serverless
Result:
done (8s) id: etn0ejcvmjm4******** folder_id: b1geoelk7fld******** created_at: "2023-03-30T15:01:19Z" name: game-data status: PROVISIONING endpoint: grpcs://ydb.serverless.yandexcloud.net:2135/?database=/ru-central1/b1gia87mbaom********/etn0ejcvmjm4******** serverless_database: storage_size_limit: "53687091200" location_id: ru-central1 ...
-
Save the
endpoint
value from the previous command output to theYDB_ENDPOINT
variable. In our example, it equalsgrpcs://ydb.serverless.yandexcloud.net:2135
.echo "export YDB_ENDPOINT=<DB_Document_API_endpoint>" >> ~/.bashrc && . ~/.bashrc echo $YDB_ENDPOINT
-
Save the
database
value from the previous command output to theYDB_DATABASE
variable. In our example, it equals/ru-central1/b1gia87mbaom********/etn0ejcvmjm4********
.echo "export YDB_DATABASE=<table_name>" >> ~/.bashrc && . ~/.bashrc echo $YDB_DATABASE
-
Create a database named
data-streams
in serverless mode:yc ydb database create data-streams --serverless
Result:
done (7s) id: etn16k0e1757******** folder_id: b1geoelk7fld******** created_at: "2023-03-30T15:02:44Z" name: data-streams status: PROVISIONING endpoint: grpcs://ydb.serverless.yandexcloud.net:2135/?database=/ru-central1/b1gia87mbaom********/etn16k0e1757******** serverless_database: storage_size_limit: "53687091200" location_id: ru-central1
-
Save the
endpoint
value from the previous command output to theYDB_DATA_STREAMS_ENDPOINT
variable. In our example, it equalsgrpcs://ydb.serverless.yandexcloud.net:2135
.echo "export YDB_DATA_STREAMS_ENDPOINT=<DB_Document_API_endpoint>" >> ~/.bashrc && . ~/.bashrc echo $YDB_DATA_STREAMS_ENDPOINT
-
Save the
database
value from the previous command output to theYDB_DATA_STREAMS_DATABASE
variable. In our example, it equals/ru-central1/b1gia87mbaom********/etn16k0e1757********
.echo "export YDB_DATA_STREAMS_DATABASE=<table_name>" >> ~/.bashrc && . ~/.bashrc echo $YDB_DATA_STREAMS_DATABASE
-
Check that everything is done correctly:
ydb \ --endpoint $YDB_ENDPOINT \ --database $YDB_DATABASE \ --sa-key-file $SA_KEY_FILE \ discovery whoami \ --groups
Result:
User SID: ajeien4d11sc043******** Group SIDs: all-users@well-known
Create a table
-
Go to the
files
directory in theyc-serverless-game
folder. -
Create a table using the
db-example.sql
file:ydb \ --endpoint $YDB_ENDPOINT \ --database $YDB_DATABASE \ --sa-key-file $SA_KEY_FILE \ scripting yql --file db-example.sql
-
Add a record for the initial configuration:
ydb \ --endpoint $YDB_ENDPOINT \ --database $YDB_DATABASE \ --sa-key-file $SA_KEY_FILE \ scripting yql --file db-update.sql
-
Check the execution result:
ydb \ --endpoint $YDB_ENDPOINT \ --database $YDB_DATABASE \ --sa-key-file $SA_KEY_FILE \ scheme describe Config
Result:
Columns: ┌────────────────────┬─────────┬────────┬─────┐ | Name | Type | Family | Key | ├────────────────────┼─────────┼────────┼─────┤ | name | Utf8? | | K0 | | grid_cell_size | Uint32? | | | | max_active_players | Uint8? | | | | max_inactive_sec | Int32? | | | | player_size | Uint32? | | | | transport | Utf8? | | | | world_size_x | Uint32? | | | | world_size_y | Uint32? | | | └────────────────────┴─────────┴────────┴─────┘ Storage settings: Store large values in "external blobs": false Column families: ┌─────────┬──────┬─────────────┬────────────────┐ | Name | Data | Compression | Keep in memory | ├─────────┼──────┼─────────────┼────────────────┤ | default | | None | | └─────────┴──────┴─────────────┴────────────────┘ Auto partitioning settings: Partitioning by size: true Partitioning by load: false Preferred partition size (Mb): 2048 Min partitions count: 1
Create a Data Streams data stream
echo $YDB_DATA_STREAMS_DATABASE
aws kinesis create-stream \
--endpoint https://yds.serverless.yandexcloud.net \
--stream-name $YDB_DATA_STREAMS_DATABASE/notify-state-change \
--shard-count 1
Create a Yandex Lockbox secret
-
Create a secret named
game-secrets
and transfer theYDB_ENDPOINT
andYDB_DATABASE
variable values to it:yc lockbox secret create --name game-secrets \ --description "The secrets for the serverless game" \ --payload "[{'key': 'ydb_endpoint', 'text_value': $YDB_ENDPOINT},{'key': 'ydb_db', 'text_value': $YDB_DATABASE}]"
-
Save the ID of the new secret to the
LOCKBOX_SECRET_ID
variable:echo "export LOCKBOX_SECRET_ID=$(jq -r <<< \ "$(yc lockbox secret list --format json | jq '.[]' -c | grep game-secrets)" .id)" \ >> ~/.bashrc && . ~/.bashrc echo $LOCKBOX_SECRET_ID
-
Transfer the
TG_BOT_TOKEN
variable value to the secret:yc lockbox secret add-version --id $LOCKBOX_SECRET_ID \ --payload "[{'key': 'tg_bot_token', 'text_value': '$TG_BOT_TOKEN'}]"
-
Transfer keys to the secret (we will get their values later). For now, set them to
null
.yc lockbox secret add-version --id $LOCKBOX_SECRET_ID \ --payload "[{'key': 'ymq_writer_key_id', 'text_value': 'null'},\ {'key': 'ymq_writer_key_secret', 'text_value': 'null'},\ {'key': 'ymq_capture_queue_url', 'text_value': 'null'},\ {'key': 'yds_writer_key_id', 'text_value': 'null'},\ {'key': 'yds_writer_key_secret', 'text_value': 'null'},\ {'key': 'yds_state_change_stream', 'text_value': 'notify-state-change'},\ {'key': 'yds_state_change_database', 'text_value': '$YDB_DATA_STREAMS_DATABASE'}]"
Deploy the project
-
Go to the
files
directory in theyc-serverless-game
folder. -
Change the configuration for Object Storage. Since the bucket name must be unique, replace it with a custom bucket name in the following files:
-
serverless.yaml
:sls-game-files: type: yc::ObjectStorageBucket
-
upload-to-s3.ts
in thescripts
directory:Bucket: 'sls-game-files',
For example, specify
sls-game-files-example
instead ofsls-game-files
.
-
-
Set the
APP_ENV
variable to build your project:echo "export APP_ENV=production" >> ~/.bashrc && . ~/.bashrc echo $APP_ENV
-
Build and deploy the project. In the
yc-serverless-game
folder root, run the following commands one by one:nvm use nvm install npm ci npm run build npm run deploy
When you deploy the project, the following resources will be created in your working directory:
-
Cloud Functions functions:
get-state
get-config
move
capture
state-change
login
auth
ws-connect
ws-message
ws-disconnect
-
Service accounts:
functions-sa
with theeditor
roletriggers-sa
with theserverless.functions.invoker
roleyds-reader-sa
with theyds.admin
roleyds-writer-sa
with theyds.writer
roleymq-reader-sa
with theymq.reader
roleymq-writer-sa
with theymq.writer
roleapigw-s3-viewer
with thestorage.viewer
roleapigw-fn-caller
with theserverless.functions.invoker
role
-
Object Storage bucket with the name you specified in the
serverless.yaml
file -
Message Queue queue named
capturing-queue
Create access keys for service accounts
The following service accounts were created when deploying the project:
yds-writer-sa
with theyds.writer
role to write data to the Data Streams stream.ymq-writer-sa
with theymq.writer
role to write data to the Message Queue queue.
-
Create a static access key for the
yds-writer-sa
service account:echo "export YDS_WRITER_SA_ID=$(jq -r <<< \ "$(yc iam service-account list --format json | jq '.[]' -c | grep yds-writer-sa)" .id)" \ >> ~/.bashrc && . ~/.bashrc yc iam access-key create --service-account-id $YDS_WRITER_SA_ID
Result:
access_key: id: ajeibet32197******** service_account_id: ajehr6tv2eoo******** created_at: "2023-03-13T10:20:58.471421425Z" key_id: YCASD3CT9mPCVFh3K******** secret: YCNhBcdvfDdssIuBa-FDl6zZz0MSky********
-
Copy the
key_id
value and save it to theYDS_WRITER_KEY_ID
variable:echo "export YDS_WRITER_KEY_ID=<key_ID>" >> ~/.bashrc && . ~/.bashrc echo $YDS_WRITER_KEY_ID
-
Copy the value of the secret named
secret
and save it to theYDS_WRITER_KEY_SECRET
variable:echo "export YDS_WRITER_KEY_SECRET=<secret>" >> ~/.bashrc && . ~/.bashrc echo $YDS_WRITER_KEY_SECRET
-
Create a static access key for the
ymq-writer-sa
service account:echo "export YMQ_WRITER_SA_ID=$(jq -r <<< \ "$(yc iam service-account list --format json | jq '.[]' -c | grep ymq-writer-sa)" .id)" \ >> ~/.bashrc && . ~/.bashrc yc iam access-key create --service-account-id $YMQ_WRITER_SA_ID
-
Copy the
key_id
value and save it to theYMQ_WRITER_KEY_ID
variable:echo "export YMQ_WRITER_KEY_ID=<key_ID>" >> ~/.bashrc && . ~/.bashrc echo $YMQ_WRITER_KEY_ID
-
Copy the value of the secret named
secret
and save it to theYMQ_WRITER_KEY_SECRET
variable:echo "export YMQ_WRITER_KEY_SECRET=<secret>" >> ~/.bashrc && . ~/.bashrc echo $YMQ_WRITER_KEY_SECRET
Create a new secret version and deploy your project again
-
Provide new values to the
game-secrets
secret:-
In the management console
, select your working folder. -
Select Message Queue.
-
Select the
capturing-queue
queue. -
Copy the value from the URL field and save it to the
YMQ_CAPTURE_QUEUE_URL
variable:echo "export YMQ_CAPTURE_QUEUE_URL=<URL>" >> ~/.bashrc && . ~/.bashrc
-
Check the variable values to transfer to the secret:
echo $LOCKBOX_SECRET_ID echo $YMQ_WRITER_KEY_ID echo $YMQ_WRITER_KEY_SECRET echo $YDS_WRITER_KEY_ID echo $YDS_WRITER_KEY_SECRET echo $YMQ_CAPTURE_QUEUE_URL
-
Transfer the values to the secret:
yc lockbox secret add-version --id $LOCKBOX_SECRET_ID \ --payload "[{'key': 'ymq_writer_key_id', 'text_value': '$YMQ_WRITER_KEY_ID'},\ {'key': 'ymq_writer_key_secret', 'text_value': '$YMQ_WRITER_KEY_SECRET'},\ {'key': 'ymq_capture_queue_url', 'text_value': '$YMQ_CAPTURE_QUEUE_URL'},\ {'key': 'yds_writer_key_id', 'text_value': '$YDS_WRITER_KEY_ID'},\ {'key': 'yds_writer_key_secret', 'text_value': '$YDS_WRITER_KEY_SECRET'}]"
-
-
Go to the
yc-serverless-game
folder root and deploy the project again:npm run deploy
Create an API Gateway API gateway
The following service accounts were created when deploying the project:
apigw-s3-viewer
with thestorage.viewer
role to read objects from the Object Storage bucket.apigw-fn-caller
with thefunctions.functionInvoker
role to invoke Cloud Functions functions.
-
Save the IDs of the
apigw-s3-viewer
andapigw-fn-caller
service accounts to theAPIGW_S3_VIEWER_ID
andAPIGW_FN_CALLER_ID
variables:echo "export APIGW_S3_VIEWER_ID=$(jq -r <<< \ "$(yc iam service-account list --format json | jq '.[]' -c | grep apigw-s3-viewer)" .id)" \ >> ~/.bashrc && . ~/.bashrc echo "export APIGW_FN_CALLER_ID=$(jq -r <<< \ "$(yc iam service-account list --format json | jq '.[]' -c | grep apigw-fn-caller)" .id)" \ >> ~/.bashrc && . ~/.bashrc
-
Make changes to the API gateway specification. Go to the
files
directory in theyc-serverless-game
folder and run the following command:cp apigw-example.yml apigw.yml
-
Get the IDs of the resources you created at the previous steps:
echo $APIGW_S3_VIEWER_ID echo $APIGW_FN_CALLER_ID yc storage bucket list yc serverless function list
-
Make the following changes to the
apigw.yml
file:- In all
bucket: serverless-game-files
lines, replace the bucket name with one given by you. - In all
service_account_id: <sa-id-for-object-storage>
lines, replace<sa-id-for-object-storage>
with the$APIGW_S3_VIEWER_ID
variable value. - In all
service_account_id: <sa-id-for-functions>
lines, replace<sa-id-for-functions>
with the$APIGW_FN_CALLER_ID
variable value. - In line
58
, replace<yandex-cloud-nodejs-dev-get-state-function-id>
with theyandex-cloud-nodejs-dev-get-state
function ID. - In line
65
, replace<yandex-cloud-nodejs-dev-get-config-function-id>
with theyandex-cloud-nodejs-dev-get-config
function ID. - In line
72
, replace<yandex-cloud-nodejs-dev-move-function-id>
with theyandex-cloud-nodejs-dev-move
function ID. - In line
79
, replace<yandex-cloud-nodejs-dev-login-function-id>
with theyandex-cloud-nodejs-dev-login
function ID. - In line
101
, replace<yandex-cloud-nodejs-dev-ws-message-function-id>
with theyandex-cloud-nodejs-dev-ws-message
function ID. - In line
106
, replace<yandex-cloud-nodejs-dev-ws-connect-function-id>
with theyandex-cloud-nodejs-dev-ws-connect
function ID. - In line
111
, replace<yandex-cloud-nodejs-dev-ws-disconnect-function-id>
with theyandex-cloud-nodejs-dev-ws-disconnect
function ID. - In line
118
, replace<yandex-cloud-nodejs-dev-auth-function-id>
with theyandex-cloud-nodejs-dev-auth
function ID.
- In all
-
Deploy an instance of API Gateway:
yc serverless api-gateway create \ --name serverless-game-api \ --spec=apigw.yml \ --description "for serverless-game-api"
If you made an error in the API gateway specification, fix it and run this command:
yc serverless api-gateway update \ --name serverless-game-api \ --spec=apigw.yml
Connect a domain to a Telegram bot
-
Run this command:
yc serverless api-gateway get --name serverless-game-api
-
Copy the API gateway's service domain. You can find it in the previous command output in the
domain
field. -
Find a Telegram bot named BotFather
and type the/setdomain
command. -
Select your bot from the list and send it the API gateway's service domain. Add
https://
before the domain name. For example, if the API gateway's service domain isd5d920bqkitf********.apigw.yandexcloud.net
, the URL will behttps://d5d920bqkitf********.apigw.yandexcloud.net
.
Test the app
Follow the link you sent to the Telegram bot, sign in, and open a game.
The game offers player statistics. If the API gateway's service domain is d5d920bqkitf********.apigw.yandexcloud.net
, the statistics for all players will be available at https://d5d920bqkitf********.apigw.yandexcloud.net/stats.html
.
How to delete the resources you created
To stop paying for the resources you created:
- Delete the Managed Service for YDB databases.
- Delete the Data Streams stream.
- Delete the Yandex Lockbox secret.
- Delete all objects from the bucket and then delete the empty Object Storage bucket.
- Delete the API Gateway API gateway.
- Delete the Cloud Functions functions.
- Delete the Message Queue queue.