Skip to content
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

Locally load schemas #269

Merged
merged 21 commits into from
Dec 14, 2022
Merged

Locally load schemas #269

merged 21 commits into from
Dec 14, 2022

Conversation

decentralgabe
Copy link
Member

@decentralgabe decentralgabe commented Dec 13, 2022

Fixes #134

Don't think this will work for schemas that are not pre-defined. Meaning, if we're in local mode and try to load a http/s resource that's not defined it'll break. This should be fine...

It is 'turned on' by default with an init() block which isn't ideal. Could use some ideas to make this better.

@codecov-commenter
Copy link

codecov-commenter commented Dec 13, 2022

Codecov Report

Merging #269 (28bc011) into main (d46fd8a) will increase coverage by 0.18%.
The diff coverage is 71.26%.

❗ Current head 28bc011 differs from pull request most recent head a674489. Consider uploading reports for the commit a674489 to get more accurate results

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #269      +/-   ##
==========================================
+ Coverage   60.18%   60.35%   +0.18%     
==========================================
  Files          38       39       +1     
  Lines        4329     4399      +70     
==========================================
+ Hits         2605     2655      +50     
- Misses       1298     1315      +17     
- Partials      426      429       +3     
Impacted Files Coverage Δ
credential/rendering/schema.go 0.00% <0.00%> (ø)
schema/jsonschema.go 52.00% <ø> (-2.72%) ⬇️
credential/exchange/schema.go 36.36% <66.67%> (ø)
schema/loader.go 72.60% <72.60%> (ø)
credential/manifest/schema.go 52.63% <100.00%> (ø)
credential/schema/vcjsonschema.go 46.00% <100.00%> (ø)

@decentralgabe
Copy link
Member Author

Tested by turning off wifi and running the tests 😉

@decentralgabe decentralgabe marked this pull request as ready for review December 13, 2022 05:08
@@ -2,17 +2,10 @@
"$schema": "http://json-schema.org/draft-07/schema#",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did everything else get deleted but not this file?

schema/jsonschema.go Outdated Show resolved Hide resolved
Copy link
Contributor

@nitro-neal nitro-neal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job! We the way we had it before was so janky

