Initialization and Consistency Best Practices

Hello, I’d like to understand the best practice for initialisation of workers and whether all this in-memory object logic is even necessary.

This is based on the following article:

My TLDR;

  • DOs have built in in-memory caching, so we can “just use” state.storage
  • Things like await state.storage.get(...) should return instantly
  • input/output gates prevent us from having undesirable outcomes in most use cases

Question:

Why does this example still use in-memory caching, e.g. this.value?

Why not skip this step in the constructor and simply use await state.storage.get(...) in the fetch method?

Question:

I’ve implemented the code exactly as it is in the example, using this.state.blockConcurrencyWhile in my constructor, and my objects state variable this.value is uninitialised inside my fetch, unless I check it and call await state.storage.get(...) in my fetch method.

Watching @gmckeon on Cloudflare TV yesterday going through the counter example, I noticed he doesn’t use this.state.blockConcurrencyWhile in his example.

SO which is it? What should I be using to init my DO state?

Question:

Concurrency.

Should I use object state at all or just use let something = await state.storage.get(...) inside my fetch and everything will magically work out?

Question:

Concurrency.

Consider the following pseudo-code inside the fetch method for a DO with a unique ID:

let data = await state.storage.get(key)
if (data.isClaimed) {
    return new Response('already claimed')
}

// (X) external API call
await fetch(...)

data.isClaimed = true
state.storage.put(key, data)
return new Response('claimed')

If multiple requests come in for the same DO, will it ever allow the call to the external api (X) twice?

What is the specific concurrency model that prevents this?

Is this the best practice for making this atomic?

Again, do I need to use this.state.blockConcurrencyWhile at all?

Do I need to implement my own in-memory caching?

Thank you in advance for your time!

I’ve been able to answer most of the questions above with a basic example:

constructor(state, env) {
    state.blockConcurrencyWhile(async() => {
		this.counter = await state.storage.get(DATA_KEY) || 0;
	})
}

async fetch(request) {
	counter++;
	if (counter > 1) {
		return jsonRes({ counter: '> 1' });
	}
	await new Promise((r) => setTimeout(r, 1000));
	storage.put(DATA_KEY, counter);
	return jsonRes({ counter });
}

You can call this many times and you’ll only get 1 response that is counter: 1

TLDR;

  1. use state.blockConcurrencyWhile in constructor to init state vars, otherwise requests will slip through with older / uninitialized values
  2. use DO state variables, otherwise requests will slip through with older / uninitialized values
  3. given (1) + (2) you CAN rely on strong consistency and that the counter will only return ONCE with {counter: 1}

What was tricky for me to notice is how the usages of ++this.value is incrementing this.value before assigning to currentValue and therefore the input/output gates are working correctly. From this example: https://github.com/cloudflare/durable-objects-template/blob/master/src/index.mjs
I would suggest breaking up the lines that modify state, or use this.value everywhere to avoid the ambiguity for other folks.

All around, amazing tool you’ve built! I have an API raring to go that deals with some blockchain use cases so the global + consistency is REALLY important.

Thanks, Matt