Manager: allow setup to finish without Blender #104306

Manually merged
Sybren A. Stüvel merged 34 commits from abelli/flamenco:issue100195 into main 2024-09-09 11:22:42 +02:00
Contributor

Overview

This patch updates the step where the Manager cannot find blender with an option to skip.
If skipped, the end configuration file will have the blender variable set to just blender.

Fix #100195

Demo

Generated flamenco-manager.yaml

# Configuration file for Flamenco.
# For an explanation of the fields, refer to flamenco-manager-example.yaml
#
# NOTE: this file will be overwritten by Flamenco Manager's web-based configuration system.
#
# This file was written on 2024-05-11 18:14:37 -03:00 by Flamenco 3.6-alpha0

_meta:
  version: 3
manager_name: Flamenco
database: flamenco-manager.sqlite
database_check_period: 10m0s
listen: :8080
autodiscoverable: true
local_manager_storage_path: ./flamenco-manager-storage
shared_storage_path: /home/mateus/workspace/flamenco-storage
shaman:
  enabled: true
  garbageCollect:
    period: 24h0m0s
    maxAge: 744h0m0s
    extraCheckoutPaths: []
task_timeout: 10m0s
worker_timeout: 1m0s
blocklist_threshold: 3
task_fail_after_softfail_count: 3
mqtt:
  client:
    broker: ""
    clientID: flamenco
    topic_prefix: flamenco
    username: ""
    password: ""
variables:
  blender:
    values:
    - platform: linux
      value: blender
    - platform: windows
      value: blender
    - platform: darwin
      value: blender
  blenderArgs:
    values:
    - platform: all
      value: -b -y

### Overview This patch updates the step where the Manager cannot find blender with an option to skip. If skipped, the end configuration file will have the blender variable set to [just `blender`](https://flamenco.blender.org/usage/variables/blender/#just-blender). Fix #100195 ### Demo <table> <tr> <td> <img src="/attachments/fa2422db-09a4-49e2-8b00-19c91852bffd"/> </td> <td> <img src="/attachments/d3a777ec-9478-4256-86e8-efcbc38f2ce4" /> </td> </tr> </table> Generated `flamenco-manager.yaml` ```yaml # Configuration file for Flamenco. # For an explanation of the fields, refer to flamenco-manager-example.yaml # # NOTE: this file will be overwritten by Flamenco Manager's web-based configuration system. # # This file was written on 2024-05-11 18:14:37 -03:00 by Flamenco 3.6-alpha0 _meta: version: 3 manager_name: Flamenco database: flamenco-manager.sqlite database_check_period: 10m0s listen: :8080 autodiscoverable: true local_manager_storage_path: ./flamenco-manager-storage shared_storage_path: /home/mateus/workspace/flamenco-storage shaman: enabled: true garbageCollect: period: 24h0m0s maxAge: 744h0m0s extraCheckoutPaths: [] task_timeout: 10m0s worker_timeout: 1m0s blocklist_threshold: 3 task_fail_after_softfail_count: 3 mqtt: client: broker: "" clientID: flamenco topic_prefix: flamenco username: "" password: "" variables: blender: values: - platform: linux value: blender - platform: windows value: blender - platform: darwin value: blender blenderArgs: values: - platform: all value: -b -y ```
Mateus Abelli added 1 commit 2024-05-11 23:30:36 +02:00
Update the step where the Manager cannot find blender
with an option to skip. If skipped, the end configuration
file will have the blender variable set to just "blender".
Sybren A. Stüvel requested changes 2024-05-12 11:36:09 +02:00
Dismissed
Sybren A. Stüvel left a comment
Owner

Thanks for the PR, it's fixing a long-standing eye-sore :)

I feel that the code structure could use a bit of work. There's now two ways in which the 'use whatever Blender' setting is represented:

  1. the skipCustomBlenderExe boolean
  2. this.selectedBlender as set by checkBlenderExePath().

I think it would be better to have another possible value of selectedBlender.source, and have that set via the "use whatever Blender" radio button. That way there's less juggling of different representations of the same user choice.

This would require altering the OpenAPI definition in flamenco-manager.yaml, to extend the BlenderPathSource schema. For that I'd recommend reading Generating Code and OpenAPI commit guidelines

Thanks for the PR, it's fixing a long-standing eye-sore :) I feel that the code structure could use a bit of work. There's now two ways in which the 'use whatever Blender' setting is represented: 1. the `skipCustomBlenderExe` boolean 2. `this.selectedBlender` as set by `checkBlenderExePath()`. I think it would be better to have another possible value of `selectedBlender.source`, and have that set via the "use whatever Blender" radio button. That way there's less juggling of different representations of the same user choice. This would require altering the OpenAPI definition in `flamenco-manager.yaml`, to extend the `BlenderPathSource` schema. For that I'd recommend reading [Generating Code](https://flamenco.blender.org/development/generating-code/) and [OpenAPI commit guidelines](https://flamenco.blender.org/development/openapi-commit-guidelines/)
@ -103,2 +103,3 @@
@back-clicked="prevStep"
:is-next-clickable="selectedBlender != null || customBlenderExe != (null || '')"
:is-next-clickable="
selectedBlender != null || customBlenderExe != (null || '') || skipCustomBlenderExe

That original code already won't work, customBlenderExe != (null || '') is not going to do what it's intended to express. null is a false-y value, and so (null || '') gets shortcut to just '').

That's not something to address in this PR though -- this PR adds a feature, and shouldn't also try and fix existing code. But if you're willing to look into this too, a separate PR that fixes this (which could then be landed before adding this new feature) would be much appreciated.

That original code already won't work, `customBlenderExe != (null || '')` is not going to do what it's intended to express. `null` is a false-y value, and so `(null || '')` gets shortcut to just `''`). That's not something to address in this PR though -- this PR adds a feature, and shouldn't also try and fix existing code. But if you're willing to look into this too, a separate PR that fixes this (which could then be landed before adding this new feature) would be much appreciated.
Author
Contributor

I have opened #104307 to address this one.

I have opened #104307 to address this one.
abelli marked this conversation as resolved
Mateus Abelli changed title from Manager: allow setup to finish without Blender to WIP: Manager: allow setup to finish without Blender 2024-05-12 15:15:30 +02:00
Mateus Abelli changed title from WIP: Manager: allow setup to finish without Blender to Manager: allow setup to finish without Blender 2024-05-13 18:10:22 +02:00
Mateus Abelli added 1 commit 2024-05-14 01:03:28 +02:00
Author
Contributor

Hello @dr.sybren I'm able to generate and extend the OpenAPI definition but I do have a questions on how to proceed.

I'd like to confirm if by using a bit more of work could mean some more drastic changes to the original code, like refactoring and doing some arrangements to turn the "use whatever Blender" logic into one.

So that it not only can produce a selectedBlender object with the source attribute "input_path" but also "skip_input".
With the switch being our radio buttons.

Hello @dr.sybren I'm able to generate and extend the OpenAPI definition but I do have a questions on how to proceed. I'd like to confirm if by using a bit more of work could mean some more drastic changes to the original code, like refactoring and doing some arrangements to turn the "use whatever Blender" logic into one. So that it not only can produce a `selectedBlender` object with the source attribute "input_path" but also "skip_input". With the switch being our radio buttons.

I'm able to generate and extend the OpenAPI definition

👍

I'd like to confirm if by using a bit more of work could mean some more drastic changes to the original code, like refactoring and doing some arrangements to turn the "use whatever Blender" logic into one.

Yes. As an overall perspective on coding: this is Open Source, which means it's yours. It's kind of a min-max game:

  • try to minimize the changes per pull request, for example splitting off functional changes from refactoring code into a better shape, and
  • maximize how well a new feature integrates with the existing code.

In other words, avoid the "this is only my little feature, I'll just tack it onto the existing code" pitfall that so many of us fall into (including me).

So that it not only can produce a selectedBlender object with the source attribute "input_path" but also "skip_input".
With the switch being our radio buttons.

Yep. But for the API I wouldn't use the term skip_input -- that name reflects the logic for the UI, but it does not reflect the data itself. I think default would be better, as it should cause Flamenco to simply use the default "blender" string. By skipping this choice (UI), the user gets this default value (API).

> I'm able to generate and extend the OpenAPI definition :+1: > I'd like to confirm if by using a bit more of work could mean some more drastic changes to the original code, like refactoring and doing some arrangements to turn the "use whatever Blender" logic into one. Yes. As an overall perspective on coding: this is Open Source, which means it's yours. It's kind of a min-max game: - try to minimize the changes per pull request, for example splitting off functional changes from refactoring code into a better shape, and - maximize how well a new feature integrates with the existing code. In other words, avoid the "_this is only my little feature, I'll just tack it onto the existing code_" pitfall that so many of us fall into (including me). > So that it not only can produce a `selectedBlender` object with the source attribute "input_path" but also "skip_input". > With the switch being our radio buttons. Yep. But for the API I wouldn't use the term `skip_input` -- that name reflects the logic for the UI, but it does not reflect the data itself. I think `default` would be better, as it should cause Flamenco to simply use the default `"blender"` string. By skipping this choice (UI), the user gets this default value (API).
Mateus Abelli added 3 commits 2024-05-17 03:05:37 +02:00
Author
Contributor

