yoshikipom Tech Blog

Mac Setup メモ (2022)

install

command line

mac app (productivity)

  • alfred (luncher)
  • vanilla (tool bar management)
  • rectangle (window management)
  • dropover (file management)
  • karabiner (key setting)
  • Logi Options (mouse setting)

mac app (development)

Mac Setting

  • Keyboard > Shortcuts > Mission Control で Switch to Desktop # を on
  • Accessibility > Display > Reduce motion を on
  • Keyboard で modifier key、repeatの設定、function keyの設定を変更

VSCode Setting

github accountで同期できる

開発ツール for http (wiremock, REST Client plugin for VSCode)

Overview

  • wiremockのdocker-composeでの起動
  • REST Client plugin for VSCode

Environment

  • MacOS
  • docker, docker-compose

wiremock

一番シンプルなmockの設定

{
  "request": {
    "method": "GET",
    "url": "/items"
  },
  "response": {
    "status": 200,
    "jsonBody": {
      "id": 1,
      "name": "item-name"
    },
    "headers": {
      "Content-Type": "application/json"
    }
  }
}

ディレイをつける、request bodyの一致の確認、外部ファイルの利用などは以下を参照。 https://github.com/yoshikipom/sandbox/tree/main/wiremock/dev/wiremock/mappings

docker-composeでの起動

---
version: '3'
services:
  mockServer:
    image: wiremock/wiremock:latest-alpine
    volumes:
      - "./dev/wiremock/:/home/wiremock/"
    ports:
      - 9000:8080
    restart: on-failure
    command:
      - --verbose
      - --disable-banner

run

docker-compose up

REST Client plugin for VSCode

インストール

https://marketplace.visualstudio.com/items?itemName=humao.rest-client

リクエス

ファイルを準備

###
GET http://localhost:9000/items

###
POST http://localhost:9000/items
Content-Type: application/json

{
    "name": "name"
}

Send Requestを押す

vscode rest client plugin

Refference

My Repository

https://github.com/yoshikipom/sandbox/tree/main/wiremock

Spring Bootの@Asyncで非同期処理(thread poolの設定 + 動作確認)

Overview

メインスレッドの処理に大きな影響を与えずにに何かの処理をしたいときに実装した(APIでレスポンスタイムに影響を与えずになにかDBに保存したい等)。 その処理を確実に達成したい場合はMessage Queueをを使った方が無難。

以下のような要件を達成するような実装を紹介する。 - 非同期で実行し、メインスレッドはその完了を待たない - 何らかの理由でThreadが増えすぎてメイン処理を遅くすることは避けたい

Code: https://github.com/yoshikipom/java/tree/main/spring-mvc/src/main/java/com/yoshikipom/dev/api/async Ref - https://www.baeldung.com/spring-async - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.html

Code

非同期処理を実行するThread

@Configuration
public class MyTaskExecutorBean {

  @Bean(name = "myTask")
  public TaskExecutor myTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(1);
    executor.setQueueCapacity(1);
    executor.setMaxPoolSize(2);
    return executor;
  }
}

上記の非同期スレッドで実装したい処理

  • @Async("${bean名}") をメソッドにつける (publicメソッド限定)
@Service
@Slf4j
public class MyAsyncService {

  @Async("myTask")
  public void executeAsyncProcess() {
    log.info("start async process");
    log.info("sleep 1s");
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
    log.info("finish async process");
  }
}

呼び出し側

  • myAsyncService.executeAsyncProcess() が非同期処理
  • @EnableAsync を 実行クラスにつける
  • thread poolの設定のため4回連続で実行
  • maxPoolSize + queueCapacityは受け付けられない。そのときはTaskRejectedException が起こる
@SpringBootApplication
@EnableAsync
@Slf4j
public class MyApplication implements CommandLineRunner {

  @Autowired
  private MyAsyncService myAsyncService;

  public static void main(String[] args) {

    SpringApplication.run(MyApplication.class, args);
  }

  @Override
  public void run(String... args) throws Exception {
    log.info("start app");
    try {
      // max pool size:2, queue size:1
      myAsyncService.executeAsyncProcess(); // will be executed
      myAsyncService.executeAsyncProcess(); // will be executed on another thread
      myAsyncService.executeAsyncProcess(); // will be queued
      myAsyncService.executeAsyncProcess(); // will not be executed due to shortage of thread pool
    } catch (TaskRejectedException e) {
      log.warn(e.getMessage());
    }
    log.info("finish app");
  }
}

実行結果

  • メインの処理が終わってから非同期のメソッドが動いている
  • myTask-1myTask-2の二つは並列で動いている
  • 3つ目の処理はmaxPoolSizeを超えるので上記の2つのどちらかが終わるまで待ってから実行 (今回は myTask-1が先に空いた)
  • 4つ目の処理は受け付けられないため、Errorが発生 (catchに書いたWARN logが出ている)。
2022-07-07 21:44:23.914  INFO 66165 --- [           main] c.y.dev.api.async.MyApplication          : Started MyApplication in 2.479 seconds (JVM running for 2.874)
2022-07-07 21:44:23.916  INFO 66165 --- [           main] c.y.dev.api.async.MyApplication          : start app
2022-07-07 21:44:23.918  WARN 66165 --- [           main] c.y.dev.api.async.MyApplication          : Executor [java.util.concurrent.ThreadPoolExecutor@32eae6f2[Running, pool size = 2, active threads = 2, queued tasks = 1, completed tasks = 0]] did not accept task: org.springframework.aop.interceptor.AsyncExecutionInterceptor$$Lambda$785/0x0000000800fe2a40@550e9be6
2022-07-07 21:44:23.918  INFO 66165 --- [           main] c.y.dev.api.async.MyApplication          : finish app
2022-07-07 21:44:23.924  INFO 66165 --- [       myTask-1] c.y.dev.api.async.MyAsyncService         : start async process
2022-07-07 21:44:23.924  INFO 66165 --- [       myTask-1] c.y.dev.api.async.MyAsyncService         : sleep 1s
2022-07-07 21:44:23.924  INFO 66165 --- [       myTask-2] c.y.dev.api.async.MyAsyncService         : start async process
2022-07-07 21:44:23.924  INFO 66165 --- [       myTask-2] c.y.dev.api.async.MyAsyncService         : sleep 1s
2022-07-07 21:44:24.928  INFO 66165 --- [       myTask-1] c.y.dev.api.async.MyAsyncService         : finish async process
2022-07-07 21:44:24.928  INFO 66165 --- [       myTask-2] c.y.dev.api.async.MyAsyncService         : finish async process
2022-07-07 21:44:24.929  INFO 66165 --- [       myTask-1] c.y.dev.api.async.MyAsyncService         : start async process
2022-07-07 21:44:24.929  INFO 66165 --- [       myTask-1] c.y.dev.api.async.MyAsyncService         : sleep 1s
2022-07-07 21:44:25.933  INFO 66165 --- [       myTask-1] c.y.dev.api.async.MyAsyncService         : finish async process

Conclusion

Thread数(とキューイングする数)を制限した非同期処理が実装できた。 CompletableFutureFuture を非同期処理の戻り値に使えば非同期処理を待って戻り値を得ることも可能。

JSONから特定のキーを削除するスクリプト

Overview

同僚が大量のJSONテストデータを手作業で編集するのが辛いと言っていたときに書いた。

Usage

yoshiki@yoshiki-mbp:python/script ‹main*›$ cat ./test_data/data_remove.json                                 
{
    "key": "value",
    "remove_target1": "value",
    "object": {
        "key": "value",
        "remove_target2": "value"
    }
}
yoshiki@yoshiki-mbp:python/script ‹main*›$ python remove_keys_from_json_file.py ./test_data/data_remove.json
{
  "key": "value",
  "object": {
    "key": "value"
  }
}

Script

  • REMOVE_ITEM_LIST に消したいキーのリストをハードコードしている。引数にすることもできるが、特定のタスクでしか使わないのと連続実行することが多い(しないなら手作業で十分)なので、このままにしている。
  • 指定したキーがネストしたオブジェクトの中にあっても削除される。(上の例の remove_target2
  • 簡単なロジック説明
    • 対象がリストだったら中のオブジェクトを探索する
    • 対象がオブジェクトだったら削除対象チェック+中のオブジェクトとリストを探索する

remove_keys_from_json_file.py

import json
from collections import OrderedDict
import sys

REMOVE_ITEM_LIST = ["remove_target1", "remove_target2"]

def remove_fields(arg):
    if isinstance(arg, list):
        for item in arg:
            remove_fields(item)
    elif isinstance(arg, dict):
        for remove_item in REMOVE_ITEM_LIST:
            if remove_item in arg:
                del(arg[remove_item])
        for value in arg.values():
            remove_fields(value)

if __name__ == '__main__':
    args = sys.argv
    path = args[1]

    with open(path) as f:
        od = json.load(f, object_pairs_hook=OrderedDict)
        remove_fields(od)
        print(json.dumps(od, indent=2, ensure_ascii=False))

Enhance

対象のキー名が十分にユニークなものだったのでこれで上手くいった。 間違ったものを削除しないようにするためにはJSON path (e.g. $.object.remove_target2) で指定できるようにする方法が考えられる。 jsonpath-python というライブラリが便利そう。

pypi.org

探索するロジック自体は他の要件にも使える。 (特定のオブジェクトに特定のkey, valueを追加したい等)

初転職時で入ってから気づいた大変だったこと (新卒4年目に転職、ソフトウェアエンジニア)

Overview

転職したときはそれなりにギャップがあって大変だったが、現状は楽しく働けているので、転職していろいろ辛いと思っている人の助けになればと思う。

Condition

  • 前の会社
    • 新卒で入社
    • 大きいWeb企業のソフトウェア開発をする部署
    • バックエンドエンジニアとして3年と少し
    • エンジニアが1000人以上
    • 英語使わない
  • 今の会社
    • 小売業界のIT部署
    • バックエンドエンジニアを継続して現在一年弱
    • IT部署は会社の一部
    • 英語使う

入ってから気づいた大変だった点

期待値

新卒時の初期の期待値は例年の新卒の平均になるので、周りを見れば自分ができている点・できていない点がわかった。 しかし、中途採用は面接時は自己経歴が期待値になるので、それを超えないと期待を満たせないというプレッシャーがあった。 周りからの評価を気にして漠然と不安になっていたが、自己評価すべき。

同期がいない

軽い相談をできる先がない、他部署に知り合いがいない、一緒にランチに行く友達がいない、など入ってから同期の偉大さを知った。 新卒で同期がたくさんいるのはメンタル的にはかなり良い。

英語

ある程度はコミュニケーションが取れるようになってきたが、聞き返すことも聞き返されることも多く、想像の数倍苦労している。 ポジティブな気持ちのときは、英語を使って仕事をするのは楽しいが、特に仕事がうまくいかない時、技術的に難しいことをしている時に言語の壁もプラスであるのが精神的にキツかった。 コミュニケーションを諦めるのが一番良くないので、言語以外のところで価値を発揮できるように積極的にコミュニケーションをがんばった。

書類・手続き・研修

新卒なら同期と同じ状況なのでなにも思わなかったが、入社時の書類、手続き、フォローアップの研修などを1人で進める必要があり大変だった。 また、前の会社は情報が上手であまりドキュメントを探すのに困らなかった、今の会社はわからないことがたくさんあって辛かった。 わからないものはわからないので、積極的に質問して、次の人が同じ罠にかからないようにドキュメントを残すしかない。

(おまけ) 想定外に良かった点

  • グローバル: ビジネスも働いている人もグローバル。仕事を通して世界を知れるのは楽しい。
  • 多様性: 転職組が多いのでいろいろな考えを知ることができる。また、前よりビジネスサイドとの距離が近い。
  • 複雑なシステム: 課題が多い反面、学ぶことも多いので楽しい。

開発ツール for Kafka (docker-compose, kcat)

Overview

  • docker-composeでの起動
  • CLI tool (kcat)

Environment

  • MacOS
  • docker, docker-compose

docker-composeでの起動

zookeeperも起動する必要があるようなのでdocker-composeで起動。

---
version: '3'
services:
  zookeeper:
    image: confluentinc/cp-zookeeper:7.0.1
    container_name: zookeeper
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
  broker:
    image: confluentinc/cp-kafka:7.0.1
    container_name: broker
    ports:
      - "9092:9092"
    depends_on:
      - zookeeper
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://broker:29092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1

run

docker-compose up

CLI tool (kcat)

インストール

brew install kcat

コマンド

produce (interactive)

$ kcat -b localhost:9092 -t new_topic -P
test
# end by ctrl-D

produce (file)

$ kcat -b localhost:9092 -t new_topic -P -l test.json

consume

# topic list
$ kcat -b localhost:9092 -t new_topic -C
% Reached end of topic new_topic [0] at offset 0
test
test2
% Reached end of topic new_topic [0] at offset 2
test3
% Reached end of topic new_topic [0] at offset 3

topic list

# topic list
$ kcat -b localhost:9092 -L
Metadata for all topics (from broker 1: localhost:9092/1):
 1 brokers:
  broker 1 at localhost:9092 (controller)
 1 topics:
  topic "new_topic" with 1 partitions:
    partition 0, leader 1, replicas: 1, isrs: 1

参考

スーパーでの「来週なにつくる?」に答えを出すツールをReactで作った

なぜ作ったか

週1回の買い物時にできるだけ必要なものは買い揃えたい。 そのために何を作るか決める必要があるが、大体スーパーについてから考えてしまうのである程度自動化したい。

メイン機能

準備したリストの中からランダムな重複なしリストを取得するアプリケーション。 ここにデプロイしてあります。 https://yoshikipom.github.io/dinnerslot-web/

input
result

工夫点

  • 1アイテムだけチェンジする機能 -> 最近作った、材料がないなどでチェンジするための機能。全部チェンジするとなかなか理想の結果が得られないことがある。
  • 結果をLINEに出力するボタン(SHARE BY LINE ) -> 結果の永続化、シェア
  • 入力したリストはブラウザに保存 -> 入力時間削減

LINE share

利用技術

ソースコードと主要部分の実装

リポジトリ: https://github.com/yoshikipom/dinnerslot-web

ページは index.tsx のみ。 ページ内でListInput.tsx(リスト入力) と Slot.tsx(結果生成) を呼び出し、タブで出し分ける。

$ tree src 
src
├── components
│   ├── Footer.tsx
│   ├── LineShareButton.tsx
│   ├── ListInput.tsx
│   └── Slot.tsx
├── model
│   └── Food.ts
└── pages
    ├── _app.tsx
    └── index.tsx

結果生成 - 入力されたリストをコピー - ランダムに並び替え - 欲しい数だけ前から切り取る

    const slot = () => {
        const copiedFoodList = foodList.concat();
        for (let i = copiedFoodList.length - 1; i > 0; i--) {
            const r = Math.floor(Math.random() * (i + 1));
            const tmp = copiedFoodList[i];
            copiedFoodList[i] = copiedFoodList[r];
            copiedFoodList[r] = tmp;
        }
        updateResult(copiedFoodList.slice(0, Number(count)));
    }

一点チェンジ - 入力されたリストをコピー - 選ばれているアイテムを除去 - 候補がなければ終了 - ランダムで一点選択 - 上書き

    const slotOneItem = (index: number) => {
        const copiedFoodList: Food[] = foodList.concat();
        const remainingList: Food[] = copiedFoodList.filter((food) => !results.includes(food))
    
        if (remainingList.length == 0) {
            return;
        }

        const r = Math.floor(Math.random() * (remainingList.length));
        results[index] = remainingList[r];
        updateResult(results.slice(0, Number(count)));
    }

LINE Shareボタン - 「LINE URLスキーム」を利用 - ref https://developers.line.biz/ja/docs/line-login/using-line-url-scheme/ - 値はpropsで渡す

const LineShareButton = (props: any) => {
    return (
        <Button sx={{ mx: 2, my: 2 }} variant="outlined" href={`https://line.me/R/share?text=${encodeURI(props.message)}`}>
            Share by LINE
        </Button>
    );
};

入力リスト保存 - localStorage利用 - 取得は初回呼び出しのみ - 書き込みは入力(foodListInputRaw)が変更が変更するたび

    useEffect(() => {
        const foodListInputRaw = window.localStorage.getItem(STORAGE_KEY);
        if (foodListInputRaw !== null) {
            setFoodListInputRaw(foodListInputRaw);
        }
    }, []);

    useEffect(() => {
        const strList = foodListInputRaw.split('\n').filter(val => val);
        let index = 1;
        const newFoodList: Food[] = strList.map(name => ({ id: index++, name }));
        setFoodList(newFoodList);
        window.localStorage.setItem(STORAGE_KEY, foodListInputRaw);
    }, [foodListInputRaw, setFoodList]);

感想

localStorage、LINEシェアは初めて試したが簡単に使えてよかった。 技術の復習としてバックエンドも作りたかったが、要件的に不要になってしまい残念。 自分のほしいアプリケーションを作ると勉強したい技術が使えるとは