Salesforce Functionsを試してみる（ローカルでの実行まで）

## 1. やること
* Salesforce Functionsを触ってみる

## 2. バージョンや背景など
* Summer '21 (2021/9/30時点)
* FunctionsのBeta登録に遅れたため、組織に接続できない
* FunctionsはWinter '22でGAだけど、リリース直前でまだGAになっていない

## 3. 試したこと

#### 3.1. Beta登録を試みた
  * https://sfdc.co/functions-beta
  * Beta登録のためのアンケートがなくなってしまった様子
    ![スクリーンショット 2021-09-30 10.08.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13874/ca39f1d4-259e-ebca-fce2-5afe293c83dd.png)


#### 3.2. Salesforce Functions SFDX Pluginのインストールを試みた
  * https://developer.salesforce.com/docs/platform/functions/guide/install_intro.html
    * Install the Salesforce Functions SFDX Plugin
  * 以下のコマンドを実施
 
```
% sfdx update
% sfdx plugins:install @salesforce/plugin-functions
```
  * 実施結果
  
```
% sfdx --version
sfdx-cli/7.119.2 darwin-x64 node-v14.17.6
% sfdx plugins
functions 0.2.50
└─ @oclif/plugin-not-found 1.2.4
```

#### 3.3. Salesforce組織でFunctionsの有効化を試みた
  * https://developer.salesforce.com/docs/platform/functions/guide/devhub-auth.html
  * [設定 > 機能設定 > 関数]のページを表示したが、やはりまだ有効化できない様子
    * クイック検索で「関数」だけでなく「function」でも、該当ページがヒットした
    * [User]では「ユーザ」ページはヒットしなかった
    * 英語でもヒットするようになると少し嬉しいかもしれない

![スクリーンショット 2021-09-30 10.19.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13874/55296ba0-0cb0-0c4b-c008-0293fb3b5114.png)

### 3.4. Functionの作成を試みた
  * https://developer.salesforce.com/docs/platform/functions/guide/create-dx-project.html
  * SFDXプロジェクトを作成する
    * FunctionはSFDXプロジェクト内に作成するため
    * `sfdx generate:project -n MyFunctionProject`

```
% sfdx generate:project -n MyFunctionProject
   create MyFunctionProject/config/project-scratch-def.json
   create MyFunctionProject/README.md
   create MyFunctionProject/sfdx-project.json
   create MyFunctionProject/.husky/pre-commit
   create MyFunctionProject/.vscode/extensions.json
   create MyFunctionProject/.vscode/launch.json
   create MyFunctionProject/.vscode/settings.json
   create MyFunctionProject/force-app/main/default/lwc/.eslintrc.json
   create MyFunctionProject/force-app/main/default/aura/.eslintrc.json
   create MyFunctionProject/scripts/soql/account.soql
   create MyFunctionProject/scripts/apex/hello.apex
   create MyFunctionProject/.eslintignore
   create MyFunctionProject/.forceignore
   create MyFunctionProject/.gitignore
   create MyFunctionProject/.prettierignore
   create MyFunctionProject/.prettierrc
   create MyFunctionProject/jest.config.js
   create MyFunctionProject/package.json
```
  * Functionを作成する
    * 公式サイトの例ではjavascriptだが、javaを選択してみる
    * 2021/9/30時点で、公式サイトの例のオプション名が古いよう。--nameを使用すると--function-nameを使ってほしいとのメッセージが出た
    * `sfdx generate:function --function-name myfunction --language java`

```
% cd MyFunctionProject 
% sfdx generate:function --function-name myfunction --language java
Created java function myfunction in ~~省略~~/MyFunctionProject/functions/myfunction.

Before creating Scratch Orgs for development, please ensure that:
1. Enable Functions in your DevHub org
2. Add Functions to the "features" list in your scratch org definition JSON file, e.g. "features": ["Functions"]
```
スクラッチ組織を作成する前に実施すべきことが記載されている。
`1. Enable Functions in your DevHub org`は、先ほど試みたが、今(2021/9/30)はまだできない

#### 3.3. Functionsをfeaturesに設定して、スクラッチ組織の作成を試みた
  * DevHub組織でFunctionsを有効化できていないので失敗すると思われる
  * `config/project-scratch-def.json`内のfeaturesにFunctionsを追加

```
% cat config/project-scratch-def.json 
{
  "orgName": "company",
  "edition": "Developer",
  "features": ["EnableSetPasswordInApi", "Functions"],
  "settings": {
    "lightningExperienceSettings": {
      "enableS1DesktopEnabled": true
    },
    "mobileSettings": {
      "enableS1EncryptedStoragePref2": false
    }
  }
}
```

  * スクラッチ組織を作成してみる
    * Functionsが有効化していないため、スクラッチ組織の作成に失敗する様子
 
```
% sfdx force:org:create -f config/project-scratch-def.json -a scratch -v devhub
ERROR running force:org:create:  Functions は有効な機能値ではありません。
```

#### 3.4. 作成されたFunctionsのコードを確認してみた
  * SFDXプロジェクトの配下に`functions`というフォルダが作成される

```
% ls
README.md		force-app		jest.config.js		scripts
config			functions		package.json		sfdx-project.json
```

  * `functions`フォルダ内に作成した`myFunction`がある

```
% ls functions 
myfunction
```

  * 作成されたFunctionはMaven形式の様子

```
% cd functions/myfunction 
% ls
mvnw		mvnw.cmd	pom.xml		project.toml	src
```

  * 作成されたpom.xmlは以下のようになっていた
    * Javaバージョンは1.8
    * `com.salesforce.functions:sf-fx-sdk-java` をdependsしている
    * JUnit4だったりする
  * `mvnw clean package` でBUILD SUCCESSすることを確認
 
```
shigeo.tejima@tejimasgeonoair myfunction % cat pom.xml 
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>myfunction</artifactId>
    <version>1.0-SNAPSHOT</version>

    <name>myfunction</name>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.salesforce.functions</groupId>
            <artifactId>sf-fx-sdk-java</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>3.6.28</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest</artifactId>
            <version>2.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.spotify</groupId>
            <artifactId>hamcrest-pojo</artifactId>
            <version>1.2.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <artifactId>maven-clean-plugin</artifactId>
                    <version>3.1.0</version>
                </plugin>
                <plugin>
                    <artifactId>maven-resources-plugin</artifactId>
                    <version>3.0.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.0</version>
                </plugin>
                <plugin>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>2.22.1</version>
                </plugin>
                <plugin>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>3.0.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-install-plugin</artifactId>
                    <version>2.5.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-deploy-plugin</artifactId>
                    <version>2.8.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-site-plugin</artifactId>
                    <version>3.7.1</version>
                </plugin>
                <plugin>
                    <artifactId>maven-project-info-reports-plugin</artifactId>
                    <version>3.0.0</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>
```

  * 作成されたコードは以下のようになっていた

```
% cd src/main/java/com/example/
% ls
Account.java		FunctionInput.java	FunctionOutput.java	MyfunctionFunction.java
```

```
% cat Account.java 
package com.example;

public class Account {
    private final String id;
    private final String name;

    public Account(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}
```

```
% cat FunctionInput.java 
package com.example;

public class FunctionInput {
}
```

```
% cat FunctionOutput.java 
package com.example;

import java.util.List;

public class FunctionOutput {
  private final List<Account> accounts;

  public FunctionOutput(List<Account> accounts) {
    this.accounts = accounts;
  }

  public List<Account> getAccounts() {
    return accounts;
  }
}

```

```
% cat MyfunctionFunction.java 
package com.example;

import com.salesforce.functions.jvm.sdk.Context;
import com.salesforce.functions.jvm.sdk.InvocationEvent;
import com.salesforce.functions.jvm.sdk.SalesforceFunction;
import com.salesforce.functions.jvm.sdk.data.Record;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

/**
 * Describe MyfunctionFunction here.
 */
public class MyfunctionFunction implements SalesforceFunction<FunctionInput, FunctionOutput> {
  private static final Logger LOGGER = LoggerFactory.getLogger(MyfunctionFunction.class);

  @Override
  public FunctionOutput apply(InvocationEvent<FunctionInput> event, Context context)
      throws Exception {

    List<Record> records =
        context.getOrg().get().getDataApi().query("SELECT Id, Name FROM Account").getRecords();

    LOGGER.info("Function successfully queried {} account records!", records.size());

    List<Account> accounts = new ArrayList<>();
    for (Record record : records) {
      String id = record.getStringField("Id").get();
      String name = record.getStringField("Name").get();

      accounts.add(new Account(id, name));
    }

    return new FunctionOutput(accounts);
  }
}
```

  * 組織から取引先(Account)のId,Nameを検索した結果を返す処理の様子
    * 入力, 出力ともにPOJOで書けるようだ
    * Functionは`SalesforceFunction<T,S>`を実装する

  * 作成されたテストコードを確認してみる

```
% cd src/test/java/com/example 
% ls
FunctionTest.java
```

```
% cat FunctionTest.java 
package com.example;

import com.salesforce.functions.jvm.sdk.Context;
import com.salesforce.functions.jvm.sdk.InvocationEvent;
import com.salesforce.functions.jvm.sdk.Org;
import com.salesforce.functions.jvm.sdk.data.Record;
import com.salesforce.functions.jvm.sdk.data.RecordQueryResult;
import org.junit.Test;
import org.mockito.Mockito;

import java.util.Arrays;
import java.util.Optional;

import static com.spotify.hamcrest.pojo.IsPojo.pojo;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItems;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class FunctionTest {

  @Test
  public void test() throws Exception {
    MyfunctionFunction function = new MyfunctionFunction();
    FunctionOutput functionOutput = function.apply(createEventMock(), createContextMock());

    assertThat(
        functionOutput.getAccounts(),
        hasItems(
            pojo(Account.class)
                .withProperty("id", equalTo("5003000000D8cuIQAA"))
                .withProperty("name", equalTo("Account One, inc.")),
            pojo(Account.class)
                .withProperty("id", equalTo("6003000000D8cuIQAA"))
                .withProperty("name", equalTo("Account Two, inc."))));
  }

  private Context createContextMock() {
    Context mockContext = mock(Context.class);

    when(mockContext.getOrg()).then(i1 -> {
      Org mockOrg = mock(Org.class, Mockito.RETURNS_DEEP_STUBS);

      when(mockOrg.getDataApi().query("SELECT Id, Name FROM Account")).then(i2 -> {
        RecordQueryResult mockResult = mock(RecordQueryResult.class);

        Record firstRecord = mock(Record.class);
        when(firstRecord.getStringField("Id")).thenReturn(Optional.of("5003000000D8cuIQAA"));
        when(firstRecord.getStringField("Name")).thenReturn(Optional.of("Account One, inc."));

        Record secondRecord = mock(Record.class);
        when(secondRecord.getStringField("Id")).thenReturn(Optional.of("6003000000D8cuIQAA"));
        when(secondRecord.getStringField("Name")).thenReturn(Optional.of("Account Two, inc."));

        when(mockResult.getRecords()).thenReturn(Arrays.asList(firstRecord, secondRecord));

        return mockResult;
      });

      return Optional.of(mockOrg);
    });

    return mockContext;
  }

  private InvocationEvent<FunctionInput> createEventMock() {
    return mock(InvocationEvent.class);
  }
}
```

  * 組織に接続が必要な箇所や`InvocationEvent`などは、モックしてテストできる


#### 3.5. 作成されたFunctionをローカルで動かしてみた
  * https://developer.salesforce.com/docs/platform/functions/guide/test-local-function.html
  * Functionsが有効なスクラッチ組織がないため、作成されたコードの内`MyfunctionFunction.java`を以下のように組織に依存しない内容に変更する

```
    /*
    List<Record> records =
        context.getOrg().get().getDataApi().query("SELECT Id, Name FROM Account").getRecords();

    LOGGER.info("Function successfully queried {} account records!", records.size());

    List<Account> accounts = new ArrayList<>();
    for (Record record : records) {
      String id = record.getStringField("Id").get();
      String name = record.getStringField("Name").get();

      accounts.add(new Account(id, name));
    }
    */

    // demo code without org
    List<Account> accounts = new ArrayList<>();
    accounts.add(new Account("id-1", "account 1"));
    accounts.add(new Account("id-2", "account 2"));
```

  * `sfdx run:function:start`
    * 実行時のログは以下のGistに添付
    * https://gist.github.com/ShigeoTejima/1bc51717032825ebdd08db6f7130e21b
  * `sfdx run:function -l http://localhost:8080 -p '{}'`
    * FunctionInputにはパラメータがないため、上記のようなリクエストを送信

```
% sfdx run:function -l http://localhost:8080 -p '{}'
 ›   Warning: No -o connected org or defaultusername found, context will be partially initialized
Using defaultusername undefined login credential to initialize context
POST http://localhost:8080... 200
{
  "accounts": [
    {
      "id": "id-1",
      "name": "account 1"
    },
    {
      "id": "id-2",
      "name": "account 2"
    }
  ]
}
```

  * 期待通りのレスポンスが返ってきた
  * -oオプションで指定するか、defaultusernameを設定するとその組織が使用されそうである
  * 以下のようにhelpを表示すると、-oオプションの説明にそのような記載がある

```
% sfdx help run:function
Send a cloudevent to a function.

USAGE
  $ sfdx run:function

OPTIONS
  -H, --headers=headers              Set headers.
  -l, --function-url=function-url    Url of the function to run.
  -o, --connected-org=connected-org  Username or alias for the target org; overrides default target org.
  -p, --payload=payload              Set the payload of the cloudevent. also accepts @file.txt format.
  -s, --structured                   Set the cloudevent to be emitted as a structured cloudevent (json).

EXAMPLES
  $ sfdx run:function -l http://localhost:8080 -p '{"id": 12345}'
  $ sfdx run:function -l http://localhost:8080 -p '@file.json'
  $ echo '{"id": 12345}' | sfdx run:function -l http://localhost:8080
  $ sfdx run:function -l http://localhost:8080 -p '{"id": 12345}' --structured

COMMANDS
  run:function:start  Build and run function image locally.
```

#### 3.6. スクラッチ組織を作成して、ローカルで動かしたFunctionから接続できるか試してみた
  * スクラッチ組織を作成する
    * `sfdx force:org:create -f config/project-scratch-def.json -a scratch -v devhub `
    * DevHub組織はFunctionsが有効化されていない
    * `config/project-scratch-def.json`のfeaturesにFunctionsは追加していない
  * 取引先(Account)をいくつか作成する
![スクリーンショット 2021-09-30 11.51.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13874/61164da6-3fa4-f4ae-f23c-aca8d7dce841.png)
  * 作成されたFunctionのコード `MyfunctionFunction.java` を元に戻す
  * `sfdx run:function:start`
  * `sfdx run:function -l http://localhost:8080 -p '{}' -o scratch`
  * 以下のようにスクラッチ組織の取引先(Account)の情報がレスポンスとして返ってきた
    * defaultusernameにスクラッチ組織が設定されている場合は、-oオプションなしでも同様の結果となった

```
% sfdx run:function -l http://localhost:8080 -p '{}' -o scratch
Using scratch login credential to initialize context
POST http://localhost:8080... 200
{
  "accounts": [
    {
      "id": "0010l00001M37iwAAB",
      "name": "取引先Bar"
    },
    {
      "id": "0010l00001M35GQAAZ",
      "name": "エンタイトルメントの取引先のサンプル"
    },
    {
      "id": "0010l00001M37imAAB",
      "name": "Account Foo"
    }
  ]
}
```

  * defaultusernameが未設定、かつ-oオプションでスクラッチ組織を指定しないで、組織に接続しようとすると以下のようなエラーとなった

```
% sfdx run:function -l http://localhost:8080 -p '{}'           
 ›   Warning: No -o connected org or defaultusername found, context will be partially initialized
Using defaultusername undefined login credential to initialize context
POST http://localhost:8080... 500 Internal Server Error
 ›   Error: Function threw exception: com.salesforce.functions.jvm.sdk.data.error.DataApiException (IOException while executing API 
 ›   request!)
 ›   com.salesforce.functions.jvm.sdk.data.error.DataApiException: IOException while executing API request!
 ›   	at com.salesforce.functions.jvm.runtime.sdk.DataApiImpl.executeRequest(DataApiImpl.java:188)
 ›   	at com.salesforce.functions.jvm.runtime.sdk.DataApiImpl.query(DataApiImpl.java:57)
 ›   	at com.example.MyfunctionFunction.apply(MyfunctionFunction.java:25)
 ›   	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 ›   	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 ›   	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 ›   	at java.lang.reflect.Method.invoke(Method.java:498)
 ›   	at com.salesforce.functions.jvm.runtime.sfjavafunction.SalesforceFunctionsProjectFunctionsScanner.lambda$scan$0(SalesforceFunct
 ›   ionsProjectFunctionsScanner.java:402)
 ›   	at com.salesforce.functions.jvm.runtime.sfjavafunction.Slf4j1MdcDataInvocationWrapper.invoke(Slf4j1MdcDataInvocationWrapper.jav
 ›   a:81)
 ›   	at com.salesforce.functions.jvm.runtime.sfjavafunction.SalesforceFunction.apply(SalesforceFunction.java:68)
 ›   	at com.salesforce.functions.jvm.runtime.sfjavafunction.SalesforceFunction.apply(SalesforceFunction.java:31)
 ›   	at com.salesforce.functions.jvm.runtime.invocation.undertow.UndertowInvocationInterface$ProjectFunctionHandler.handleRequest(Un
 ›   dertowInvocationInterface.java:160)
 ›   	at io.undertow.server.Connectors.executeRootHandler(Connectors.java:387)
 ›   	at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:852)
 ›   	at org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
 ›   	at org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:2019)
 ›   	at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1558)
 ›   	at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1449)
 ›   	at org.xnio.XnioWorker$WorkerThreadFactory$1$1.run(XnioWorker.java:1280)
 ›   	at java.lang.Thread.run(Thread.java:748)
 ›   Caused by: org.apache.http.client.ClientProtocolException
 ›   	at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:187)
 ›   	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83)
 ›   	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108)
 ›   	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56)
 ›   	at com.salesforce.functions.jvm.runtime.sdk.restapi.RestApi.execute(RestApi.java:75)
 ›   	at com.salesforce.functions.jvm.runtime.sdk.DataApiImpl.executeRequest(DataApiImpl.java:182)
 ›   	... 19 more
 ›   Caused by: org.apache.http.ProtocolException: Target host is not specified
 ›   	at org.apache.http.impl.conn.DefaultRoutePlanner.determineRoute(DefaultRoutePlanner.java:71)
 ›   	at org.apache.http.impl.client.InternalHttpClient.determineRoute(InternalHttpClient.java:125)
 ›   	at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:184)
 ›   	... 24 more
 ›
```

### 3.6. dockerのイメージとコンテナを確認してみた
  * Dockerのイメージ
    * `heroku/pack`、`heroku/buildpacks`が追加されている
    * `myfunction`という作成したFunctionと同名のイメージが追加されている
    * `<none>`は`myfunction`の前回のイメージ

```
% docker images
REPOSITORY             TAG       IMAGE ID       CREATED         SIZE
heroku/pack            20        822ae55e45ce   3 hours ago     567MB
<none>                 <none>    74806d6c3d7b   41 years ago    710MB
myfunction             latest    c871af01d206   41 years ago    710MB
heroku/buildpacks      20        094f2b3353e5   41 years ago    1.54GB
```

  * Dockerのコンテナ
    * `myfunction`イメージでコンテナが起動している

```
% docker ps
CONTAINER ID   IMAGE        COMMAND              CREATED         STATUS         PORTS                                            NAMES
fd1e377bacb2   myfunction   "/cnb/process/web"   7 minutes ago   Up 6 minutes   0.0.0.0:8080->8080/tcp, 0.0.0.0:9229->9229/tcp   myfunction
```

### 3.7. Java11を試みた
  * pom.xmlでバージョン指定している箇所を修正

```
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <java.version>11</java.version>
    </properties>
```

  * ローカルで`mvnw clean package`して問題ないことを確認
  * `sfdx run:function:start` してみる
    * 以下のようにエラーで落ちた

```
% sfdx run:function:start
Building myfunction
20: Pulling from heroku/buildpacks
Digest: sha256:e007b9a6a71bfdbb80e412cae139d46bc98ffa580e3e780f653e89b3055be9b6
Status: Image is up to date for heroku/buildpacks:20
20: Pulling from heroku/pack
Digest: sha256:694f287e23692b3914d32aaa9c0227fc95359c16bc12ef68f8251ab3a7a6c706
Status: Image is up to date for heroku/pack:20
===> DETECTING
======== Output: heroku/ruby@0.0.1 ========
no
err:  heroku/ruby@0.0.1 (1)
heroku/jvm                  0.1.8
heroku/maven                0.2.5
heroku/jvm-function-invoker 0.5.3
===> ANALYZING
Restoring metadata for "heroku/jvm:jre" from app image
Restoring metadata for "heroku/jvm:jdk" from cache
Restoring metadata for "heroku/jvm:utils" from cache
Restoring metadata for "heroku/maven:maven-repository" from cache
Restoring metadata for "heroku/jvm-function-invoker:bundle" from app image
Restoring metadata for "heroku/jvm-function-invoker:opt" from app image
Restoring metadata for "heroku/jvm-function-invoker:runtime" from app image
===> RESTORING
Restoring data for "heroku/jvm:jdk" from cache
Restoring data for "heroku/jvm:utils" from cache
Restoring data for "heroku/maven:maven-repository" from cache
Restoring data for "heroku/jvm-function-invoker:runtime" from cache
===> BUILDING
[Installing Java]
JDK 1.8 installed from cache
JRE 1.8 installed from cache
[Installing Maven]
[INFO] Maven wrapper detected, skipping installation.
[Executing Maven]
[INFO] $ ./mvnw -DskipTests clean install
[INFO] Scanning for projects...
[INFO] 
[INFO] --
---------------------< com.example:myfunction >-----------------------
[
INFO] Building myfunction 1.0-SNAPSHOT
[INFO] --------------
------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-
clean-plugin:3.1.0:clean (default-clean) @ myfunction ---
[INFO] Deleting /workspace/target
[INFO] 
[INFO] --- maven-resources-plugin:3.0.2
:resources (default-resources) @ myfunction ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip
 non existing resourceDirectory /workspace/src/main/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.0:compile (default-compile) @ myfunction ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 4 source files to /workspace/target/classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.146 s
[INFO] Finished at: 2021-09-30T04:08:34Z
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.0:compile (default-compile) on project myfunction: Fatal error compiling: invalid target release: 11 -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException
[ERROR: Failed to build app with Maven]
We're sorry this build is failing! If you can't find the issue in application code,
please submit a ticket so we can help: https://help.heroku.com/
ERROR: failed to build: exit status 1
ExitError: EEXIT: 0
    at Object.exit (/Users/shigeo.tejima/.local/share/sfdx/node_modules/@oclif/errors/lib/index.js:19:11)
    at EventEmitter.<anonymous> (/Users/shigeo.tejima/.local/share/sfdx/node_modules/@salesforce/plugin-functions/lib/commands/run/function/start.js:59:30)
    at EventEmitter.emit (events.js:400:28)
    at EventEmitter.emit (domain.js:470:12)
    at Interface.<anonymous> (/Users/shigeo.tejima/.local/share/sfdx/node_modules/@heroku/functions-core/dist/benny.js:106:39)
    at Interface.emit (events.js:400:28)
    at Interface.emit (domain.js:470:12)
    at Interface._onLine (readline.js:364:10)
    at Interface._normalWrite (readline.js:509:12)
    at Socket.ondata (readline.js:216:10) {
  oclif: { exit: 0 },
  code: 'EEXIT'
}
ExitError: EEXIT: 0
    at Object.exit (/Users/shigeo.tejima/.local/share/sfdx/node_modules/@oclif/errors/lib/index.js:19:11)
    at EventEmitter.<anonymous> (/Users/shigeo.tejima/.local/share/sfdx/node_modules/@salesforce/plugin-functions/lib/commands/run/function/start.js:59:30)
    at EventEmitter.emit (events.js:400:28)
    at EventEmitter.emit (domain.js:470:12)
    at Interface.<anonymous> (/Users/shigeo.tejima/.local/share/sfdx/node_modules/@heroku/functions-core/dist/benny.js:106:39)
    at Interface.emit (events.js:400:28)
    at Interface.emit (domain.js:470:12)
    at Interface._onLine (readline.js:364:10)
    at Interface._normalWrite (readline.js:509:12)
    at Socket.ondata (readline.js:216:10) {
  oclif: { exit: 0 },
  code: 'EEXIT'
}
ExitError: EEXIT: 0
    at Object.exit (/Users/shigeo.tejima/.local/share/sfdx/node_modules/@oclif/errors/lib/index.js:19:11)
    at EventEmitter.<anonymous> (/Users/shigeo.tejima/.local/share/sfdx/node_modules/@salesforce/plugin-functions/lib/commands/run/function/start.js:59:30)
    at EventEmitter.emit (events.js:400:28)
    at EventEmitter.emit (domain.js:470:12)
    at Interface.<anonymous> (/Users/shigeo.tejima/.local/share/sfdx/node_modules/@heroku/functions-core/dist/benny.js:106:39)
    at Interface.emit (events.js:400:28)
    at Interface.emit (domain.js:470:12)
    at Interface._onLine (readline.js:364:10)
    at Interface._normalWrite (readline.js:509:12)
    at Socket.ondata (readline.js:216:10) {
  oclif: { exit: 0 },
  code: 'EEXIT'
}
ExitError: EEXIT: 0
    at Object.exit (/Users/shigeo.tejima/.local/share/sfdx/node_modules/@oclif/errors/lib/index.js:19:11)
    at EventEmitter.<anonymous> (/Users/shigeo.tejima/.local/share/sfdx/node_modules/@salesforce/plugin-functions/lib/commands/run/function/start.js:59:30)
    at EventEmitter.emit (events.js:400:28)
    at EventEmitter.emit (domain.js:470:12)
    at Interface.<anonymous> (/Users/shigeo.tejima/.local/share/sfdx/node_modules/@heroku/functions-core/dist/benny.js:106:39)
    at Interface.emit (events.js:400:28)
    at Interface.emit (domain.js:470:12)
    at Interface._onLine (readline.js:364:10)
    at Interface._normalWrite (readline.js:509:12)
    at Socket.ondata (readline.js:216:10) {
  oclif: { exit: 0 },
  code: 'EEXIT'
}
ExitError: EEXIT: 0
    at Object.exit (/Users/shigeo.tejima/.local/share/sfdx/node_modules/@oclif/errors/lib/index.js:19:11)
    at EventEmitter.<anonymous> (/Users/shigeo.tejima/.local/share/sfdx/node_modules/@salesforce/plugin-functions/lib/commands/run/function/start.js:59:30)
    at EventEmitter.emit (events.js:400:28)
    at EventEmitter.emit (domain.js:470:12)
    at Interface.<anonymous> (/Users/shigeo.tejima/.local/share/sfdx/node_modules/@heroku/functions-core/dist/benny.js:106:39)
    at Interface.emit (events.js:400:28)
    at Interface.emit (domain.js:470:12)
    at Interface._onLine (readline.js:364:10)
    at Interface._normalWrite (readline.js:509:12)
    at Socket.ondata (readline.js:216:10) {
  oclif: { exit: 0 },
  code: 'EEXIT'
}
ExitError: EEXIT: 0
    at Object.exit (/Users/shigeo.tejima/.local/share/sfdx/node_modules/@oclif/errors/lib/index.js:19:11)
    at EventEmitter.<anonymous> (/Users/shigeo.tejima/.local/share/sfdx/node_modules/@salesforce/plugin-functions/lib/commands/run/function/start.js:59:30)
    at EventEmitter.emit (events.js:400:28)
    at EventEmitter.emit (domain.js:470:12)
    at Interface.<anonymous> (/Users/shigeo.tejima/.local/share/sfdx/node_modules/@heroku/functions-core/dist/benny.js:106:39)
    at Interface.emit (events.js:400:28)
    at Interface.emit (domain.js:470:12)
    at Interface._onLine (readline.js:364:10)
    at Interface._normalWrite (readline.js:509:12)
    at Socket.ondata (readline.js:216:10) {
  oclif: { exit: 0 },
  code: 'EEXIT'
}
    Error: Build exited with: executing lifecycle: failed with status code: 145
```

  * heroku-buildpack-javaを眺める
    * https://github.com/heroku/heroku-buildpack-java
  * 以下のような記述が見つかったので、試してみる

```
Choose a JDK
Create a system.properties file in the root of your project directory and set java.runtime.version=1.8.
```

  * system.propertiesファイルを作成して、java.runtime.versionを指定する

```
% echo "java.runtime.version=11" > system.properties
% cat system.properties 
java.runtime.version=11
% ls
mvnw			pom.xml			src			target
mvnw.cmd		project.toml		system.properties
```

  * `sfdx run:function:start` してみる
    * 以下のようにJDK 11がインストールされたログが表示された
    * 正常に起動して、functionの呼び出しも成功した

```
[Installing Java]
JDK 11 installed
JRE 11 installed
```

### 3.8. Java17を試みた

  * `java.runtime.version=17` も正常に動作した

```
[Installing Java]
JDK 17 installed
JRE 17 installed
```

### (追記)
### 3.9. キャッシュのクリア
  * イメージを削除した後で再度作成する場合など、キャッシュが残っていて正常に動作しない場合があった
    * 事象が発生した例
      * Java8でイメージを作成して起動
      * Java11でイメージを作成して起動
      * イメージを削除
      * Java11でイメージを作成しようとしたら、起動しなかった
  * 以下のコマンドでキャッシュをクリアしてイメージを作成して起動する
    * `sfdx run:function:start -v --clear-cache`

## 4. 試せなかったこと
  * Functionのデプロイ
  * ApexからのFunction実行

## 5. 参考
* https://developer.salesforce.com/docs/platform/functions/guide/gs_intro.html
* https://developer.salesforce.com/blogs/2021/06/functions-announcement