I did some updates to the code, it didn't felt like drastic changes were required as I previously thought, but I tried to follow your tips for a better coding result.

Although the updated code looks similar my latest commit will not work. I'd like to ask your help to solve this, so far here is what happens:

  • User selects the "skip" radio button
  • Button syncs the selectedBlender with the return object of blenderFromDefaultSource()
  • User clicks Next and Confirm

A POST request is made to http://192.168.0.16:8080/api/v3/configuration/setup-assistant
with the following payload:

{
    "storageLocation": "/home/mateus/workspace/flamenco-storage",
    "blenderExecutable": {
        "input": "blender",
        "path": "blender",
        "source": "default",
        "is_usable": true,
        "cause": "blender"
    }
}

And my final flamenco-manager.yaml will have the blender variables empty.

variables:
  blender:
    values:
    - platform: linux
      value: ""
    - platform: windows
      value: ""
    - platform: darwin
      value: ""
  blenderArgs:
    values:
    - platform: all
      value: -b -y

If I updated the source attribute to be input_path instead of default it will generate a flamenco-manager.yaml with my blender variables as just "blender", as expected.

I did some updates to the code, it didn't felt like drastic changes were required as I previously thought, but I tried to follow your tips for a better coding result. Although the updated code looks similar my latest commit **will not work**. I'd like to ask your help to solve this, so far here is what happens: - User selects the "skip" radio button - Button syncs the `selectedBlender` with the return object of `blenderFromDefaultSource()` - User clicks Next and Confirm A POST request is made to http://192.168.0.16:8080/api/v3/configuration/setup-assistant with the following payload: ```json { "storageLocation": "/home/mateus/workspace/flamenco-storage", "blenderExecutable": { "input": "blender", "path": "blender", "source": "default", "is_usable": true, "cause": "blender" } } ``` And my final `flamenco-manager.yaml` will have the blender variables empty. ```yaml variables: blender: values: - platform: linux value: "" - platform: windows value: "" - platform: darwin value: "" blenderArgs: values: - platform: all value: -b -y ``` If I updated the source attribute to be `input_path` instead of `default` it will generate a `flamenco-manager.yaml` with my blender variables as just "blender", as expected.

A POST request is made to http://192.168.0.16:8080/api/v3/configuration/setup-assistant
with the following payload:

{
    "storageLocation": "/home/mateus/workspace/flamenco-storage",
    "blenderExecutable": {
        "input": "blender",
        "path": "blender",
        "source": "default",
        "is_usable": true,
        "cause": "blender"
    }
}

I think this payload has a few too many fields set. With source="default" it shouldn't be necessary to provide any other value. It's a matter of 'ownership': The Manager should be in control over what those defaults are, not the web interface.

And my final flamenco-manager.yaml will have the blender variables empty. [...] If I updated the source attribute to be input_path instead of default it will generate a flamenco-manager.yaml with my blender variables as just "blender", as expected.

Sure, but that's trying to work around an issue in the Manager API by changing the webapp. That's not the right way to go.

I think you've uncovered two issues at once:

  1. The current code has a bug, where it accepts values that lead to an invalid state. This should be fixed as a separate PR. It's probably best to return a 400 Bad Request response in such a case.
  2. There is no handling yet for source="default", which should be added.
> A POST request is made to http://192.168.0.16:8080/api/v3/configuration/setup-assistant > with the following payload: > ```json > { > "storageLocation": "/home/mateus/workspace/flamenco-storage", > "blenderExecutable": { > "input": "blender", > "path": "blender", > "source": "default", > "is_usable": true, > "cause": "blender" > } > } > ``` I think this payload has a few too many fields set. With `source="default"` it shouldn't be necessary to provide any other value. It's a matter of 'ownership': The Manager should be in control over what those defaults are, not the web interface. > And my final `flamenco-manager.yaml` will have the blender variables empty. [...] If I updated the source attribute to be `input_path` instead of `default` it will generate a `flamenco-manager.yaml` with my blender variables as just "blender", as expected. Sure, but that's trying to work around an issue in the Manager API by changing the webapp. That's not the right way to go. I think you've uncovered two issues at once: 1. The current code has a bug, where it accepts values that lead to an invalid state. This should be fixed as a separate PR. It's probably best to return a `400 Bad Request` response in such a case. 2. There is no handling yet for `source="default"`, which should be added.
Mateus Abelli added 2 commits 2024-06-08 00:03:01 +02:00
Mateus Abelli added 1 commit 2024-06-08 02:44:08 +02:00
Author
Contributor

I had to keep the payload because if I only required the source field in BlenderPathCheckResult of flamenco-openapi.yaml It would cause errors on code that depended on the other fields, for example BlenderExecutable.IsUsable in meta.go

I don't like it and I wish to do it properly. I'd like to request your help again 😅

