Skip to content

proposal: context: Add unique context ID accessible via context.Context.Value() #69805

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
andriiyaremenko opened this issue Oct 7, 2024 · 10 comments
Labels
Milestone

Comments

@andriiyaremenko
Copy link

andriiyaremenko commented Oct 7, 2024

Proposal Details

What:

I propose to add context.ContextIDKey (or any other better name), that will be used as (example):

func(ctx context.Context) {
	ctxID, ok := ctx.Value(context.ContextIDKey)
}

It should return unique ID (not string, int64 maybe?) for every context instance and be accessible for any context instance created via go standart library.

Why:

Sometimes, not often, I find myself struggling to find a good way to associate objects with context.Context passed into the function, when I do not want to add them to the context via .WithValue() or have no option to use it.

For example: I have a map[context.Contex]SomeService, where I lookup values based on context passed into the function as argument.
It is possible and would work, but it takes considerably more time compared to working with map[int]SomeService.

When I'm saying considerably more time consider this benchmark:

type key int

var ctxKey key

func getCtx() context.Context {
	return context.WithoutCancel(context.Background())
}

func BenchmarkCtxEquivalence(b *testing.B) {
	m := make(map[context.Context]struct{})
	ctx := getCtx()
	m[ctx] = struct{}{}
	m[getCtx()] = struct{}{}
	m[getCtx()] = struct{}{}
	m[getCtx()] = struct{}{}
	m[getCtx()] = struct{}{}
	m[getCtx()] = struct{}{}
	m[getCtx()] = struct{}{}
	m[getCtx()] = struct{}{}
	m[getCtx()] = struct{}{}
	m[getCtx()] = struct{}{}

	for i := 0; i < b.N; i++ {
		_ = m[ctx]
	}
}

func BenchmarkCtxValueEquivalence(b *testing.B) {
	m := make(map[int]struct{})
	ctx := context.WithValue(context.Background(), ctxKey, 3)
	m[0] = struct{}{}
	m[1] = struct{}{}
	m[2] = struct{}{}
	m[3] = struct{}{}
	m[4] = struct{}{}
	m[5] = struct{}{}
	m[6] = struct{}{}
	m[7] = struct{}{}
	m[8] = struct{}{}
	m[9] = struct{}{}

	for i := 0; i < b.N; i++ {
		val := ctx.Value(ctxKey)
		_ = m[val.(int)]
	}
}

func BenchmarkCtxEquivalenceWithAssignment(b *testing.B) {
	m := make(map[context.Context]struct{})
	ctx := getCtx()
	ctx1 := getCtx()
	ctx2 := getCtx()
	ctx3 := getCtx()
	ctx4 := getCtx()
	ctx5 := getCtx()
	ctx6 := getCtx()
	ctx7 := getCtx()
	ctx8 := getCtx()
	ctx9 := getCtx()

	for i := 0; i < b.N; i++ {
		m[ctx] = struct{}{}
		m[ctx1] = struct{}{}
		m[ctx2] = struct{}{}
		m[ctx3] = struct{}{}
		m[ctx4] = struct{}{}
		m[ctx5] = struct{}{}
		m[ctx6] = struct{}{}
		m[ctx7] = struct{}{}
		m[ctx8] = struct{}{}
		m[ctx9] = struct{}{}
		_ = m[ctx]
	}
}

func BenchmarkCtxValueEquivalenceWithAssignment(b *testing.B) {
	m := make(map[int]struct{})
	ctx := context.WithValue(context.Background(), ctxKey, 3)

	for i := 0; i < b.N; i++ {
		m[0] = struct{}{}
		m[1] = struct{}{}
		m[2] = struct{}{}
		m[3] = struct{}{}
		m[4] = struct{}{}
		m[5] = struct{}{}
		m[6] = struct{}{}
		m[7] = struct{}{}
		m[8] = struct{}{}
		m[9] = struct{}{}
		val := ctx.Value(ctxKey)
		_ = m[val.(int)]
	}
}

Here are the results:

goos: darwin
goarch: arm64
cpu: Apple M1
BenchmarkCtxEquivalence-8                                       26787438               106.4 ns/op             0 B/op          0 allocs/op
BenchmarkCtxValueEquivalence-8                                  42403700                57.76 ns/op            0 B/op          0 allocs/op
BenchmarkCtxEquivalenceWithAssignment-8                           987766              1436 ns/op               0 B/op          0 allocs/op
BenchmarkCtxValueEquivalenceWithAssignment-8                     5911315               170.7 ns/op             0 B/op          0 allocs/op
PASS

And this difference can accumulate when working with a lot of instances of context simultaneously.

