diff --git a/Makefile b/Makefile index e4ed62a9..1641dccd 100644 --- a/Makefile +++ b/Makefile @@ -6,12 +6,17 @@ COMPOSE = docker-compose # SubmitQueue compose files COMPOSE_FILE = service/submitqueue/docker-compose.yml +PROVIDER_COMPOSE_FILE = service/submitqueue/docker-compose.provider.yml GATEWAY_COMPOSE_FILE = service/submitqueue/gateway/server/docker-compose.yml ORCHESTRATOR_COMPOSE_FILE = service/submitqueue/orchestrator/server/docker-compose.yml # Fixed project name for local manual testing (tests use unique random names) SUBMITQUEUE_LOCAL_PROJECT = submitqueue +# Separate project for the provider demo stack, so it can run alongside the plain +# local stack without the two sharing containers or volumes. +PROVIDER_LOCAL_PROJECT = submitqueue-provider + # Stovepipe compose file (single Ping-only service) STOVEPIPE_COMPOSE_FILE = service/stovepipe/docker-compose.yml STOVEPIPE_DEBUG_COMPOSE_FILE = service/stovepipe/docker-compose.debug.yml @@ -41,6 +46,17 @@ PROTO_PACKAGES = api/base/change api/base/mergestrategy api/base/messagequeue ap # Set REPO_ROOT for docker-compose export REPO_ROOT := $(shell pwd) +# Which provider the demo stack targets. Selects a configuration directory rather +# than a code path, so adding a provider means adding a directory — see +# service/submitqueue/demo/provider/README.md. +PROVIDER ?= github +export SQ_PROVIDER_CONFIG_DIR ?= $(REPO_ROOT)/service/submitqueue/demo/provider/$(PROVIDER) + +# Defaults for `make land` against the provider demo stack. +QUEUE ?= demo-queue +STRATEGY ?= SQUASH_REBASE +GATEWAY_ADDR ?= localhost:8081 + # Fails if git working tree is dirty. Usage: $(call assert_clean,fix command) define assert_clean @if ! git diff --quiet; then \ @@ -136,7 +152,7 @@ deps: tidy-go ## Download and tidy Go dependencies e2e-git-test: ## Run the hermetic git E2E (real merger against a bare repo; no credentials) @echo "Running hermetic git end-to-end tests..." - @$(BAZEL) test //test/e2e/submitqueue:go_default_test --test_output=errors \\ + @$(BAZEL) test //test/e2e/submitqueue:go_default_test --test_output=errors \ --test_filter='TestGitMergeE2E' e2e-test: ## Run end-to-end tests (hermetic; Bazel builds all inputs; runs in parallel) @@ -174,6 +190,26 @@ integration-test-submitqueue-orchestrator: ## Run Orchestrator integration tests @echo "Running Orchestrator integration tests..." @$(BAZEL) test //test/integration/submitqueue/orchestrator:go_default_test --test_output=streamed +land: ## Land a change or a stack (PR=, PRS=" ", or URI=) + @if [ -z "$(PR)$(PRS)$(URI)$(URIS)" ]; then \ + echo "Usage: make land PR=https://github.com/owner/repo/pull/7"; \ + echo " stack: make land PRS=\" \" (order is the stack order)"; \ + echo " by uri: make land URI=github://github.com/owner/repo/pull/7/"; \ + echo " opts: QUEUE=$(QUEUE) STRATEGY=$(STRATEGY) GATEWAY_ADDR=$(GATEWAY_ADDR)"; \ + exit 2; \ + fi + @$(BAZEL) run //service/submitqueue/gateway/client:gateway -- \ + -addr $(GATEWAY_ADDR) land \ + -queue $(QUEUE) \ + -strategy $(STRATEGY) \ + $(if $(PR),-pr $(PR)) $(foreach p,$(PRS),-pr $(p)) \ + $(if $(URI),-uri $(URI)) $(foreach u,$(URIS),-uri $(u)) + +land-status: ## Read a landed request's status (SQID=... [QUEUE=demo-queue]) + @if [ -z "$(SQID)" ]; then echo "Usage: make land-status SQID=demo-queue/1 [QUEUE=demo-queue]"; exit 2; fi + @$(BAZEL) run //service/submitqueue/gateway/client:gateway -- \ + -addr $(GATEWAY_ADDR) status -queue $(QUEUE) -sqid $(SQID) + license-fix: ## Add missing license headers to source files @$(BAZEL) run //tool/linter/licenseheader -- --fix @@ -214,6 +250,26 @@ local-submitqueue-gateway-stop: ## Stop Gateway service @$(COMPOSE) -f $(GATEWAY_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down @echo "Gateway services stopped." +local-provider-start: build-all-linux ## Start the full stack against a real provider (PROVIDER=github; needs GITHUB_TOKEN) + @echo "Starting full stack against provider '$(PROVIDER)' ($(SQ_PROVIDER_CONFIG_DIR))..." + @test -f "$(SQ_PROVIDER_CONFIG_DIR)/merge.yaml" \ + || { echo "No such provider '$(PROVIDER)': $(SQ_PROVIDER_CONFIG_DIR)/merge.yaml not found"; exit 2; } + @$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -p $(PROVIDER_LOCAL_PROJECT) up -d --build --wait + @echo "Applying database schemas..." + @$(MAKE) -s local-init-submitqueue-schemas SUBMITQUEUE_LOCAL_PROJECT=$(PROVIDER_LOCAL_PROJECT) + @echo "" + @echo "✅ Stack is running against provider '$(PROVIDER)'." + @echo "" + @echo "Gateway gRPC port: $$(docker port $(PROVIDER_LOCAL_PROJECT)-gateway-service-1 8080 2>/dev/null | cut -d: -f2 || echo 'unknown')" + @echo "" + @echo "Land a change with:" + @echo " make land PR=https://github.com/owner/repo/pull/7 GATEWAY_ADDR=localhost:" + +local-provider-stop: ## Stop the provider demo stack + @echo "Stopping provider stack..." + @$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -p $(PROVIDER_LOCAL_PROJECT) down + @echo "Provider stack stopped." + local-init-submitqueue-schemas: ## Manually apply all database schemas @echo "Applying storage schema to mysql-app..." @for file in submitqueue/extension/storage/mysql/schema/*.sql; do \ diff --git a/doc/howto/PROVIDER-E2E.md b/doc/howto/PROVIDER-E2E.md new file mode 100644 index 00000000..4437349b --- /dev/null +++ b/doc/howto/PROVIDER-E2E.md @@ -0,0 +1,169 @@ +# Landing real changes against a provider + +How to run the whole pipeline against a live repository and watch a change actually land. This is the manual tier: it needs a scratch repository and a token, which is why it is not automated in CI. + +Two tiers below it run with no credentials at all and cover most of what can break: + +| | Command | Covers | Secrets | +|---|---|---|---| +| Tier 1 | `make e2e-test` | pipeline choreography, on the noop merger | none | +| Tier 2 | `make e2e-git-test` | real git: provisioning, cherry-pick, atomic push, head-branch updates | none | +| Tier 3 | this document | the change provider: reading metadata, its CI, changes marked merged | a token | + +Run tier 2 first. If the merge machinery is broken, it will say so in under a minute and without a repository to clean up afterwards. + +## What you need + +A **scratch repository** you are willing to have commits pushed to and branches force-moved on. Do not point this at anything you care about — the merger pushes to the target branch and rewrites the head branch of every change it lands. + +A **token** for it, scoped to that one repository. + +For a **fine-grained** token, grant these repository permissions. Each is here because a specific component needs it, so you can drop the last two if you are not using those pieces: + +| Permission | Access | Needed by | +|---|---|---| +| Metadata | Read | mandatory on every fine-grained token; GitHub adds it for you | +| Contents | Read and write | the git merger — clone, fetch, push to the target branch, and force-move each landed change's head branch | +| Pull requests | Read | the change provider reads pull request metadata, and `land -pr` reads the head commit | +| Pull requests | Read **and write** | only for `make demo-prs`, which opens pull requests | +| Actions | Read and write | only if you switch the build runner to GitHub Actions — dispatch a run, poll it, cancel it | + +A **classic** PAT needs `repo`, plus `workflow` if you use the GitHub Actions build runner. + +Two things people get caught by. Fine-grained tokens must have the repository explicitly selected under "Repository access" — org-owned repositories also need the org to have approved fine-grained tokens at all. And **Contents: Read and write is the one that cannot be reduced**: landing *is* pushing, so a read-only token fails at the last step, after everything else has appeared to work. + +## Configure + +Everything provider-specific is one directory: [`service/submitqueue/demo/provider/github/`](../../service/submitqueue/demo/provider/github). Edit the three marked lines in `merge.yaml`: + +```yaml +remoteUrl: https://github.com//.git +target: main +checkoutPath: /var/runway/checkouts/ +``` + +Neither file holds a secret — `tokenEnv: GITHUB_TOKEN` names the variable, and the value comes from your environment. + +## Run + +```bash +export GITHUB_TOKEN=ghp_... +make local-provider-start PROVIDER=github +``` + +The stack refuses to start without the token rather than falling back to the fake integrations. That is deliberate: a stack that silently runs on fakes reports changes as landed without having gone near the provider, which is a much worse way to find out. + +`local-provider-start` prints the gateway's port. Export it so the commands below are shorter: + +```bash +export GATEWAY_ADDR=localhost: +``` + +## Land a single change + +Open a pull request against `main` in the scratch repo, then: + +```bash +make land PR=https://github.com///pull/1 +``` + +`land` resolves the pull request's head commit and prints the change URI it built, so there is no 40-character SHA to copy. It returns an `sqid`; follow it with: + +```bash +make land-status SQID= GATEWAY_ADDR=$GATEWAY_ADDR +``` + +The status walks `accepted → started → validated → batched → landed`. When it reaches `landed`, on GitHub the pull request shows **Merged** and its commit is on `main`. + +Worth understanding *why* it shows merged, because nothing called an API to close it. A provider marks a change merged once its head commit is reachable from the target branch. `SQUASH_REBASE` rewrites the commits, so the pull request's original head is nowhere in `main` — and `updateHeadBranch` therefore moves the pull request's branch to the commit it landed as. GitHub draws its own conclusion from that. + +## Land a stack + +Open a chain of pull requests where each targets the previous one's branch, then submit them in order: + +```bash +make land PRS="https://github.com///pull/1 \ + https://github.com///pull/2 \ + https://github.com///pull/3" +``` + +The order of `PRS` is the stack order. All three land as **one push** to `main` — there is no window where a reader sees the stack half-applied — and all three show as merged. Tier 2 asserts the single-push property mechanically, by counting ref updates in the target's reflog. + +## Watching it work + +```bash +docker compose -p submitqueue-provider logs -f runway-service +``` + +Runway logs each merge and each head-branch move: + +``` +moved change head branch to its landed commit {"change": "you/repo#1", "branch": "refs/heads/feature-a", ...} +``` + +## When it does not work + +**The push is rejected on the first try.** Branch protection on `main` — required status checks, or a linear-history or no-force-push rule — applies to the merger like anyone else. Either relax it on the scratch repo or add the token's identity to the bypass list. + +**The change lands but the pull request stays open.** Two causes, distinguishable in Runway's logs. + +If the change came from a **fork**, this is expected and permanent: the head branch lives in the contributor's repository, which this stack has no business writing to. The log says `no head branch on this remote for change`. The change is on `main`; only the pull request's status is wrong. + +Otherwise it is **protection on the head branch** blocking the force update. The log says `could not move change head branch`. Note the land itself succeeded — the failure is reported and deliberately not retried, because the push already happened and cannot be undone. + +**A change is rejected as stale.** Its head moved after it was submitted, so the commit named is no longer the one under review. Re-submit it. This also happens if you re-land a change that already landed, since landing moved its branch. + +**Everything reports `error` immediately.** Check the queue name exists in [`queues.yaml`](../../service/submitqueue/gateway/server/queues.yaml) and matches the one in `profiles.yaml` and `merge.yaml`. A queue with no entry in `merge.yaml` gets the noop merger by design, so it will appear to land without pushing anything. + +## Using real CI + +The demo keeps the build runner fake so a land finishes in seconds. Switching to real GitHub Actions takes three things. + +**1. The workflow must be dispatchable.** The runner triggers builds with `POST /actions/workflows/{id}/dispatches`, which only works if the workflow declares `workflow_dispatch`. A typical scratch-repo `ci.yml` triggered on `pull_request` alone cannot be dispatched at all — GitHub rejects it. Add the trigger and the inputs the runner sends: + +```yaml +on: + pull_request: + merge_group: + workflow_dispatch: + inputs: + sq_head_uris: + description: "JSON array of change URIs in the batch under test" + required: false + sq_base_uris: + description: "JSON array of in-flight change URIs this batch speculates on top of" + required: false + sq_queue: + description: "SubmitQueue queue name" + required: false + sq_metadata: + description: "Caller-supplied build metadata, as JSON" + required: false +``` + +**2. Point the runner at it.** Replace the `buildRunner` line for the queue in `profiles.yaml`: + +```yaml +buildRunner: + type: githubactions + owner: behinddwalls + repo: sq-demo + workflow: ci.yml # file name or numeric workflow id + ref: main # the branch the workflow definition is read from +``` + +**3. Grant Actions: Read and write** on the token (see the permissions table above). + +One caveat worth understanding before you rely on the result. A workflow that only checks out the pull request tests *that change alone* — which is not what a submit queue is for. The point of speculation is to test the **combination**: `sq_base_uris` are the in-flight changes assumed to land first, and `sq_head_uris` is the batch under test on top of them. Until the workflow actually applies both, a green run says nothing about whether the batch lands cleanly, and the queue is only exercising its trigger-and-poll loop. + +## Clean up + +```bash +make local-provider-stop +``` + +The scratch repository keeps whatever landed; reset it with `git push --force` from a known-good commit. + +## Another provider + +Nothing above is GitHub-specific except the contents of the config directory and the URI parser behind it. Adding GitLab or another provider is a new directory here plus a handful of new files beside the existing ones — the complete list is in [`service/submitqueue/demo/provider/README.md`](../../service/submitqueue/demo/provider/README.md). diff --git a/service/submitqueue/demo/provider/README.md b/service/submitqueue/demo/provider/README.md new file mode 100644 index 00000000..ecb1f525 --- /dev/null +++ b/service/submitqueue/demo/provider/README.md @@ -0,0 +1,42 @@ +# Provider configurations + +Each directory here is one **provider** — a code-hosting system SubmitQueue lands changes on — described entirely as configuration. A directory holds two files: + +| File | Selects | +|---|---| +| `profiles.yaml` | the change provider, build runner, and conflict analyzer each queue resolves to (read by the orchestrator) | +| `merge.yaml` | the merge target each queue lands on (read by Runway) | + +Neither holds a secret. Each integration names the *environment variable* carrying its credential, so these files stay committable and rotating a token needs no edit. + +Pick one with `make local-provider-start PROVIDER=`, which bind-mounts that directory into the orchestrator and Runway. Because the choice is a mount rather than a build input, switching providers needs no rebuild. + +| Directory | What it demonstrates | +|---|---| +| [`github/`](github) | a live provider: GitHub change metadata, a real repository, pull requests marked merged | +| [`local/`](local) | a plain git remote with no provider at all — used by the hermetic git E2E (`make e2e-git-test`) | + +`local/` is worth reading first. It is proof that the merge machinery has no provider in it: the same Runway code path lands changes against a bare repository addressed by path, with no credential and no API. + +## Adding a provider + +Everything provider-specific is reached through an existing seam, so a new provider is new code beside the old, not a change to it. The complete list: + +| Touchpoint | Why | +|---|---| +| `platform/base/change/{provider}/change_id.go` | parse `{provider}://…` change URIs | +| `runway/extension/merger/git/changeref.go` | one `case` in `resolveChange`, mapping the URI to the ref the provider publishes a change's head under — GitHub `refs/pull/{n}/head`, GitLab `refs/merge-requests/{iid}/head` | +| `submitqueue/extension/changeprovider/{provider}/` and one case in `changeprovider/routing` | fetch change metadata | +| `submitqueue/extension/buildrunner/{provider}/` | only if the provider's CI is not already covered by the Buildkite or GitHub Actions runners | +| `service/submitqueue/gateway/client/main.go` | one case in `resolvePullRequest`, so `land -pr ` accepts the provider's change URLs | +| this directory | a `{provider}/` with the two files above | + +What is **not** on that list is the point of it: the merger's apply and push paths, the head-branch update, the orchestrator pipeline, the wire contract, the compose overlay, and the hermetic git E2E are all provider-independent and need no change. + +Two of those deserve explanation. + +**The merger stays provider-neutral** because `resolveChange` reduces every URI to the same three things — the commit to apply, the ref it lives under, and a label — before any git command runs. The apply paths never learn which provider a change came from. + +**Marking a change merged needs no provider API.** A provider decides whether a change merged while it processes the push to the target branch, comparing the change's recorded head against what that push makes reachable. `MERGE` and `PROMOTE` satisfy that by construction; the rewriting strategies do not, so `updateHeadBranch` moves the change's head branch to the commit it landed as — as its own push, immediately before the target is pushed. The ordering is the mechanism: a head moved *after* the target has been pushed, or in the same atomic push, is recorded too late, and the provider marks the change closed rather than merged even though its head is demonstrably on the target. That works by matching a SHA against the remote's branch tips — no change number, no API call — so it behaves identically for a GitHub pull request and a GitLab merge request. The one case it cannot serve is a change proposed from a fork, whose head branch lives in another repository: such a change lands and stays open. + +See [doc/howto/PROVIDER-E2E.md](../../../../doc/howto/PROVIDER-E2E.md) for running a real land end to end. diff --git a/service/submitqueue/demo/provider/github/merge.yaml b/service/submitqueue/demo/provider/github/merge.yaml new file mode 100644 index 00000000..a407971f --- /dev/null +++ b/service/submitqueue/demo/provider/github/merge.yaml @@ -0,0 +1,48 @@ +# Merge targets for the GitHub demo. +# +# Edit the three placeholders below to point at your own scratch repository, +# then: make local-provider-start PROVIDER=github +# +# This file holds no secret. `tokenEnv` names the environment variable carrying +# the credential, so the file stays committable and rotating the token needs no +# edit here. + +defaults: + # Any queue without an entry below does not merge for real, so a typo in a + # queue name fails safe rather than landing somewhere unintended. + merger: {type: noop} + +queues: + - name: demo-queue + merger: + type: git + # ---- point these at your own scratch repo ---- + remoteUrl: https://github.com/behinddwalls/sq-demo.git + target: main + checkoutPath: /var/runway/checkouts/sq-demo + # -------------------------- + + remote: origin + # SQUASH_REBASE collapses each pull request to one commit on the target. + # REBASE keeps every commit; MERGE preserves the original hashes under a + # merge commit; PROMOTE fast-forwards to an existing commit. + defaultStrategy: SQUASH_REBASE + # Refuse a change whose head has moved since it was submitted, rather + # than landing a commit nobody is looking at any more. + checkStaleness: true + # SQUASH_REBASE rewrites commits, so the pull request's original head is + # not reachable from the target and GitHub would leave it open. Moving the + # head branch to the commit it landed as is what makes GitHub mark it + # merged — no API call, no close-PR permission. + # + # This cannot work for a pull request opened from a fork: its head branch + # lives in another repository. Such a change lands and stays open. + updateHeadBranch: true + # The credential for cloning, fetching, and pushing. Written into the + # checkout as an HTTP Authorization header — never into the remote URL, + # which git would echo back in error messages. + # + # For SSH instead, drop tokenEnv, use an ssh:// remoteUrl, and mount a + # deploy key: the merger already passes SSH_AUTH_SOCK and + # GIT_SSH_COMMAND through to git. + tokenEnv: GITHUB_TOKEN diff --git a/service/submitqueue/demo/provider/github/profiles.yaml b/service/submitqueue/demo/provider/github/profiles.yaml new file mode 100644 index 00000000..a0757fa0 --- /dev/null +++ b/service/submitqueue/demo/provider/github/profiles.yaml @@ -0,0 +1,39 @@ +# Extension profiles for the GitHub demo. +# +# Selects which implementation of each extension every queue resolves to. Holds +# no secret: each integration names the environment variable carrying its +# credential. + +defaults: + # Fake everywhere by default, so a queue that is not listed below cannot + # reach GitHub by accident. + changeProvider: {type: fake} + buildRunner: {type: fake} + analyzer: {type: all} + +queues: + - name: demo-queue + # Reads pull request metadata from GitHub's GraphQL API. For GitHub + # Enterprise, add `github: {baseUrl: https://ghe.example.com/api/v3}`. + changeProvider: {type: github} + + # Serializes only batches that touch a shared file, so independent changes + # land in parallel. Swap for {type: all} to serialize the queue entirely, or + # {type: none} for maximum parallelism with no conflict detection. + analyzer: {type: fileoverlap} + + # Every build succeeds instantly, so a land completes in seconds and the + # demo exercises the merge rather than waiting on CI. + buildRunner: {type: fake} + # + # To run real CI instead, replace the line above with the block below. It + # needs a workflow in the target repository that is triggerable by + # workflow_dispatch and accepts the sq_base_uris / sq_head_uris inputs, and + # a token with the `workflow` scope. + # + # buildRunner: + # type: githubactions + # owner: behinddwalls + # repo: sq-demo + # workflow: submitqueue-validate.yml + # ref: main diff --git a/service/submitqueue/docker-compose.provider.yml b/service/submitqueue/docker-compose.provider.yml new file mode 100644 index 00000000..ed226986 --- /dev/null +++ b/service/submitqueue/docker-compose.provider.yml @@ -0,0 +1,48 @@ +# Compose overlay: point the stack at a real provider. +# +# Layered on top of docker-compose.yml, which it does not modify: +# +# docker compose -f docker-compose.yml -f docker-compose.provider.yml up +# +# or, more simply, `make local-provider-start PROVIDER=github`. +# +# Nothing here names a provider. Which one is reached is decided entirely by the +# configuration directory bind-mounted at /etc/submitqueue — see +# demo/provider// — so adding a provider needs no change to this file. +# +# Required in the environment: +# SQ_PROVIDER_CONFIG_DIR directory holding profiles.yaml and merge.yaml +# GITHUB_TOKEN (for the github example) the credential named by +# tokenEnv in those files +# +# The token is required rather than defaulted: falling back to the fake +# integrations would let the stack report a change as landed without having gone +# anywhere near the provider, which is a far worse failure than not starting. + +services: + orchestrator-service: + environment: + # Which change provider, build runner, and conflict analyzer each queue + # resolves to. + - PROFILES_CONFIG_PATH=/etc/submitqueue/profiles.yaml + # Read by the change provider and, if configured, the CI build runner. + - GITHUB_TOKEN=${GITHUB_TOKEN:?set GITHUB_TOKEN to reach the provider} + volumes: + - ${SQ_PROVIDER_CONFIG_DIR:?set SQ_PROVIDER_CONFIG_DIR, e.g. ./service/submitqueue/demo/provider/github}:/etc/submitqueue:ro + + runway-service: + environment: + # Per-queue merge targets. Without this Runway falls back to the noop + # merger and nothing is ever pushed. + - MERGE_CONFIG_PATH=/etc/submitqueue/merge.yaml + # Used to clone, fetch, and push. Written into the checkout as an HTTP + # Authorization header, never into the remote URL. + - GITHUB_TOKEN=${GITHUB_TOKEN:?set GITHUB_TOKEN to reach the provider} + volumes: + - ${SQ_PROVIDER_CONFIG_DIR:?set SQ_PROVIDER_CONFIG_DIR, e.g. ./service/submitqueue/demo/provider/github}:/etc/submitqueue:ro + # Working trees the merger owns. A named volume so a restart re-uses the + # clone instead of fetching the repository again. + - runway-checkouts:/var/runway/checkouts + +volumes: + runway-checkouts: diff --git a/service/submitqueue/gateway/server/queues.yaml b/service/submitqueue/gateway/server/queues.yaml index b5cf4331..72f01c09 100644 --- a/service/submitqueue/gateway/server/queues.yaml +++ b/service/submitqueue/gateway/server/queues.yaml @@ -12,5 +12,9 @@ queues: # orchestrator example server. - name: e2e-conflict-error-queue # Used by the hermetic git E2E, where Runway is wired to a real git merger - # against a bare repository. See service/submitqueue/example/provider/local. + # against a bare repository. See service/submitqueue/demo/provider/local. - name: e2e-git-queue + # Used by the provider demo stack (make local-provider-start), where the whole + # pipeline runs against a real repository. See + # service/submitqueue/demo/provider and doc/howto/PROVIDER-E2E.md. + - name: demo-queue