I had to keep the payload because if I only required the `source` field in `BlenderPathCheckResult` of [flamenco-openapi.yaml](https://projects.blender.org/studio/flamenco/src/branch/main/pkg/api/flamenco-openapi.yaml) It would cause errors on code that depended on the other fields, for example `BlenderExecutable.IsUsable` in [meta.go](https://projects.blender.org/studio/flamenco/src/branch/main/internal/manager/api_impl/meta.go) I don't like it and I wish to do it properly. I'd like to request your help again 😅

It would cause errors on code that depended on the other fields, for example BlenderExecutable.IsUsable in meta.go

What errors?

> It would cause errors on code that depended on the other fields, for example `BlenderExecutable.IsUsable` in [meta.go](https://projects.blender.org/studio/flamenco/src/branch/main/internal/manager/api_impl/meta.go) What errors?
Author
Contributor

When I run make generate it gives me the errors below.
I assume it's because of fields that were previously required are now unable to be used as before.

mateus@ubuntu:~/workspace/flamenco$ make generate
make generate-go
make[1]: Entrando no diretório '/home/mateus/workspace/flamenco'
go generate ./pkg/api/...
go generate ./internal/...
# projects.blender.org/studio/flamenco/internal/manager/api_impl
../meta.go:232:11: cannot use command (variable of type string) as *string value in struct literal
../meta.go:238:20: cannot use "Blender could not be found" (untyped string constant) as *string value in assignment
../meta.go:240:20: cannot use fmt.Sprintf("There was an error running the command: %v", err) (value of type string) as *string value in assignment
../meta.go:242:23: cannot use true (untyped bool constant) as *bool value in assignment
../meta.go:243:19: cannot use checkResult.FoundLocation (variable of type string) as *string value in assignment
../meta.go:244:20: cannot use fmt.Sprintf("Found %v", checkResult.BlenderVersion) (value of type string) as *string value in assignment
../meta.go:248:16: cannot use response.Input (variable of type *string) as string value in argument to logger.Info().Str
../meta.go:249:24: cannot use response.Path (variable of type *string) as string value in argument to logger.Info().Str("input", response.Input).Str
../meta.go:250:17: cannot use response.Cause (variable of type *string) as string value in argument to logger.Info().Str("input", response.Input).Str("foundLocation", response.Path).Str
../meta.go:251:20: cannot use response.IsUsable (variable of type *bool) as bool value in argument to logger.Info().Str("input", response.Input).Str("foundLocation", response.Path).Str("result", response.Cause).Bool
../meta.go:251:20: too many errors
# projects.blender.org/studio/flamenco/internal/manager/api_impl
../meta.go:232:11: cannot use command (variable of type string) as *string value in struct literal
../meta.go:238:20: cannot use "Blender could not be found" (untyped string constant) as *string value in assignment
../meta.go:240:20: cannot use fmt.Sprintf("There was an error running the command: %v", err) (value of type string) as *string value in assignment
../meta.go:242:23: cannot use true (untyped bool constant) as *bool value in assignment
../meta.go:243:19: cannot use checkResult.FoundLocation (variable of type string) as *string value in assignment
../meta.go:244:20: cannot use fmt.Sprintf("Found %v", checkResult.BlenderVersion) (value of type string) as *string value in assignment
../meta.go:248:16: cannot use response.Input (variable of type *string) as string value in argument to logger.Info().Str
../meta.go:249:24: cannot use response.Path (variable of type *string) as string value in argument to logger.Info().Str("input", response.Input).Str
../meta.go:250:17: cannot use response.Cause (variable of type *string) as string value in argument to logger.Info().Str("input", response.Input).Str("foundLocation", response.Path).Str
../meta.go:251:20: cannot use response.IsUsable (variable of type *bool) as bool value in argument to logger.Info().Str("input", response.Input).Str("foundLocation", response.Path).Str("result", response.Cause).Bool
../meta.go:251:20: too many errors
prog.go:12:2: no required module provides package github.com/golang/mock/mockgen/model: go.mod file not found in current directory or any parent directory; see 'go help modules'
prog.go:14:2: no required module provides package projects.blender.org/studio/flamenco/internal/manager/api_impl: go.mod file not found in current directory or any parent directory; see 'go help modules'
2024/06/13 20:28:19 Loading input failed: exit status 1
exit status 1
internal/manager/api_impl/interfaces.go:30: running "go": exit status 1
make[1]: *** [Makefile:132: generate-go] Erro 1
make[1]: Saindo do diretório '/home/mateus/workspace/flamenco'
make: *** [Makefile:126: generate] Erro 2
When I run `make generate` it gives me the errors below. I assume it's because of fields that were previously required are now unable to be used as before. ```bash mateus@ubuntu:~/workspace/flamenco$ make generate make generate-go make[1]: Entrando no diretório '/home/mateus/workspace/flamenco' go generate ./pkg/api/... go generate ./internal/... # projects.blender.org/studio/flamenco/internal/manager/api_impl ../meta.go:232:11: cannot use command (variable of type string) as *string value in struct literal ../meta.go:238:20: cannot use "Blender could not be found" (untyped string constant) as *string value in assignment ../meta.go:240:20: cannot use fmt.Sprintf("There was an error running the command: %v", err) (value of type string) as *string value in assignment ../meta.go:242:23: cannot use true (untyped bool constant) as *bool value in assignment ../meta.go:243:19: cannot use checkResult.FoundLocation (variable of type string) as *string value in assignment ../meta.go:244:20: cannot use fmt.Sprintf("Found %v", checkResult.BlenderVersion) (value of type string) as *string value in assignment ../meta.go:248:16: cannot use response.Input (variable of type *string) as string value in argument to logger.Info().Str ../meta.go:249:24: cannot use response.Path (variable of type *string) as string value in argument to logger.Info().Str("input", response.Input).Str ../meta.go:250:17: cannot use response.Cause (variable of type *string) as string value in argument to logger.Info().Str("input", response.Input).Str("foundLocation", response.Path).Str ../meta.go:251:20: cannot use response.IsUsable (variable of type *bool) as bool value in argument to logger.Info().Str("input", response.Input).Str("foundLocation", response.Path).Str("result", response.Cause).Bool ../meta.go:251:20: too many errors # projects.blender.org/studio/flamenco/internal/manager/api_impl ../meta.go:232:11: cannot use command (variable of type string) as *string value in struct literal ../meta.go:238:20: cannot use "Blender could not be found" (untyped string constant) as *string value in assignment ../meta.go:240:20: cannot use fmt.Sprintf("There was an error running the command: %v", err) (value of type string) as *string value in assignment ../meta.go:242:23: cannot use true (untyped bool constant) as *bool value in assignment ../meta.go:243:19: cannot use checkResult.FoundLocation (variable of type string) as *string value in assignment ../meta.go:244:20: cannot use fmt.Sprintf("Found %v", checkResult.BlenderVersion) (value of type string) as *string value in assignment ../meta.go:248:16: cannot use response.Input (variable of type *string) as string value in argument to logger.Info().Str ../meta.go:249:24: cannot use response.Path (variable of type *string) as string value in argument to logger.Info().Str("input", response.Input).Str ../meta.go:250:17: cannot use response.Cause (variable of type *string) as string value in argument to logger.Info().Str("input", response.Input).Str("foundLocation", response.Path).Str ../meta.go:251:20: cannot use response.IsUsable (variable of type *bool) as bool value in argument to logger.Info().Str("input", response.Input).Str("foundLocation", response.Path).Str("result", response.Cause).Bool ../meta.go:251:20: too many errors prog.go:12:2: no required module provides package github.com/golang/mock/mockgen/model: go.mod file not found in current directory or any parent directory; see 'go help modules' prog.go:14:2: no required module provides package projects.blender.org/studio/flamenco/internal/manager/api_impl: go.mod file not found in current directory or any parent directory; see 'go help modules' 2024/06/13 20:28:19 Loading input failed: exit status 1 exit status 1 internal/manager/api_impl/interfaces.go:30: running "go": exit status 1 make[1]: *** [Makefile:132: generate-go] Erro 1 make[1]: Saindo do diretório '/home/mateus/workspace/flamenco' make: *** [Makefile:126: generate] Erro 2 ```

That's to be expected. You'll have to make the existing code work with the now-optional fields. Basically the code handling any other case than source="default" needs to check that the pointers are not nil and return errors when they are.

That's to be expected. You'll have to make the existing code work with the now-optional fields. Basically the code handling any other case than `source="default"` needs to check that the pointers are not `nil` and return errors when they are.
Mateus Abelli added 5 commits 2024-06-22 01:00:39 +02:00
Mateus Abelli added 2 commits 2024-06-23 22:49:43 +02:00
Author
Contributor

Hi, I updated the code and decided to keep the payload to just { source: 'default', isUsable: true } because of the many checks present in both frontend and API implementation layer that will not allow us to proceed without isUsable being a truthy value.

When I was fixing the meta_test.go file I created an anonymous struct to serve as a mocked payload so that I could feed the newly converted pointer fields. I'm not sure if this is the proper way of doing, so please let me know.

I also encountered an edge case on a if statement in SaveSetupAssistantConfig from meta.go. The problem was that, at least in Go it will stop evaluating our conditions as soon as it gets satisfied first, so a long conditional chain won't go deep if the first link yields something.

To solve it, I created two boolean variables isConfigInvalid, responsible to check if we have core fields and isConfigIncomplete to check if optional fields were not supplied in the payload.

So the operation is the following, check first if we have a valid config, using Go's principle mentioned above, evaluate and fail immediately if we don't, OR (assuming it is valid) if source is not 'default' and we don't have the optional fields, fail as well.

I believe that this should also cover the bug where it accepts values that lead to an invalid state, but please let me know what do you think and if it makes sense.

Hi, I updated the code and decided to keep the payload to just `{ source: 'default', isUsable: true }` because of the many checks present in both frontend and API implementation layer that will not allow us to proceed without `isUsable` being a truthy value. When I was fixing the `meta_test.go` file I created an anonymous struct to serve as a mocked payload so that I could feed the newly converted pointer fields. I'm not sure if this is the proper way of doing, so please let me know. I also encountered an edge case on a if statement in `SaveSetupAssistantConfig` from `meta.go`. The problem was that, at least in `Go` it will stop evaluating our conditions as soon as it gets satisfied first, so a long conditional chain won't go deep if the first link yields something. To solve it, I created two boolean variables `isConfigInvalid`, responsible to check if we have core fields and `isConfigIncomplete` to check if optional fields were not supplied in the payload. So the operation is the following, check first if we have a valid config, using Go's principle mentioned above, evaluate and fail immediately if we don't, OR (assuming it is valid) if source is not 'default' and we don't have the optional fields, fail as well. I believe that this should also cover the bug where it accepts values that lead to an invalid state, but please let me know what do you think and if it makes sense.
Sybren A. Stüvel requested changes 2024-06-27 15:41:42 +02:00
Dismissed
Sybren A. Stüvel left a comment
Owner

Hi, I updated the code and decided to keep the payload to just { source: 'default', isUsable: true } because of the many checks present in both frontend and API implementation layer that will not allow us to proceed without isUsable being a truthy value.

👍

When I was fixing the meta_test.go file I created an anonymous struct to serve as a mocked payload so that I could feed the newly converted pointer fields. I'm not sure if this is the proper way of doing, so please let me know.

This is not the right way to go. Go doesn't allow you to take the address of a literal, but you can use a little trick to make it almost work that way: have a function return a pointer to its argument. In some of the test files you'll already see this little function:

func ptr[T any](value T) *T {
	return &value
}

which makes it possible to say somePointer = ptr("some literal"). This function should only be used in tests, though, as it's a bit of a hacky one. For non-test code, I think this would be better:

case errors.Is(err, exec.ErrNotFound): 
    cause := "Blender could not be found" 
    response.Cause = &cause

I also encountered an edge case on a if statement in SaveSetupAssistantConfig from meta.go. The problem was that, at least in Go it will stop evaluating our conditions as soon as it gets satisfied first, so a long conditional chain won't go deep if the first link yields something.

This is common, it's in pretty much every programming language I know of. It's called 'short-circuiting'.

To solve it, I created two boolean variables isConfigInvalid, responsible to check if we have core fields and isConfigIncomplete to check if optional fields were not supplied in the payload.

So the operation is the following, check first if we have a valid config, using Go's principle mentioned above, evaluate and fail immediately if we don't, OR (assuming it is valid) if source is not 'default' and we don't have the optional fields, fail as well.

The code now got a bit hairy, as it tries to mix two quite different cases into one.

  • default: the input and path fields should not be given.
  • any other: these fields should be given.

Mixing those cases into one makes it harder to write the conditions, so it's better to split them out.

Another complication is that, now that so many of the fields are optional, there's two different "no value" states: nil or "", and both should be handled as "no information". It's not enough to just test for nil.

So with those complications, do you still want to keep those fields optional? Or make them required and have explicit empty strings there? I think it'll make a lot of the code easier.

You could also investigate keeping them optional and giving them an empty string as default value in the flamenco-openapi.yaml, but I think that won't be reflected in the generated Go code (it'll likely still be pointers).

> Hi, I updated the code and decided to keep the payload to just `{ source: 'default', isUsable: true }` because of the many checks present in both frontend and API implementation layer that will not allow us to proceed without `isUsable` being a truthy value. :+1: > When I was fixing the `meta_test.go` file I created an anonymous struct to serve as a mocked payload so that I could feed the newly converted pointer fields. I'm not sure if this is the proper way of doing, so please let me know. This is not the right way to go. Go doesn't allow you to take the address of a literal, but you can use a little trick to make it almost work that way: have a function return a pointer to its argument. In some of the test files you'll already see this little function: ```go func ptr[T any](value T) *T { return &value } ``` which makes it possible to say `somePointer = ptr("some literal")`. This function should only be used in tests, though, as it's a bit of a hacky one. For non-test code, I think this would be better: ```go case errors.Is(err, exec.ErrNotFound): cause := "Blender could not be found" response.Cause = &cause ``` > I also encountered an edge case on a if statement in `SaveSetupAssistantConfig` from `meta.go`. The problem was that, at least in `Go` it will stop evaluating our conditions as soon as it gets satisfied first, so a long conditional chain won't go deep if the first link yields something. This is common, it's in pretty much every programming language I know of. It's called 'short-circuiting'. > To solve it, I created two boolean variables `isConfigInvalid`, responsible to check if we have core fields and `isConfigIncomplete` to check if optional fields were not supplied in the payload. > > So the operation is the following, check first if we have a valid config, using Go's principle mentioned above, evaluate and fail immediately if we don't, OR (assuming it is valid) if source is not 'default' and we don't have the optional fields, fail as well. The code now got a bit hairy, as it tries to mix two quite different cases into one. - `default`: the `input` and `path` fields should *not* be given. - any other: these fields should be given. Mixing those cases into one makes it harder to write the conditions, so it's better to split them out. Another complication is that, now that so many of the fields are optional, there's two different "no value" states: `nil` or `""`, and both should be handled as "no information". It's not enough to just test for `nil`. So with those complications, do you still want to keep those fields optional? Or make them required and have explicit empty strings there? I think it'll make a lot of the code easier. You could also investigate keeping them optional and giving them an empty string as default value in the `flamenco-openapi.yaml`, but I think that won't be reflected in the generated Go code (it'll likely still be pointers).
Mateus Abelli added 7 commits 2024-06-29 16:54:28 +02:00
Author
Contributor

