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

[ BUG ] - Object creation and usage #7

Open
saniyar-dev opened this issue Feb 17, 2025 · 2 comments
Open

[ BUG ] - Object creation and usage #7

saniyar-dev opened this issue Feb 17, 2025 · 2 comments
Assignees
Labels
bug Something isn't working good first issue Good for newcomers help wanted Extra attention is needed

Comments

@saniyar-dev
Copy link
Owner

Describe the bug
There is some features on the design document such as these:

  • Need of TCP.open module output to be used on dial parameter of Client module
import { TCP } from 'k6/x/net';
import { Client } from 'k6/x/net/http';

export default async function () {
  const client = new Client({
    dial: async address => {
      return await TCP.open(address, { keepAlive: true });
    },
    proxy: 'https://myproxy',
    headers: { 'User-Agent': 'k6' },  // set some global headers
  });
  await client.post('http://10.0.0.10/post', {
    json: { name: 'k6' }, // automatically adds 'Content-Type: application/json' header
  });
}
import { TCP } from 'k6/x/net';
import { Client } from 'k6/x/net/http';

export default async function () {
  const client = new Client({
    dial: async address => {
      return await TCP.open(address, { tls: false });
    },
    version: [2],
  });
  await client.get('http://10.0.0.10/');
  • Need of passing Request and Response modules to other modules like Client
import { Client, Request } from 'k6/x/net/http';

export default async function () {
  const client = new Client({
    headers: { 'User-Agent': 'k6' },  // set some global headers
  });
  const request = new Request('https://httpbin.test.k6.io/get', {
    // These will be merged with the Client options.
    headers: { 'Case-Sensitive-Header': 'somevalue' },
  });
  const response = await client.get(request, {
    // These will override any options for this specific submission.
    headers: { 'Case-Sensitive-Header': 'anothervalue' },
  });
  const jsonData = await response.json();
  console.log(jsonData);
}

Thus these features are important part of the design documentation, I couldn't implement them because of how i implement these modules.

These modules are generally implementing Object interface i wrote

// Object interface defines the common behavior/functionalities each object exported on the main API should have.
// Client, Request, TCP, etc. are objects
type Object interface {
	Define() error
	ParseParams(*sobek.Runtime, []sobek.Value) (Params, error)
}

There is two bug with this implementation:

  1. Can't restore the original module's object when i pass them to another module. For example when i want to pass Request to Client like this:
import { Client, Request } from 'k6/x/net/http';

export default async function () {
  const client = new Client({
    headers: { 'User-Agent': 'k6' },  // set some global headers
  });
  const request = new Request('https://httpbin.test.k6.io/get', {
    // These will be merged with the Client options.
    headers: { 'Case-Sensitive-Header': 'somevalue' },
  });
  const response = await client.get(request, {
    // These will override any options for this specific submission.
    headers: { 'Case-Sensitive-Header': 'anothervalue' },
  });
  const jsonData = await response.json();
  console.log(jsonData);
}

I encountered an error while restoring the object in go when i used the sobek.Object.Export method because it will return a map[string]Value or something like this.

	// if the input is an req object then everything has been set before so we just add defaults and return
	if v, ok := arg.Export().(*request.Request); ok {
		addDefault(v)
		return v, nil
	}

	if v, ok := arg.Export().(string); ok {
		r, err := http.NewRequest(method, v, body)
		req := &request.Request{Request: r}
		addDefault(req)
		return req, err
	}

To run the code above. I needed to do a trick initializing the Request module:

func (i *HTTPAPI) initRequest(sc sobek.ConstructorCall) *sobek.Object {
	rt := i.vu.Runtime()

	r := &request.Request{
		Vu: i.vu,
	}

	helpers.Must(rt, func() error {
		_, err := r.ParseParams(rt, sc.Arguments)
		return err
	}())

	// TODO: find another way to reconstruct the original Object cause this way we cannot implement other functionality to the object
	r.Obj = rt.ToValue(r).ToObject(rt)
	helpers.Must(rt, r.Define())

	return r.Obj
}

The trick is rt.ToValue(r).ToObject(rt) so as the documentation on sobek engine says, in this way when we call Export() on a *sobek.Object type it returns the original object in go.

But this is just a trick to continue development so i need to re-implement it another way.

To Reproduce
Not using the trick mentioned above will raise us not knowing what is the original object in golang is.

	rt := i.vu.Runtime()

	c := &client.Client{
		Vu:  i.vu,
		Obj: rt.NewObject(),
	}

	helpers.Must(rt, func() error {
		_, err := c.ParseParams(rt, sc.Arguments)
		return err
	}())
	helpers.Must(rt, c.Define())

	return c.Obj

can't do this:

	// if the input is an req object then everything has been set before so we just add defaults and return
	if v, ok := arg.Export().(*request.Request); ok {
		addDefault(v)
		return v, nil
	}

	if v, ok := arg.Export().(string); ok {
		r, err := http.NewRequest(method, v, body)
		req := &request.Request{Request: r}
		addDefault(req)
		return req, err
	}

Expected implementation
I need to find a way to wrap the original object and restore it whenever i want, so i can use its functionalities after i pass the module to another.

Additional context
I did some research and found out that we can somehow use sobek.DynamicObject, but i don't know exactly how.

I would be very happy if anyone have the solution, please share the solution you think of.

@saniyar-dev saniyar-dev added bug Something isn't working good first issue Good for newcomers help wanted Extra attention is needed labels Feb 17, 2025
@saniyar-dev
Copy link
Owner Author

Update

I extended the Object interface from:

// Object interface defines the common behavior/functionalities each object exported on the main API should have.
// Client, Request, TCP, etc. are objects
type Object interface {
	Define() error
	ParseParams(*sobek.Runtime, []sobek.Value) (Params, error)
}

To:

// Object interface defines the common behavior/functionalities each object exported on the main API should have.
// Client, Request, TCP, etc. are objects
type Object interface {
	sobek.DynamicObject
	Define() error
	ParseParams(*sobek.Runtime, []sobek.Value) (Params, error)
}

From now on each Object in golang / module in javascript is an extended implementation of sobek.DynamicObject so we can use Export on each and have the original Object in our go code to use its functionalities in go. Read more to understand why


Example usage of Export in createRequest function which is also a wrapper for creating requests to attach custom parameters from Client and Request both.

func (c *Client) createRequest(method string, arg sobek.Value, body io.Reader) (*request.Request, error) {
	// add default options to requests function
	addDefault := func(req *request.Request) {
		for k, vlist := range c.params.headers {
			if len(vlist) == 0 {
				continue
			}
			for _, v := range vlist {
				req.Header.Add(k, v)
			}
		}
	}

	// if the input is an req object then everything has been set before so we just add defaults and return
	if v, ok := arg.Export().(*request.Request); ok {
		addDefault(v)
		return v, nil
	}

	if v, ok := arg.Export().(string); ok {
		r, err := http.NewRequest(method, v, body)
		req := &request.Request{Request: r}
		addDefault(req)
		return req, err
	}

	return &request.Request{}, fmt.Errorf(
		"invalid input! couldn't make the request from argument: %+v",
		arg.Export())
}

For implementing such a thing we need to have a map[string]sobek.Value in our structs and implement some other functions defined in sobek.DynamicObject interface.

The full example of implementation would be like this:

// Client struct is the Client object type that users is going to use in js like this:
//
// const client = new Client();
// const response = await client.get('https://httpbin.test.k6.io/get');
//
// you can see more usage examples in js through examples dir.
type Client struct {
	// The http.Client struct to have all the functionalities of a http.Client in Client struct
	http.Client

	// Multiple vus in k6 can create multiple Client objects so we need to have access the vu Runtime, etc.
	Vu modules.VU

	M map[string]sobek.Value

	// Params is the way to config the global params for Client object to do requests.
	params *Clientparams
}

var _ interfaces.Object = &Client{}

func (c *Client) Delete(k string) bool {
	delete(c.M, k)
	return true
}

func (c *Client) Get(k string) sobek.Value {
	return c.M[k]
}

func (c *Client) Has(k string) bool {
	_, exists := c.M[k]
	return exists
}

func (c *Client) Keys() []string {
	keys := make([]string, 0, len(c.M))
	for k := range c.M {
		keys = append(keys, k)
	}
	return keys
}

func (c *Client) Set(k string, val sobek.Value) bool {
	c.M[k] = val
	return true
}

// Define func defines data properties on obj attatched to Client struct.
func (c *Client) Define() error {
	rt := c.Vu.Runtime()

	c.Set("get", rt.ToValue(c.getAsync))
	return nil
}

// ParseParams parses Client params and save them to it's instance
func (c *Client) ParseParams(rt *sobek.Runtime, args []sobek.Value) (interfaces.Params, error) {
	parsed := &Clientparams{
		headers: make(http.Header),
	}
	if len(args) == 0 {
		c.params = parsed
		return parsed, nil
	}
	if len(args) > 1 {
		return nil, fmt.Errorf(
			"you can't have multiple arguments when creating a new Client, but you've had %d args",
			len(args),
		)
	}

	rawParams := args[0]
	params := rawParams.ToObject(rt)
	for _, k := range params.Keys() {
		switch k {
		case "headers":
			headers := params.Get(k)
			if sobek.IsUndefined(headers) || sobek.IsNull(headers) {
				continue
			}
			headersObj := headers.ToObject(rt)
			if headersObj == nil {
				continue
			}
			for _, key := range headersObj.Keys() {
				parsed.headers.Set(key, headersObj.Get(key).String())
			}

		case "proxy":
			proxy := params.Get(k)
			if sobek.IsUndefined(proxy) || sobek.IsNull(proxy) {
				continue
			}
			if v, ok := proxy.Export().(*url.URL); ok {
				parsed.proxy = *v
			}

		case "url":
			urlV := params.Get(k)
			if sobek.IsUndefined(urlV) || sobek.IsNull(urlV) {
				continue
			}
			if v, ok := urlV.Export().(string); ok {
				addr, err := url.Parse(v)
				if err != nil {
					return parsed, fmt.Errorf(
						"invalid url for Client: %s",
						v,
					)
				}
				parsed.url = *addr
			} else {
				return parsed, fmt.Errorf(
					"invalid url for Client: %s",
					v,
				)
			}

		default:
			return parsed, fmt.Errorf(
				"unknown Client's option: %s",
				k,
			)
		}
	}

	c.params = parsed

	return parsed, nil
}

It was easier than i thought :)

@saniyar-dev
Copy link
Owner Author

I have fixed and merged this issue with mentioned implementation on pull request #8
Still tough i want to leave this issue open for any further discussion on the implementation.

@saniyar-dev saniyar-dev self-assigned this Feb 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working good first issue Good for newcomers help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

1 participant