Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

### Fixes

- `mkdb`, `mkcon`, `create database`, and `create container` now work on serverless accounts. They previously requested autoscale throughput even when `--scale` and `--ru` were omitted, which serverless accounts reject. Omitting both options now creates the resource without throughput settings; supplying either option on a serverless account fails with an explanation. Provisioned accounts keep the existing autoscale default of 1000 RU/s. ([#218](https://github.com/Azure/CosmosDBShell/issues/218))
- Vector `ORDER BY`, `ORDER BY RANK` relevance ranking, and object-shaped `DISTINCT` projections no longer fail with a continuation-token error. These query pipelines execute successfully but cannot export a resumable token, which was previously reported as a command failure. Such queries now return their documents; through MCP they keep reading until the requested limit instead of stopping after one page, and a truncated result is reported as `resultIncomplete` rather than as an exhausted result set. ([#219](https://github.com/Azure/CosmosDBShell/issues/219))
- Local emulator outages are now detected across Cosmos DB commands. Requests fail promptly with an error and return the shell to its disconnected state instead of leaving an unresponsive session labeled as connected.
- A failed or cancelled export no longer destroys its destination file. Exports are written to a temporary file in the destination directory and moved into place only after they complete, so an existing file survives query failures, write failures, and cancellation. An abrupt process termination can leave an unfinished `.cosmos-export-*.tmp` file behind. ([#207](https://github.com/Azure/CosmosDBShell/pull/207))
Expand Down
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ You can develop and test without an Azure subscription by using the [Azure Cosmo
dotnet run --project CosmosDBShell/CosmosDBShell.csproj -- --connect "https://localhost:8081"
```

### Live Serverless Smoke Test

The emulator cannot reproduce serverless behavior. To verify database and container creation against a real serverless account, set `COSMOSDB_SHELL_SERVERLESS_TEST_CONNECTION_STRING` to that account's connection string and run the `LiveServerless` test category. The test creates and then deletes a uniquely named database; without the variable, it is skipped.

```bash
dotnet test CosmosDBShell.Tests/CosmosDBShell.Tests.csproj --filter "Category=LiveServerless"
```

## Submitting a Pull Request

1. Fork the repository and create a feature branch from `main`.
Expand Down
349 changes: 349 additions & 0 deletions CosmosDBShell.Tests/CommandTests/ServerlessCreationThroughputTests.cs

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions CosmosDBShell.Tests/Integration/ServerlessCreationSmokeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------

namespace CosmosShell.Tests.Integration;

using Azure.Data.Cosmos.Shell.Core;
using Azure.Data.Cosmos.Shell.Util;

// Opt-in: the emulator cannot establish serverless behavior, so this needs a real serverless account.
[Trait("Category", "LiveServerless")]
public class ServerlessCreationSmokeTests : IntegrationTestBase
{
internal const string ConnectionStringVariable = "COSMOSDB_SHELL_SERVERLESS_TEST_CONNECTION_STRING";

[Fact]
public async Task MkdbAndMkcon_WithoutThroughputOptions_SucceedOnServerlessAccount()
{
var connectionString = Environment.GetEnvironmentVariable(ConnectionStringVariable);
Assert.SkipWhen(string.IsNullOrWhiteSpace(connectionString), $"Set {ConnectionStringVariable} to a serverless account connection string to run this test.");

await Shell.ConnectAsync(connectionString!, null, token: TestContext.Current.CancellationToken);
var databaseName = $"shell-serverless-{Guid.NewGuid():N}";
try
{
var database = await RunScriptAsync($"mkdb {databaseName}");
Assert.False(database is ErrorCommandState, FormatError(database));

var container = await RunScriptAsync($"mkcon Items /pk --database {databaseName}");
Assert.False(container is ErrorCommandState, FormatError(container));

var rejected = await RunScriptAsync($"mkcon Rejected /pk --database {databaseName} --ru 400");
Assert.Equal(MessageService.GetString("error-serverless_throughput_not_supported"), GetErrorMessage(rejected));
}
finally
{
await RunScriptAsync($"rmdb {databaseName} true");
}
}
}
19 changes: 19 additions & 0 deletions CosmosDBShell.Tests/UtilTest/ThroughputErrorsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,23 @@ public void NullMessage_IsNotDetected()
{
Assert.False(ThroughputErrors.IsServerlessThroughputError(null));
}

[Theory]
[InlineData("Setting offer throughput or autopilot on container is not supported for serverless accounts.")]
[InlineData("Setting offer throughput or autopilot on database is not supported for serverless accounts.")]
[InlineData("Provisioned THROUGHPUT is not supported for SERVERLESS accounts.")]
public void CreationThroughputRejection_IsDetected(string message)
{
Assert.True(ThroughputErrors.IsServerlessCreationThroughputError(message));
}

[Theory]
[InlineData("The serverless account policy rejected the request.")]
[InlineData("Reading or replacing offers is not supported for serverless accounts.")]
[InlineData("Setting throughput is not supported for this account.")]
[InlineData(null)]
public void UnrelatedCreationFailure_IsNotDetected(string? message)
{
Assert.False(ThroughputErrors.IsServerlessCreationThroughputError(message));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ private async Task<CommandState> CreateContainerInDatabaseAsync(ConnectedState s
{
throw new CommandException("mkcon", MessageService.GetString("command-mkcon-error_invalid_index_policy"), ex);
}
catch (ServerlessThroughputNotSupportedException ex)
{
throw new CommandException("mkcon", MessageService.GetString("error-serverless_throughput_not_supported"), ex);
}

CosmosCompleteCommand.ClearContainers();
var commandState = new CommandState();
Expand Down
19 changes: 11 additions & 8 deletions CosmosDBShell/Azure.Data.Cosmos.Shell.Commands/MakeDbCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,7 @@ internal class MakeDbCommand : CosmosCommand, IStateVisitor<CommandState, ShellI

public static ThroughputProperties CreateThroughputProperties(string? scale, int? maxru)
{
var ru = maxru ?? 1000;
if (string.Equals(scale, "manual", StringComparison.InvariantCultureIgnoreCase) || string.Equals(scale, "m", StringComparison.InvariantCultureIgnoreCase))
{
return ThroughputProperties.CreateManualThroughput(ru);
}

return ThroughputProperties.CreateAutoscaleThroughput(ru);
return CreationThroughput.CreateProperties(scale, maxru);
}

public async override Task<CommandState> ExecuteAsync(ShellInterpreter shell, CommandState commandState, string commandText, CancellationToken token)
Expand All @@ -48,7 +42,16 @@ Task<CommandState> IStateVisitor<CommandState, ShellInterpreter>.VisitDisconnect

async Task<CommandState> IStateVisitor<CommandState, ShellInterpreter>.VisitConnectedStateAsync(ConnectedState state, ShellInterpreter shell, CancellationToken token)
{
var databaseName = await CosmosResourceFacade.CreateDatabaseAsync(state, this.Name ?? string.Empty, this.Scale, this.MaxRU, token);
string databaseName;
try
{
databaseName = await CosmosResourceFacade.CreateDatabaseAsync(state, this.Name ?? string.Empty, this.Scale, this.MaxRU, token);
}
catch (ServerlessThroughputNotSupportedException ex)
{
throw new CommandException("mkdb", MessageService.GetString("error-serverless_throughput_not_supported"), ex);
}

CosmosCompleteCommand.ClearDatabases();

var commandState = new CommandState();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ public static async Task<CosmosDBSqlDatabaseResource> CreateDatabaseAsync(ArmCos
context.Account.Data.Location,
new CosmosDBSqlDatabaseResourceInfo(databaseName))
{
Options = CreateUpdateConfig(scale, maxRu),
Options = CreateUpdateConfig(context.Account.Data, scale, maxRu),
};

var operation = await context.Account.GetCosmosDBSqlDatabases().CreateOrUpdateAsync(WaitUntil.Completed, databaseName, content, token);
Expand Down Expand Up @@ -273,7 +273,7 @@ public static async Task<CosmosDBSqlContainerResource> CreateContainerAsync(

var content = new CosmosDBSqlContainerCreateOrUpdateContent(context.Account.Data.Location, resource)
{
Options = CreateUpdateConfig(scale, maxRu),
Options = CreateUpdateConfig(context.Account.Data, scale, maxRu),
};

var operation = await database.GetCosmosDBSqlContainers().CreateOrUpdateAsync(WaitUntil.Completed, containerName, content, token);
Expand All @@ -297,11 +297,17 @@ public static IReadOnlyList<string> GetPartitionKeyPaths(CosmosDBSqlContainerRes
return container.Data.Resource.PartitionKey?.Paths?.ToArray() ?? [];
}

public static CosmosDBCreateUpdateConfig CreateUpdateConfig(string? scale, int? maxRu)
public static CosmosDBCreateUpdateConfig? CreateUpdateConfig(CosmosDBAccountData account, string? scale, int? maxRu)
{
var ru = maxRu ?? 1000;
if (string.Equals(scale, "manual", StringComparison.InvariantCultureIgnoreCase) ||
string.Equals(scale, "m", StringComparison.InvariantCultureIgnoreCase))
if (IsServerless(account))
{
return CreationThroughput.IsSpecified(scale, maxRu)
? throw new ServerlessThroughputNotSupportedException()
: null;
}

var ru = maxRu ?? CreationThroughput.DefaultMaxRu;
if (CreationThroughput.IsManual(scale))
{
return new CosmosDBCreateUpdateConfig
{
Expand All @@ -315,6 +321,12 @@ public static CosmosDBCreateUpdateConfig CreateUpdateConfig(string? scale, int?
};
}

internal static bool IsServerless(CosmosDBAccountData account)
{
return account.CapacityMode == CapacityMode.Serverless
|| account.Capabilities.Any(capability => string.Equals(capability.Name, "EnableServerless", StringComparison.OrdinalIgnoreCase));
}

public static string WriteArmModel<T>(T model)
where T : IPersistableModel<T>
{
Expand Down
29 changes: 29 additions & 0 deletions CosmosDBShell/Azure.Data.Cosmos.Shell.Core/CreationThroughput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------

namespace Azure.Data.Cosmos.Shell.Core;

using Microsoft.Azure.Cosmos;

/// <summary>
/// Resolves the provisioned throughput requested by <c>mkdb</c> and <c>mkcon</c>.
/// </summary>
internal static class CreationThroughput
{
internal const int DefaultMaxRu = 1000;

internal static bool IsSpecified(string? scale, int? maxRu) => scale is not null || maxRu.HasValue;

internal static bool IsManual(string? scale) =>
string.Equals(scale, "manual", StringComparison.OrdinalIgnoreCase) ||
string.Equals(scale, "m", StringComparison.OrdinalIgnoreCase);

internal static ThroughputProperties CreateProperties(string? scale, int? maxRu)
{
var ru = maxRu ?? DefaultMaxRu;
return IsManual(scale)
? ThroughputProperties.CreateManualThroughput(ru)
: ThroughputProperties.CreateAutoscaleThroughput(ru);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,10 @@ public async Task<bool> ContainerExistsAsync(string databaseName, string contain

public async Task<string> CreateDatabaseAsync(string databaseName, string? scale, int? maxRu, CancellationToken token)
{
var throughput = CreateThroughputProperties(scale, maxRu);
var response = await client.CreateDatabaseIfNotExistsAsync(databaseName, throughput, cancellationToken: token);
var response = await CreateWithServerlessFallbackAsync(
scale,
maxRu,
throughput => client.CreateDatabaseIfNotExistsAsync(databaseName, throughput, cancellationToken: token));
RequestChargeContext.Record(response.RequestCharge);
return response.Database.Id;
}
Expand Down Expand Up @@ -109,9 +111,11 @@ public async Task<string> CreateContainerAsync(
props.IndexingPolicy = ParseIndexingPolicy(indexPolicyJson);
}

var throughput = CreateThroughputProperties(scale, maxRu);
var database = client.GetDatabase(databaseName);
var response = await database.CreateContainerIfNotExistsAsync(props, throughput, cancellationToken: token);
var response = await CreateWithServerlessFallbackAsync(
scale,
maxRu,
throughput => database.CreateContainerIfNotExistsAsync(props, throughput, cancellationToken: token));
RequestChargeContext.Record(response.RequestCharge);
return response.Container.Id;
}
Expand Down Expand Up @@ -452,15 +456,22 @@ private static ConflictResolutionPolicy BuildConflictResolutionPolicy(ConflictRe

private static string? NullIfEmpty(string? value) => string.IsNullOrEmpty(value) ? null : value;

private static ThroughputProperties CreateThroughputProperties(string? scale, int? maxRu)
// The data plane cannot read the capacity mode, so serverless is detected from the service's rejection.
internal static async Task<T> CreateWithServerlessFallbackAsync<T>(string? scale, int? maxRu, Func<ThroughputProperties?, Task<T>> create)
{
var ru = maxRu ?? 1000;
if (string.Equals(scale, "manual", StringComparison.OrdinalIgnoreCase) ||
string.Equals(scale, "m", StringComparison.OrdinalIgnoreCase))
try
{
return ThroughputProperties.CreateManualThroughput(ru);
return await create(CreationThroughput.CreateProperties(scale, maxRu));
}
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.BadRequest && ThroughputErrors.IsServerlessCreationThroughputError(ex.Message))
{
if (CreationThroughput.IsSpecified(scale, maxRu))
{
throw new ServerlessThroughputNotSupportedException(ex);
}

return ThroughputProperties.CreateAutoscaleThroughput(ru);
RequestChargeContext.Record(ex.RequestCharge);
return await create(null);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------
namespace Azure.Data.Cosmos.Shell.Core;

/// <summary>
/// Thrown when <c>--scale</c> or <c>--ru</c> is supplied while creating a database or
/// container on a serverless account, which cannot have provisioned throughput.
/// </summary>
internal sealed class ServerlessThroughputNotSupportedException : System.InvalidOperationException
{
public ServerlessThroughputNotSupportedException(System.Exception? innerException = null)
: base("Provisioned throughput is not supported on a serverless account.", innerException)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,13 @@ internal static bool IsServerlessThroughputError(string? message)
return message is not null
&& message.Contains("serverless", StringComparison.OrdinalIgnoreCase);
}

// Narrower than IsServerlessThroughputError: creation retries without throughput, so an
// unrelated serverless failure must not be retried with a different request.
internal static bool IsServerlessCreationThroughputError(string? message)
{
return IsServerlessThroughputError(message)
&& (message!.Contains("throughput", StringComparison.OrdinalIgnoreCase)
|| message.Contains("autopilot", StringComparison.OrdinalIgnoreCase));
}
}
13 changes: 7 additions & 6 deletions CosmosDBShell/lang/en.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ error-startup-navigation-requires-connect = Startup navigation options require '
error-shell-not-initialized = Shell is not initialized
error-unable_to_read_container = Unable to read container.
error-arm-context-required = Database and container resource operations require Azure Resource Manager context. Reconnect with Entra ID and provide --subscription and --resource-group, or use --connect-subscription and --connect-resource-group at startup. Alternatively, use an identity that can discover the Cosmos DB account through ARM.
error-serverless_throughput_not_supported = Serverless accounts do not support provisioned throughput. Omit --scale and --ru to create the resource without throughput settings.
error-arm-context-incomplete = Provide subscription and resource group together to use an explicit Azure Resource Manager account context. The account name is inferred from the endpoint.
error-arm-context-ambiguous = Multiple Cosmos DB Azure Resource Manager accounts match the connected endpoint. Reconnect and provide subscription and resource group explicitly.
error-arm-context-endpoint-mismatch = The Azure Resource Manager account endpoint '{ $armEndpoint }' does not match the connected Cosmos DB endpoint '{ $dataPlaneEndpoint }'. Reconnect with the subscription and resource group that own the connected account.
Expand Down Expand Up @@ -503,17 +504,17 @@ command-import-error-csv_pk_conflict = CSV column '{ $column }' conflicts with t

command-mkdb-description = Creates new database
command-mkdb-description-name = The database name to create
command-mkdb-description-scale = Container scale (manual or auto)
command-mkdb-description-ru = Container Max RU/s (default: 1000)
command-mkdb-description-scale = Database throughput mode (manual or auto). Not supported on serverless accounts.
command-mkdb-description-ru = Database max RU/s (default: 1000; omitted on serverless accounts)
command-mkdb-database_created = Created database { $db }
command-mkdb-error-only_auto_or_manual_allowed = Only manual or autoscale are allowed. Not both.

command-mkcon-description = Creates a new container in the current database.
command-mkcon-description-name = The container to create.
command-mkcon-description-partition_key = The partition key path(s) for the container. Use a single path (e.g. /categoryId) or comma-separated paths for hierarchical partition keys (e.g. /tenantId,/userId or /tenantId,/userId,/sessionId).
command-mkcon-description-unique_key = The unique keys for the container to create.
command-mkcon-description-scale = Container scale (manual or auto)
command-mkcon-description-ru = Database Max RU/s (default: 1000)
command-mkcon-description-scale = Container throughput mode (manual or auto). Not supported on serverless accounts.
Comment thread
mkrueger marked this conversation as resolved.
command-mkcon-description-ru = Container max RU/s (default: 1000; omitted on serverless accounts)
command-mkcon-description-database = The database where the container should be created
command-mkcon-CreatedContainer = Created container { $container }
command-mkcon-error_partition_key_empty = Partition key path cannot be empty. Provide a path that starts with '/', for example: mkcon name /pk.
Expand Down Expand Up @@ -837,8 +838,8 @@ command-create-description-item = The object to create item, container or databa
command-create-description-name = The container or database name to create.
command-create-description-partition_key = { command-mkcon-description-partition_key }
command-create-description-unique_key = { command-mkcon-description-unique_key }
command-create-description-scale = { command-mkcon-description-scale }
command-create-description-ru = Database or Container Max RU/s (default: 1000)
command-create-description-scale = Database or container throughput mode (manual or auto). Not supported on serverless accounts.
command-create-description-ru = Database or container max RU/s (default: 1000; omitted on serverless accounts)
command-create-description-data = JSON data for the item to create
command-create-description-database = The database for the create operation
command-create-description-container = The container for creating items
Expand Down
Loading
Loading