This is not the right way to go. Go doesn't allow you to take the address of a literal, but you can use a little trick to make it almost work that way: have a function return a pointer to its argument. In some of the test files you'll already see this little function:

func ptr[T any](value T) *T {
	return &value
}

which makes it possible to say somePointer = ptr("some literal"). This function should only be used in tests, though, as it's a bit of a hacky one. For non-test code, I think this would be better:

case errors.Is(err, exec.ErrNotFound): 
    cause := "Blender could not be found" 
    response.Cause = &cause

👍 That's interesting, thanks for the trick!

The code now got a bit hairy, as it tries to mix two quite different cases into one.

  • default: the input and path fields should not be given.
  • any other: these fields should be given.

Mixing those cases into one makes it harder to write the conditions, so it's better to split them out.

I've tried splitting them as suggested so I'd have a bool variable to check if source is default and if we do (not) have input and path.
Then, I had a second variable to check if anything else that was not of source default had those fields in there.
So the code was something like this:

// default: the input and path fields should not be given.
myVar1 := setupAssistantCfg.BlenderExecutable.Source == "default" &&
		setupAssistantCfg.BlenderExecutable.Path != "" ||
		setupAssistantCfg.BlenderExecutable.Input != ""

// any other: these fields should be given.
myVar2 := setupAssistantCfg.BlenderExecutable.Source != "default" &&
		setupAssistantCfg.BlenderExecutable.Path == "" ||
		setupAssistantCfg.BlenderExecutable.Input == ""

So far it was simple, however what to do with the other core fields such as StorageLocation and IsUsable, I could include them in the vars from the example above or have a third one for checking them but every time I played around the code started look less and less comfortable due to the short-circuiting nature of the language.

This is a core validation, we cannot no matter what, allow it proceed without them, so duplicating it to use with both vars above didn't looked very nice.

myVar3 := setupAssistantCfg.StorageLocation == "" ||
		!setupAssistantCfg.BlenderExecutable.IsUsable

To fix it, I used the original validation code and extracted only the parts that checked path and included input so that both could be later be evaluated against source being default or not:

	isConfigIncomplete := setupAssistantCfg.BlenderExecutable.Path == "" ||
		setupAssistantCfg.BlenderExecutable.Input == ""

	if setupAssistantCfg.StorageLocation == "" ||
		!setupAssistantCfg.BlenderExecutable.IsUsable ||
		isConfigIncomplete && setupAssistantCfg.BlenderExecutable.Source != "default" {
		logger.Warn().Msg("setup assistant: configuration is incomplete, unable to accept")
		return sendAPIError(e, http.StatusBadRequest, "configuration is incomplete")
	}

It works as expected and it doesn't mind if path or input is provided when sending default as the source, because later the switch will completely ignore those fields and set a executable to just blender.

The problem is that now that I check if input is being given a test is failing, this one:

	// Test situation where file association with .blend files resulted in a blender executable.
	{
		savedConfig := doTest(api.SetupAssistantConfig{
			StorageLocation: mf.tempdir,
			BlenderExecutable: api.BlenderPathCheckResult{
				IsUsable: true,
				Input:    "",
				Path:     "/path/to/blender",
				Source:   api.BlenderPathSourceFileAssociation,
			},
		})
       [ ... ]
	}

Another complication is that, now that so many of the fields are optional, there's two different "no value" states: nil or "", and both should be handled as "no information". It's not enough to just test for nil.

👍 It was a learning mistake, I thought that in theory since Go initialize it's variables such as string to "" it would also initialize a *string to "" and consider that being nil, so "" == nil but that's not correct.

So with those complications, do you still want to keep those fields optional? Or make them required and have explicit empty strings there? I think it'll make a lot of the code easier.

Nope, I reverted my commits. I was going through a spiral that didn't seem to improve too much of the code, I'd also prefer to keep them explicit empty strings and document it somewhere that for source default the payload should be like that.

You could also investigate keeping them optional and giving them an empty string as default value in the flamenco-openapi.yaml, but I think that won't be reflected in the generated Go code (it'll likely still be pointers).

I did attempted to have them with default values, but after running the generator, I still had pointers like you said.

> This is not the right way to go. Go doesn't allow you to take the address of a literal, but you can use a little trick to make it almost work that way: have a function return a pointer to its argument. In some of the test files you'll already see this little function: > > ```go > func ptr[T any](value T) *T { > return &value > } > ``` > which makes it possible to say `somePointer = ptr("some literal")`. This function should only be used in tests, though, as it's a bit of a hacky one. For non-test code, I think this would be better: > > ```go > case errors.Is(err, exec.ErrNotFound): > cause := "Blender could not be found" > response.Cause = &cause > ``` 👍 That's interesting, thanks for the trick! > The code now got a bit hairy, as it tries to mix two quite different cases into one. > > - `default`: the `input` and `path` fields should *not* be given. > - any other: these fields should be given. > > Mixing those cases into one makes it harder to write the conditions, so it's better to split them out. I've tried splitting them as suggested so I'd have a bool variable to check if source is `default` and if we do (not) have `input` and `path`. Then, I had a second variable to check if anything else that was not of source `default` had those fields in there. So the code was something like this: ```go // default: the input and path fields should not be given. myVar1 := setupAssistantCfg.BlenderExecutable.Source == "default" && setupAssistantCfg.BlenderExecutable.Path != "" || setupAssistantCfg.BlenderExecutable.Input != "" // any other: these fields should be given. myVar2 := setupAssistantCfg.BlenderExecutable.Source != "default" && setupAssistantCfg.BlenderExecutable.Path == "" || setupAssistantCfg.BlenderExecutable.Input == "" ``` So far it was simple, however what to do with the other core fields such as `StorageLocation` and `IsUsable`, I could include them in the vars from the example above or have a third one for checking them but every time I played around the code started look less and less comfortable due to the short-circuiting nature of the language. This is a core validation, we cannot no matter what, allow it proceed without them, so duplicating it to use with both vars above didn't looked very nice. ```go myVar3 := setupAssistantCfg.StorageLocation == "" || !setupAssistantCfg.BlenderExecutable.IsUsable ``` To fix it, I used the original validation code and extracted only the parts that checked `path` and included `input` so that both could be later be evaluated against source being `default` or not: ```go isConfigIncomplete := setupAssistantCfg.BlenderExecutable.Path == "" || setupAssistantCfg.BlenderExecutable.Input == "" if setupAssistantCfg.StorageLocation == "" || !setupAssistantCfg.BlenderExecutable.IsUsable || isConfigIncomplete && setupAssistantCfg.BlenderExecutable.Source != "default" { logger.Warn().Msg("setup assistant: configuration is incomplete, unable to accept") return sendAPIError(e, http.StatusBadRequest, "configuration is incomplete") } ``` It works as expected and it doesn't mind if `path` or `input` is provided when sending `default` as the source, because later the switch will completely ignore those fields and set a executable to just `blender`. The problem is that now that I check if `input` is being given a test is failing, this one: ```go // Test situation where file association with .blend files resulted in a blender executable. { savedConfig := doTest(api.SetupAssistantConfig{ StorageLocation: mf.tempdir, BlenderExecutable: api.BlenderPathCheckResult{ IsUsable: true, Input: "", Path: "/path/to/blender", Source: api.BlenderPathSourceFileAssociation, }, }) [ ... ] } ``` > Another complication is that, now that so many of the fields are optional, there's two different "no value" states: `nil` or `""`, and both should be handled as "no information". It's not enough to just test for `nil`. 👍 It was a learning mistake, I thought that in theory since `Go` initialize it's variables such as `string` to `""` it would also initialize a `*string` to `""` and consider that being `nil`, so `""` == `nil` but that's not correct. > So with those complications, do you still want to keep those fields optional? Or make them required and have explicit empty strings there? I think it'll make a lot of the code easier. Nope, I reverted my commits. I was going through a spiral that didn't seem to improve too much of the code, I'd also prefer to keep them explicit empty strings and document it somewhere that for source `default` the payload should be like that. > You could also investigate keeping them optional and giving them an empty string as default value in the `flamenco-openapi.yaml`, but I think that won't be reflected in the generated Go code (it'll likely still be pointers). I did attempted to have them with default values, but after running the generator, I still had pointers like you said.