dwn/model.go Outdated Show resolved Hide resolved
@@ -2,17 +2,10 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Presentation Submission Claim Format Designations",
"type": "object",
"additionalProperties": false,
"properties": {
"definitions": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's different about this schema that you didn't delete it? Is it because it's not published in the web?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't delete most schemas. I deleted the -deref.json files. This schema actually had an update which I pulled in. cc: @nitro-neal

func init() {
httploader.Client = &http.Client{
Timeout: time.Second * 10,
}
}

const (
// defaultSchemaURL is a placeholder that's needed to load any schema
defaultSchemaURL = "schema.json"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I follow why this is needed, can you elaborate?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the library requires you to provide a url for the schema and we don't really care what this is but it needs to be a placeholder https://github.com/santhosh-tekuri/jsonschema/blob/master/compiler.go#L65

schema/loader.go Outdated Show resolved Hide resolved
schema/loader.go Outdated
}

// DisableLocalLoad disables the ability to load schemas locally, clearing current locally loaded schemas
func DisableLocalLoad() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this being used?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, but I believe it should remain as an option for users of the lib

schema/loader.go Outdated
Comment on lines 71 to 84
jsonschema.Loaders["https"] = func(url string) (io.ReadCloser, error) {
schema, ok := localSchemas[strings.TrimPrefix(url, "https://")]
if !ok {
return nil, fmt.Errorf("%q not found", url)
}
return io.NopCloser(strings.NewReader(schema)), nil
}
jsonschema.Loaders["http"] = func(url string) (io.ReadCloser, error) {
schema, ok := localSchemas[strings.TrimPrefix(url, "http://")]
if !ok {
return nil, fmt.Errorf("%q not found", url)
}
return io.NopCloser(strings.NewReader(schema)), nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a couple of suggestions for your consideration:

  • Rework LocalLoader into LocalLoaderFactory.
  • Remove the jsonschema.Loader directives from this file, and the init functions (let users of the SDK do it themselves).
  • Provide a method for creating loaders, something like what's below.
type Loader func(url string) (io.ReadCloser, error)

func (l *LocalLoaderFactory) ProtocolLoader(protocol string) Loader {
  return func(url string) (io.ReadCloser, error) {
		schema, ok := localSchemas[strings.TrimPrefix(url, protocol + "://")]
		if !ok {
			return nil, fmt.Errorf("%q not found", url)
		}
		return io.NopCloser(strings.NewReader(schema)), nil
	}
}

Once that is done, then user's of the SDK can decide where to override the jsonschema loader.

// Somewhere in the ssi-service

loaderFactory := schema.NewLocalLoaderFactory()
jsonschema.Loader["http"] = loaderFactory.ProtocolLoader("http")
jsonschema.Loader["https"] = loaderFactory.ProtocolLoader("https")

Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm open to this but I think it's important we're able to enable local loading for CI. how do you think we should go about that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about defining the TestMain (as described in this https://pkg.go.dev/testing#hdr-Main)? Within that function we can patch the jsonschema lib.

schema/loader.go Outdated
}

// AddCachedSchema adds a schema to be cached
func (cl *CachingLoader) AddCachedSchema(schemaURI, schema string) error {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adjusted this to be a caching loader with a test. no more init.

schema/loader.go Outdated
// NewCachingLoader returns a new CachingLoader that enables the ability to cache http and https schemas
func NewCachingLoader() CachingLoader {
localSchemas := make(map[string]string)
jsonschema.Loaders["https"] = func(url string) (io.ReadCloser, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it odd that a constructor has side effects. What's the rationale of patching jsonschema within the constructor?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

most, if not all, schemas will be loaded by http/s. having users configure additional loaders leaks details of the library that they shouldn't know or need to care about. this loader will work fine for the 99%

schema/loader.go Show resolved Hide resolved
schema/loader.go Outdated Show resolved Hide resolved
"github.com/stretchr/testify/assert"
)

func TestCachingLoader(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider splitting this large test into multiple smaller unit tests that exercise a single behavior.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

split into 2

schema/loader.go Outdated
func NewCachingLoader() CachingLoader {
localSchemas := make(map[string]string)
jsonschema.Loaders["https"] = func(url string) (io.ReadCloser, error) {
schema, ok := localSchemas[strings.TrimPrefix(url, "https://")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might not be safe for concurrent use (and cause a panic), depending on how users of the SDK exercise it.

A goroutine might be reading from the map, while another one writes to the map by calling AddCachedSchema.

Consider writing once to the map only, in the constructor. Concurrent reads on a map are safe.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this constructor should only be called once - and I don't imagine callers will be further configuring the loaders...if they do then yes they may have concurrency issues. is this worth caring about?

jsonschema.Loaders["https"] = cl.cachingLoaderForProtocol("https")
}

func (cl *CachingLoader) cachingLoaderForProtocol(protocol string) func(url string) (io.ReadCloser, error) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated to lock on map writes.

}

// read the contents and cache and prevent future lookups
contents, err := io.ReadAll(loaded)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added cache on load from http

schema/loader.go Outdated
}

func (cl *CachingLoader) cachingLoaderForProtocol(protocol string) func(url string) (io.ReadCloser, error) {
var m sync.Mutex
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a shared lock across all loaders (i.e. move this variable to be a member of CachingLoader. As it stands, you can have a goroutine writing via a schema fetched from the http protocol, while another goroutine reads a schema fetched from the https protocol. Does this make sense?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes but isn't that fine, since they're different protocols?

schema/loader.go Outdated Show resolved Hide resolved
schema/loader.go Outdated Show resolved Hide resolved
schema/loader.go Outdated Show resolved Hide resolved
schema/loader.go Outdated
}

func (cl *CachingLoader) cachingLoaderForProtocol(protocol string) func(url string) (io.ReadCloser, error) {
var m sync.Mutex
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you consider using a sync.RWMutex in order to lock more granularly? It could increase read throughput (though that's a small optimization).

decentralgabe and others added 3 commits December 14, 2022 13:26
Co-authored-by: Andres Uribe <auribe@tbd.email>
Co-authored-by: Andres Uribe <auribe@tbd.email>
Copy link
Contributor

@andresuribe87 andresuribe87 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome, I'm so excited about this change. Thanks for tackling!

schema/loader.go Outdated Show resolved Hide resolved
schema/loader.go Outdated

// NewCachingLoader returns a new CachingLoader that enables the ability to cache http and https schemas
func NewCachingLoader(schemas map[string]string) (*CachingLoader, error) {
// make sure only one process can write to the map at a time
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: move this to be a comment for the schemas function.

decentralgabe and others added 2 commits December 14, 2022 14:01
Co-authored-by: Andres Uribe <auribe@tbd.email>
@decentralgabe decentralgabe merged commit e2badfb into main Dec 14, 2022
@decentralgabe decentralgabe deleted the cache-schemas branch December 14, 2022 22:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Have the ability to cache / load local copies of referenced schemas
4 participants