@@ -33,6 +33,95 @@ all records have been fetched, so e.g.
3333will iterate through * all* tickets. Most likely you will want to implement your own cut-off process to stop iterating
3434when you have got enough data.
3535
36+ Idempotency
37+ -----------
38+
39+ The Zendesk API supports [ idempotency keys] ( https://developer.zendesk.com/api-reference/ticketing/introduction/#idempotency )
40+ to safely retry operations without creating duplicate resources. This client supports idempotent
41+ ticket creation via ` createTicketIdempotent ` and ` createTicketIdempotentAsync ` .
42+ Either method may throw a ` ZendeskResponseIdempotencyConflictException ` if the same idempotency key
43+ is used in two requests with non-identical payloads.
44+
45+ ### Usage Example
46+
47+ The following example illustrates a usage pattern for publishing updates to a Zendesk ticket
48+ that tracks some application specific issue. It ensures that only one ticket is created per
49+ issue, even if multiple updates are published concurrently for the same issue, or if the update is
50+ retried due to a transient failure after the ticket has already been created.
51+
52+ ``` java
53+ class FooIssueService {
54+
55+ private final Zendesk zendesk;
56+ private final Logger logger = LoggerFactory . getLogger(FooIssueService . class);
57+
58+ // Simple use case: the ticket payload depends only on the issue itself
59+ public void postIssueUpdateSimple (FooIssue issue , String update ) {
60+ IdempotentResult<Ticket > result = zendesk. createTicketIdempotent(
61+ toTicketSimple(issue),
62+ toIdempotencyKey(issue));
63+
64+ if (! result. isDuplicateRequest()) {
65+ logger. info(" Created new ticket (id = {})" , result. get(). getId());
66+ }
67+
68+ postIssueComment(result. get(). getId(), update);
69+ }
70+
71+ // Advanced use case: the ticket payload depends on the update
72+ public void postIssueUpdateAdvanced (FooIssue issue , String update ) {
73+ // Fast path pre-check, would be unsafe without idempotency b/c TOCTOU.
74+ Optional<Ticket > optTicket = findTicket(issue);
75+ if (optTicket. isPresent()) {
76+ postIssueComment(optTicket. get(). getId(), update);
77+ return ;
78+ }
79+
80+ try {
81+ IdempotentResult<Ticket > result = zendesk. createTicketIdempotent(
82+ toTicketAdvanced(issue, update),
83+ toIdempotencyKey(issue));
84+
85+ if (! result. isDuplicateRequest()) {
86+ logger. info(" Created new ticket (id = {})" , result. get(). getId());
87+ }
88+ } catch (ZendeskResponseIdempotencyConflictException e) {
89+ Ticket ticket = findTicket(issue). orElseThrow(
90+ () - > new IllegalStateException (
91+ String . format(" Ticket not found for issue %s" , issue. getId()), e));
92+ postIssueComment(ticket. getId(), update);
93+ }
94+ }
95+
96+ private static Ticket toTicketSimple (FooIssue issue ) {
97+ return toTicketAdvanced(issue, " See comments for details" );
98+ }
99+
100+ private static Ticket toTicketAdvanced (FooIssue issue , String update ) {
101+ Ticket ticket = new Ticket (issue. getRequesterId(), issue. getTitle(), new Comment (update));
102+ ticket. setExternalId(toIdempotencyKey(issue));
103+ return ticket;
104+ }
105+
106+ private static String toIdempotencyKey (FooIssue issue ) {
107+ // Must map the issue 1-to-1, so that retries for the same issue use the same key.
108+ return String . format(" foo-issue-%s" , issue. getId());
109+ }
110+
111+ private void postIssueComment (long ticketId , String update ) {
112+ Comment comment = zendesk. createComment(ticketId, new Comment (update));
113+ logger. info(" Added comment (id = {}) to ticket (id = {})" , comment. getId(), ticketId);
114+ }
115+
116+ private Optional<Ticket > findTicket (FooIssue issue ) {
117+ Iterator<Ticket > ticketsIt = zendesk. getTicketsByExternalId(issue. getId()). iterator();
118+ return ticketsIt. hasNext()
119+ ? Optional . of(ticketsIt. next())
120+ : Optional . empty();
121+ }
122+ }
123+ ```
124+
36125Community
37126-------------
38127
0 commit comments