I've tried splitting them as suggested so I'd have a bool variable to check if source is default and if we do (not) have input and path.
Then, I had a second variable to check if anything else that was not of source default had those fields in there.
So the code was something like this:

// default: the input and path fields should not be given.
myVar1 := setupAssistantCfg.BlenderExecutable.Source == "default" &&
		setupAssistantCfg.BlenderExecutable.Path != "" ||
		setupAssistantCfg.BlenderExecutable.Input != ""

Try not to mix && and || in the same expression without using parentheses. (A && B) || C is quite different from A && (B || C), and writing A && B || C is somewhat ambiguous. I'm sure Go has a way to evaluate this (probably left-to-right), but since it's so easy to make a mistake here, I would always recommend parentheses.

So far it was simple, however what to do with the other core fields such as StorageLocation and IsUsable, I could include them in the vars from the example above or have a third one for checking them but every time I played around the code started look less and less comfortable due to the short-circuiting nature of the language.

I think this is not due to the short-circuiting, but rather due to the approach of trying to put everything into one condition. Extracting this into a different function will make things considerably easier, because then you can do things like this (warning, I didn't check this, just wrote it here in this comment):

func isBlenderPathCheckResultComplete(checkResult BlenderPathCheckResult) bool {
  switch (checkResult.Source) {
    case api.BlenderPathSourceDefault:
      return true
    case api.BlenderPathSourceFileAssociation:
      return // other condition here
    case // ... and so on, maybe combine multiple cases together if they have the same logic
  }

  // maybe log something here about unknown / unexpected values
  return false
}

👍 It was a learning mistake, I thought that in theory since Go initialize it's variables such as string to "" it would also initialize a *string to "" and consider that being nil, so "" == nil but that's not correct.

There is very little magic in Go, which is a good thing. It's a simple language, and it won't make assumptions for you.

Nope, I reverted my commits. I was going through a spiral that didn't seem to improve too much of the code, I'd also prefer to keep them explicit empty strings and document it somewhere that for source default the payload should be like that.

👍

> I've tried splitting them as suggested so I'd have a bool variable to check if source is `default` and if we do (not) have `input` and `path`. > Then, I had a second variable to check if anything else that was not of source `default` had those fields in there. > So the code was something like this: > > ```go > // default: the input and path fields should not be given. > myVar1 := setupAssistantCfg.BlenderExecutable.Source == "default" && > setupAssistantCfg.BlenderExecutable.Path != "" || > setupAssistantCfg.BlenderExecutable.Input != "" Try not to mix `&&` and `||` in the same expression without using parentheses. `(A && B) || C` is quite different from `A && (B || C)`, and writing `A && B || C` is somewhat ambiguous. I'm sure Go has a way to evaluate this (probably left-to-right), but since it's so easy to make a mistake here, I would always recommend parentheses. > So far it was simple, however what to do with the other core fields such as `StorageLocation` and `IsUsable`, I could include them in the vars from the example above or have a third one for checking them but every time I played around the code started look less and less comfortable due to the short-circuiting nature of the language. I think this is not due to the short-circuiting, but rather due to the approach of trying to put everything into one condition. Extracting this into a different function will make things considerably easier, because then you can do things like this (warning, I didn't check this, just wrote it here in this comment): ```go func isBlenderPathCheckResultComplete(checkResult BlenderPathCheckResult) bool { switch (checkResult.Source) { case api.BlenderPathSourceDefault: return true case api.BlenderPathSourceFileAssociation: return // other condition here case // ... and so on, maybe combine multiple cases together if they have the same logic } // maybe log something here about unknown / unexpected values return false } ``` > 👍 It was a learning mistake, I thought that in theory since `Go` initialize it's variables such as `string` to `""` it would also initialize a `*string` to `""` and consider that being `nil`, so `""` == `nil` but that's not correct. There is very little magic in Go, which is a good thing. It's a simple language, and it won't make assumptions for you. > Nope, I reverted my commits. I was going through a spiral that didn't seem to improve too much of the code, I'd also prefer to keep them explicit empty strings and document it somewhere that for source `default` the payload should be like that. :+1:
Mateus Abelli added 1 commit 2024-07-03 00:51:18 +02:00
Mateus Abelli added 2 commits 2024-07-03 00:57:09 +02:00
Mateus Abelli added 1 commit 2024-07-04 01:56:08 +02:00
Author
Contributor

Try not to mix && and || in the same expression without using parentheses. (A && B) || C is quite different from A && (B || C), and writing A && B || C is somewhat ambiguous. I'm sure Go has a way to evaluate this (probably left-to-right), but since it's so easy to make a mistake here, I would always recommend parentheses.

Got it, again another learning mistake of mine 😅 when they said that the language doesn't use parentheses I took it way too literally, should've tried it, but your solution below is much better.

I think this is not due to the short-circuiting, but rather due to the approach of trying to put everything into one condition. Extracting this into a different function will make things considerably easier, because then you can do things like this (warning, I didn't check this, just wrote it here in this comment):

func isBlenderPathCheckResultComplete(checkResult BlenderPathCheckResult) bool {
  switch (checkResult.Source) {
    case api.BlenderPathSourceDefault:
      return true
    case api.BlenderPathSourceFileAssociation:
      return // other condition here
    case // ... and so on, maybe combine multiple cases together if they have the same logic
  }

  // maybe log something here about unknown / unexpected values
  return false
}

I did some minor tweaks, the major one was switching the function for denial instead of acceptance to better match what we are doing in the if statement.

As we discussed earlier in Blender Chat, I've passed the logger and added it in case the payload doesn't contain any valid blender path source, so in this situation we log:

2024-07-03T20:49:45-03:00 WRN received an unexpected blender path source config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"blender"},"storageLocation":"/home/mateus/workspace/flamenco-storage"}

The above is an addition to what we already have in case a missing field is hit while using any valid blender path source:

2024-07-03T20:51:55-03:00 WRN setup assistant: configuration is incomplete, unable to accept config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"path_envvar"},"storageLocation":"/home/mateus/workspace/flamenco-storage"}
> Try not to mix `&&` and `||` in the same expression without using parentheses. `(A && B) || C` is quite different from `A && (B || C)`, and writing `A && B || C` is somewhat ambiguous. I'm sure Go has a way to evaluate this (probably left-to-right), but since it's so easy to make a mistake here, I would always recommend parentheses. Got it, again another learning mistake of mine 😅 when they said that the language doesn't use parentheses I took it way too literally, should've tried it, but your solution below is much better. > I think this is not due to the short-circuiting, but rather due to the approach of trying to put everything into one condition. Extracting this into a different function will make things considerably easier, because then you can do things like this (warning, I didn't check this, just wrote it here in this comment): > > ```go > func isBlenderPathCheckResultComplete(checkResult BlenderPathCheckResult) bool { > switch (checkResult.Source) { > case api.BlenderPathSourceDefault: > return true > case api.BlenderPathSourceFileAssociation: > return // other condition here > case // ... and so on, maybe combine multiple cases together if they have the same logic > } > > // maybe log something here about unknown / unexpected values > return false > } > ``` I did some minor tweaks, the major one was switching the function for denial instead of acceptance to better match what we are doing in the if statement. As we discussed earlier in Blender Chat, I've passed the logger and added it in case the payload doesn't contain any valid blender path source, so in this situation we log: ``` 2024-07-03T20:49:45-03:00 WRN received an unexpected blender path source config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"blender"},"storageLocation":"/home/mateus/workspace/flamenco-storage"} ``` The above is an addition to what we already have in case a missing field is hit while using any valid blender path source: ``` 2024-07-03T20:51:55-03:00 WRN setup assistant: configuration is incomplete, unable to accept config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"path_envvar"},"storageLocation":"/home/mateus/workspace/flamenco-storage"} ```
Sybren A. Stüvel requested changes 2024-07-05 08:28:29 +02:00
Dismissed
Sybren A. Stüvel left a comment
Owner

I did some minor tweaks, the major one was switching the function for denial instead of acceptance to better match what we are doing in the if statement.

I wouldn't necessarily write a function to suit where it's used. I'd write it to make it clear what it does, and to minimize the number of mental inversions you have to make. if !isComplete(...) is fine, if isIncomplete(...) is also fine, so at the condition it doesn't really matter.

		if !checkResult.IsUsable {
			return true
		}
		return false

Here we get into more confusing territory. Since checkResult is using positive terminology, but the function is checking for the negative, in order to understand it you constantly have to flip between those two perspectives in order to understand the code. I'd recommend keeping the terminology consistent, and just check the positive. So it's not just about the number of negations, it's also about the different perspectives the reader has to have.

Furthermore, that particular code can be shortened to return !checkResult.IsUsable, and without the negation, that just becomes return checkResult.IsUsable.

The above is an addition to what we already have in case a missing field is hit while using any valid blender path source:

2024-07-03T20:51:55-03:00 WRN setup assistant: configuration is incomplete, unable to accept config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"path_envvar"},"storageLocation":"/home/mateus/workspace/flamenco-storage"}

In such cases, try to include why Flamenco thinks the configuration is incomplete. As a design guideline, messages should guide the user towards a working solution. Just saying what is wrong doesn't help them.

Same goes for logger.Warn().Msg("received an unexpected blender path source"): when you say something has an unexpected value, show that value. Otherwise you're creating a hard-to-debug situation.

> I did some minor tweaks, the major one was switching the function for denial instead of acceptance to better match what we are doing in the if statement. I wouldn't necessarily write a function to suit where it's used. I'd write it to make it clear what it does, and to minimize the number of mental inversions you have to make. `if !isComplete(...)` is fine, `if isIncomplete(...)` is also fine, so at the condition it doesn't really matter. ```go if !checkResult.IsUsable { return true } return false ``` Here we get into more confusing territory. Since `checkResult` is using positive terminology, but the function is checking for the negative, in order to understand it you constantly have to flip between those two perspectives in order to understand the code. I'd recommend keeping the terminology consistent, and just check the positive. So it's not just about the number of negations, it's also about the different perspectives the reader has to have. Furthermore, that particular code can be shortened to `return !checkResult.IsUsable`, and without the negation, that just becomes `return checkResult.IsUsable`. > The above is an addition to what we already have in case a missing field is hit while using any valid blender path source: > ``` > 2024-07-03T20:51:55-03:00 WRN setup assistant: configuration is incomplete, unable to accept config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"path_envvar"},"storageLocation":"/home/mateus/workspace/flamenco-storage"} > ``` In such cases, try to include _why_ Flamenco thinks the configuration is incomplete. As a design guideline, messages should guide the user towards a working solution. Just saying what is wrong doesn't help them. Same goes for `logger.Warn().Msg("received an unexpected blender path source")`: when you say something has an unexpected value, show that value. Otherwise you're creating a hard-to-debug situation.
Sybren A. Stüvel reviewed 2024-07-05 08:32:08 +02:00
@ -339,0 +359,4 @@
return false
}
logger.Warn().Msg("received an unexpected blender path source")

This function could return an error instead of a boolean. That error can then not only be used to communicate what is still missing, but also cover this case. That'll avoid the need to pass a logger to the function.

This function could return an error instead of a boolean. That error can then not only be used to communicate what is still missing, but also cover this case. That'll avoid the need to pass a logger to the function.

Unfortunately Gitea does not support multiple comments for the same source line. So I'll reply to the previous one, even though at this line number there is now different code.

		return errors.New("unknown 'source' field value: " + string(config.BlenderExecutable.Source)) 

This should use fmt.Errorf() instead.

Unfortunately Gitea does not support multiple comments for the same source line. So I'll reply to the previous one, even though at this line number there is now different code. ``` return errors.New("unknown 'source' field value: " + string(config.BlenderExecutable.Source)) ``` This should use `fmt.Errorf()` instead.
abelli marked this conversation as resolved
Sybren A. Stüvel reviewed 2024-07-05 08:33:12 +02:00
@ -339,0 +340,4 @@
func isBlenderPathCheckResultIncomplete(checkResult api.BlenderPathCheckResult, logger zerolog.Logger) bool {
switch checkResult.Source {
case api.BlenderPathSourceDefault:
if !checkResult.IsUsable {

The check for IsUsable can be moved out of the switch since it's the same for every case anyway.

The check for `IsUsable` can be moved out of the `switch` since it's the same for every `case` anyway.
abelli marked this conversation as resolved
Mateus Abelli added 1 commit 2024-07-09 22:38:01 +02:00
Author
Contributor

I wouldn't necessarily write a function to suit where it's used. I'd write it to make it clear what it does, and to minimize the number of mental inversions you have to make. if !isComplete(...) is fine, if isIncomplete(...) is also fine, so at the condition it doesn't really matter.

👍 Thanks for the insight!

Here we get into more confusing territory. Since checkResult is using positive terminology, but the function is checking for the negative, in order to understand it you constantly have to flip between those two perspectives in order to understand the code. I'd recommend keeping the terminology consistent, and just check the positive. So it's not just about the number of negations, it's also about the different perspectives the reader has to have.

I can't deny the fact I did had to warp my head around while forcing the negative terminology, not something that I plan to repeat again, thanks you :)

Furthermore, that particular code can be shortened to return !checkResult.IsUsable, and without the negation, that just becomes return checkResult.IsUsable.

I did kept the check for isUsable in a if statement for better clarity as it now returns a nice message as suggested below.

In such cases, try to include why Flamenco thinks the configuration is incomplete. As a design guideline, messages should guide the user towards a working solution. Just saying what is wrong doesn't help them.

Got it, I wrote some better messages to guide the user, explaining why that particular payload was not accepted.
This is a demo for a few cases that I tested to show you how its looking now:

  • storageLocation is empty
2024-07-09T16:40:24-03:00 WRN setup assistant: configuration is invalid or incomplete, 'storageLocation' field must not be empty config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"default"},"storageLocation":""}
  • isUsable is false
2024-07-09T16:41:31-03:00 WRN setup assistant: configuration is invalid or incomplete, sources cannot have the 'is_usable' field set to false config={"blenderExecutable":{"cause":"","input":"","is_usable":false,"path":"","source":"default"},"storageLocation":"/home/mateus/workspace/flamenco-storage"}
  • Path and Input is empty while source is path_input:
2024-07-09T16:42:34-03:00 WRN setup assistant: configuration is invalid or incomplete, 'path' or 'input' fields must not be empty while using the 'input_path' or 'path_envvar' sources config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"input_path"},"storageLocation":"/home/mateus/workspace/flamenco-storage"}

Same goes for logger.Warn().Msg("received an unexpected blender path source"): when you say something has an unexpected value, show that value. Otherwise you're creating a hard-to-debug situation.

Done, sending an unknown source will receive this now:

2024-07-09T17:26:54-03:00 WRN setup assistant: configuration is invalid or incomplete, unknown 'source' field value: blah config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"blah"},"storageLocation":"/home/mateus/workspace/flamenco-storage"}

The function was refactored as well, now it returns an error as suggested. I adjusted everything trying to follow some practices already present in code nearby, the only thing that I wasn't too sure is how to properly produce the returned error as I could do a return fmt.Errorf("My message") or return errors.New("My message").

I also have a question regarding pointers, would it be a good practice if I passed the address &setupAssistantCfg to my function and worked with pointers inside of it? I became wondering a lot about this because we don't change anything on that function, we just check things and leave it alone.

> I wouldn't necessarily write a function to suit where it's used. I'd write it to make it clear what it does, and to minimize the number of mental inversions you have to make. `if !isComplete(...)` is fine, `if isIncomplete(...)` is also fine, so at the condition it doesn't really matter. 👍 Thanks for the insight! > Here we get into more confusing territory. Since `checkResult` is using positive terminology, but the function is checking for the negative, in order to understand it you constantly have to flip between those two perspectives in order to understand the code. I'd recommend keeping the terminology consistent, and just check the positive. So it's not just about the number of negations, it's also about the different perspectives the reader has to have. I can't deny the fact I did had to warp my head around while forcing the negative terminology, not something that I plan to repeat again, thanks you :) > Furthermore, that particular code can be shortened to `return !checkResult.IsUsable`, and without the negation, that just becomes `return checkResult.IsUsable`. I did kept the check for `isUsable` in a if statement for better clarity as it now returns a nice message as suggested below. > In such cases, try to include _why_ Flamenco thinks the configuration is incomplete. As a design guideline, messages should guide the user towards a working solution. Just saying what is wrong doesn't help them. Got it, I wrote some better messages to guide the user, explaining why that particular payload was not accepted. This is a demo for a few cases that I tested to show you how its looking now: - **storageLocation** is empty ``` 2024-07-09T16:40:24-03:00 WRN setup assistant: configuration is invalid or incomplete, 'storageLocation' field must not be empty config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"default"},"storageLocation":""} ``` - **isUsable** is false ``` 2024-07-09T16:41:31-03:00 WRN setup assistant: configuration is invalid or incomplete, sources cannot have the 'is_usable' field set to false config={"blenderExecutable":{"cause":"","input":"","is_usable":false,"path":"","source":"default"},"storageLocation":"/home/mateus/workspace/flamenco-storage"} ``` - **Path** and **Input** is empty while source is **path_input**: ``` 2024-07-09T16:42:34-03:00 WRN setup assistant: configuration is invalid or incomplete, 'path' or 'input' fields must not be empty while using the 'input_path' or 'path_envvar' sources config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"input_path"},"storageLocation":"/home/mateus/workspace/flamenco-storage"} ``` > Same goes for `logger.Warn().Msg("received an unexpected blender path source")`: when you say something has an unexpected value, show that value. Otherwise you're creating a hard-to-debug situation. Done, sending an unknown source will receive this now: ``` 2024-07-09T17:26:54-03:00 WRN setup assistant: configuration is invalid or incomplete, unknown 'source' field value: blah config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"blah"},"storageLocation":"/home/mateus/workspace/flamenco-storage"} ``` The function was refactored as well, now it returns an error as suggested. I adjusted everything trying to follow some practices already present in code nearby, the only thing that I wasn't too sure is how to properly produce the returned error as I could do a `return fmt.Errorf("My message")` or `return errors.New("My message")`. I also have a question regarding pointers, would it be a good practice if I passed the address `&setupAssistantCfg` to my function and worked with pointers inside of it? I became wondering a lot about this because we don't change anything on that function, we just check things and leave it alone.

Nice, this is getting more hand-hold'y, I like it.

"configuration is invalid or incomplete" -- I think that can just become "configuration is incomplete". It's maybe slightly less correct, but it is simpler to read, and also conveys "this cannot be used as-is", which is the most important part.

  • storageLocation is empty
2024-07-09T16:40:24-03:00 WRN setup assistant: configuration is invalid or incomplete, 'storageLocation' field must not be empty config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"default"},"storageLocation":""}

👍

  • isUsable is false
2024-07-09T16:41:31-03:00 WRN setup assistant: configuration is invalid or incomplete, sources cannot have the 'is_usable' field set to false config={"blenderExecutable":{"cause":"","input":"","is_usable":false,"path":"","source":"default"},"storageLocation":"/home/mateus/workspace/flamenco-storage"}

"sources cannot have the 'is_usable' field set to false" is, strictly speaking, cause for confusion. Because if it *cannot be set to false, why is it complaining that it is false? Apparently it is possible for it to be false. Change "cannot" to "should not", that'll correct things.

  • Path and Input is empty while source is path_input:
2024-07-09T16:42:34-03:00 WRN setup assistant: configuration is invalid or incomplete, 'path' or 'input' fields must not be empty while using the 'input_path' or 'path_envvar' sources config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"input_path"},"storageLocation":"/home/mateus/workspace/flamenco-storage"}

👍

Done, sending an unknown source will receive this now:

2024-07-09T17:26:54-03:00 WRN setup assistant: configuration is invalid or incomplete, unknown 'source' field value: blah config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"blah"},"storageLocation":"/home/mateus/workspace/flamenco-storage"}

👍

the only thing that I wasn't too sure is how to properly produce the returned error as I could do a return fmt.Errorf("My message") or return errors.New("My message").

As a rule of thumb: if you don't need to format a string, don't use the fmt package. In that case you can actually declare those errors as package-level variables, and just return those.

var ErrConfigIncompleteUnknownSource = errors.New("...")

The caller can then use them to distinguish between various errors, if they want to, via errors.Is(err, ErrConfigIncompleteUnknownSource).

And maybe that name is a bit on the long/verbose side. Use your own judgement to see what's the best name, given the package context and the other errors.

I also have a question regarding pointers, would it be a good practice if I passed the address &setupAssistantCfg to my function and worked with pointers inside of it? I became wondering a lot about this because we don't change anything on that function, we just check things and leave it alone.

Copies are generally cheap. Go (unfortunately) doesn't have 'const pointer' or 'const reference' concepts, so the easiest way to ensure that a function doesn't change the data held by the caller is to pass that data by value.

Nice, this is getting more hand-hold'y, I like it. "configuration is invalid or incomplete" -- I think that can just become "configuration is incomplete". It's maybe slightly less correct, but it is simpler to read, and also conveys "this cannot be used as-is", which is the most important part. > - **storageLocation** is empty > ``` > 2024-07-09T16:40:24-03:00 WRN setup assistant: configuration is invalid or incomplete, 'storageLocation' field must not be empty config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"default"},"storageLocation":""} > ``` :+1: > - **isUsable** is false > ``` > 2024-07-09T16:41:31-03:00 WRN setup assistant: configuration is invalid or incomplete, sources cannot have the 'is_usable' field set to false config={"blenderExecutable":{"cause":"","input":"","is_usable":false,"path":"","source":"default"},"storageLocation":"/home/mateus/workspace/flamenco-storage"} > ``` "sources cannot have the 'is_usable' field set to false" is, strictly speaking, cause for confusion. Because if it **cannot* be set to `false`, why is it complaining that it is `false`? Apparently it is possible for it to be `false`. Change "cannot" to "should not", that'll correct things. > - **Path** and **Input** is empty while source is **path_input**: > ``` > 2024-07-09T16:42:34-03:00 WRN setup assistant: configuration is invalid or incomplete, 'path' or 'input' fields must not be empty while using the 'input_path' or 'path_envvar' sources config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"input_path"},"storageLocation":"/home/mateus/workspace/flamenco-storage"} > ``` :+1: > Done, sending an unknown source will receive this now: > ``` > 2024-07-09T17:26:54-03:00 WRN setup assistant: configuration is invalid or incomplete, unknown 'source' field value: blah config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"blah"},"storageLocation":"/home/mateus/workspace/flamenco-storage"} > ``` :+1: > the only thing that I wasn't too sure is how to properly produce the returned error as I could do a `return fmt.Errorf("My message")` or `return errors.New("My message")`. As a rule of thumb: if you don't need to format a string, don't use the `fmt` package. In that case you can actually declare those errors as package-level variables, and just return those. ```go var ErrConfigIncompleteUnknownSource = errors.New("...") ``` The caller can then use them to distinguish between various errors, if they want to, via `errors.Is(err, ErrConfigIncompleteUnknownSource)`. And maybe that name is a bit on the long/verbose side. Use your own judgement to see what's the best name, given the package context and the other errors. > I also have a question regarding pointers, would it be a good practice if I passed the address `&setupAssistantCfg` to my function and worked with pointers inside of it? I became wondering a lot about this because we don't change anything on that function, we just check things and leave it alone. Copies are generally cheap. Go (unfortunately) doesn't have 'const pointer' or 'const reference' concepts, so the easiest way to ensure that a function doesn't change the data held by the caller is to pass that data by value.
Sybren A. Stüvel requested changes 2024-07-11 10:42:16 +02:00
Dismissed
Sybren A. Stüvel left a comment
Owner

Things are improving well!

Things are improving well!
@ -271,2 +268,2 @@
logger.Warn().Msg("setup assistant: configuration is incomplete, unable to accept")
return sendAPIError(e, http.StatusBadRequest, "configuration is incomplete")
if err := checkSetupAssistantConfig(setupAssistantCfg); err != nil {
logger.Warn().Msg("setup assistant: configuration is invalid or incomplete, " + err.Error())

The work cannot be done, and so this should be logged as an error, not a warning.

I usually log errors as a 'cause' field:

logger.Error().AnErr("cause", err).Msg("setup assistant: configuration is incomplete")

Alternatively, if you want to keep the error message as part of the log message, you could use .Msgf(...) instead:

logger.Error().Msgf("setup assistant: configuration is incomplete, %v", err)

%v is Go's generic "put the thing's value here", and should result in the value of err.Error().

The work cannot be done, and so this should be logged as an error, not a warning. I usually log errors as a 'cause' field: ```go logger.Error().AnErr("cause", err).Msg("setup assistant: configuration is incomplete") ``` Alternatively, if you want to keep the error message as part of the log message, you could use `.Msgf(...)` instead: ```go logger.Error().Msgf("setup assistant: configuration is incomplete, %v", err) ``` `%v` is Go's generic "put the thing's value here", and should result in the value of `err.Error()`.
abelli marked this conversation as resolved
@ -272,1 +268,3 @@
return sendAPIError(e, http.StatusBadRequest, "configuration is incomplete")
if err := checkSetupAssistantConfig(setupAssistantCfg); err != nil {
logger.Warn().Msg("setup assistant: configuration is invalid or incomplete, " + err.Error())
return sendAPIError(e, http.StatusBadRequest, "configuration is invalid or incomplete")

It's fine to include the actual error message in the response as well, so that the caller knows what's wrong without checking the Manager log.

It's fine to include the actual error message in the response as well, so that the caller knows what's wrong without checking the Manager log.
abelli marked this conversation as resolved
@ -339,0 +358,4 @@
config.BlenderExecutable.Input == "" {
return errors.New("'path' or 'input' fields must not be empty while using the 'input_path' or 'path_envvar' sources")
}
default:

Add another case "". That way the dynamically generated error below doesn't just show "unknown 'source' field value: " if the source field is empty.

Another solution would be to use the %q format specifier in the below-suggested call to fmt.Errorf(), but that would always put quotes around the value. If that error is then subsequently logged with something like log.Error().Err(err).Msg("whatever"), those quotes will get escaped and things will be uglier than necessary. So that's why I think it's nicer to handle the empty string as a special case, and keep the non-empty-string case without quotes.

Add another `case ""`. That way the dynamically generated error below doesn't just show `"unknown 'source' field value: "` if the `source` field is empty. Another solution would be to use the `%q` format specifier in the below-suggested call to `fmt.Errorf()`, but that would _always_ put quotes around the value. If that error is then subsequently logged with something like `log.Error().Err(err).Msg("whatever")`, those quotes will get escaped and things will be uglier than necessary. So that's why I think it's nicer to handle the empty string as a special case, and keep the non-empty-string case without quotes.
abelli marked this conversation as resolved
@ -1539,6 +1539,8 @@ components:
- path_envvar
# The input path was used as-is, as it points to an existing binary.
- input_path
# The input path was not provided, use whatever blender is available.

Add "…is available, without checking" to make it explicit that this will forego any checks on the path.

Add "…is available, without checking" to make it explicit that this will forego any checks on the path.
abelli marked this conversation as resolved
Mateus Abelli added 2 commits 2024-07-17 01:46:43 +02:00
Mateus Abelli added 2 commits 2024-07-17 02:02:03 +02:00
Author
Contributor

Thank you, I'm learning a ton in this PR 👍
I've tried my best to apply your feedback and I think it's looking better.

Here are some highlights of my changes:

There are now declared errors just below the imports, I tried to copy the style from other files.

var (
	ErrSetupConfigUnusableSource       = errors.New("sources should not have the 'is_usable' field set to false")
	ErrSetupConfigEmptyStorageLocation = errors.New("'storageLocation' field must not be empty")
    [...]
)

I took some time to pick a name I thought it would fit and ended up with the prefix ErrSetupConfig. It is still quite long but I think its fitting in the context of what it does, but please let know what you think.

I really enjoyed
logger.Error().AnErr("cause", err).Msg("setup assistant: configuration is incomplete")
It looks prettier than the inline method I was using and more concise with other Flamenco logs, below I'll show a demo.

The empty source case has been implemented, now it has its own ErrSetupConfigEmptySource and it will output this:

2024-07-16T20:55:28-03:00 ERR setup assistant: configuration is incomplete cause="'source' field must not be empty" config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":""},"storageLocation":"/home/mateus/workspace/flamenco-storage"}

This is for the dynamic error built with fmt.Errorf():

2024-07-16T20:56:03-03:00 ERR setup assistant: configuration is incomplete cause="unknown 'source' field value: blah" config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"blah"},"storageLocation":"/home/mateus/workspace/flamenco-storage"}
Thank you, I'm learning a ton in this PR 👍 I've tried my best to apply your feedback and I think it's looking better. Here are some highlights of my changes: There are now declared errors just below the imports, I tried to copy the style from other files. ```go var ( ErrSetupConfigUnusableSource = errors.New("sources should not have the 'is_usable' field set to false") ErrSetupConfigEmptyStorageLocation = errors.New("'storageLocation' field must not be empty") [...] ) ``` I took some time to pick a name I thought it would fit and ended up with the prefix `ErrSetupConfig`. It is still quite long but I think its fitting in the context of what it does, but please let know what you think. I really enjoyed ```logger.Error().AnErr("cause", err).Msg("setup assistant: configuration is incomplete")``` It looks prettier than the inline method I was using and more concise with other Flamenco logs, below I'll show a demo. The empty source case has been implemented, now it has its own `ErrSetupConfigEmptySource` and it will output this: ``` 2024-07-16T20:55:28-03:00 ERR setup assistant: configuration is incomplete cause="'source' field must not be empty" config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":""},"storageLocation":"/home/mateus/workspace/flamenco-storage"} ``` This is for the dynamic error built with `fmt.Errorf()`: ``` 2024-07-16T20:56:03-03:00 ERR setup assistant: configuration is incomplete cause="unknown 'source' field value: blah" config={"blenderExecutable":{"cause":"","input":"","is_usable":true,"path":"","source":"blah"},"storageLocation":"/home/mateus/workspace/flamenco-storage"} ```
Sybren A. Stüvel added this to the v3.6 milestone 2024-08-26 18:25:48 +02:00

Sorry for the long silence. If you feel this PR is ready for re-review, please press the little 'request review' button next to my name in the top-right corner. That'll make the PR show up in my review queue again.

Sorry for the long silence. If you feel this PR is ready for re-review, please press the little 'request review' button next to my name in the top-right corner. That'll make the PR show up in my review queue again.
Sybren A. Stüvel requested changes 2024-09-05 14:56:49 +02:00
Dismissed
Sybren A. Stüvel left a comment
Owner

Things keep improving :)

The one thing that I'd like to see changed, is that right now the "Skip" option is only available when Blender is not found. I think it's better to simply always show that option. It'll not only solve the issue (of the Manager requiring Blender), but also open op to situations where Blender is found on the Manager (but not in a place that's usable on the Workers).

Things keep improving :) The one thing that I'd like to see changed, is that right now the "Skip" option is only available when Blender is not found. I think it's better to simply always show that option. It'll not only solve the issue (of the Manager requiring Blender), but also open op to situations where Blender _is_ found on the Manager (but not in a place that's usable on the Workers).
Mateus Abelli added 2 commits 2024-09-08 20:29:36 +02:00
Mateus Abelli added 1 commit 2024-09-08 20:34:17 +02:00
Author
Contributor

Hi, I updated the code to include the skip option on all scenarios.

I was going to copy and paste the code for that option but I decided to simplify our existing code by removing the condition in

<fieldset v-if="autoFoundBlenders.length >= 1">

so now there's only one <fieldset> and inside of it evaluates as usual whatever is available for the user to pick, with the addition of our skip option at the end.

This is how it looks like on a system that has Blender and one that doesn't

Captura de tela de 2024-09-08 15-22-59.png

Captura de tela de 2024-09-08 15-21-12.png

Hi, I updated the code to include the skip option on all scenarios. I was going to copy and paste the code for that option but I decided to simplify our existing code by removing the condition in ```vue <fieldset v-if="autoFoundBlenders.length >= 1"> ``` so now there's only one `<fieldset>` and inside of it evaluates as usual whatever is available for the user to pick, with the addition of our skip option at the end. This is how it looks like on a system that has Blender and one that doesn't <table> <tr> <td> ![Captura de tela de 2024-09-08 15-22-59.png](/attachments/08c68577-80bb-4d10-b178-b9d3ac5e1469) </td> <td> ![Captura de tela de 2024-09-08 15-21-12.png](/attachments/5429fe84-4006-44ba-a32d-9da9c499034c) </td> </tr> </table>
Mateus Abelli requested review from Sybren A. Stüvel 2024-09-08 21:43:54 +02:00
Sybren A. Stüvel approved these changes 2024-09-09 11:04:30 +02:00
Sybren A. Stüvel left a comment
Owner

Approved!

Thanks for your work on this, it's going to make people's lives a little easier :)

I'll land the PR in a few different commits, as per Flamenco's commit guidelines.

Approved! Thanks for your work on this, it's going to make people's lives a little easier :) I'll land the PR in a few different commits, as per [Flamenco's commit guidelines](https://flamenco.blender.org/development/openapi-commit-guidelines/).
Sybren A. Stüvel manually merged commit 6baa132c43 into main 2024-09-09 11:22:42 +02:00
Mateus Abelli deleted branch issue100195 2024-09-10 01:28:52 +02:00
Sign in to join this conversation.
No description provided.