Skip to content
Draft
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
107 changes: 107 additions & 0 deletions examples/v1/BulkImportWithRetry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Authzed API examples — BulkImportWithRetry
*
* Shows how to use RetryableClient to write a batch of relationships via
* ImportBulkRelationships with automatic fallback to WriteRelationships on failure.
*/
package v1;

import com.authzed.api.v1.*;
import com.authzed.grpcutil.BearerToken;
import com.authzed.grpcutil.RetryableClient;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

public class BulkImportWithRetry {
private static final Logger logger = Logger.getLogger(BulkImportWithRetry.class.getName());
private static final String target = "grpc.authzed.com:443";
private static final String token = "tc_your_token_here";

public static void main(String[] args) throws InterruptedException {
ManagedChannel channel = ManagedChannelBuilder
.forTarget(target)
.useTransportSecurity()
.build();

try {
BearerToken bearerToken = new BearerToken(token);

// Write the schema first.
SchemaServiceGrpc.SchemaServiceBlockingStub schemaService =
SchemaServiceGrpc.newBlockingStub(channel).withCallCredentials(bearerToken);

schemaService.writeSchema(WriteSchemaRequest.newBuilder()
.setSchema("""
definition document {
relation editor: user
relation viewer: user
permission edit = editor
permission view = viewer + editor
}
definition user {}
""")
.build());

// Build the relationships to import.
List<Relationship> relationships = Arrays.asList(
relationship("document", "readme", "editor", "user", "alice"),
relationship("document", "readme", "viewer", "user", "bob"),
relationship("document", "handbook", "editor", "user", "alice")
);

RetryableClient retryableClient = new RetryableClient(channel, bearerToken);

// --- Example 1: first import succeeds directly via ImportBulkRelationships ---
logger.info("Importing relationships (first time)...");
retryableClient.retryableBulkImportRelationships(relationships, RetryableClient.ConflictStrategy.FAIL);
logger.info("Import succeeded.");

// --- Example 2: re-import with SKIP ignores duplicates silently ---
logger.info("Re-importing with SKIP strategy...");
retryableClient.retryableBulkImportRelationships(relationships, RetryableClient.ConflictStrategy.SKIP);
logger.info("SKIP import completed (duplicates ignored).");

// --- Example 3: re-import with TOUCH overwrites via WriteRelationships ---
logger.info("Re-importing with TOUCH strategy...");
retryableClient.retryableBulkImportRelationships(relationships, RetryableClient.ConflictStrategy.TOUCH);
logger.info("TOUCH import completed (relationships upserted).");

// --- Example 4: re-import with FAIL raises an error ---
logger.info("Re-importing with FAIL strategy (expecting error)...");
try {
retryableClient.retryableBulkImportRelationships(relationships, RetryableClient.ConflictStrategy.FAIL);
} catch (StatusRuntimeException e) {
logger.log(Level.WARNING, "Got expected error: {0}", e.getStatus());
}

} finally {
channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
}
}

private static Relationship relationship(
String resourceType, String resourceId,
String relation,
String subjectType, String subjectId) {
return Relationship.newBuilder()
.setResource(ObjectReference.newBuilder()
.setObjectType(resourceType)
.setObjectId(resourceId)
.build())
.setRelation(relation)
.setSubject(SubjectReference.newBuilder()
.setObject(ObjectReference.newBuilder()
.setObjectType(subjectType)
.setObjectId(subjectId)
.build())
.build())
.build();
}
}
118 changes: 118 additions & 0 deletions src/intTest/java/RetryableClientTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import com.authzed.api.v1.*;
import com.authzed.grpcutil.BearerToken;
import com.authzed.grpcutil.RetryableClient;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.util.Arrays;
import java.util.List;
import java.util.Random;

import static com.authzed.grpcutil.RetryableClient.ConflictStrategy.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class RetryableClientTest {

private ManagedChannel channel;
private SchemaServiceGrpc.SchemaServiceBlockingStub schemaService;
private RetryableClient retryableClient;

@Before
public void setUp() {
channel = ManagedChannelBuilder.forTarget("localhost:50051").usePlaintext().build();
// Each test instance gets its own token so tests use isolated SpiceDB namespaces.
String token = "tc_test_retryable_" + new Random().nextInt(100_000);
BearerToken bearerToken = new BearerToken(token);
schemaService = SchemaServiceGrpc.newBlockingStub(channel).withCallCredentials(bearerToken);
retryableClient = new RetryableClient(channel, bearerToken);
writeTestSchema();
}

@After
public void tearDown() throws InterruptedException {
channel.shutdownNow().awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS);
}

@Test
public void testSuccessfulImport() {
List<Relationship> rels = relationships("post-1", "alice");
// Should complete without throwing.
retryableClient.retryableBulkImportRelationships(rels, FAIL);
}

@Test
public void testSkipStrategyIgnoresConflict() {
List<Relationship> rels = relationships("post-2", "alice");
retryableClient.retryableBulkImportRelationships(rels, FAIL);
// Re-importing the same relationships with SKIP should succeed silently.
retryableClient.retryableBulkImportRelationships(rels, SKIP);
}

@Test
public void testTouchStrategyRetriesOnConflict() {
List<Relationship> rels = relationships("post-3", "alice");
retryableClient.retryableBulkImportRelationships(rels, FAIL);
// Re-importing with TOUCH falls back to WriteRelationships and succeeds.
retryableClient.retryableBulkImportRelationships(rels, TOUCH);
}

@Test
public void testFailStrategyThrowsOnConflict() {
List<Relationship> rels = relationships("post-4", "alice");
retryableClient.retryableBulkImportRelationships(rels, FAIL);

assertThatThrownBy(() -> retryableClient.retryableBulkImportRelationships(rels, FAIL))
.isInstanceOf(StatusRuntimeException.class)
.satisfies(e -> assertThat(((StatusRuntimeException) e).getStatus().getCode())
.isEqualTo(Status.Code.ALREADY_EXISTS));
}

@Test
public void testMultipleRelationshipsImport() {
List<Relationship> rels = Arrays.asList(
relationship("post-multi", "writer", "alice"),
relationship("post-multi", "reader", "bob")
);
retryableClient.retryableBulkImportRelationships(rels, FAIL);
// Both relationships should be importable with TOUCH on re-import.
retryableClient.retryableBulkImportRelationships(rels, TOUCH);
}

// --- helpers ---

private List<Relationship> relationships(String postId, String userId) {
return Arrays.asList(relationship(postId, "writer", userId));
}

private Relationship relationship(String postId, String relation, String userId) {
return Relationship.newBuilder()
.setResource(ObjectReference.newBuilder()
.setObjectType("post")
.setObjectId(postId)
.build())
.setRelation(relation)
.setSubject(SubjectReference.newBuilder()
.setObject(ObjectReference.newBuilder()
.setObjectType("user")
.setObjectId(userId)
.build())
.build())
.build();
}

private void writeTestSchema() {
String schema = "definition post {\n"
+ " relation writer: user\n"
+ " relation reader: user\n"
+ " permission view = reader + writer\n"
+ "}\n"
+ "definition user {}";
schemaService.writeSchema(WriteSchemaRequest.newBuilder().setSchema(schema).build());
}
}
Loading
Loading