I'm not sure it will be widely used, but it will for sure help in some cases like described above.

If it is a duplicate - I'm sorry. I tried to find if anyone had already proposed it, but found no issue dedicated to it.

@gopherbot gopherbot added this to the Proposal milestone Oct 7, 2024
@gabyhelp
Copy link

gabyhelp commented Oct 7, 2024

Related Issues and Documentation

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

@ianlancetaylor ianlancetaylor moved this to Incoming in Proposals Oct 8, 2024
@ianlancetaylor
Copy link
Member

It seems to me that map[context.Context]SomeService will work. And of course context.WithValue will work. I don't see a strong reason to add a third way to do this. It doesn't seem like something very many people would ever use.

@rittneje
Copy link
Contributor

rittneje commented Oct 8, 2024

@andriiyaremenko What exactly are you trying to do? Why do you need a map at all, instead of just using context.WithValue to add the value in question (SomeService in your example) to the context, and the Value method to fetch it back out?

@andriiyaremenko
Copy link
Author

andriiyaremenko commented Oct 8, 2024

I'm sorry, it's my first proposal and I'm clearly bad describing problems.
I'm talking about situation (a library for example) when:

  1. Objects are big enough and carry not only information but functionality as well (access to database, etc.) and
  2. Code has no way to control the context that is being passed into the function and as a result no way to use WithValue, but simply hoping that user will not forget to put it in every place where it is needed.

I'm trying to describe a situation when I'm trying to always return the same service for the same context, for example in dependency injection library.

The only reason I created this proposal was my thought that I can't be the only one who wished there was a better way to identify the context and distinguish two contexts in better and faster way than storing and comparing them. If I'm wrong, I'll close this issue and try to use what Go already has.

@seankhliao
Copy link
Member

seankhliao commented Oct 8, 2024

This feels like misusing contexts as an identifier, you should probably have some other identifier that you can more reliably create and use, especially since this ID is not like how most context values work within a tree of contexts

@andriiyaremenko
Copy link
Author

This feels like misusing contexts as an identifier, you should probably have some other identifier that you can more reliably create and use, especially since this ID is not like how most context values work within a tree of contexts

That's precisely the reason this proposal was created. I want reliably distinguish between contexts and tell when I received the same context. I suggested adding it as an ID that can be obtained from Values, because I dint't want to suggest changing contex.Context interface by adding ID() uint64 method. If this is would be a better proposal though - please tell me, I'll rewrite this proposal.

@ianlancetaylor
Copy link
Member

We can't change the context.Context interface, as that would violate the Go 1 compatibility guarantee.

It occurs to me that context.Context is designed such that people can provide their own implementations of the interface, and there is existing code that actually does that in practice. I don't know how to implement this proposal without breaking cases that use that existing code and also want to access the context ID.

@andriiyaremenko
Copy link
Author

andriiyaremenko commented Oct 8, 2024

We can't change the context.Context interface, as that would violate the Go 1 compatibility guarantee.

It occurs to me that context.Context is designed such that people can provide their own implementations of the interface, and there is existing code that actually does that in practice. I don't know how to implement this proposal without breaking cases that use that existing code and also want to access the context ID.

That's also the reason why I didn't want to suggest changing the interface.
I get that Values are copied on child context creation. I thought that for this particular value it will be possible to implement uniqueness for every context created and not yet being cancelled via core library. So existing libraries can choose either to ignore or to support it.

I imagine something like:

type withID struct {
	id     uint64
	parent Context
}

func (w *withID) Deadline() (deadline time.Time, ok bool) {
	return w.parent.Deadline()
}

func (w *withID) Done() <-chan struct{} {
	return w.parent.Done()
}

func (w *withID) Err() error {
	return w.parent.Err()
}

func (w *withID) Value(key any) any {
	switch {
	case key == ContextIDKey && w.Err() == nil:
		return w.id
	case key == ContextIDKey:
		return uint64(0)
	default:
		return w.parent.Value(key)
	}
}

func WithID(ctx Context) Context {
	withID := &withID{parent: ctx}
	withID.id = uint64(reflect.ValueOf(withID).Pointer())

	return withID
}

but add part with setting id and Value method update for Context implementations in core library.

@seankhliao
Copy link
Member

I'm not convinced that this is a widely used pattern that needs native support.
Having a special key that works differently does not seem like a good ux.
Plus, here's already unique.Make.

Note that context.Background returns a zero sized context, a unique identity for those isn't possible. The application really should create the IDs it needs.

@andriiyaremenko
Copy link
Author

Makes sense. I'm closing the issue.
Thank you for looking into it!)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants