LoginSignup
31
16

More than 5 years have passed since last update.

Rustを勉強してて何か実際に使ってみたかった頃に、RailsなWebアプリケーションでCSVをダウンロードする仕組みがあって、普通に作ってみたところメモリを食いつぶして動かなかったので、Rustで作ってみました。

Railsでは以下のように行っていました。
1. 作成するCSVに関するパラメーター取得
2. DBからSELECT
3. CSVファイル作成
4. ZIP圧縮
5. レスポンスで返す

ここで2,3をRustで実行するようにしました。
さらにRailsを楽にするため
4は外部コマンドで実行
5はX-Accel-Redirectを使ってnginxに任せました。

CSVの出力範囲は1日を想定していて
Railsからはffi経由で呼び出しています。

結果、14万件のデーターが6分くらいでダウンロードできました。ZIPファイルのサイズは27MBでした。
メモリは10MBくらい使っていました。
Railsだと4GBメモリを積んだマシンでメモリ食いつぶしてサーバーごとお亡くなりだったので、Rustの威力を見せつけられました。
RailsとRustを組み合わせると効率的なアプリケーションが組めると実感しました。

Rust1.13.0で作りました。

Cargo.toml
[package]
name = "make_csv"
version = "0.1.0"
authors = ["aoyagikouhei"]

[lib]
name = "make_csv"
crate-type = ["dylib"]

[dependencies]
libc = "*"
chrono = "*"
postgres = { version="*", features=["with-chrono"]}
lib.rs
extern crate libc;
extern crate postgres;
extern crate chrono;

mod csv;

use std::ffi::CStr;

#[no_mangle]
pub extern fn execute(
    connection: *const libc::c_char, 
    file_path: *const libc::c_char, 
    start_at: *const libc::c_char, 
    end_at: *const libc::c_char
){
    let connection_cstr = unsafe { CStr::from_ptr(connection) };
    let file_path_cstr = unsafe { CStr::from_ptr(file_path) };
    let start_at_cstr = unsafe { CStr::from_ptr(start_at) };
    let end_at_cstr = unsafe { CStr::from_ptr(end_at) };
    let connection_str = connection_cstr.to_str().unwrap();
    let file_path_str = file_path_cstr.to_str().unwrap();
    let start_at_str = start_at_cstr.to_str().unwrap();
    let end_at_str = end_at_cstr.to_str().unwrap();

    let mut maker = csv::CsvMaker::new(
        connection_str,
        file_path_str,
        start_at_str,
        end_at_str
    );
    maker.execute();
}
csv.rs
extern crate postgres;
extern crate chrono;

use postgres::{Connection, TlsMode};
use std::io::prelude::*;
use std::fs::File;
use chrono::{DateTime, Local, Duration};
use std::cell::RefCell;

fn make_sql() -> String {
    let sql = r#"
        SELECT
            COALESCE(t1.tweet_json->'user'->>'screen_name', '')
            ,COALESCE(t1.tweet_json->>'text', '')
            ,COALESCE(to_char(
                to_timestamp(
                    t1.tweet_json->>'created_at'
                    ,'Dy Mon DD HH24:MI:SS +0000 YYYY'
                )::TIMESTAMPTZ + interval'9 hour'
                ,'YYYY/MM/DD HH24:MI'
            ), '')
            ,COALESCE(t1.tweet_json->'user'->>'statuses_count', '')
            ,COALESCE(t1.tweet_json->'user'->>'friends_count', '')
            ,COALESCE(t1.tweet_json->'user'->>'followers_count', '')
            ,COALESCE(t1.tweet_json->'user'->>'listed_count', '')
            ,COALESCE(t1.tweet_json->'user'->>'description', '')
            ,COALESCE(to_char(
                to_timestamp(
                    t1.tweet_json->'user'->>'created_at'
                    ,'Dy Mon DD HH24:MI:SS +0000 YYYY'
                )::TIMESTAMPTZ + interval'9 hour'
                ,'YYYY/MM/DD HH24:MI'
            ), '')
        FROM
            public.tweets AS t1
        WHERE
            t1.created_at >= $1
            AND t1.created_at < $2
        ORDER BY
            t1.created_at ASC
    "#;
    sql.to_string()
}

pub struct CsvMaker {
    pub connection: Connection,
    pub file: RefCell<File>,
    pub start_at: DateTime<Local>,
    pub end_at: DateTime<Local>,
    pub sql: String
}

impl CsvMaker {
    pub fn new(
        connection_str: &str,
        file_path: &str,
        start_at_str: &str,
        end_at_str: &str
    ) -> CsvMaker {
        let connection = Connection::connect(
            connection_str, TlsMode::None).unwrap();
        let file = RefCell::new(File::create(file_path).unwrap());
        let start_at = start_at_str.parse::<DateTime<Local>>().unwrap();
        let end_at = end_at_str.parse::<DateTime<Local>>().unwrap();
        let sql = make_sql();
        CsvMaker {
            connection: connection,
            file: file,
            start_at: start_at,
            end_at: end_at,
            sql: sql
        }
    }

    pub fn execute(&mut self) {
        let header = format!(
            "\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\"\n", 
            "アカウント名", 
            "ツイート内容",
            "投稿日時",
            "ツイート数",
            "フォロー数",
            "フォロワー数",
            "いいね数",
            "リスト数",
            "自己紹介",
            "アカウント登録日時"
            );
        {
            let mut f = self.file.borrow_mut();
            let _ = f.write(b"\xEF\xBB\xBF");
            let _ = f.write(header.as_bytes());
        }

        let mut next_at: DateTime<Local> = self.start_at.clone();

        loop {
            self.execute_one(&next_at);
            next_at = next_at + chrono::Duration::seconds(3600);
            if next_at >= self.end_at {
                break;
            }
        }
        {
            let mut f = self.file.borrow_mut();
            let _ = f.flush();
        }
    }

    fn execute_one(&self, next_at: &DateTime<Local>) {
        let next_end_at: DateTime<Local> = *next_at + Duration::seconds(3600);
        let mut f = self.file.borrow_mut();
        for row in &self.connection.query(&self.sql, &[&next_at, &next_end_at]).unwrap() {
            let screen_name: String = row.get(0);
            let tweet_content: String = row.get(1);
            let tweet_created_at: String = row.get(2);
            let statuses_count: String = row.get(3);
            let friends_count: String = row.get(4);
            let followers_count: String = row.get(5);
            let favourites_count: String = row.get(6);
            let listed_count: String = row.get(7);
            let description: String = row.get(8);
            let user_created_at: String = row.get(9);

            let line = format!(
                "\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\"\n", 
                screen_name,
                tweet_content,
                tweet_created_at,
                statuses_count,
                friends_count,
                followers_count,
                favourites_count,
                listed_count,
                description,
                user_created_at
            );
            let _ = f.write(line.as_bytes());
        }
    }
}
csv.rb
require 'ffi'
require 'systemu'

module App 
  class Csv 
    extend FFI::Library
    ffi_lib '../make_csv/target/release/libmake_csv.so'
    attach_function :execute, [:string, :string, :string, :string]

     def initialize()
      Dir.mkdir("/tmp/tweets") if !Dir.exist?("/tmp/tweets")
    end

    def exec(start_at, end_at)
      dir = Dir.mktmpdir("tweets/")
      self.class.execute(
        "postgres://localhost:5432/web_development", 
        dir + "/tweets.csv", 
        start_at, 
        end_at)
      Dir.chdir(dir) do
        systemu "/usr/bin/zip -m tweets.zip tweets.csv"
      end
      dir + "/tweets.zip"
    end
  end
end
31
16
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
16